goldmark/extension/definition_list.go
Karel Bilek 7cdc0fb06f Fix leading tabs with codeblocks
Note that this is a breaking change and will require new goldmark major version.

I have tried to fix problem with leading tabs in fenced code blocks (and probably normal code blocks too).

Important note - tabs do not behave like "just 4 spaces". They "finish" 4 space columns. So tab can behave like anything between 1 space to 4 spaces, depending on position.

If you have MD like this (. represents space, [tb] , [t] or [] tabs)

```
*.some.text
..```
..foo
..[]foo
..```
```

you expect the tab to be kept in the code. This did not work properly in goldmark and I fixed that.

However, if you have a code like this

```
*.some.text
..```
..foo
.[t]foo
..```
```

what should happen? I decided that it should be two spaces, as the tab is not "completely" in the code block. Similarly, what should happen in this case

```
*.some.text
..```
..foo
.[t][tb]foo
..```
```

I decided that it should be first three spaces and then tab. Not sure what even is the correct solution here...

The crux of the fix is - text segments don't have just padding, but also remember what chars is the padding and then print that, if they are called to do so in the code blocks. In other cases, the paddingChars are ignored.

This should fix #177 .
2021-01-29 14:55:55 +07:00

270 lines
8 KiB
Go

package extension
import (
"github.com/yuin/goldmark"
gast "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type definitionListParser struct {
}
var defaultDefinitionListParser = &definitionListParser{}
// NewDefinitionListParser return a new parser.BlockParser that
// can parse PHP Markdown Extra Definition lists.
func NewDefinitionListParser() parser.BlockParser {
return defaultDefinitionListParser
}
func (b *definitionListParser) Trigger() []byte {
return []byte{':'}
}
func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
if _, ok := parent.(*ast.DefinitionList); ok {
return nil, parser.NoChildren
}
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
indent := pc.BlockIndent()
if pos < 0 || line[pos] != ':' || indent != 0 {
return nil, parser.NoChildren
}
last := parent.LastChild()
// need 1 or more spaces after ':'
w, _ := util.IndentWidth(line[pos+1:], pos+1)
if w < 1 {
return nil, parser.NoChildren
}
if w >= 8 { // starts with indented code
w = 5
}
w += pos + 1 /* 1 = ':' */
para, lastIsParagraph := last.(*gast.Paragraph)
var list *ast.DefinitionList
status := parser.HasChildren
var ok bool
if lastIsParagraph {
list, ok = last.PreviousSibling().(*ast.DefinitionList)
if ok { // is not first item
list.Offset = w
list.TemporaryParagraph = para
} else { // is first item
list = ast.NewDefinitionList(w, para)
status |= parser.RequireParagraph
}
} else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
list.Offset = w
list.TemporaryParagraph = nil
} else {
return nil, parser.NoChildren
}
return list, status
}
func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
line, _ := reader.PeekLine()
if util.IsBlank(line) {
return parser.Continue | parser.HasChildren
}
list, _ := node.(*ast.DefinitionList)
w, _ := util.IndentWidth(line, reader.LineOffset())
if w < list.Offset {
return parser.Close
}
pos, padding, chars := util.IndentPosition(line, reader.LineOffset(), list.Offset)
reader.AdvanceAndSetPadding(pos, padding, chars)
return parser.Continue | parser.HasChildren
}
func (b *definitionListParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
// nothing to do
}
func (b *definitionListParser) CanInterruptParagraph() bool {
return true
}
func (b *definitionListParser) CanAcceptIndentedLine() bool {
return false
}
type definitionDescriptionParser struct {
}
var defaultDefinitionDescriptionParser = &definitionDescriptionParser{}
// NewDefinitionDescriptionParser return a new parser.BlockParser that
// can parse definition description starts with ':'.
func NewDefinitionDescriptionParser() parser.BlockParser {
return defaultDefinitionDescriptionParser
}
func (b *definitionDescriptionParser) Trigger() []byte {
return []byte{':'}
}
func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
indent := pc.BlockIndent()
if pos < 0 || line[pos] != ':' || indent != 0 {
return nil, parser.NoChildren
}
list, _ := parent.(*ast.DefinitionList)
if list == nil {
return nil, parser.NoChildren
}
para := list.TemporaryParagraph
list.TemporaryParagraph = nil
if para != nil {
lines := para.Lines()
l := lines.Len()
for i := 0; i < l; i++ {
term := ast.NewDefinitionTerm()
segment := lines.At(i)
term.Lines().Append(segment.TrimRightSpace(reader.Source()))
list.AppendChild(list, term)
}
para.Parent().RemoveChild(para.Parent(), para)
}
cpos, padding, chars := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
reader.AdvanceAndSetPadding(cpos, padding, chars)
return ast.NewDefinitionDescription(), parser.HasChildren
}
func (b *definitionDescriptionParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
// definitionListParser detects end of the description.
// so this method will never be called.
return parser.Continue | parser.HasChildren
}
func (b *definitionDescriptionParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
desc := node.(*ast.DefinitionDescription)
desc.IsTight = !desc.HasBlankPreviousLines()
if desc.IsTight {
for gc := desc.FirstChild(); gc != nil; gc = gc.NextSibling() {
paragraph, ok := gc.(*gast.Paragraph)
if ok {
textBlock := gast.NewTextBlock()
textBlock.SetLines(paragraph.Lines())
desc.ReplaceChild(desc, paragraph, textBlock)
}
}
}
}
func (b *definitionDescriptionParser) CanInterruptParagraph() bool {
return true
}
func (b *definitionDescriptionParser) CanAcceptIndentedLine() bool {
return false
}
// DefinitionListHTMLRenderer is a renderer.NodeRenderer implementation that
// renders DefinitionList nodes.
type DefinitionListHTMLRenderer struct {
html.Config
}
// NewDefinitionListHTMLRenderer returns a new DefinitionListHTMLRenderer.
func NewDefinitionListHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
r := &DefinitionListHTMLRenderer{
Config: html.NewConfig(),
}
for _, opt := range opts {
opt.SetHTMLOption(&r.Config)
}
return r
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindDefinitionList, r.renderDefinitionList)
reg.Register(ast.KindDefinitionTerm, r.renderDefinitionTerm)
reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
}
// DefinitionListAttributeFilter defines attribute names which dl elements can have.
var DefinitionListAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<dl")
html.RenderAttributes(w, n, DefinitionListAttributeFilter)
_, _ = w.WriteString(">\n")
} else {
_, _ = w.WriteString("<dl>\n")
}
} else {
_, _ = w.WriteString("</dl>\n")
}
return gast.WalkContinue, nil
}
// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<dt")
html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<dt>")
}
} else {
_, _ = w.WriteString("</dt>\n")
}
return gast.WalkContinue, nil
}
// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
n := node.(*ast.DefinitionDescription)
_, _ = w.WriteString("<dd")
if n.Attributes() != nil {
html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
}
if n.IsTight {
_, _ = w.WriteString(">")
} else {
_, _ = w.WriteString(">\n")
}
} else {
_, _ = w.WriteString("</dd>\n")
}
return gast.WalkContinue, nil
}
type definitionList struct {
}
// DefinitionList is an extension that allow you to use PHP Markdown Extra Definition lists.
var DefinitionList = &definitionList{}
func (e *definitionList) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(parser.WithBlockParsers(
util.Prioritized(NewDefinitionListParser(), 101),
util.Prioritized(NewDefinitionDescriptionParser(), 102),
))
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(NewDefinitionListHTMLRenderer(), 500),
))
}