Skip to content

Commit 779298d

Browse files
committed
fix: #537
1 parent 6e1560e commit 779298d

2 files changed

Lines changed: 76 additions & 78 deletions

File tree

extra_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/yuin/goldmark/parser"
1414
"github.com/yuin/goldmark/renderer/html"
1515
"github.com/yuin/goldmark/testutil"
16+
"github.com/yuin/goldmark/text"
1617
)
1718

1819
var testTimeoutMultiplier = 1.0
@@ -219,3 +220,28 @@ func TestDangerousURLStringCase(t *testing.T) {
219220
t.Error("Dangerous URL should ignore cases:\n" + string(testutil.DiffPretty(expected, b.Bytes())))
220221
}
221222
}
223+
224+
func TestNestedATXHeadingAttributes(t *testing.T) {
225+
markdown := New(WithParserOptions(
226+
parser.WithAutoHeadingID(),
227+
parser.WithAttribute(),
228+
))
229+
230+
source := []byte(`# Heading {test=[a, simple, "attribute", { with="nested values" }]}`)
231+
c := parser.NewContext()
232+
n := markdown.Parser().Parse(text.NewReader(source), parser.WithContext(c))
233+
heading := n.FirstChild()
234+
if heading.Kind() != ast.KindHeading {
235+
t.Fatalf("expected first node to be heading, got %s", heading.Kind().String())
236+
}
237+
tv, ok := heading.Attribute([]byte("test"))
238+
if !ok {
239+
t.Fatal("expected to find attribute 'test'")
240+
}
241+
wv := tv.([]any)[3].(parser.Attributes)
242+
_, ok = wv.Find([]byte("with"))
243+
if !ok {
244+
t.Fatal("expected to find nested attribute 'with'")
245+
}
246+
247+
}

parser/atx_heading.go

Lines changed: 50 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type HeadingConfig struct {
1313
}
1414

