Skip to content

Commit 4a3408c

Browse files
authored
passthrough: Ensure walk does not terminate early
Fixes #4 Co-authored-by: Jeremy Kun <j2kun@users.noreply.github.com>
1 parent eacfb02 commit 4a3408c

File tree

2 files changed

+92
-20
lines changed

2 files changed

+92
-20
lines changed

‎passthrough/passthrough.go‎

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type PassthroughInline struct {
3636
Delimiters *Delimiters
3737
}
3838

39-
func NewPassthroughInline(segment text.Segment, delimiters *Delimiters) *PassthroughInline {
39+
func newPassthroughInline(segment text.Segment, delimiters *Delimiters) *PassthroughInline {
4040
return &PassthroughInline{
4141
Segment: segment,
4242
Delimiters: delimiters,
@@ -69,15 +69,15 @@ type inlinePassthroughParser struct {
6969
PassthroughDelimiters []Delimiters
7070
}
7171

72-
func NewInlinePassthroughParser(ds []Delimiters) parser.InlineParser {
72+
func newInlinePassthroughParser(ds []Delimiters) parser.InlineParser {
7373
return &inlinePassthroughParser{
7474
PassthroughDelimiters: ds,
7575
}
7676
}
7777

7878
// Determine if the input slice starts with a full valid opening delimiter.
7979
// If so, returns the delimiter struct, otherwise returns nil.
80-
func GetFullOpeningDelimiter(delims []Delimiters, line []byte) *Delimiters {
80+
func getFullOpeningDelimiter(delims []Delimiters, line []byte) *Delimiters {
8181
for _, d := range delims {
8282
if startsWith(line, d.Open) {
8383
return &d
@@ -92,7 +92,7 @@ func GetFullOpeningDelimiter(delims []Delimiters, line []byte) *Delimiters {
9292
// `Parse` will be executed once for each character that is in this list of
9393
// allowed trigger characters. Our parse function needs to do some additional
9494
// checks because Trigger only works for single-byte delimiters.
95-
func OpenersFirstByte(delims []Delimiters) []byte {
95+
func openersFirstByte(delims []Delimiters) []byte {
9696
var firstBytes []byte
9797
containsBackslash := false
9898
for _, d := range delims {
@@ -111,7 +111,7 @@ func OpenersFirstByte(delims []Delimiters) []byte {
111111
}
112112

113113
// Determine if the input list of delimiters contains the given delimiter pair
114-
func ContainsDelimiters(delims []Delimiters, toFind *Delimiters) bool {
114+
func containsDelimiters(delims []Delimiters, toFind *Delimiters) bool {
115115
for _, d := range delims {
116116
if d.Open == toFind.Open && d.Close == toFind.Close {
117117
return true
@@ -122,7 +122,7 @@ func ContainsDelimiters(delims []Delimiters, toFind *Delimiters) bool {
122122
}
123123

124124
func (s *inlinePassthroughParser) Trigger() []byte {
125-
return OpenersFirstByte(s.PassthroughDelimiters)
125+
return openersFirstByte(s.PassthroughDelimiters)
126126
}
127127

128128
func (s *inlinePassthroughParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
@@ -132,7 +132,7 @@ func (s *inlinePassthroughParser) Parse(parent ast.Node, block text.Reader, pc p
132132
// of multiple triggers with parser.Context state saved between calls.
133133
line, startSegment := block.PeekLine()
134134

135-
fencePair := GetFullOpeningDelimiter(s.PassthroughDelimiters, line)
135+
fencePair := getFullOpeningDelimiter(s.PassthroughDelimiters, line)
136136
// fencePair == nil can happen if only the first byte of an opening delimiter
137137
// matches, but it is not the complete opening delimiter. The trigger causes
138138
// this Parse function to execute, but the trigger interface is limited to
@@ -141,7 +141,7 @@ func (s *inlinePassthroughParser) Parse(parent ast.Node, block text.Reader, pc p
141141
// double-backslash. In this case, we advance and return nil.
142142
if fencePair == nil {
143143
if len(line) > 2 && line[0] == '\\' && line[1] == '\\' {
144-
fencePair = GetFullOpeningDelimiter(s.PassthroughDelimiters, line[2:])
144+
fencePair = getFullOpeningDelimiter(s.PassthroughDelimiters, line[2:])
145145
if fencePair != nil {
146146
// Opening delimiter is escaped, return the escaped opener as plain text
147147
// So that the characters are not processed again.
@@ -178,7 +178,7 @@ func (s *inlinePassthroughParser) Parse(parent ast.Node, block text.Reader, pc p
178178
}
179179

180180
block.Advance(closingDelimiterPos + len(fencePair.Close))
181-
return NewPassthroughInline(seg, fencePair)
181+
return newPassthroughInline(seg, fencePair)
182182
}
183183
}
184184

@@ -213,8 +213,8 @@ func (n *PassthroughBlock) Kind() ast.NodeKind {
213213
return KindPassthroughBlock
214214
}
215215

216-
// NewPassthroughBlock return a new PassthroughBlock node.
217-
func NewPassthroughBlock() *PassthroughBlock {
216+
// newPassthroughBlock return a new PassthroughBlock node.
217+
func newPassthroughBlock() *PassthroughBlock {
218218
return &PassthroughBlock{
219219
BaseBlock: ast.BaseBlock{},
220220
}
@@ -247,18 +247,32 @@ type passthroughInlineTransformer struct {
247247

248248
var PassthroughInlineTransformer = &passthroughInlineTransformer{}
249249

250+
const passthroughMarkedForDeletion = "passthrough_marked_for_deletion"
251+
const passthroughProcessed = "passthrough_processed"
252+
250253
// Note, this transformer destroys the RawText attributes of the paragraph
251254
// nodes that it transforms. However, this does not seem to have an impact on
252255
// rendering.
253256
func (p *passthroughInlineTransformer) Transform(
254257
doc *ast.Document, reader text.Reader, pc parser.Context) {
255-
258+
// Goldmark's walking algorithm is simplistic, and doesn't handle the
259+
// possibility of replacing the current node being walked with a new node. So
260+
// as a workaround, we split the walk in two. The first walk inserts new
261+
// nodes, and marks the original nodes for deletion. The second walk deletes
262+
// the marked nodes. To avoid an infinite loop, we also need to mark the
263+
// newly inserted nodes as "processed" so that they are not re-processed as
264+
// the walk continues.
256265
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
257266
// Anchor on paragraphs
258267
if n.Kind() != ast.KindParagraph || !entering {
259268
return ast.WalkContinue, nil
260269
}
261270

271+
val, found := n.AttributeString(passthroughProcessed)
272+
if found && val == "true" {
273+
return ast.WalkContinue, nil
274+
}
275+
262276
// If no direct children are passthroughs, skip it.
263277
foundInlinePassthrough := false
264278
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
@@ -287,16 +301,19 @@ func (p *passthroughInlineTransformer) Transform(
287301
inline := currentNode.(*PassthroughInline)
288302

289303
// Only split into a new block if the delimiters are block delimiters
290-
if !ContainsDelimiters(p.BlockDelimiters, inline.Delimiters) {
304+
if !containsDelimiters(p.BlockDelimiters, inline.Delimiters) {
291305
currentParagraph.AppendChild(currentParagraph, currentNode)
292306
currentNode = nextNode
293307
continue
294308
}
295309

296-
newBlock := NewPassthroughBlock()
310+
newBlock := newPassthroughBlock()
297311
newBlock.Lines().Append(inline.Segment)
298312
if len(currentParagraph.Text(reader.Source())) > 0 {
299313
parent.InsertAfter(parent, insertionPoint, currentParagraph)
314+
// Since we're not removing the original paragraph, we need to ensure
315+
// that this paragraph is not re-processed as the walk continues
316+
currentParagraph.SetAttributeString(passthroughProcessed, "true")
300317
insertionPoint = currentParagraph
301318
}
302319
parent.InsertAfter(parent, insertionPoint, newBlock)
@@ -308,14 +325,41 @@ func (p *passthroughInlineTransformer) Transform(
308325

309326
if currentParagraph.ChildCount() > 0 {
310327
parent.InsertAfter(parent, insertionPoint, currentParagraph)
328+
// Since we're not removing the original paragraph, we need to ensure
329+
// that this paragraph is not re-processed as the walk continues
330+
currentParagraph.SetAttributeString(passthroughProcessed, "true")
331+
}
332+
333+
// At this point, we don't remove the original paragraph, but mark it
334+
// for removal in the second walk.
335+
n.SetAttributeString(passthroughMarkedForDeletion, "true")
336+
return ast.WalkContinue, nil
337+
})
338+
339+
// Now delete any marked nodes
340+
ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
341+
if !entering {
342+
return ast.WalkContinue, nil
343+
}
344+
345+
for c := n.FirstChild(); c != nil; {
346+
// Have to eagerly fetch this because `c` may be removed from the tree,
347+
// destroying its link to the next sibling.
348+
next := c.NextSibling()
349+
if c.Kind() == ast.KindParagraph {
350+
val, found := c.AttributeString(passthroughMarkedForDeletion)
351+
if found && val == "true" {
352+
n.RemoveChild(n, c)
353+
}
354+
}
355+
c = next
311356
}
312357

313-
parent.RemoveChild(parent, n)
314358
return ast.WalkContinue, nil
315359
})
316360
}
317361

318-
func NewPassthroughInlineTransformer(ds []Delimiters) parser.ASTTransformer {
362+
func newPassthroughInlineTransformer(ds []Delimiters) parser.ASTTransformer {
319363
return &passthroughInlineTransformer{
320364
BlockDelimiters: ds,
321365
}
@@ -331,7 +375,7 @@ func (r *passthroughBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRe
331375
reg.Register(KindPassthroughBlock, r.renderRawBlock)
332376
}
333377

334-
func NewPassthroughInlineRenderer() renderer.NodeRenderer {
378+
func newPassthroughInlineRenderer() renderer.NodeRenderer {
335379
return &passthroughInlineRenderer{}
336380
}
337381

@@ -368,15 +412,15 @@ func NewPassthroughWithDelimiters(
368412
func (e *passthrough) Extend(m goldmark.Markdown) {
369413
m.Parser().AddOptions(
370414
parser.WithInlineParsers(
371-
util.Prioritized(NewInlinePassthroughParser(e.InlineDelimiters), 201),
415+
util.Prioritized(newInlinePassthroughParser(e.InlineDelimiters), 201),
372416
),
373417
parser.WithASTTransformers(
374-
util.Prioritized(NewPassthroughInlineTransformer(e.BlockDelimiters), 0),
418+
util.Prioritized(newPassthroughInlineTransformer(e.BlockDelimiters), 0),
375419
),
376420
)
377421

378422
m.Renderer().AddOptions(renderer.WithNodeRenderers(
379-
util.Prioritized(NewPassthroughInlineRenderer(), 101),
423+
util.Prioritized(newPassthroughInlineRenderer(), 101),
380424
util.Prioritized(NewPassthroughBlockRenderer(), 99),
381425
))
382426
}

‎passthrough/passthrough_test.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,31 @@ $$y$$
518518
c := qt.New(t)
519519
c.Assert(actual, qt.Equals, expected)
520520
}
521+
522+
func TestExample27(t *testing.T) {
523+
input := `Block $$a^*=x-b^*$$ equation
524+
525+
Inline $a^*=x-b^*$ equation`
526+
expected := `<p>Block </p>
527+
$$a^*=x-b^*$$
528+
<p> equation</p>
529+
<p>Inline $a^*=x-b^*$ equation</p>`
530+
actual := Parse(t, input)
531+
532+
c := qt.New(t)
533+
c.Assert(actual, qt.Equals, expected)
534+
}
535+
536+
func TestExample28(t *testing.T) {
537+
input := `Inline $a^*=x-b^*$ equation
538+
539+
Block $$a^*=x-b^*$$ equation`
540+
expected := `<p>Inline $a^*=x-b^*$ equation</p>
541+
<p>Block </p>
542+
$$a^*=x-b^*$$
543+
<p> equation</p>`
544+
actual := Parse(t, input)
545+
546+
c := qt.New(t)
547+
c.Assert(actual, qt.Equals, expected)
548+
}

0 commit comments

Comments
 (0)