pax_global_header 0000666 0000000 0000000 00000000064 14325445506 0014522 g ustar 00root root 0000000 0000000 52 comment=3c5ceaed91ccbd5824a8e629510aa432126880fe
glamour-0.6.0/ 0000775 0000000 0000000 00000000000 14325445506 0013173 5 ustar 00root root 0000000 0000000 glamour-0.6.0/.github/ 0000775 0000000 0000000 00000000000 14325445506 0014533 5 ustar 00root root 0000000 0000000 glamour-0.6.0/.github/dependabot.yml 0000664 0000000 0000000 00000000423 14325445506 0017362 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
glamour-0.6.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14325445506 0016570 5 ustar 00root root 0000000 0000000 glamour-0.6.0/.github/workflows/build.yml 0000664 0000000 0000000 00000001311 14325445506 0020406 0 ustar 00root root 0000000 0000000 name: build
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
go-version: [~1.13, ^1]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
COLORTERM: "truecolor"
COLORFGBG: "7;0"
TERM: "xterm-256color"
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
- name: Download Go modules
run: go mod download
- name: Build
run: go build -v ./...
- name: Test
run: go test ./...
if: matrix.os != 'windows-latest'
glamour-0.6.0/.github/workflows/coverage.yml 0000664 0000000 0000000 00000001321 14325445506 0021103 0 ustar 00root root 0000000 0000000 name: coverage
on: [push, pull_request]
jobs:
coverage:
strategy:
matrix:
go-version: [^1]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
GO111MODULE: "on"
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
GO111MODULE=off go get github.com/mattn/goveralls
$(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
glamour-0.6.0/.github/workflows/lint.yml 0000664 0000000 0000000 00000001275 14325445506 0020266 0 ustar 00root root 0000000 0000000 name: lint
on:
push:
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
pull-requests: read
jobs:
golangci:
name: lint-soft
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ^1
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true
glamour-0.6.0/.github/workflows/soft-serve.yml 0000664 0000000 0000000 00000000325 14325445506 0021410 0 ustar 00root root 0000000 0000000 name: soft-serve
on:
push:
branches:
- master
jobs:
soft-serve:
uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
secrets:
ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"
glamour-0.6.0/.gitignore 0000664 0000000 0000000 00000000005 14325445506 0015156 0 ustar 00root root 0000000 0000000 cmd/
glamour-0.6.0/.golangci.yml 0000664 0000000 0000000 00000000551 14325445506 0015560 0 ustar 00root root 0000000 0000000 run:
tests: false
issues:
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- bodyclose
- dupl
- exportloopref
- goconst
- godot
- godox
- goimports
- gomnd
- goprintffuncname
- gosec
- misspell
- prealloc
- rowserrcheck
- sqlclosecheck
- unconvert
- unparam
- whitespace
glamour-0.6.0/LICENSE 0000664 0000000 0000000 00000002063 14325445506 0014201 0 ustar 00root root 0000000 0000000 MIT License
Copyright (c) 2019 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
glamour-0.6.0/README.md 0000664 0000000 0000000 00000007354 14325445506 0014463 0 ustar 00root root 0000000 0000000 # Glamour

Stylesheet-based markdown rendering for your CLI apps.

`glamour` lets you render [markdown](https://en.wikipedia.org/wiki/Markdown)
documents & templates on [ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code)
compatible terminals. You can create your own stylesheet or simply use one of
the stylish defaults.
## Usage
```go
import "github.com/charmbracelet/glamour"
in := `# Hello World
This is a simple example of Markdown rendering with Glamour!
Check out the [other examples](https://github.com/charmbracelet/glamour/tree/master/examples) too.
Bye!
`
out, err := glamour.Render(in, "dark")
fmt.Print(out)
```
### Custom Renderer
```go
import "github.com/charmbracelet/glamour"
r, _ := glamour.NewTermRenderer(
// detect background color and pick either the default dark or light theme
glamour.WithAutoStyle(),
// wrap output at specific width (default is 80)
glamour.WithWordWrap(40),
)
out, err := r.Render(in)
fmt.Print(out)
```
## Styles
You can find all available default styles in our [gallery](https://github.com/charmbracelet/glamour/tree/master/styles/gallery).
Want to create your own style? [Learn how!](https://github.com/charmbracelet/glamour/tree/master/styles)
There are a few options for using a custom style:
1. Call `glamour.Render(inputText, "desiredStyle")`
1. Set the `GLAMOUR_STYLE` environment variable to your desired default style or a file location for a style and call `glamour.RenderWithEnvironmentConfig(inputText)`
1. Set the `GLAMOUR_STYLE` environment variable and pass `glamour.WithEnvironmentConfig()` to your custom renderer
## Glamourous Projects
Check out these projects, which use `glamour`:
- [Glow](https://github.com/charmbracelet/glow), a markdown renderer for
the command-line.
- [GitHub CLI](https://github.com/cli/cli), GitHub’s official command line tool.
- [GLab](https://github.com/profclems/glab), an open source GitLab command line tool.
- [Gitea CLI](https://gitea.com/gitea/tea), Gitea's official command line tool.
- [Meteor](https://github.com/odpf/meteor), an easy-to-use, plugin-driven metadata collection framework.
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
* [Twitter](https://twitter.com/charmcli)
* [The Fediverse](https://mastodon.social/@charmcli)
* [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/glamour/raw/master/LICENSE)
***
Part of [Charm](https://charm.sh).
Charm热爱开源 • Charm loves open source
glamour-0.6.0/ansi/ 0000775 0000000 0000000 00000000000 14325445506 0014125 5 ustar 00root root 0000000 0000000 glamour-0.6.0/ansi/baseelement.go 0000664 0000000 0000000 00000005040 14325445506 0016737 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"io"
"strings"
"text/template"
"github.com/muesli/termenv"
)
// BaseElement renders a styled primitive element.
type BaseElement struct {
Token string
Prefix string
Suffix string
Style StylePrimitive
}
func formatToken(format string, token string) (string, error) {
var b bytes.Buffer
v := make(map[string]interface{})
v["text"] = token
tmpl, err := template.New(format).Funcs(TemplateFuncMap).Parse(format)
if err != nil {
return "", err
}
err = tmpl.Execute(&b, v)
return b.String(), err
}
func renderText(w io.Writer, p termenv.Profile, rules StylePrimitive, s string) {
if len(s) == 0 {
return
}
out := termenv.String(s)
if rules.Upper != nil && *rules.Upper {
out = termenv.String(strings.ToUpper(s))
}
if rules.Lower != nil && *rules.Lower {
out = termenv.String(strings.ToLower(s))
}
if rules.Title != nil && *rules.Title {
out = termenv.String(strings.Title(s))
}
if rules.Color != nil {
out = out.Foreground(p.Color(*rules.Color))
}
if rules.BackgroundColor != nil {
out = out.Background(p.Color(*rules.BackgroundColor))
}
if rules.Underline != nil && *rules.Underline {
out = out.Underline()
}
if rules.Bold != nil && *rules.Bold {
out = out.Bold()
}
if rules.Italic != nil && *rules.Italic {
out = out.Italic()
}
if rules.CrossedOut != nil && *rules.CrossedOut {
out = out.CrossOut()
}
if rules.Overlined != nil && *rules.Overlined {
out = out.Overline()
}
if rules.Inverse != nil && *rules.Inverse {
out = out.Reverse()
}
if rules.Blink != nil && *rules.Blink {
out = out.Blink()
}
_, _ = w.Write([]byte(out.String()))
}
func (e *BaseElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, e.Prefix)
defer func() {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, e.Suffix)
}()
rules := bs.With(e.Style)
// render unstyled prefix/suffix
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
defer func() {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
}()
// render styled prefix/suffix
renderText(w, ctx.options.ColorProfile, rules, rules.Prefix)
defer func() {
renderText(w, ctx.options.ColorProfile, rules, rules.Suffix)
}()
s := e.Token
if len(rules.Format) > 0 {
var err error
s, err = formatToken(rules.Format, s)
if err != nil {
return err
}
}
renderText(w, ctx.options.ColorProfile, rules, s)
return nil
}
glamour-0.6.0/ansi/blockelement.go 0000664 0000000 0000000 00000002676 14325445506 0017133 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"io"
"github.com/muesli/reflow/wordwrap"
)
// BlockElement provides a render buffer for children of a block element.
// After all children have been rendered into it, it applies indentation and
// margins around them and writes everything to the parent rendering buffer.
type BlockElement struct {
Block *bytes.Buffer
Style StyleBlock
Margin bool
Newline bool
}
func (e *BlockElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
bs.Push(*e)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, e.Style.BlockPrefix)
renderText(bs.Current().Block, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, e.Style.Prefix)
return nil
}
func (e *BlockElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
if e.Margin {
mw := NewMarginWriter(ctx, w, bs.Current().Style)
_, err := mw.Write(
wordwrap.Bytes(bs.Current().Block.Bytes(), int(bs.Width(ctx))))
if err != nil {
return err
}
if e.Newline {
_, err = mw.Write([]byte("\n"))
if err != nil {
return err
}
}
} else {
_, err := bs.Parent().Block.Write(bs.Current().Block.Bytes())
if err != nil {
return err
}
}
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, e.Style.Suffix)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, e.Style.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
glamour-0.6.0/ansi/blockstack.go 0000664 0000000 0000000 00000003572 14325445506 0016603 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
)
// BlockStack is a stack of block elements, used to calculate the current
// indentation & margin level during the rendering process.
type BlockStack []BlockElement
// Len returns the length of the stack.
func (s *BlockStack) Len() int {
return len(*s)
}
// Push appends an item to the stack.
func (s *BlockStack) Push(e BlockElement) {
*s = append(*s, e)
}
// Pop removes the last item on the stack.
func (s *BlockStack) Pop() {
stack := *s
if len(stack) == 0 {
return
}
stack = stack[0 : len(stack)-1]
*s = stack
}
// Indent returns the current indentation level of all elements in the stack.
func (s BlockStack) Indent() uint {
var i uint
for _, v := range s {
if v.Style.Indent == nil {
continue
}
i += *v.Style.Indent
}
return i
}
// Margin returns the current margin level of all elements in the stack.
func (s BlockStack) Margin() uint {
var i uint
for _, v := range s {
if v.Style.Margin == nil {
continue
}
i += *v.Style.Margin
}
return i
}
// Width returns the available rendering width
func (s BlockStack) Width(ctx RenderContext) uint {
if s.Indent()+s.Margin()*2 > uint(ctx.options.WordWrap) {
return 0
}
return uint(ctx.options.WordWrap) - s.Indent() - s.Margin()*2
}
// Parent returns the current BlockElement's parent.
func (s BlockStack) Parent() BlockElement {
if len(s) == 1 {
return BlockElement{
Block: &bytes.Buffer{},
}
}
return s[len(s)-2]
}
// Current returns the current BlockElement.
func (s BlockStack) Current() BlockElement {
if len(s) == 0 {
return BlockElement{
Block: &bytes.Buffer{},
}
}
return s[len(s)-1]
}
// With returns a StylePrimitive that inherits the current BlockElement's style.
func (s BlockStack) With(child StylePrimitive) StylePrimitive {
sb := StyleBlock{}
sb.StylePrimitive = child
return cascadeStyle(s.Current().Style, sb, false).StylePrimitive
}
glamour-0.6.0/ansi/codeblock.go 0000664 0000000 0000000 00000010746 14325445506 0016411 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"sync"
"github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/quick"
"github.com/alecthomas/chroma/styles"
"github.com/muesli/reflow/indent"
"github.com/muesli/termenv"
)
const (
// The chroma style theme name used for rendering.
chromaStyleTheme = "charm"
)
var (
// mutex for synchronizing access to the chroma style registry.
// Related https://github.com/alecthomas/chroma/pull/650
mutex = sync.Mutex{}
)
// A CodeBlockElement is used to render code blocks.
type CodeBlockElement struct {
Code string
Language string
}
func chromaStyle(style StylePrimitive) string {
var s string
if style.Color != nil {
s = *style.Color
}
if style.BackgroundColor != nil {
if s != "" {
s += " "
}
s += "bg:" + *style.BackgroundColor
}
if style.Italic != nil && *style.Italic {
if s != "" {
s += " "
}
s += "italic"
}
if style.Bold != nil && *style.Bold {
if s != "" {
s += " "
}
s += "bold"
}
if style.Underline != nil && *style.Underline {
if s != "" {
s += " "
}
s += "underline"
}
return s
}
func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
var indentation uint
var margin uint
rules := ctx.options.Styles.CodeBlock
if rules.Indent != nil {
indentation = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
theme := rules.Theme
if rules.Chroma != nil && ctx.options.ColorProfile != termenv.Ascii {
theme = chromaStyleTheme
mutex.Lock()
// Don't register the style if it's already registered.
_, ok := styles.Registry[theme]
if !ok {
styles.Register(chroma.MustNewStyle(theme,
chroma.StyleEntries{
chroma.Text: chromaStyle(rules.Chroma.Text),
chroma.Error: chromaStyle(rules.Chroma.Error),
chroma.Comment: chromaStyle(rules.Chroma.Comment),
chroma.CommentPreproc: chromaStyle(rules.Chroma.CommentPreproc),
chroma.Keyword: chromaStyle(rules.Chroma.Keyword),
chroma.KeywordReserved: chromaStyle(rules.Chroma.KeywordReserved),
chroma.KeywordNamespace: chromaStyle(rules.Chroma.KeywordNamespace),
chroma.KeywordType: chromaStyle(rules.Chroma.KeywordType),
chroma.Operator: chromaStyle(rules.Chroma.Operator),
chroma.Punctuation: chromaStyle(rules.Chroma.Punctuation),
chroma.Name: chromaStyle(rules.Chroma.Name),
chroma.NameBuiltin: chromaStyle(rules.Chroma.NameBuiltin),
chroma.NameTag: chromaStyle(rules.Chroma.NameTag),
chroma.NameAttribute: chromaStyle(rules.Chroma.NameAttribute),
chroma.NameClass: chromaStyle(rules.Chroma.NameClass),
chroma.NameConstant: chromaStyle(rules.Chroma.NameConstant),
chroma.NameDecorator: chromaStyle(rules.Chroma.NameDecorator),
chroma.NameException: chromaStyle(rules.Chroma.NameException),
chroma.NameFunction: chromaStyle(rules.Chroma.NameFunction),
chroma.NameOther: chromaStyle(rules.Chroma.NameOther),
chroma.Literal: chromaStyle(rules.Chroma.Literal),
chroma.LiteralNumber: chromaStyle(rules.Chroma.LiteralNumber),
chroma.LiteralDate: chromaStyle(rules.Chroma.LiteralDate),
chroma.LiteralString: chromaStyle(rules.Chroma.LiteralString),
chroma.LiteralStringEscape: chromaStyle(rules.Chroma.LiteralStringEscape),
chroma.GenericDeleted: chromaStyle(rules.Chroma.GenericDeleted),
chroma.GenericEmph: chromaStyle(rules.Chroma.GenericEmph),
chroma.GenericInserted: chromaStyle(rules.Chroma.GenericInserted),
chroma.GenericStrong: chromaStyle(rules.Chroma.GenericStrong),
chroma.GenericSubheading: chromaStyle(rules.Chroma.GenericSubheading),
chroma.Background: chromaStyle(rules.Chroma.Background),
}))
}
mutex.Unlock()
}
iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ")
})
if len(theme) > 0 {
renderText(iw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
err := quick.Highlight(iw, e.Code, e.Language, "terminal256", theme)
if err != nil {
return err
}
renderText(iw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
return nil
}
// fallback rendering
el := &BaseElement{
Token: e.Code,
Style: rules.StylePrimitive,
}
return el.Render(iw, ctx)
}
glamour-0.6.0/ansi/context.go 0000664 0000000 0000000 00000001367 14325445506 0016147 0 ustar 00root root 0000000 0000000 package ansi
import (
"html"
"strings"
"github.com/microcosm-cc/bluemonday"
)
// RenderContext holds the current rendering options and state.
type RenderContext struct {
options Options
blockStack *BlockStack
table *TableElement
stripper *bluemonday.Policy
}
// NewRenderContext returns a new RenderContext.
func NewRenderContext(options Options) RenderContext {
return RenderContext{
options: options,
blockStack: &BlockStack{},
table: &TableElement{},
stripper: bluemonday.StrictPolicy(),
}
}
// SanitizeHTML sanitizes HTML content.
func (ctx RenderContext) SanitizeHTML(s string, trimSpaces bool) string {
s = ctx.stripper.Sanitize(s)
if trimSpaces {
s = strings.TrimSpace(s)
}
return html.UnescapeString(s)
}
glamour-0.6.0/ansi/elements.go 0000664 0000000 0000000 00000020650 14325445506 0016273 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"fmt"
"html"
"io"
"strings"
east "github.com/yuin/goldmark-emoji/ast"
"github.com/yuin/goldmark/ast"
astext "github.com/yuin/goldmark/extension/ast"
)
// ElementRenderer is called when entering a markdown node.
type ElementRenderer interface {
Render(w io.Writer, ctx RenderContext) error
}
// ElementFinisher is called when leaving a markdown node.
type ElementFinisher interface {
Finish(w io.Writer, ctx RenderContext) error
}
// An Element is used to instruct the renderer how to handle individual markdown
// nodes.
type Element struct {
Entering string
Exiting string
Renderer ElementRenderer
Finisher ElementFinisher
}
// NewElement returns the appropriate render Element for a given node.
func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
ctx := tr.context
// fmt.Print(strings.Repeat(" ", ctx.blockStack.Len()), node.Type(), node.Kind())
// defer fmt.Println()
switch node.Kind() {
// Document
case ast.KindDocument:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: ctx.options.Styles.Document,
Margin: true,
}
return Element{
Renderer: e,
Finisher: e,
}
// Heading
case ast.KindHeading:
n := node.(*ast.Heading)
he := &HeadingElement{
Level: n.Level,
First: node.PreviousSibling() == nil,
}
return Element{
Exiting: "",
Renderer: he,
Finisher: he,
}
// Paragraph
case ast.KindParagraph:
if node.Parent() != nil && node.Parent().Kind() == ast.KindListItem {
return Element{}
}
return Element{
Renderer: &ParagraphElement{
First: node.PreviousSibling() == nil,
},
Finisher: &ParagraphElement{},
}
// Blockquote
case ast.KindBlockquote:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.BlockQuote, false),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
// Lists
case ast.KindList:
s := ctx.options.Styles.List.StyleBlock
if s.Indent == nil {
var i uint
s.Indent = &i
}
n := node.Parent()
for n != nil {
if n.Kind() == ast.KindList {
i := ctx.options.Styles.List.LevelIndent
s.Indent = &i
break
}
n = n.Parent()
}
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, s, false),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
case ast.KindListItem:
var l uint
var e uint
l = 1
n := node
for n.PreviousSibling() != nil && (n.PreviousSibling().Kind() == ast.KindListItem) {
l++
n = n.PreviousSibling()
}
if node.Parent().(*ast.List).IsOrdered() {
e = l
if node.Parent().(*ast.List).Start != 1 {
e += uint(node.Parent().(*ast.List).Start) - 1
}
}
post := "\n"
if (node.LastChild() != nil && node.LastChild().Kind() == ast.KindList) ||
node.NextSibling() == nil {
post = ""
}
if node.FirstChild() != nil &&
node.FirstChild().FirstChild() != nil &&
node.FirstChild().FirstChild().Kind() == astext.KindTaskCheckBox {
nc := node.FirstChild().FirstChild().(*astext.TaskCheckBox)
return Element{
Exiting: post,
Renderer: &TaskElement{
Checked: nc.IsChecked,
},
}
}
return Element{
Exiting: post,
Renderer: &ItemElement{
IsOrdered: node.Parent().(*ast.List).IsOrdered(),
Enumeration: e,
},
}
// Text Elements
case ast.KindText:
n := node.(*ast.Text)
s := string(n.Segment.Value(source))
if n.HardLineBreak() || (n.SoftLineBreak()) {
s += "\n"
}
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(s),
Style: ctx.options.Styles.Text,
},
}
case ast.KindEmphasis:
n := node.(*ast.Emphasis)
s := string(n.Text(source))
style := ctx.options.Styles.Emph
if n.Level > 1 {
style = ctx.options.Styles.Strong
}
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(s),
Style: style,
},
}
case astext.KindStrikethrough:
n := node.(*astext.Strikethrough)
s := string(n.Text(source))
style := ctx.options.Styles.Strikethrough
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(s),
Style: style,
},
}
case ast.KindThematicBreak:
return Element{
Entering: "",
Exiting: "",
Renderer: &BaseElement{
Style: ctx.options.Styles.HorizontalRule,
},
}
// Links
case ast.KindLink:
n := node.(*ast.Link)
return Element{
Renderer: &LinkElement{
Text: textFromChildren(node, source),
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
},
}
case ast.KindAutoLink:
n := node.(*ast.AutoLink)
u := string(n.URL(source))
label := string(n.Label(source))
if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(u), "mailto:") {
u = "mailto:" + u
}
return Element{
Renderer: &LinkElement{
Text: label,
BaseURL: ctx.options.BaseURL,
URL: u,
},
}
// Images
case ast.KindImage:
n := node.(*ast.Image)
text := string(n.Text(source))
return Element{
Renderer: &ImageElement{
Text: text,
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
},
}
// Code
case ast.KindFencedCodeBlock:
n := node.(*ast.FencedCodeBlock)
l := n.Lines().Len()
s := ""
for i := 0; i < l; i++ {
line := n.Lines().At(i)
s += string(line.Value(source))
}
return Element{
Entering: "\n",
Renderer: &CodeBlockElement{
Code: s,
Language: string(n.Language(source)),
},
}
case ast.KindCodeBlock:
n := node.(*ast.CodeBlock)
l := n.Lines().Len()
s := ""
for i := 0; i < l; i++ {
line := n.Lines().At(i)
s += string(line.Value(source))
}
return Element{
Entering: "\n",
Renderer: &CodeBlockElement{
Code: s,
},
}
case ast.KindCodeSpan:
// n := node.(*ast.CodeSpan)
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.Code, false),
}
return Element{
Renderer: e,
Finisher: e,
}
// Tables
case astext.KindTable:
te := &TableElement{}
return Element{
Entering: "\n",
Renderer: te,
Finisher: te,
}
case astext.KindTableCell:
s := ""
n := node.FirstChild()
for n != nil {
s += string(n.Text(source))
// s += string(n.LinkData.Destination)
n = n.NextSibling()
}
return Element{
Renderer: &TableCellElement{
Text: s,
Head: node.Parent().Kind() == astext.KindTableHeader,
},
}
case astext.KindTableHeader:
return Element{
Finisher: &TableHeadElement{},
}
case astext.KindTableRow:
return Element{
Finisher: &TableRowElement{},
}
// HTML Elements
case ast.KindHTMLBlock:
n := node.(*ast.HTMLBlock)
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(string(n.Text(source)), true),
Style: ctx.options.Styles.HTMLBlock.StylePrimitive,
},
}
case ast.KindRawHTML:
n := node.(*ast.RawHTML)
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(string(n.Text(source)), true),
Style: ctx.options.Styles.HTMLSpan.StylePrimitive,
},
}
// Definition Lists
case astext.KindDefinitionList:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.DefinitionList, false),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
case astext.KindDefinitionTerm:
return Element{
Renderer: &BaseElement{
Style: ctx.options.Styles.DefinitionTerm,
},
}
case astext.KindDefinitionDescription:
return Element{
Renderer: &BaseElement{
Style: ctx.options.Styles.DefinitionDescription,
},
}
// Handled by parents
case astext.KindTaskCheckBox:
// handled by KindListItem
return Element{}
case ast.KindTextBlock:
return Element{}
case east.KindEmoji:
n := node.(*east.Emoji)
return Element{
Renderer: &BaseElement{
Token: string(n.Value.Unicode),
},
}
// Unknown case
default:
fmt.Println("Warning: unhandled element", node.Kind().String())
return Element{}
}
}
func textFromChildren(node ast.Node, source []byte) string {
var s string
for c := node.FirstChild(); c != nil; c = c.NextSibling() {
if c.Kind() == ast.KindText {
cn := c.(*ast.Text)
s += string(cn.Segment.Value(source))
if cn.HardLineBreak() || (cn.SoftLineBreak()) {
s += "\n"
}
} else {
s += string(c.Text(source))
}
}
return s
}
glamour-0.6.0/ansi/heading.go 0000664 0000000 0000000 00000004126 14325445506 0016056 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"io"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/wordwrap"
)
// A HeadingElement is used to render headings.
type HeadingElement struct {
Level int
First bool
}
func (e *HeadingElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := ctx.options.Styles.Heading
switch e.Level {
case 1:
rules = cascadeStyles(true, rules, ctx.options.Styles.H1)
case 2:
rules = cascadeStyles(true, rules, ctx.options.Styles.H2)
case 3:
rules = cascadeStyles(true, rules, ctx.options.Styles.H3)
case 4:
rules = cascadeStyles(true, rules, ctx.options.Styles.H4)
case 5:
rules = cascadeStyles(true, rules, ctx.options.Styles.H5)
case 6:
rules = cascadeStyles(true, rules, ctx.options.Styles.H6)
}
if !e.First {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, "\n")
}
be := BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(bs.Current().Style, rules, false),
}
bs.Push(be)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, rules.BlockPrefix)
renderText(bs.Current().Block, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.Prefix)
return nil
}
func (e *HeadingElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := bs.Current().Style
var indentation uint
var margin uint
if rules.Indent != nil {
indentation = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, " ")
})
flow := wordwrap.NewWriter(int(bs.Width(ctx) - indentation - margin*2))
_, err := flow.Write(bs.Current().Block.Bytes())
if err != nil {
return err
}
flow.Close()
_, err = iw.Write(flow.Bytes())
if err != nil {
return err
}
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.Suffix)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, rules.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
glamour-0.6.0/ansi/image.go 0000664 0000000 0000000 00000001236 14325445506 0015540 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
)
// An ImageElement is used to render images elements.
type ImageElement struct {
Text string
BaseURL string
URL string
Child ElementRenderer // FIXME
}
func (e *ImageElement) Render(w io.Writer, ctx RenderContext) error {
if len(e.Text) > 0 {
el := &BaseElement{
Token: e.Text,
Style: ctx.options.Styles.ImageText,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
if len(e.URL) > 0 {
el := &BaseElement{
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: " ",
Style: ctx.options.Styles.Image,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
return nil
}
glamour-0.6.0/ansi/link.go 0000664 0000000 0000000 00000002760 14325445506 0015416 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"net/url"
)
// A LinkElement is used to render hyperlinks.
type LinkElement struct {
Text string
BaseURL string
URL string
Child ElementRenderer // FIXME
}
func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
var textRendered bool
if len(e.Text) > 0 && e.Text != e.URL {
textRendered = true
el := &BaseElement{
Token: e.Text,
Style: ctx.options.Styles.LinkText,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
/*
if node.LastChild != nil {
if node.LastChild.Type == bf.Image {
el := tr.NewElement(node.LastChild)
err := el.Renderer.Render(w, node.LastChild, tr)
if err != nil {
return err
}
}
if len(node.LastChild.Literal) > 0 &&
string(node.LastChild.Literal) != string(node.LinkData.Destination) {
textRendered = true
el := &BaseElement{
Token: string(node.LastChild.Literal),
Style: ctx.style[LinkText],
}
err := el.Render(w, node.LastChild, tr)
if err != nil {
return err
}
}
}
*/
u, err := url.Parse(e.URL)
if err == nil &&
"#"+u.Fragment != e.URL { // if the URL only consists of an anchor, ignore it
pre := " "
style := ctx.options.Styles.Link
if !textRendered {
pre = ""
style.BlockPrefix = ""
style.BlockSuffix = ""
}
el := &BaseElement{
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: pre,
Style: style,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
return nil
}
glamour-0.6.0/ansi/listitem.go 0000664 0000000 0000000 00000000763 14325445506 0016314 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"strconv"
)
// An ItemElement is used to render items inside a list.
type ItemElement struct {
IsOrdered bool
Enumeration uint
}
func (e *ItemElement) Render(w io.Writer, ctx RenderContext) error {
var el *BaseElement
if e.IsOrdered {
el = &BaseElement{
Style: ctx.options.Styles.Enumeration,
Prefix: strconv.FormatInt(int64(e.Enumeration), 10),
}
} else {
el = &BaseElement{
Style: ctx.options.Styles.Item,
}
}
return el.Render(w, ctx)
}
glamour-0.6.0/ansi/margin.go 0000664 0000000 0000000 00000002121 14325445506 0015725 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/padding"
)
// MarginWriter is a Writer that applies indentation and padding around
// whatever you write to it.
type MarginWriter struct {
w io.Writer
pw *padding.Writer
iw *indent.Writer
}
// NewMarginWriter returns a new MarginWriter.
func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter {
bs := ctx.blockStack
var indentation uint
var margin uint
if rules.Indent != nil {
indentation = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
pw := padding.NewWriterPipe(w, bs.Width(ctx), func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, rules.StylePrimitive, " ")
})
ic := " "
if rules.IndentToken != nil {
ic = *rules.IndentToken
}
iw := indent.NewWriterPipe(pw, indentation+margin, func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, ic)
})
return &MarginWriter{
w: w,
pw: pw,
iw: iw,
}
}
func (w *MarginWriter) Write(b []byte) (int, error) {
return w.iw.Write(b)
}
glamour-0.6.0/ansi/paragraph.go 0000664 0000000 0000000 00000002657 14325445506 0016433 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"io"
"strings"
"github.com/muesli/reflow/wordwrap"
)
// A ParagraphElement is used to render individual paragraphs.
type ParagraphElement struct {
First bool
}
func (e *ParagraphElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := ctx.options.Styles.Paragraph
if !e.First {
_, _ = w.Write([]byte("\n"))
}
be := BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(bs.Current().Style, rules, false),
}
bs.Push(be)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, rules.BlockPrefix)
renderText(bs.Current().Block, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.Prefix)
return nil
}
func (e *ParagraphElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := bs.Current().Style
mw := NewMarginWriter(ctx, w, rules)
if len(strings.TrimSpace(bs.Current().Block.String())) > 0 {
flow := wordwrap.NewWriter(int(bs.Width(ctx)))
flow.KeepNewlines = ctx.options.PreserveNewLines
_, _ = flow.Write(bs.Current().Block.Bytes())
flow.Close()
_, err := mw.Write(flow.Bytes())
if err != nil {
return err
}
_, _ = mw.Write([]byte("\n"))
}
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.Suffix)
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, rules.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
glamour-0.6.0/ansi/renderer.go 0000664 0000000 0000000 00000011010 14325445506 0016253 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"net/url"
"strings"
"github.com/muesli/termenv"
east "github.com/yuin/goldmark-emoji/ast"
"github.com/yuin/goldmark/ast"
astext "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// Options is used to configure an ANSIRenderer.
type Options struct {
BaseURL string
WordWrap int
PreserveNewLines bool
ColorProfile termenv.Profile
Styles StyleConfig
}
// ANSIRenderer renders markdown content as ANSI escaped sequences.
type ANSIRenderer struct {
context RenderContext
}
// NewRenderer returns a new ANSIRenderer with style and options set.
func NewRenderer(options Options) *ANSIRenderer {
return &ANSIRenderer{
context: NewRenderContext(options),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
reg.Register(ast.KindDocument, r.renderNode)
reg.Register(ast.KindHeading, r.renderNode)
reg.Register(ast.KindBlockquote, r.renderNode)
reg.Register(ast.KindCodeBlock, r.renderNode)
reg.Register(ast.KindFencedCodeBlock, r.renderNode)
reg.Register(ast.KindHTMLBlock, r.renderNode)
reg.Register(ast.KindList, r.renderNode)
reg.Register(ast.KindListItem, r.renderNode)
reg.Register(ast.KindParagraph, r.renderNode)
reg.Register(ast.KindTextBlock, r.renderNode)
reg.Register(ast.KindThematicBreak, r.renderNode)
// inlines
reg.Register(ast.KindAutoLink, r.renderNode)
reg.Register(ast.KindCodeSpan, r.renderNode)
reg.Register(ast.KindEmphasis, r.renderNode)
reg.Register(ast.KindImage, r.renderNode)
reg.Register(ast.KindLink, r.renderNode)
reg.Register(ast.KindRawHTML, r.renderNode)
reg.Register(ast.KindText, r.renderNode)
reg.Register(ast.KindString, r.renderNode)
// tables
reg.Register(astext.KindTable, r.renderNode)
reg.Register(astext.KindTableHeader, r.renderNode)
reg.Register(astext.KindTableRow, r.renderNode)
reg.Register(astext.KindTableCell, r.renderNode)
// definitions
reg.Register(astext.KindDefinitionList, r.renderNode)
reg.Register(astext.KindDefinitionTerm, r.renderNode)
reg.Register(astext.KindDefinitionDescription, r.renderNode)
// footnotes
reg.Register(astext.KindFootnote, r.renderNode)
reg.Register(astext.KindFootnoteList, r.renderNode)
reg.Register(astext.KindFootnoteLink, r.renderNode)
reg.Register(astext.KindFootnoteBacklink, r.renderNode)
// checkboxes
reg.Register(astext.KindTaskCheckBox, r.renderNode)
// strikethrough
reg.Register(astext.KindStrikethrough, r.renderNode)
// emoji
reg.Register(east.KindEmoji, r.renderNode)
}
func (r *ANSIRenderer) renderNode(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// _, _ = w.Write([]byte(node.Type.String()))
writeTo := io.Writer(w)
bs := r.context.blockStack
// children get rendered by their parent
if isChild(node) {
return ast.WalkContinue, nil
}
e := r.NewElement(node, source)
if entering {
// everything below the Document element gets rendered into a block buffer
if bs.Len() > 0 {
writeTo = io.Writer(bs.Current().Block)
}
_, _ = writeTo.Write([]byte(e.Entering))
if e.Renderer != nil {
err := e.Renderer.Render(writeTo, r.context)
if err != nil {
return ast.WalkStop, err
}
}
} else {
// everything below the Document element gets rendered into a block buffer
if bs.Len() > 0 {
writeTo = io.Writer(bs.Parent().Block)
}
// if we're finished rendering the entire document,
// flush to the real writer
if node.Type() == ast.TypeDocument {
writeTo = w
}
if e.Finisher != nil {
err := e.Finisher.Finish(writeTo, r.context)
if err != nil {
return ast.WalkStop, err
}
}
_, _ = bs.Current().Block.Write([]byte(e.Exiting))
}
return ast.WalkContinue, nil
}
func isChild(node ast.Node) bool {
if node.Parent() != nil && node.Parent().Kind() == ast.KindBlockquote {
// skip paragraph within blockquote to avoid reflowing text
return true
}
for n := node.Parent(); n != nil; n = n.Parent() {
// These types are already rendered by their parent
switch n.Kind() {
case ast.KindLink, ast.KindImage, ast.KindEmphasis, astext.KindStrikethrough, astext.KindTableCell:
return true
}
}
return false
}
func resolveRelativeURL(baseURL string, rel string) string {
u, err := url.Parse(rel)
if err != nil {
return rel
}
if u.IsAbs() {
return rel
}
u.Path = strings.TrimPrefix(u.Path, "/")
base, err := url.Parse(baseURL)
if err != nil {
return rel
}
return base.ResolveReference(u).String()
}
glamour-0.6.0/ansi/renderer_test.go 0000664 0000000 0000000 00000006477 14325445506 0017337 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/muesli/termenv"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
const (
generateExamples = false
generateIssues = false
examplesDir = "../styles/examples/"
issuesDir = "../testdata/issues/"
)
func TestRenderer(t *testing.T) {
files, err := filepath.Glob(examplesDir + "*.md")
if err != nil {
t.Fatal(err)
}
for _, f := range files {
bn := strings.TrimSuffix(filepath.Base(f), ".md")
sn := filepath.Join(examplesDir, bn+".style")
tn := filepath.Join("../testdata", bn+".test")
in, err := ioutil.ReadFile(f)
if err != nil {
t.Fatal(err)
}
b, err := ioutil.ReadFile(sn)
if err != nil {
t.Fatal(err)
}
options := Options{
WordWrap: 80,
ColorProfile: termenv.TrueColor,
}
err = json.Unmarshal(b, &options.Styles)
if err != nil {
t.Fatal(err)
}
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.DefinitionList,
emoji.Emoji,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
)
ar := NewRenderer(options)
md.SetRenderer(
renderer.NewRenderer(
renderer.WithNodeRenderers(util.Prioritized(ar, 1000))))
var buf bytes.Buffer
err = md.Convert(in, &buf)
if err != nil {
t.Error(err)
}
// generate
if generateExamples {
err = ioutil.WriteFile(tn, buf.Bytes(), 0644)
if err != nil {
t.Fatal(err)
}
continue
}
// verify
td, err := ioutil.ReadFile(tn)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(td, buf.Bytes()) {
t.Errorf("Rendered output for %s doesn't match!\nExpected: `\n%s`\nGot: `\n%s`\n",
bn, string(td), buf.String())
}
}
}
func TestRendererIssues(t *testing.T) {
files, err := filepath.Glob(issuesDir + "*.md")
if err != nil {
t.Fatal(err)
}
for _, f := range files {
bn := strings.TrimSuffix(filepath.Base(f), ".md")
t.Run(bn, func(t *testing.T) {
tn := filepath.Join(issuesDir, bn+".test")
in, err := ioutil.ReadFile(f)
if err != nil {
t.Fatal(err)
}
b, err := ioutil.ReadFile("../styles/dark.json")
if err != nil {
t.Fatal(err)
}
options := Options{
WordWrap: 80,
ColorProfile: termenv.TrueColor,
}
err = json.Unmarshal(b, &options.Styles)
if err != nil {
t.Fatal(err)
}
md := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.DefinitionList,
emoji.Emoji,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
)
ar := NewRenderer(options)
md.SetRenderer(
renderer.NewRenderer(
renderer.WithNodeRenderers(util.Prioritized(ar, 1000))))
var buf bytes.Buffer
err = md.Convert(in, &buf)
if err != nil {
t.Error(err)
}
// generate
if generateIssues {
err = ioutil.WriteFile(tn, buf.Bytes(), 0644)
if err != nil {
t.Fatal(err)
}
return
}
// verify
td, err := ioutil.ReadFile(tn)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(td, buf.Bytes()) {
t.Errorf("Rendered output for %s doesn't match!\nExpected: `\n%s`\nGot: `\n%s`\n",
bn, string(td), buf.String())
}
})
}
}
glamour-0.6.0/ansi/style.go 0000664 0000000 0000000 00000017703 14325445506 0015624 0 ustar 00root root 0000000 0000000 package ansi
// Chroma holds all the chroma settings.
type Chroma struct {
Text StylePrimitive `json:"text,omitempty"`
Error StylePrimitive `json:"error,omitempty"`
Comment StylePrimitive `json:"comment,omitempty"`
CommentPreproc StylePrimitive `json:"comment_preproc,omitempty"`
Keyword StylePrimitive `json:"keyword,omitempty"`
KeywordReserved StylePrimitive `json:"keyword_reserved,omitempty"`
KeywordNamespace StylePrimitive `json:"keyword_namespace,omitempty"`
KeywordType StylePrimitive `json:"keyword_type,omitempty"`
Operator StylePrimitive `json:"operator,omitempty"`
Punctuation StylePrimitive `json:"punctuation,omitempty"`
Name StylePrimitive `json:"name,omitempty"`
NameBuiltin StylePrimitive `json:"name_builtin,omitempty"`
NameTag StylePrimitive `json:"name_tag,omitempty"`
NameAttribute StylePrimitive `json:"name_attribute,omitempty"`
NameClass StylePrimitive `json:"name_class,omitempty"`
NameConstant StylePrimitive `json:"name_constant,omitempty"`
NameDecorator StylePrimitive `json:"name_decorator,omitempty"`
NameException StylePrimitive `json:"name_exception,omitempty"`
NameFunction StylePrimitive `json:"name_function,omitempty"`
NameOther StylePrimitive `json:"name_other,omitempty"`
Literal StylePrimitive `json:"literal,omitempty"`
LiteralNumber StylePrimitive `json:"literal_number,omitempty"`
LiteralDate StylePrimitive `json:"literal_date,omitempty"`
LiteralString StylePrimitive `json:"literal_string,omitempty"`
LiteralStringEscape StylePrimitive `json:"literal_string_escape,omitempty"`
GenericDeleted StylePrimitive `json:"generic_deleted,omitempty"`
GenericEmph StylePrimitive `json:"generic_emph,omitempty"`
GenericInserted StylePrimitive `json:"generic_inserted,omitempty"`
GenericStrong StylePrimitive `json:"generic_strong,omitempty"`
GenericSubheading StylePrimitive `json:"generic_subheading,omitempty"`
Background StylePrimitive `json:"background,omitempty"`
}
// StylePrimitive holds all the basic style settings.
type StylePrimitive struct {
BlockPrefix string `json:"block_prefix,omitempty"`
BlockSuffix string `json:"block_suffix,omitempty"`
Prefix string `json:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty"`
Color *string `json:"color,omitempty"`
BackgroundColor *string `json:"background_color,omitempty"`
Underline *bool `json:"underline,omitempty"`
Bold *bool `json:"bold,omitempty"`
Upper *bool `json:"upper,omitempty"`
Lower *bool `json:"lower,omitempty"`
Title *bool `json:"title,omitempty"`
Italic *bool `json:"italic,omitempty"`
CrossedOut *bool `json:"crossed_out,omitempty"`
Faint *bool `json:"faint,omitempty"`
Conceal *bool `json:"conceal,omitempty"`
Overlined *bool `json:"overlined,omitempty"`
Inverse *bool `json:"inverse,omitempty"`
Blink *bool `json:"blink,omitempty"`
Format string `json:"format,omitempty"`
}
// StyleTask holds the style settings for a task item.
type StyleTask struct {
StylePrimitive
Ticked string `json:"ticked,omitempty"`
Unticked string `json:"unticked,omitempty"`
}
// StyleBlock holds the basic style settings for block elements.
type StyleBlock struct {
StylePrimitive
Indent *uint `json:"indent,omitempty"`
IndentToken *string `json:"indent_token,omitempty"`
Margin *uint `json:"margin,omitempty"`
}
// StyleCodeBlock holds the style settings for a code block.
type StyleCodeBlock struct {
StyleBlock
Theme string `json:"theme,omitempty"`
Chroma *Chroma `json:"chroma,omitempty"`
}
// StyleList holds the style settings for a list.
type StyleList struct {
StyleBlock
LevelIndent uint `json:"level_indent,omitempty"`
}
// StyleTable holds the style settings for a table.
type StyleTable struct {
StyleBlock
CenterSeparator *string `json:"center_separator,omitempty"`
ColumnSeparator *string `json:"column_separator,omitempty"`
RowSeparator *string `json:"row_separator,omitempty"`
}
// StyleConfig is used to configure the styling behavior of an ANSIRenderer.
type StyleConfig struct {
Document StyleBlock `json:"document,omitempty"`
BlockQuote StyleBlock `json:"block_quote,omitempty"`
Paragraph StyleBlock `json:"paragraph,omitempty"`
List StyleList `json:"list,omitempty"`
Heading StyleBlock `json:"heading,omitempty"`
H1 StyleBlock `json:"h1,omitempty"`
H2 StyleBlock `json:"h2,omitempty"`
H3 StyleBlock `json:"h3,omitempty"`
H4 StyleBlock `json:"h4,omitempty"`
H5 StyleBlock `json:"h5,omitempty"`
H6 StyleBlock `json:"h6,omitempty"`
Text StylePrimitive `json:"text,omitempty"`
Strikethrough StylePrimitive `json:"strikethrough,omitempty"`
Emph StylePrimitive `json:"emph,omitempty"`
Strong StylePrimitive `json:"strong,omitempty"`
HorizontalRule StylePrimitive `json:"hr,omitempty"`
Item StylePrimitive `json:"item,omitempty"`
Enumeration StylePrimitive `json:"enumeration,omitempty"`
Task StyleTask `json:"task,omitempty"`
Link StylePrimitive `json:"link,omitempty"`
LinkText StylePrimitive `json:"link_text,omitempty"`
Image StylePrimitive `json:"image,omitempty"`
ImageText StylePrimitive `json:"image_text,omitempty"`
Code StyleBlock `json:"code,omitempty"`
CodeBlock StyleCodeBlock `json:"code_block,omitempty"`
Table StyleTable `json:"table,omitempty"`
DefinitionList StyleBlock `json:"definition_list,omitempty"`
DefinitionTerm StylePrimitive `json:"definition_term,omitempty"`
DefinitionDescription StylePrimitive `json:"definition_description,omitempty"`
HTMLBlock StyleBlock `json:"html_block,omitempty"`
HTMLSpan StyleBlock `json:"html_span,omitempty"`
}
func cascadeStyles(toBlock bool, s ...StyleBlock) StyleBlock {
var r StyleBlock
for _, v := range s {
r = cascadeStyle(r, v, toBlock)
}
return r
}
func cascadeStyle(parent StyleBlock, child StyleBlock, toBlock bool) StyleBlock {
s := child
s.Color = parent.Color
s.BackgroundColor = parent.BackgroundColor
s.Underline = parent.Underline
s.Bold = parent.Bold
s.Upper = parent.Upper
s.Title = parent.Title
s.Lower = parent.Lower
s.Italic = parent.Italic
s.CrossedOut = parent.CrossedOut
s.Faint = parent.Faint
s.Conceal = parent.Conceal
s.Overlined = parent.Overlined
s.Inverse = parent.Inverse
s.Blink = parent.Blink
if toBlock {
s.Indent = parent.Indent
s.Margin = parent.Margin
s.BlockPrefix = parent.BlockPrefix
s.BlockSuffix = parent.BlockSuffix
s.Prefix = parent.Prefix
s.Suffix = parent.Suffix
}
if child.Color != nil {
s.Color = child.Color
}
if child.BackgroundColor != nil {
s.BackgroundColor = child.BackgroundColor
}
if child.Indent != nil {
s.Indent = child.Indent
}
if child.Margin != nil {
s.Margin = child.Margin
}
if child.Underline != nil {
s.Underline = child.Underline
}
if child.Bold != nil {
s.Bold = child.Bold
}
if child.Upper != nil {
s.Upper = child.Upper
}
if child.Lower != nil {
s.Lower = child.Lower
}
if child.Title != nil {
s.Title = child.Title
}
if child.Italic != nil {
s.Italic = child.Italic
}
if child.CrossedOut != nil {
s.CrossedOut = child.CrossedOut
}
if child.Faint != nil {
s.Faint = child.Faint
}
if child.Conceal != nil {
s.Conceal = child.Conceal
}
if child.Overlined != nil {
s.Overlined = child.Overlined
}
if child.Inverse != nil {
s.Inverse = child.Inverse
}
if child.Blink != nil {
s.Blink = child.Blink
}
if child.BlockPrefix != "" {
s.BlockPrefix = child.BlockPrefix
}
if child.BlockSuffix != "" {
s.BlockSuffix = child.BlockSuffix
}
if child.Prefix != "" {
s.Prefix = child.Prefix
}
if child.Suffix != "" {
s.Suffix = child.Suffix
}
if child.Format != "" {
s.Format = child.Format
}
return s
}
glamour-0.6.0/ansi/stylewriter.go 0000664 0000000 0000000 00000001277 14325445506 0017060 0 ustar 00root root 0000000 0000000 package ansi
import (
"bytes"
"io"
)
// StyleWriter is a Writer that applies styling on whatever you write to it.
type StyleWriter struct {
ctx RenderContext
w io.Writer
buf bytes.Buffer
rules StylePrimitive
}
// NewStyleWriter returns a new StyleWriter.
func NewStyleWriter(ctx RenderContext, w io.Writer, rules StylePrimitive) *StyleWriter {
return &StyleWriter{
ctx: ctx,
w: w,
rules: rules,
}
}
func (w *StyleWriter) Write(b []byte) (int, error) {
return w.buf.Write(b)
}
// Close must be called when you're finished writing to a StyleWriter.
func (w *StyleWriter) Close() error {
renderText(w.w, w.ctx.options.ColorProfile, w.rules, w.buf.String())
return nil
}
glamour-0.6.0/ansi/table.go 0000664 0000000 0000000 00000005464 14325445506 0015554 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
"github.com/muesli/reflow/indent"
"github.com/olekukonko/tablewriter"
)
// A TableElement is used to render tables.
type TableElement struct {
writer *tablewriter.Table
styleWriter *StyleWriter
header []string
cell []string
}
// A TableRowElement is used to render a single row in a table.
type TableRowElement struct {
}
// A TableHeadElement is used to render a table's head element.
type TableHeadElement struct {
}
// A TableCellElement is used to render a single cell in a row.
type TableCellElement struct {
Text string
Head bool
}
func (e *TableElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
var indentation uint
var margin uint
rules := ctx.options.Styles.Table
if rules.Indent != nil {
indentation = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) {
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ")
})
style := bs.With(rules.StylePrimitive)
ctx.table.styleWriter = NewStyleWriter(ctx, iw, style)
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
renderText(ctx.table.styleWriter, ctx.options.ColorProfile, style, rules.Prefix)
ctx.table.writer = tablewriter.NewWriter(ctx.table.styleWriter)
return nil
}
func (e *TableElement) Finish(w io.Writer, ctx RenderContext) error {
rules := ctx.options.Styles.Table
ctx.table.writer.SetBorders(tablewriter.Border{Left: false, Top: false, Right: false, Bottom: false})
if rules.CenterSeparator != nil {
ctx.table.writer.SetCenterSeparator(*rules.CenterSeparator)
}
if rules.ColumnSeparator != nil {
ctx.table.writer.SetColumnSeparator(*rules.ColumnSeparator)
}
if rules.RowSeparator != nil {
ctx.table.writer.SetRowSeparator(*rules.RowSeparator)
}
ctx.table.writer.Render()
ctx.table.writer = nil
renderText(ctx.table.styleWriter, ctx.options.ColorProfile, ctx.blockStack.With(rules.StylePrimitive), rules.Suffix)
renderText(ctx.table.styleWriter, ctx.options.ColorProfile, ctx.blockStack.Current().Style.StylePrimitive, rules.BlockSuffix)
return ctx.table.styleWriter.Close()
}
func (e *TableRowElement) Finish(w io.Writer, ctx RenderContext) error {
if ctx.table.writer == nil {
return nil
}
ctx.table.writer.Append(ctx.table.cell)
ctx.table.cell = []string{}
return nil
}
func (e *TableHeadElement) Finish(w io.Writer, ctx RenderContext) error {
if ctx.table.writer == nil {
return nil
}
ctx.table.writer.SetHeader(ctx.table.header)
ctx.table.header = []string{}
return nil
}
func (e *TableCellElement) Render(w io.Writer, ctx RenderContext) error {
if e.Head {
ctx.table.header = append(ctx.table.header, e.Text)
} else {
ctx.table.cell = append(ctx.table.cell, e.Text)
}
return nil
}
glamour-0.6.0/ansi/task.go 0000664 0000000 0000000 00000000674 14325445506 0015425 0 ustar 00root root 0000000 0000000 package ansi
import (
"io"
)
// A TaskElement is used to render tasks inside a todo-list.
type TaskElement struct {
Checked bool
}
func (e *TaskElement) Render(w io.Writer, ctx RenderContext) error {
var el *BaseElement
pre := ctx.options.Styles.Task.Unticked
if e.Checked {
pre = ctx.options.Styles.Task.Ticked
}
el = &BaseElement{
Prefix: pre,
Style: ctx.options.Styles.Task.StylePrimitive,
}
return el.Render(w, ctx)
}
glamour-0.6.0/ansi/templatehelper.go 0000664 0000000 0000000 00000003776 14325445506 0017504 0 ustar 00root root 0000000 0000000 package ansi
import (
"regexp"
"strings"
"text/template"
)
// TemplateFuncMap contains a few useful template helpers
var (
TemplateFuncMap = template.FuncMap{
"Left": func(values ...interface{}) string {
s := values[0].(string)
n := values[1].(int)
if n > len(s) {
n = len(s)
}
return s[:n]
},
"Matches": func(values ...interface{}) bool {
ok, _ := regexp.MatchString(values[1].(string), values[0].(string))
return ok
},
"Mid": func(values ...interface{}) string {
s := values[0].(string)
l := values[1].(int)
if l > len(s) {
l = len(s)
}
if len(values) > 2 {
r := values[2].(int)
if r > len(s) {
r = len(s)
}
return s[l:r]
}
return s[l:]
},
"Right": func(values ...interface{}) string {
s := values[0].(string)
n := len(s) - values[1].(int)
if n < 0 {
n = 0
}
return s[n:]
},
"Last": func(values ...interface{}) string {
return values[0].([]string)[len(values[0].([]string))-1]
},
// strings functions
"Compare": strings.Compare, // 1.5+ only
"Contains": strings.Contains,
"ContainsAny": strings.ContainsAny,
"Count": strings.Count,
"EqualFold": strings.EqualFold,
"HasPrefix": strings.HasPrefix,
"HasSuffix": strings.HasSuffix,
"Index": strings.Index,
"IndexAny": strings.IndexAny,
"Join": strings.Join,
"LastIndex": strings.LastIndex,
"LastIndexAny": strings.LastIndexAny,
"Repeat": strings.Repeat,
"Replace": strings.Replace,
"Split": strings.Split,
"SplitAfter": strings.SplitAfter,
"SplitAfterN": strings.SplitAfterN,
"SplitN": strings.SplitN,
"Title": strings.Title,
"ToLower": strings.ToLower,
"ToTitle": strings.ToTitle,
"ToUpper": strings.ToUpper,
"Trim": strings.Trim,
"TrimLeft": strings.TrimLeft,
"TrimPrefix": strings.TrimPrefix,
"TrimRight": strings.TrimRight,
"TrimSpace": strings.TrimSpace,
"TrimSuffix": strings.TrimSuffix,
}
)
glamour-0.6.0/dracula.go 0000664 0000000 0000000 00000011720 14325445506 0015136 0 ustar 00root root 0000000 0000000 package glamour
import "github.com/charmbracelet/glamour/ansi"
var DraculaStyleConfig = ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockPrefix: "\n",
BlockSuffix: "\n",
Color: stringPtr("#f8f8f2"),
},
Margin: uintPtr(2),
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr("#f1fa8c"),
Italic: boolPtr(true),
},
Indent: uintPtr(2),
},
List: ansi.StyleList{
LevelIndent: 2,
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr("#f8f8f2"),
},
},
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
Color: stringPtr("#bd93f9"),
Bold: boolPtr(true),
},
},
H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "# ",
},
},
H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "## ",
},
},
H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "### ",
},
},
H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "#### ",
},
},
H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "##### ",
},
},
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ",
},
},
Strikethrough: ansi.StylePrimitive{
CrossedOut: boolPtr(true),
},
Emph: ansi.StylePrimitive{
Color: stringPtr("#f1fa8c"),
Italic: boolPtr(true),
},
Strong: ansi.StylePrimitive{
Bold: boolPtr(true),
Color: stringPtr("#ffb86c"),
},
HorizontalRule: ansi.StylePrimitive{
Color: stringPtr("#6272A4"),
Format: "\n--------\n",
},
Item: ansi.StylePrimitive{
BlockPrefix: "• ",
},
Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ",
Color: stringPtr("#8be9fd"),
},
Task: ansi.StyleTask{
StylePrimitive: ansi.StylePrimitive{},
Ticked: "[✓] ",
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
Image: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
Underline: boolPtr(true),
},
ImageText: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
Format: "Image: {{.text}} →",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr("#50fa7b"),
},
},
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr("#ffb86c"),
},
Margin: uintPtr(2),
},
Chroma: &ansi.Chroma{
Text: ansi.StylePrimitive{
Color: stringPtr("#f8f8f2"),
},
Error: ansi.StylePrimitive{
Color: stringPtr("#f8f8f2"),
BackgroundColor: stringPtr("#ff5555"),
},
Comment: ansi.StylePrimitive{
Color: stringPtr("#6272A4"),
},
CommentPreproc: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
Keyword: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
KeywordReserved: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
KeywordNamespace: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
KeywordType: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
},
Operator: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
Punctuation: ansi.StylePrimitive{
Color: stringPtr("#f8f8f2"),
},
Name: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
},
NameBuiltin: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
},
NameTag: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
NameAttribute: ansi.StylePrimitive{
Color: stringPtr("#50fa7b"),
},
NameClass: ansi.StylePrimitive{
Color: stringPtr("#8be9fd"),
},
NameConstant: ansi.StylePrimitive{
Color: stringPtr("#bd93f9"),
},
NameDecorator: ansi.StylePrimitive{
Color: stringPtr("#50fa7b"),
},
NameFunction: ansi.StylePrimitive{
Color: stringPtr("#50fa7b"),
},
LiteralNumber: ansi.StylePrimitive{
Color: stringPtr("#6EEFC0"),
},
LiteralString: ansi.StylePrimitive{
Color: stringPtr("#f1fa8c"),
},
LiteralStringEscape: ansi.StylePrimitive{
Color: stringPtr("#ff79c6"),
},
GenericDeleted: ansi.StylePrimitive{
Color: stringPtr("#ff5555"),
},
GenericEmph: ansi.StylePrimitive{
Color: stringPtr("#f1fa8c"),
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
Color: stringPtr("#50fa7b"),
},
GenericStrong: ansi.StylePrimitive{
Color: stringPtr("#ffb86c"),
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
Color: stringPtr("#bd93f9"),
},
Background: ansi.StylePrimitive{
BackgroundColor: stringPtr("#282a36"),
},
},
},
Table: ansi.StyleTable{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{},
},
CenterSeparator: stringPtr("┼"),
ColumnSeparator: stringPtr("│"),
RowSeparator: stringPtr("─"),
},
DefinitionDescription: ansi.StylePrimitive{
BlockPrefix: "\n🠶 ",
},
}
glamour-0.6.0/examples.sh 0000775 0000000 0000000 00000001051 14325445506 0015345 0 ustar 00root root 0000000 0000000 #!/bin/bash
set -e
for element in ./styles/examples/*.md; do
echo "Generating screenshot for element ${element}"
basename="`basename -s .md ${element}`"
stylename="${basename}.style"
filename="${basename}.png"
# take screenshot
./termshot -o ./styles/examples/ -f "$filename" glow -s ./styles/examples/${stylename} ${element}
# add border
convert -bordercolor black -border 16x16 "./styles/examples/$filename" "./styles/examples/$filename"
# optimize filesize
pngcrush -ow "./styles/examples/$filename"
done
glamour-0.6.0/examples/ 0000775 0000000 0000000 00000000000 14325445506 0015011 5 ustar 00root root 0000000 0000000 glamour-0.6.0/examples/custom_renderer/ 0000775 0000000 0000000 00000000000 14325445506 0020211 5 ustar 00root root 0000000 0000000 glamour-0.6.0/examples/custom_renderer/main.go 0000664 0000000 0000000 00000000512 14325445506 0021462 0 ustar 00root root 0000000 0000000 package main
import (
"fmt"
"github.com/charmbracelet/glamour"
)
func main() {
in := `# Custom Renderer
Word-wrapping will occur when lines exceed the limit of 40 characters.
`
r, _ := glamour.NewTermRenderer(
glamour.WithStandardStyle("dark"),
glamour.WithWordWrap(40),
)
out, _ := r.Render(in)
fmt.Print(out)
}
glamour-0.6.0/examples/helloworld/ 0000775 0000000 0000000 00000000000 14325445506 0017164 5 ustar 00root root 0000000 0000000 glamour-0.6.0/examples/helloworld/helloworld.png 0000664 0000000 0000000 00000067761 14325445506 0022066 0 ustar 00root root 0000000 0000000 PNG
IHDR ^ 5q oIDATxgaBMD5kڰmۑlf)>?gwaU_ @ B ! A @ B ! A @ B ! A @ B ! A @ B ! A @ B ! A @ ! A @ B ! A @ B ! Anw:3v7l7 p:-Yijj 0M B f B