diff --git a/README.md b/README.md index 7dd8b7d..b7735a5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,14 @@ [GoldMark](https://github.com/yuin/goldmark/) inline attributes extension. +This implements the [`bracketed_spans`](https://pandoc.org/MANUAL.html#extension-bracketed_spans) of pandoc. + ```markdown -[Attention]{.underline} some text +[This is *some text*]{.class key="val"} outside text ``` ```html -

Attention some text

+

This is some text outside text

``` ```go diff --git a/extend_test.go b/extend_test.go index 7a797b5..1007aa9 100644 --- a/extend_test.go +++ b/extend_test.go @@ -10,7 +10,7 @@ import ( func TestAttributes(t *testing.T) { source := []byte(` -[Text underlined]{.underline} +[This is *some text*]{.class key="val"} outside text `) var md = goldmark.New(Enable) diff --git a/parser.go b/parser.go index 2cebb8c..efe30c4 100644 --- a/parser.go +++ b/parser.go @@ -1,13 +1,16 @@ package attributes import ( - "bytes" + "fmt" + "strings" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" ) +var spanContentStateKey = parser.NewContextKey() + type attributesParser struct { } @@ -20,35 +23,155 @@ func NewAttributesParser() parser.InlineParser { } func (s *attributesParser) Trigger() []byte { - return []byte{'['} + return []byte{'[', ']'} } -func (s *attributesParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { - savedLine, savedPosition := block.Position() +var attributeBottom = parser.NewContextKey() +func (s *attributesParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { line, seg := block.PeekLine() - endText := bytes.Index(line, []byte{']'}) - if endText < 0 { - return nil // must close on the same line + if line[0] == '[' { + pc.Set(attributeBottom, pc.LastDelimiter()) + return processSpanContentOpen(block, seg.Start, pc) } - if len(line) <= endText || line[endText+1] != '{' { + // line[0] == ']' + tlist := pc.Get(spanContentStateKey) + if tlist == nil { + return nil + } + + last := tlist.(*spanContentState).Last + if last == nil { + return nil + } + block.Advance(1) + + removeSpanContentState(pc, last) + + c := block.Peek() + if c != '{' { return nil } - block.Advance(endText + 1) attrs, ok := parser.ParseAttributes(block) if !ok { - block.SetPosition(savedLine, savedPosition) return nil } - n := &Node{} + span := &Node{} for _, attr := range attrs { - n.SetAttribute(attr.Name, attr.Value) + span.SetAttribute(attr.Name, attr.Value) } - n.AppendChild(n, ast.NewTextSegment(text.NewSegment(seg.Start+1, seg.Start+endText))) - return n + var bottom ast.Node + if v := pc.Get(attributeBottom); v != nil { + bottom = v.(ast.Node) + } + pc.Set(attributeBottom, nil) + parser.ProcessDelimiters(bottom, pc) + for c := last.NextSibling(); c != nil; { + next := c.NextSibling() + parent.RemoveChild(parent, c) + span.AppendChild(span, c) + c = next + } + + last.Parent().RemoveChild(last.Parent(), last) + return span +} + +type spanContentState struct { + ast.BaseInline + + Segment text.Segment + + Prev *spanContentState + + Next *spanContentState + + First *spanContentState + + Last *spanContentState +} + +func (s *spanContentState) Text(source []byte) []byte { + return s.Segment.Value(source) +} + +func (s *spanContentState) Dump(source []byte, level int) { + fmt.Printf("%sspanContentState: \"%s\"\n", strings.Repeat(" ", level), s.Text(source)) +} + +var kindSpanContentState = ast.NewNodeKind("SpanContentState") + +func (s *spanContentState) Kind() ast.NodeKind { + return kindSpanContentState +} + +func spanContentStateLength(v *spanContentState) int { + if v == nil || v.Last == nil || v.First == nil { + return 0 + } + return v.Last.Segment.Stop - v.First.Segment.Start +} + +func pushSpanContentState(pc parser.Context, v *spanContentState) { + tlist := pc.Get(spanContentStateKey) + var list *spanContentState + if tlist == nil { + list = v + v.First = v + v.Last = v + pc.Set(spanContentStateKey, list) + } else { + list = tlist.(*spanContentState) + l := list.Last + list.Last = v + l.Next = v + v.Prev = l + } +} + +func removeSpanContentState(pc parser.Context, d *spanContentState) { + tlist := pc.Get(spanContentStateKey) + var list *spanContentState + if tlist == nil { + return + } + list = tlist.(*spanContentState) + + if d.Prev == nil { + list = d.Next + if list != nil { + list.First = d + list.Last = d.Last + list.Prev = nil + pc.Set(spanContentStateKey, list) + } else { + pc.Set(spanContentStateKey, nil) + } + } else { + d.Prev.Next = d.Next + if d.Next != nil { + d.Next.Prev = d.Prev + } + } + if list != nil && d.Next == nil { + list.Last = d.Prev + } + d.Next = nil + d.Prev = nil + d.First = nil + d.Last = nil +} + +func processSpanContentOpen(block text.Reader, pos int, pc parser.Context) *spanContentState { + state := &spanContentState{ + Segment: text.NewSegment(pos, pos+1), + } + pushSpanContentState(pc, state) + block.Advance(1) + return state }