1package extension
2
3import (
4 "github.com/yuin/goldmark"
5 gast "github.com/yuin/goldmark/ast"
6 "github.com/yuin/goldmark/extension/ast"
7 "github.com/yuin/goldmark/parser"
8 "github.com/yuin/goldmark/renderer"
9 "github.com/yuin/goldmark/renderer/html"
10 "github.com/yuin/goldmark/text"
11 "github.com/yuin/goldmark/util"
12)
13
14type definitionListParser struct {
15}
16
17var defaultDefinitionListParser = &definitionListParser{}
18
19// NewDefinitionListParser return a new parser.BlockParser that
20// can parse PHP Markdown Extra Definition lists.
21func NewDefinitionListParser() parser.BlockParser {
22 return defaultDefinitionListParser
23}
24
25func (b *definitionListParser) Trigger() []byte {
26 return []byte{':'}
27}
28
29func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
30 if _, ok := parent.(*ast.DefinitionList); ok {
31 return nil, parser.NoChildren
32 }
33 line, _ := reader.PeekLine()
34 pos := pc.BlockOffset()
35 indent := pc.BlockIndent()
36 if pos < 0 || line[pos] != ':' || indent != 0 {
37 return nil, parser.NoChildren
38 }
39
40 last := parent.LastChild()
41 // need 1 or more spaces after ':'
42 w, _ := util.IndentWidth(line[pos+1:], pos+1)
43 if w < 1 {
44 return nil, parser.NoChildren
45 }
46 if w >= 8 { // starts with indented code
47 w = 5
48 }
49 w += pos + 1 /* 1 = ':' */
50
51 para, lastIsParagraph := last.(*gast.Paragraph)
52 var list *ast.DefinitionList
53 status := parser.HasChildren
54 var ok bool
55 if lastIsParagraph {
56 list, ok = last.PreviousSibling().(*ast.DefinitionList)
57 if ok { // is not first item
58 list.Offset = w
59 list.TemporaryParagraph = para
60 } else { // is first item
61 list = ast.NewDefinitionList(w, para)
62 status |= parser.RequireParagraph
63 }
64 } else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
65 list.Offset = w
66 list.TemporaryParagraph = nil
67 } else {
68 return nil, parser.NoChildren
69 }
70
71 return list, status
72}
73
74func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
75 line, _ := reader.PeekLine()
76 if util.IsBlank(line) {
77 return parser.Continue | parser.HasChildren
78 }
79 list, _ := node.(*ast.DefinitionList)
80 w, _ := util.IndentWidth(line, reader.LineOffset())
81 if w < list.Offset {
82 return parser.Close
83 }
84 pos, padding := util.IndentPosition(line, reader.LineOffset(), list.Offset)
85 reader.AdvanceAndSetPadding(pos, padding)
86 return parser.Continue | parser.HasChildren
87}
88
89func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
90 // nothing to do
91}
92
93func (b *definitionListParser) CanInterruptParagraph() bool {
94 return true
95}
96
97func (b *definitionListParser) CanAcceptIndentedLine() bool {
98 return false
99}
100
101type definitionDescriptionParser struct {
102}
103
104var defaultDefinitionDescriptionParser = &definitionDescriptionParser{}
105
106// NewDefinitionDescriptionParser return a new parser.BlockParser that
107// can parse definition description starts with ':'.
108func NewDefinitionDescriptionParser() parser.BlockParser {
109 return defaultDefinitionDescriptionParser
110}
111
112func (b *definitionDescriptionParser) Trigger() []byte {
113 return []byte{':'}
114}
115
116func (b *definitionDescriptionParser) Open(
117 parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
118 line, _ := reader.PeekLine()
119 pos := pc.BlockOffset()
120 indent := pc.BlockIndent()
121 if pos < 0 || line[pos] != ':' || indent != 0 {
122 return nil, parser.NoChildren
123 }
124 list, _ := parent.(*ast.DefinitionList)
125 if list == nil {
126 return nil, parser.NoChildren
127 }
128 para := list.TemporaryParagraph
129 list.TemporaryParagraph = nil
130 if para != nil {
131 lines := para.Lines()
132 l := lines.Len()
133 for i := 0; i < l; i++ {
134 term := ast.NewDefinitionTerm()
135 segment := lines.At(i)
136 term.Lines().Append(segment.TrimRightSpace(reader.Source()))
137 list.AppendChild(list, term)
138 }
139 para.Parent().RemoveChild(para.Parent(), para)
140 }
141 cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
142 reader.AdvanceAndSetPadding(cpos+1, padding)
143
144 return ast.NewDefinitionDescription(), parser.HasChildren
145}
146
147func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
148 // definitionListParser detects end of the description.
149 // so this method will never be called.
150 return parser.Continue | parser.HasChildren
151}
152
153func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
154 desc := node.(*ast.DefinitionDescription)
155 desc.IsTight = !desc.HasBlankPreviousLines()
156 if desc.IsTight {
157 for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
158 paragraph, ok := gc.(*gast.Paragraph)
159 if ok {
160 textBlock := gast.NewTextBlock()
161 textBlock.SetLines(paragraph.Lines())
162 desc.ReplaceChild(desc, paragraph, textBlock)
163 }
164 }
165 }
166}
167
168func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
169 return true
170}
171
172func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
173 return false
174}
175
176// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
177// renders DefinitionList nodes.
178type DefinitionListHTMLRenderer struct {
179 html.Config
180}
181
182// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
183func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
184 r := &DefinitionListHTMLRenderer{
185 Config: html.NewConfig(),
186 }
187 for _, opt := range opts {
188 opt.SetHTMLOption(&r.Config)
189 }
190 return r
191}
192
193// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
194func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
195 reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
196 reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
197 reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
198}
199
200// DefinitionListAttributeFilter defines attribute names which dl elements can have.
201var DefinitionListAttributeFilter = html.GlobalAttributeFilter
202
203func (r *DefinitionListHTMLRenderer) renderDefinitionList(
204 w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
205 if entering {
206 if n.Attributes() != nil {
207 _, _ = w.WriteString("<dl")
208 html.RenderAttributes(w, n, DefinitionListAttributeFilter)
209 _, _ = w.WriteString(">\n")
210 } else {
211 _, _ = w.WriteString("<dl>\n")
212 }
213 } else {
214 _, _ = w.WriteString("</dl>\n")
215 }
216 return gast.WalkContinue, nil
217}
218
219// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
220var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
221
222func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(
223 w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
224 if entering {
225 if n.Attributes() != nil {
226 _, _ = w.WriteString("<dt")
227 html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
228 _ = w.WriteByte('>')
229 } else {
230 _, _ = w.WriteString("<dt>")
231 }
232 } else {
233 _, _ = w.WriteString("</dt>\n")
234 }
235 return gast.WalkContinue, nil
236}
237
238// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
239var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
240
241func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(
242 w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
243 if entering {
244 n := node.(*ast.DefinitionDescription)
245 _, _ = w.WriteString("<dd")
246 if n.Attributes() != nil {
247 html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
248 }
249 if n.IsTight {
250 _, _ = w.WriteString(">")
251 } else {
252 _, _ = w.WriteString(">\n")
253 }
254 } else {
255 _, _ = w.WriteString("</dd>\n")
256 }
257 return gast.WalkContinue, nil
258}
259
260type definitionList struct {
261}
262
263// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
264var DefinitionList = &definitionList{}
265
266func (e *definitionList) Extend(m goldmark.Markdown) {
267 m.Parser().AddOptions(parser.WithBlockParsers(
268 util.Prioritized(NewDefinitionListParser(), 101),
269 util.Prioritized(NewDefinitionDescriptionParser(), 102),
270 ))
271 m.Renderer().AddOptions(renderer.WithNodeRenderers(
272 util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
273 ))
274}