Skip to content

Commit 15db676

Browse files
committed
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 #299
1 parent da6d8fa commit 15db676

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+458
-412
lines changed

β€Žansi/blockelement.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (e *BlockElement) Finish(w io.Writer, ctx RenderContext) error {
3636
" ,.;-+|",
3737
)
3838

39-
mw := NewMarginWriter(ctx, w, bs.Current().Style)
39+
mw := NewMarginWriter(ctx, w, bs.Current().Style, true)
4040
if _, err := io.WriteString(mw, s); err != nil {
4141
return err
4242
}

β€Žansi/codeblock.go

+65-57
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package ansi
22

33
import (
4+
"bytes"
45
"io"
56
"sync"
67

78
"github.com/alecthomas/chroma/v2"
89
"github.com/alecthomas/chroma/v2/quick"
910
"github.com/alecthomas/chroma/v2/styles"
10-
"github.com/muesli/reflow/indent"
1111
"github.com/muesli/termenv"
1212
)
1313

@@ -62,82 +62,90 @@ func chromaStyle(style StylePrimitive) string {
6262

6363
func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error {
6464
bs := ctx.blockStack
65-
66-
var indentation uint
67-
var margin uint
6865
rules := ctx.options.Styles.CodeBlock
69-
if rules.Indent != nil {
70-
indentation = *rules.Indent
71-
}
72-
if rules.Margin != nil {
73-
margin = *rules.Margin
66+
67+
be := BlockElement{
68+
Block: &bytes.Buffer{},
69+
Style: rules.StyleBlock,
7470
}
75-
theme := rules.Theme
71+
bs.Push(be)
72+
return nil
73+
}
74+
75+
func (e *CodeBlockElement) Finish(w io.Writer, ctx RenderContext) error {
76+
bs := ctx.blockStack
77+
rules := bs.Current().Style
78+
79+
cb := ctx.options.Styles.CodeBlock
80+
theme := cb.Theme
81+
chromaRules := cb.Chroma
7682

77-
if rules.Chroma != nil && ctx.options.ColorProfile != termenv.Ascii {
83+
if chromaRules != nil && ctx.options.ColorProfile != termenv.Ascii {
7884
theme = chromaStyleTheme
7985
mutex.Lock()
8086
// Don't register the style if it's already registered.
8187
_, ok := styles.Registry[theme]
8288
if !ok {
8389
styles.Register(chroma.MustNewStyle(theme,
8490
chroma.StyleEntries{
85-
chroma.Text: chromaStyle(rules.Chroma.Text),
86-
chroma.Error: chromaStyle(rules.Chroma.Error),
87-
chroma.Comment: chromaStyle(rules.Chroma.Comment),
88-
chroma.CommentPreproc: chromaStyle(rules.Chroma.CommentPreproc),
89-
chroma.Keyword: chromaStyle(rules.Chroma.Keyword),
90-
chroma.KeywordReserved: chromaStyle(rules.Chroma.KeywordReserved),
91-
chroma.KeywordNamespace: chromaStyle(rules.Chroma.KeywordNamespace),
92-
chroma.KeywordType: chromaStyle(rules.Chroma.KeywordType),
93-
chroma.Operator: chromaStyle(rules.Chroma.Operator),
94-
chroma.Punctuation: chromaStyle(rules.Chroma.Punctuation),
95-
chroma.Name: chromaStyle(rules.Chroma.Name),
96-
chroma.NameBuiltin: chromaStyle(rules.Chroma.NameBuiltin),
97-
chroma.NameTag: chromaStyle(rules.Chroma.NameTag),
98-
chroma.NameAttribute: chromaStyle(rules.Chroma.NameAttribute),
99-
chroma.NameClass: chromaStyle(rules.Chroma.NameClass),
100-
chroma.NameConstant: chromaStyle(rules.Chroma.NameConstant),
101-
chroma.NameDecorator: chromaStyle(rules.Chroma.NameDecorator),
102-
chroma.NameException: chromaStyle(rules.Chroma.NameException),
103-
chroma.NameFunction: chromaStyle(rules.Chroma.NameFunction),
104-
chroma.NameOther: chromaStyle(rules.Chroma.NameOther),
105-
chroma.Literal: chromaStyle(rules.Chroma.Literal),
106-
chroma.LiteralNumber: chromaStyle(rules.Chroma.LiteralNumber),
107-
chroma.LiteralDate: chromaStyle(rules.Chroma.LiteralDate),
108-
chroma.LiteralString: chromaStyle(rules.Chroma.LiteralString),
109-
chroma.LiteralStringEscape: chromaStyle(rules.Chroma.LiteralStringEscape),
110-
chroma.GenericDeleted: chromaStyle(rules.Chroma.GenericDeleted),
111-
chroma.GenericEmph: chromaStyle(rules.Chroma.GenericEmph),
112-
chroma.GenericInserted: chromaStyle(rules.Chroma.GenericInserted),
113-
chroma.GenericStrong: chromaStyle(rules.Chroma.GenericStrong),
114-
chroma.GenericSubheading: chromaStyle(rules.Chroma.GenericSubheading),
115-
chroma.Background: chromaStyle(rules.Chroma.Background),
91+
chroma.Text: chromaStyle(chromaRules.Text),
92+
chroma.Error: chromaStyle(chromaRules.Error),
93+
chroma.Comment: chromaStyle(chromaRules.Comment),
94+
chroma.CommentPreproc: chromaStyle(chromaRules.CommentPreproc),
95+
chroma.Keyword: chromaStyle(chromaRules.Keyword),
96+
chroma.KeywordReserved: chromaStyle(chromaRules.KeywordReserved),
97+
chroma.KeywordNamespace: chromaStyle(chromaRules.KeywordNamespace),
98+
chroma.KeywordType: chromaStyle(chromaRules.KeywordType),
99+
chroma.Operator: chromaStyle(chromaRules.Operator),
100+
chroma.Punctuation: chromaStyle(chromaRules.Punctuation),
101+
chroma.Name: chromaStyle(chromaRules.Name),
102+
chroma.NameBuiltin: chromaStyle(chromaRules.NameBuiltin),
103+
chroma.NameTag: chromaStyle(chromaRules.NameTag),
104+
chroma.NameAttribute: chromaStyle(chromaRules.NameAttribute),
105+
chroma.NameClass: chromaStyle(chromaRules.NameClass),
106+
chroma.NameConstant: chromaStyle(chromaRules.NameConstant),
107+
chroma.NameDecorator: chromaStyle(chromaRules.NameDecorator),
108+
chroma.NameException: chromaStyle(chromaRules.NameException),
109+
chroma.NameFunction: chromaStyle(chromaRules.NameFunction),
110+
chroma.NameOther: chromaStyle(chromaRules.NameOther),
111+
chroma.Literal: chromaStyle(chromaRules.Literal),
112+
chroma.LiteralNumber: chromaStyle(chromaRules.LiteralNumber),
113+
chroma.LiteralDate: chromaStyle(chromaRules.LiteralDate),
114+
chroma.LiteralString: chromaStyle(chromaRules.LiteralString),
115+
chroma.LiteralStringEscape: chromaStyle(chromaRules.LiteralStringEscape),
116+
chroma.GenericDeleted: chromaStyle(chromaRules.GenericDeleted),
117+
chroma.GenericEmph: chromaStyle(chromaRules.GenericEmph),
118+
chroma.GenericInserted: chromaStyle(chromaRules.GenericInserted),
119+
chroma.GenericStrong: chromaStyle(chromaRules.GenericStrong),
120+
chroma.GenericSubheading: chromaStyle(chromaRules.GenericSubheading),
121+
chroma.Background: chromaStyle(chromaRules.Background),
116122
}))
117123
}
118124
mutex.Unlock()
119125
}
120126

121-
iw := indent.NewWriterPipe(w, indentation+margin, func(wr io.Writer) {
122-
renderText(w, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, " ")
123-
})
124-
127+
mw := NewMarginWriter(ctx, w, bs.Current().Style, false)
128+
renderText(mw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
125129
if len(theme) > 0 {
126-
renderText(iw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
127-
128-
err := quick.Highlight(iw, e.Code, e.Language, "terminal256", theme)
130+
err := quick.Highlight(mw, e.Code, e.Language, "terminal256", theme)
129131
if err != nil {
130132
return err
131133
}
132-
renderText(iw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
133-
return nil
134-
}
134+
} else {
135+
// fallback rendering
136+
el := &BaseElement{
137+
Token: e.Code,
138+
Style: rules.StylePrimitive,
139+
}
135140

136-
// fallback rendering
137-
el := &BaseElement{
138-
Token: e.Code,
139-
Style: rules.StylePrimitive,
141+
err := el.Render(mw, ctx)
142+
if err != nil {
143+
return err
144+
}
140145
}
146+
renderText(mw, ctx.options.ColorProfile, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
141147

142-
return el.Render(iw, ctx)
148+
bs.Current().Block.Reset()
149+
bs.Pop()
150+
return nil
143151
}

β€Žansi/elements.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,14 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
282282
line := n.Lines().At(i)
283283
s += string(line.Value(source))
284284
}
285+
e := &CodeBlockElement{
286+
Code: s,
287+
Language: string(n.Language(source)),
288+
}
285289
return Element{
286290
Entering: "\n",
287-
Renderer: &CodeBlockElement{
288-
Code: s,
289-
Language: string(n.Language(source)),
290-
},
291+
Renderer: e,
292+
Finisher: e,
291293
}
292294

293295
case ast.KindCodeBlock:
@@ -298,11 +300,13 @@ func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
298300
line := n.Lines().At(i)
299301
s += string(line.Value(source))
300302
}
303+
e := &CodeBlockElement{
304+
Code: s,
305+
}
301306
return Element{
302307
Entering: "\n",
303-
Renderer: &CodeBlockElement{
304-
Code: s,
305-
},
308+
Renderer: e,
309+
Finisher: e,
306310
}
307311

308312
case ast.KindCodeSpan:

β€Žansi/heading.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (e *HeadingElement) Render(w io.Writer, ctx RenderContext) error {
5959
func (e *HeadingElement) Finish(w io.Writer, ctx RenderContext) error {
6060
bs := ctx.blockStack
6161
rules := bs.Current().Style
62-
mw := NewMarginWriter(ctx, w, rules)
62+
mw := NewMarginWriter(ctx, w, rules, true)
6363

6464
flow := wordwrap.NewWriter(int(bs.Width(ctx)))
6565
_, err := flow.Write(bs.Current().Block.Bytes())

β€Žansi/margin.go

+53-21
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,80 @@ import (
55

66
"github.com/muesli/reflow/indent"
77
"github.com/muesli/reflow/padding"
8+
"github.com/muesli/termenv"
89
)
910

1011
// MarginWriter is a Writer that applies indentation and padding around
1112
// whatever you write to it.
1213
type MarginWriter struct {
14+
indentation, margin uint
15+
indentPos, marginPos uint
16+
indentToken string
17+
18+
profile termenv.Profile
19+
rules, parentRules StylePrimitive
20+
1321
w io.Writer
1422
pw *padding.Writer
1523
iw *indent.Writer
1624
}
1725

1826
// NewMarginWriter returns a new MarginWriter.
19-
func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter {
27+
func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock, padded bool) *MarginWriter {
2028
bs := ctx.blockStack
29+
mw := &MarginWriter{
30+
w: w,
31+
profile: ctx.options.ColorProfile,
32+
rules: rules.StylePrimitive,
33+
parentRules: bs.Parent().Style.StylePrimitive,
34+
}
2135

22-
var indentation uint
23-
var margin uint
2436
if rules.Indent != nil {
25-
indentation = *rules.Indent
37+
mw.indentation = *rules.Indent
38+
mw.indentToken = " "
39+
if rules.IndentToken != nil {
40+
mw.indentToken = *rules.IndentToken
41+
}
2642
}
2743
if rules.Margin != nil {
28-
margin = *rules.Margin
44+
mw.margin = *rules.Margin
2945
}
3046

31-
pw := padding.NewWriterPipe(w, bs.Width(ctx), func(wr io.Writer) {
32-
renderText(w, ctx.options.ColorProfile, rules.StylePrimitive, " ")
33-
})
34-
35-
ic := " "
36-
if rules.IndentToken != nil {
37-
ic = *rules.IndentToken
38-
}
39-
iw := indent.NewWriterPipe(pw, indentation+margin, func(wr io.Writer) {
40-
renderText(w, ctx.options.ColorProfile, bs.Parent().Style.StylePrimitive, ic)
41-
})
42-
43-
return &MarginWriter{
44-
w: w,
45-
pw: pw,
46-
iw: iw,
47+
fwd := mw.w
48+
if padded {
49+
mw.pw = padding.NewWriterPipe(mw.w, bs.Width(ctx), func(wr io.Writer) {
50+
renderText(mw.w, mw.profile, mw.rules, "")
51+
})
52+
fwd = mw.pw
4753
}
54+
55+
mw.iw = indent.NewWriterPipe(fwd, mw.indentation+(mw.margin*2), mw.indentFunc)
56+
return mw
4857
}
4958

5059
func (w *MarginWriter) Write(b []byte) (int, error) {
5160
return w.iw.Write(b)
5261
}
62+
63+
// indentFunc is called when writing each the margin and indentation tokens.
64+
// The margin is written first, using an empty space character as the token.
65+
// The indentation is written next, using the token specified in the rules.
66+
func (w *MarginWriter) indentFunc(iw io.Writer) {
67+
ic := " "
68+
switch {
69+
case w.margin == 0 && w.indentation == 0:
70+
return
71+
case w.margin >= 1 && w.indentation == 0:
72+
break
73+
case w.margin >= 1 && w.marginPos < w.margin:
74+
w.marginPos++
75+
case w.indentation >= 1 && w.indentPos < w.indentation:
76+
w.indentPos++
77+
ic = w.indentToken
78+
if w.indentPos == w.indentation {
79+
w.marginPos = 0
80+
w.indentPos = 0
81+
}
82+
}
83+
renderText(w.w, w.profile, w.parentRules, ic)
84+
}

β€Žansi/paragraph.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (e *ParagraphElement) Finish(w io.Writer, ctx RenderContext) error {
3535
bs := ctx.blockStack
3636
rules := bs.Current().Style
3737

38-
mw := NewMarginWriter(ctx, w, rules)
38+
mw := NewMarginWriter(ctx, w, rules, true)
3939
if len(strings.TrimSpace(bs.Current().Block.String())) > 0 {
4040
flow := wordwrap.NewWriter(int(bs.Width(ctx)))
4141
flow.KeepNewlines = ctx.options.PreserveNewLines

β€Žansi/testdata/TestRenderer/block_quote.golden

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/code.golden

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/code_block.golden

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/emoji.golden

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/emph.golden

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/enumeration.golden

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/heading.golden

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/image.golden

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žansi/testdata/TestRenderer/link.golden

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
Β (0)