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(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
117	line, _ := reader.PeekLine()
118	pos := pc.BlockOffset()
119	indent := pc.BlockIndent()
120	if pos < 0 || line[pos] != ':' || indent != 0 {
121		return nil, parser.NoChildren
122	}
123	list, _ := parent.(*ast.DefinitionList)
124	if list == nil {
125		return nil, parser.NoChildren
126	}
127	para := list.TemporaryParagraph
128	list.TemporaryParagraph = nil
129	if para != nil {
130		lines := para.Lines()
131		l := lines.Len()
132		for i := 0; i < l; i++ {
133			term := ast.NewDefinitionTerm()
134			segment := lines.At(i)
135			term.Lines().Append(segment.TrimRightSpace(reader.Source()))
136			list.AppendChild(list, term)
137		}
138		para.Parent().RemoveChild(para.Parent(), para)
139	}
140	cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
141	reader.AdvanceAndSetPadding(cpos+1, padding)
142
143	return ast.NewDefinitionDescription(), parser.HasChildren
144}
145
146func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
147	// definitionListParser detects end of the description.
148	// so this method will never be called.
149	return parser.Continue | parser.HasChildren
150}
151
152func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
153	desc := node.(*ast.DefinitionDescription)
154	desc.IsTight = !desc.HasBlankPreviousLines()
155	if desc.IsTight {
156		for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
157			paragraph, ok := gc.(*gast.Paragraph)
158			if ok {
159				textBlock := gast.NewTextBlock()
160				textBlock.SetLines(paragraph.Lines())
161				desc.ReplaceChild(desc, paragraph, textBlock)
162			}
163		}
164	}
165}
166
167func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
168	return true
169}
170
171func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
172	return false
173}
174
175// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
176// renders DefinitionList nodes.
177type DefinitionListHTMLRenderer struct {
178	html.Config
179}
180
181// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
182func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
183	r := &DefinitionListHTMLRenderer{
184		Config: html.NewConfig(),
185	}
186	for _, opt := range opts {
187		opt.SetHTMLOption(&r.Config)
188	}
189	return r
190}
191
192// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
193func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
194	reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
195	reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
196	reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
197}
198
199// DefinitionListAttributeFilter defines attribute names which dl elements can have.
200var DefinitionListAttributeFilter = html.GlobalAttributeFilter
201
202func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
203	if entering {
204		if n.Attributes() != nil {
205			_, _ = w.WriteString("<dl")
206			html.RenderAttributes(w, n, DefinitionListAttributeFilter)
207			_, _ = w.WriteString(">\n")
208		} else {
209			_, _ = w.WriteString("<dl>\n")
210		}
211	} else {
212		_, _ = w.WriteString("</dl>\n")
213	}
214	return gast.WalkContinue, nil
215}
216
217// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
218var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
219
220func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
221	if entering {
222		if n.Attributes() != nil {
223			_, _ = w.WriteString("<dt")
224			html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
225			_ = w.WriteByte('>')
226		} else {
227			_, _ = w.WriteString("<dt>")
228		}
229	} else {
230		_, _ = w.WriteString("</dt>\n")
231	}
232	return gast.WalkContinue, nil
233}
234
235// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
236var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
237
238func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
239	if entering {
240		n := node.(*ast.DefinitionDescription)
241		_, _ = w.WriteString("<dd")
242		if n.Attributes() != nil {
243			html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
244		}
245		if n.IsTight {
246			_, _ = w.WriteString(">")
247		} else {
248			_, _ = w.WriteString(">\n")
249		}
250	} else {
251		_, _ = w.WriteString("</dd>\n")
252	}
253	return gast.WalkContinue, nil
254}
255
256type definitionList struct {
257}
258
259// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
260var DefinitionList = &definitionList{}
261
262func (e *definitionList) Extend(m goldmark.Markdown) {
263	m.Parser().AddOptions(parser.WithBlockParsers(
264		util.Prioritized(NewDefinitionListParser(), 101),
265		util.Prioritized(NewDefinitionDescriptionParser(), 102),
266	))
267	m.Renderer().AddOptions(renderer.WithNodeRenderers(
268		util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
269	))
270}