definition_list.go

  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}