1package parser
2
3import (
4 "bytes"
5
6 "github.com/yuin/goldmark/ast"
7 "github.com/yuin/goldmark/text"
8 "github.com/yuin/goldmark/util"
9)
10
11type fencedCodeBlockParser struct {
12}
13
14var defaultFencedCodeBlockParser = &fencedCodeBlockParser{}
15
16// NewFencedCodeBlockParser returns a new BlockParser that
17// parses fenced code blocks.
18func NewFencedCodeBlockParser() BlockParser {
19 return defaultFencedCodeBlockParser
20}
21
22type fenceData struct {
23 char byte
24 indent int
25 length int
26 node ast.Node
27}
28
29var fencedCodeBlockInfoKey = NewContextKey()
30
31func (b *fencedCodeBlockParser) Trigger() []byte {
32 return []byte{'~', '`'}
33}
34
35func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
36 line, segment := reader.PeekLine()
37 pos := pc.BlockIndent()
38 findent := pos
39 fenceChar := line[pos]
40 i := pos
41 for ; i < len(line) && line[i] == fenceChar; i++ {
42 }
43 oFenceLength := i - pos
44 if oFenceLength < 3 {
45 return nil, NoChildren
46 }
47 var info *ast.Text
48 if i < len(line)-1 {
49 rest := line[i:]
50 left := util.TrimLeftSpaceLength(rest)
51 right := util.TrimRightSpaceLength(rest)
52 if left < len(rest)-right {
53 infoStart, infoStop := segment.Start-segment.Padding+i+left, segment.Stop-right
54 value := rest[left : len(rest)-right]
55 if fenceChar == '`' && bytes.IndexByte(value, '`') > -1 {
56 return nil, NoChildren
57 } else if infoStart != infoStop {
58 info = ast.NewTextSegment(text.NewSegment(infoStart, infoStop))
59 }
60 }
61 }
62 node := ast.NewFencedCodeBlock(info)
63 pc.Set(fencedCodeBlockInfoKey, &fenceData{fenceChar, findent, oFenceLength, node})
64 return node, NoChildren
65
66}
67
68func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
69 line, segment := reader.PeekLine()
70 fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData)
71
72 w, pos := util.IndentWidth(line, reader.LineOffset())
73 if w < 4 {
74 i := pos
75 for ; i < len(line) && line[i] == fdata.char; i++ {
76 }
77 length := i - pos
78 if length >= fdata.length && util.IsBlank(line[i:]) {
79 reader.AdvanceToEOL()
80 return Close
81 }
82 }
83 pos, padding := util.IndentPositionPadding(line, reader.LineOffset(), segment.Padding, fdata.indent)
84 if pos < 0 {
85 pos = max(0, util.FirstNonSpacePosition(line)) - segment.Padding
86 padding = 0
87 }
88 seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding)
89 // if code block line starts with a tab, keep a tab as it is.
90 if padding != 0 {
91 preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent)
92 }
93 seg.ForceNewline = true // EOF as newline
94 node.Lines().Append(seg)
95 reader.AdvanceToEOL()
96 return Continue | NoChildren
97}
98
99func (b *fencedCodeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) {
100 fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData)
101 if fdata.node == node {
102 pc.Set(fencedCodeBlockInfoKey, nil)
103 }
104}
105
106func (b *fencedCodeBlockParser) CanInterruptParagraph() bool {
107 return true
108}
109
110func (b *fencedCodeBlockParser) CanAcceptIndentedLine() bool {
111 return false
112}