From ffae7b7fc08ad8ce26b24243fe237752d7792d52 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Sat, 3 Aug 2024 00:09:06 -0400 Subject: [PATCH] feat: Add code block indentation token support This change incorporates the specified indentation token when rendering code blocks. It includes fixes to how the margin and indentation tokens are rendered globally. When both are specified, the margin is rendered first as an empty space. This PR is another attempt at adding the functionality from https://github.com/charmbracelet/glamour/pull/299 --- ansi/codeblock.go | 122 ++++++++++--------- ansi/elements.go | 18 +-- ansi/margin.go | 66 +++++++--- ansi/testdata/TestRenderer/code_block.golden | 2 +- ansi/testdata/TestRendererIssues/107.golden | 4 +- ansi/testdata/TestRendererIssues/257.golden | 2 +- ansi/testdata/TestRendererIssues/48.golden | 2 +- ansi/testdata/TestRendererIssues/79.golden | 2 +- testdata/TestRenderHelpers.golden | 2 +- testdata/TestTermRenderer.golden | 2 +- testdata/TestTermRendererWriter.golden | 2 +- 11 files changed, 132 insertions(+), 92 deletions(-) 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