diff --git a/ansi/codeblock.go b/ansi/codeblock.go index f0c3017f..076f936b 100644 --- a/ansi/codeblock.go +++ b/ansi/codeblock.go @@ -1,13 +1,13 @@ package ansi import ( + "bytes" "io" "sync" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/quick" "github.com/alecthomas/chroma/v2/styles" - "github.com/muesli/reflow/indent" "github.com/muesli/termenv" ) @@ -62,19 +62,25 @@ func chromaStyle(style StylePrimitive) string { 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 + + be := BlockElement{ + Block: &bytes.Buffer{}, + Style: rules.StyleBlock, } - theme := rules.Theme + bs.Push(be) + return nil +} + +func (e *CodeBlockElement) Finish(w io.Writer, ctx RenderContext) error { + bs := ctx.blockStack + rules := bs.Current().Style + + cb := ctx.options.Styles.CodeBlock + theme := cb.Theme + chromaRules := cb.Chroma - if rules.Chroma != nil && ctx.options.ColorProfile != termenv.Ascii { + if chromaRules != nil && ctx.options.ColorProfile != termenv.Ascii { theme = chromaStyleTheme mutex.Lock() // Don't register the style if it's already registered. @@ -82,62 +88,64 @@ func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error { 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), + chroma.Text: chromaStyle(chromaRules.Text), + chroma.Error: chromaStyle(chromaRules.Error), + chroma.Comment: chromaStyle(chromaRules.Comment), + chroma.CommentPreproc: chromaStyle(chromaRules.CommentPreproc), + chroma.Keyword: chromaStyle(chromaRules.Keyword), + chroma.KeywordReserved: chromaStyle(chromaRules.KeywordReserved), + chroma.KeywordNamespace: chromaStyle(chromaRules.KeywordNamespace), + chroma.KeywordType: chromaStyle(chromaRules.KeywordType), + chroma.Operator: chromaStyle(chromaRules.Operator), + chroma.Punctuation: chromaStyle(chromaRules.Punctuation), + chroma.Name: chromaStyle(chromaRules.Name), + chroma.NameBuiltin: chromaStyle(chromaRules.NameBuiltin), + chroma.NameTag: chromaStyle(chromaRules.NameTag), + chroma.NameAttribute: chromaStyle(chromaRules.NameAttribute), + chroma.NameClass: chromaStyle(chromaRules.NameClass), + chroma.NameConstant: chromaStyle(chromaRules.NameConstant), + chroma.NameDecorator: chromaStyle(chromaRules.NameDecorator), + chroma.NameException: chromaStyle(chromaRules.NameException), + chroma.NameFunction: chromaStyle(chromaRules.NameFunction), + chroma.NameOther: chromaStyle(chromaRules.NameOther), + chroma.Literal: chromaStyle(chromaRules.Literal), + chroma.LiteralNumber: chromaStyle(chromaRules.LiteralNumber), + chroma.LiteralDate: chromaStyle(chromaRules.LiteralDate), + chroma.LiteralString: chromaStyle(chromaRules.LiteralString), + chroma.LiteralStringEscape: chromaStyle(chromaRules.LiteralStringEscape), + chroma.GenericDeleted: chromaStyle(chromaRules.GenericDeleted), + chroma.GenericEmph: chromaStyle(chromaRules.GenericEmph), + chroma.GenericInserted: chromaStyle(chromaRules.GenericInserted), + chroma.GenericStrong: chromaStyle(chromaRules.GenericStrong), + chroma.GenericSubheading: chromaStyle(chromaRules.GenericSubheading), + chroma.Background: chromaStyle(chromaRules.Background), })) } mutex.Unlock() } - iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) { - renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ") - }) - + mw := NewMarginWriter(ctx, w, bs.Current().Style) + renderText(mw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix) 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) + err := quick.Highlight(mw, e.Code, e.Language, "terminal256", theme) if err != nil { return err } - renderText(iw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix) - return nil - } + } else { + // fallback rendering + el := &BaseElement{ + Token: e.Code, + Style: rules.StylePrimitive, + } - // fallback rendering - el := &BaseElement{ - Token: e.Code, - Style: rules.StylePrimitive, + err := el.Render(mw, ctx) + if err != nil { + return err + } } + renderText(mw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix) - return el.Render(iw, ctx) + bs.Current().Block.Reset() + bs.Pop() + return nil } diff --git a/ansi/elements.go b/ansi/elements.go index 161857ab..14d572f2 100644 --- a/ansi/elements.go +++ b/ansi/elements.go @@ -282,12 +282,14 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element { line := n.Lines().At(i) s += string(line.Value(source)) } + e := &CodeBlockElement{ + Code: s, + Language: string(n.Language(source)), + } return Element{ Entering: "\n", - Renderer: &CodeBlockElement{ - Code: s, - Language: string(n.Language(source)), - }, + Renderer: e, + Finisher: e, } case ast.KindCodeBlock: @@ -298,11 +300,13 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element { line := n.Lines().At(i) s += string(line.Value(source)) } + e := &CodeBlockElement{ + Code: s, + } return Element{ Entering: "\n", - Renderer: &CodeBlockElement{ - Code: s, - }, + Renderer: e, + Finisher: e, } case ast.KindCodeSpan: diff --git a/ansi/margin.go b/ansi/margin.go index e039783f..1fd6e8cc 100644 --- a/ansi/margin.go +++ b/ansi/margin.go @@ -5,11 +5,19 @@ import ( "github.com/muesli/reflow/indent" "github.com/muesli/reflow/padding" + "github.com/muesli/termenv" ) // MarginWriter is a Writer that applies indentation and padding around // whatever you write to it. type MarginWriter struct { + indentation, margin uint + indentPos, marginPos uint + indentToken string + + profile termenv.Profile + rules, parentRules StylePrimitive + w io.Writer pw *padding.Writer iw *indent.Writer @@ -18,35 +26,55 @@ type MarginWriter struct { // NewMarginWriter returns a new MarginWriter. func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter { bs := ctx.blockStack + mw := &MarginWriter{ + w: w, + profile: ctx.options.ColorProfile, + rules: rules.StylePrimitive, + parentRules: bs.Parent().Style.StylePrimitive, + } - var indentation uint - var margin uint if rules.Indent != nil { - indentation = *rules.Indent + mw.indentation = *rules.Indent + mw.indentToken = " " + if rules.IndentToken != nil { + mw.indentToken = *rules.IndentToken + } } if rules.Margin != nil { - margin = *rules.Margin + mw.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) + mw.pw = padding.NewWriterPipe(mw.w, bs.Width(ctx), func(wr io.Writer) { + renderText(mw.w, mw.profile, mw.rules, " ") }) - return &MarginWriter{ - w: w, - pw: pw, - iw: iw, - } + mw.iw = indent.NewWriterPipe(mw.pw, mw.indentation+mw.margin, mw.indentFunc) + return mw } func (w *MarginWriter) Write(b []byte) (int, error) { return w.iw.Write(b) } + +// indentFunc is called when writing each the margin and indentation tokens. +// The margin is written first, using an empty space character as the token. +// The indentation is written next, using the token specified in the rules. +func (w *MarginWriter) indentFunc(iw io.Writer) { + ic := " " + switch { + case w.margin == 0 && w.indentation == 0: + return + case w.margin >= 1 && w.indentation == 0: + break + case w.margin >= 1 && w.marginPos < w.margin: + w.marginPos++ + case w.indentation >= 1 && w.indentPos < w.indentation: + w.indentPos++ + ic = w.indentToken + if w.indentPos == w.indentation { + w.marginPos = 0 + w.indentPos = 0 + } + } + renderText(w.w, w.profile, w.parentRules, ic) +} diff --git a/ansi/testdata/TestRenderer/code_block.golden b/ansi/testdata/TestRenderer/code_block.golden index c608da7a..81107efb 100644 --- a/ansi/testdata/TestRenderer/code_block.golden +++ b/ansi/testdata/TestRenderer/code_block.golden @@ -1,3 +1,3 @@ -This is a code block.  +This is a code block.                                                             \ No newline at end of file diff --git a/ansi/testdata/TestRendererIssues/107.golden b/ansi/testdata/TestRendererIssues/107.golden index 87796cf0..0f11c0e6 100644 --- a/ansi/testdata/TestRendererIssues/107.golden +++ b/ansi/testdata/TestRendererIssues/107.golden @@ -1,7 +1,7 @@                                                                              -   [Mount]                                                                    +   [Mount]                                                                       Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,noauto,      _netdev,allow_other,uid=1000,gid=1000,IdentityFile=/PATH/TO/SSH-KEY/id_rsa,  - StrictHostKeyChecking=no                                                     + StrictHostKeyChecking=no                                                      diff --git a/ansi/testdata/TestRendererIssues/257.golden b/ansi/testdata/TestRendererIssues/257.golden index eeffc6db..56a7d22d 100644 --- a/ansi/testdata/TestRendererIssues/257.golden +++ b/ansi/testdata/TestRendererIssues/257.golden @@ -1,4 +1,4 @@                                                                              -   set runtimepath^=$XDG_CONFIG_HOME/vim                                      +   set runtimepath^=$XDG_CONFIG_HOME/vim                                       diff --git a/ansi/testdata/TestRendererIssues/48.golden b/ansi/testdata/TestRendererIssues/48.golden index a20338ce..719d8b1c 100644 --- a/ansi/testdata/TestRendererIssues/48.golden +++ b/ansi/testdata/TestRendererIssues/48.golden @@ -9,7 +9,7 @@                                                                                no emoji in code blocks                                                                                                                                   -   :octopus: :zap: :cat: = :heart:                                            +   :octopus: :zap: :cat: = :heart:                                                                                                                           no emoji in inline code                                                                                                                                   diff --git a/ansi/testdata/TestRendererIssues/79.golden b/ansi/testdata/TestRendererIssues/79.golden index 23b0f168..a8065704 100644 --- a/ansi/testdata/TestRendererIssues/79.golden +++ b/ansi/testdata/TestRendererIssues/79.golden @@ -3,7 +3,7 @@                                                                               │ 1st blockquote paragraph                                                    │                                                                            - │   quoted code block                                                        + │   quoted code block                                                         │                                                                             │ 2nd blockquote paragraph                                                   diff --git a/testdata/TestRenderHelpers.golden b/testdata/TestRenderHelpers.golden index e598ccaa..04a122cc 100644 --- a/testdata/TestRenderHelpers.golden +++ b/testdata/TestRenderHelpers.golden @@ -32,7 +32,7 @@  Style definitions located in  styles/  can be embedded into the binary by     running statik https://github.com/rakyll/statik:                                                                                                          -   statik -f -src styles -include "*.json"                                    +   statik -f -src styles -include "*.json"                                                                                                                   You can re-generate screenshots of all available styles by running            gallery.sh . This requires  termshot  and  pngcrush  installed on your       diff --git a/testdata/TestTermRenderer.golden b/testdata/TestTermRenderer.golden index e598ccaa..04a122cc 100644 --- a/testdata/TestTermRenderer.golden +++ b/testdata/TestTermRenderer.golden @@ -32,7 +32,7 @@  Style definitions located in  styles/  can be embedded into the binary by     running statik https://github.com/rakyll/statik:                                                                                                          -   statik -f -src styles -include "*.json"                                    +   statik -f -src styles -include "*.json"                                                                                                                   You can re-generate screenshots of all available styles by running            gallery.sh . This requires  termshot  and  pngcrush  installed on your       diff --git a/testdata/TestTermRendererWriter.golden b/testdata/TestTermRendererWriter.golden index e598ccaa..04a122cc 100644 --- a/testdata/TestTermRendererWriter.golden +++ b/testdata/TestTermRendererWriter.golden @@ -32,7 +32,7 @@  Style definitions located in  styles/  can be embedded into the binary by     running statik https://github.com/rakyll/statik:                                                                                                          -   statik -f -src styles -include "*.json"                                    +   statik -f -src styles -include "*.json"                                                                                                                   You can re-generate screenshots of all available styles by running            gallery.sh . This requires  termshot  and  pngcrush  installed on your