1515
// SetOption implements SetOptioner.
16-
func (b *HeadingConfig) SetOption(name OptionName, _ interface{}) {
16+
func (b *HeadingConfig) SetOption(name OptionName, _ any) {
1717
switch name {
1818
case optAutoHeadingID:
1919
b.AutoHeadingID = true
@@ -98,69 +98,48 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context)
9898
if l == 0 {
9999
return nil, NoChildren
100100
}
101-
start := i + l
102-
if start >= len(line) {
103-
start = len(line) - 1
104-
}
105-
origstart := start
106-
stop := len(line) - util.TrimRightSpaceLength(line)
107101

102+
start := min(i+l, len(line)-1)
108103
node := ast.NewHeading(level)
109-
parsed := false
110-
if b.Attribute { // handles special case like ### heading ### {#id}
111-
start--
112-
closureClose := -1
113-
closureOpen := -1
114-
for j := start; j < stop; {
115-
c := line[j]
116-
if util.IsEscapedPunctuation(line, j) {
117-
j += 2
118-
} else if util.IsSpace(c) && j < stop-1 && line[j+1] == '#' {
119-
closureOpen = j + 1
120-
k := j + 1
121-
for ; k < stop && line[k] == '#'; k++ {
122-
}
123-
closureClose = k
124-
break
125-
} else {
126-
j++
127-
}
104+
hl := text.NewSegment(
105+
segment.Start+start-segment.Padding,
106+
segment.Start+len(line)-segment.Padding)
107+
hl = hl.TrimRightSpace(reader.Source())
108+
if hl.Len() == 0 {
109+
reader.AdvanceToEOL()
110+
return node, NoChildren
111+
}
112+
113+
if b.Attribute {
114+
node.Lines().Append(hl)
115+
parseLastLineAttributes(node, reader, pc)
116+
hl = node.Lines().At(0)
117+
node.Lines().Clear()
118+
}
119+
120+
// handle closing sequence of '#' characters
121+
line = hl.Value(reader.Source())
122+
stop := len(line)
123+
if stop == 0 { // empty headings like '##[space]'
124+
stop = 0
125+
} else {
126+
i = stop - 1
127+
for ; line[i] == '#' && i > 0; i-- {
128128
}
129-
if closureClose > 0 {
130-
reader.Advance(closureClose)
131-
attrs, ok := ParseAttributes(reader)
132-
rest, _ := reader.PeekLine()
133-
parsed = ok && util.IsBlank(rest)
134-
if parsed {
135-
for _, attr := range attrs {
136-
node.SetAttribute(attr.Name, attr.Value)
137-
}
138-
node.Lines().Append(text.NewSegment(
139-
segment.Start+start+1-segment.Padding,
140-
segment.Start+closureOpen-segment.Padding))
141-
}
129+
if i == 0 { // empty headings like '### ###'
130+
reader.AdvanceToEOL()
131+
return node, NoChildren
142132
}
143-
}
144-
if !parsed {
145-
start = origstart
146-
stop := len(line) - util.TrimRightSpaceLength(line)
147-
if stop <= start { // empty headings like '##[space]'
148-
stop = start
149-
} else {
150-
i = stop - 1
151-
for ; line[i] == '#' && i >= start; i-- {
152-
}
153-
if i != stop-1 && !util.IsSpace(line[i]) {
154-
i = stop - 1
155-
}
156-
i++
133+
if i == 0 || util.IsSpace(line[i]) {
157134
stop = i
158-
}
135+
stop -= util.TrimRightSpaceLength(line[0:stop])
159136

160-
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
161-
node.Lines().Append(text.NewSegment(segment.Start+start-segment.Padding, segment.Start+stop-segment.Padding))
162137
}
163138
}
139+
hl.Stop = hl.Start + stop
140+
node.Lines().Append(hl)
141+
reader.AdvanceToEOL()
142+
164143
return node, NoChildren
165144
}
166145

@@ -169,13 +148,6 @@ func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Contex
169148
}
170149

171150
func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
172-
if b.Attribute {
173-
_, ok := node.AttributeString("id")
174-
if !ok {
175-
parseLastLineAttributes(node, reader, pc)
176-
}
177-
}
178-
179151
if b.AutoHeadingID {
180152
id, ok := node.AttributeString("id")
181153
if !ok {
@@ -205,44 +177,44 @@ func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) {
205177
node.SetAttribute(attrNameID, headingID)
206178
}
207179

208-
func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) {
180+
func parseLastLineAttributes(node ast.Node, reader text.Reader, _ Context) {
209181
lastIndex := node.Lines().Len() - 1
210182
if lastIndex < 0 { // empty headings
211183
return
212184
}
213185
lastLine := node.Lines().At(lastIndex)
214186
line := lastLine.Value(reader.Source())
215187
lr := text.NewReader(line)
216-
var attrs Attributes
217-
var ok bool
218188
var start text.Segment
219189
var sl int
220-
var end text.Segment
221190
for {
222191
c := lr.Peek()
223-
if c == text.EOF {
192+
if c == text.EOF || c == '\n' {
224193
break
225194
}
226195
if c == '\\' {
227196
lr.Advance(1)
228-
if lr.Peek() == '{' {
197+
if util.IsPunct(lr.Peek()) {
229198
lr.Advance(1)
230199
}
231200
continue
232201
}
233202
if c == '{' {
234203
sl, start = lr.Position()
235-
attrs, ok = ParseAttributes(lr)
236-
_, end = lr.Position()
204+
attrs, ok := ParseAttributes(lr)
205+
if ok {
206+
if nl, _ := lr.PeekLine(); nl == nil || util.IsBlank(nl) {
207+
for _, attr := range attrs {
208+
node.SetAttribute(attr.Name, attr.Value)
209+
}
210+
lastLine.Stop = lastLine.Start + start.Start
211+
lastLine = lastLine.TrimRightSpace(reader.Source())
212+
node.Lines().Set(lastIndex, lastLine)
213+
return
214+
}
215+
}
237216
lr.SetPosition(sl, start)
238217
}
239218
lr.Advance(1)
240219
}
241-
if ok && util.IsBlank(line[end.Start:]) {
242-
for _, attr := range attrs {
243-
node.SetAttribute(attr.Name, attr.Value)
244-
}
245-
lastLine.Stop = lastLine.Start + start.Start
246-
node.Lines().Set(lastIndex, lastLine)
247-
}
248220
}

0 commit comments

Comments
 (0)