Skip to content

Commit bc42321

Browse files
CopilotDongyuZhao
andcommitted
Refactor blockquote builder to eliminate coupling anti-pattern: consume markers and yield back to main builder
Co-authored-by: DongyuZhao <8455725+DongyuZhao@users.noreply.github.com>
1 parent 866ea12 commit bc42321

11 files changed

+96
-96
lines changed

Sources/CodeParserCollection/Markdown/MarkdownConstructState.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ public class MarkdownConstructState: CodeConstructState {
1212
/// Note: This cannot be derived from AST since reference definitions may appear
1313
/// anywhere in the document and need to be available for link resolution
1414
public var referenceDefinitions: [String: (url: String, title: String)] = [:]
15+
16+
/// Current line tokens being processed - builders can modify these
17+
/// This allows builders to consume their part and leave remaining tokens for further processing
18+
public var tokens: [any CodeToken<MarkdownTokenElement>] = []
19+
20+
/// Flag indicating if current line has been fully processed by a builder
21+
/// When false, MarkdownBlockBuilder should continue processing the remaining tokens
22+
public var currentLineProcessed: Bool = true
1523

1624
public init() {}
1725

Sources/CodeParserCollection/Markdown/Nodes/MarkdownATXHeadingBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public class MarkdownATXHeadingBuilder: MarkdownBlockBuilderProtocol {
189189
return result
190190
}
191191

192-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
192+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
193193
// ATX headings are single-line blocks, no processing needed
194194
return false
195195
}

Sources/CodeParserCollection/Markdown/Nodes/MarkdownBlockBuilder.swift

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -85,27 +85,56 @@ public class MarkdownBlockBuilder: CodeNodeBuilder {
8585
return
8686
}
8787

88-
// Check if any existing block can continue with this line
89-
if let continuingBlock = findBlockThatCanContinue(line, in: context.current) {
90-
// Let the appropriate builder process this line into the existing block
91-
processContinuationLine(line, for: continuingBlock)
92-
return
93-
}
94-
95-
// Check if this line should interrupt any existing blocks
96-
if canLineInterruptExistingBlocks(line) {
97-
closeInterruptibleBlocks(context: &context)
98-
}
99-
100-
// Try to start a new block with this line
101-
if let newBlock = tryCreateNewBlock(for: line) {
102-
// Add the new block to the AST
103-
context.current.append(newBlock as! MarkdownNodeBase)
104-
// Process the opening line
105-
processOpeningLine(line, for: newBlock)
106-
} else {
107-
// Fallback to paragraph
108-
createAndProcessParagraph(for: line, context: &context)
88+
// Store the current line in state for builders to process
89+
if var state = context.state as? MarkdownConstructState {
90+
state.tokens = line.tokens
91+
state.currentLineProcessed = false
92+
93+
// Keep processing until the line is fully processed
94+
while !state.currentLineProcessed && !state.tokens.isEmpty {
95+
state.currentLineProcessed = true // Will be set to false if a builder yields back
96+
97+
// Check if any existing block can continue with current tokens
98+
let currentLine = MarkdownLine(tokens: state.tokens, lineNumber: line.lineNumber)
99+
if let continuingBlock = findBlockThatCanContinue(currentLine, in: context.current) {
100+
// Let the appropriate builder process tokens and potentially modify state
101+
processContinuationLine(currentLine, for: continuingBlock, state: &state)
102+
} else {
103+
// Check if this line should interrupt any existing blocks
104+
if canLineInterruptExistingBlocks(MarkdownLine(tokens: state.tokens, lineNumber: line.lineNumber)) {
105+
closeInterruptibleBlocks(context: &context)
106+
}
107+
108+
// Try to start a new block with current tokens
109+
let currentLine = MarkdownLine(tokens: state.tokens, lineNumber: line.lineNumber)
110+
if let newBlock = tryCreateNewBlock(for: currentLine) {
111+
// Add the new block to the AST
112+
context.current.append(newBlock as! MarkdownNodeBase)
113+
114+
// If this is a container block (like blockquote), update context to point to it
115+
let wasContainer = isContainerBlock(newBlock)
116+
if wasContainer {
117+
context.current = newBlock as! MarkdownNodeBase
118+
}
119+
120+
// Process the opening line and potentially modify state
121+
processOpeningLine(currentLine, for: newBlock, state: &state)
122+
123+
// If we made current point to a container and processing isn't complete,
124+
// continue processing within that container
125+
if wasContainer && !state.currentLineProcessed {
126+
continue
127+
}
128+
} else {
129+
// Fallback to paragraph
130+
createAndProcessParagraph(for: currentLine, context: &context, state: &state)
131+
break // Paragraph consumes everything
132+
}
133+
}
134+
}
135+
136+
// Update context state
137+
context.state = state
109138
}
110139
}
111140

@@ -191,37 +220,37 @@ public class MarkdownBlockBuilder: CodeNodeBuilder {
191220
}
192221

193222
/// Process a line that continues an existing block
194-
private func processContinuationLine(_ line: MarkdownLine, for block: any MarkdownBlockNode) {
223+
private func processContinuationLine(_ line: MarkdownLine, for block: any MarkdownBlockNode, state: inout MarkdownConstructState) {
195224
// Find the builder for this block and let it process the line
196225
for builder in blockBuilders {
197226
if builder.canContinue(block: block, line: line) {
198-
_ = builder.processLine(block: block, line: line)
227+
_ = builder.processLine(block: block, line: line, state: &state)
199228
return
200229
}
201230
}
202231
}
203232

204233
/// Process a line that opens a new block
205-
private func processOpeningLine(_ line: MarkdownLine, for block: any MarkdownBlockNode) {
234+
private func processOpeningLine(_ line: MarkdownLine, for block: any MarkdownBlockNode, state: inout MarkdownConstructState) {
206235
// Find the builder for this block and let it process the opening line
207236
for builder in blockBuilders {
208237
if canBuilderHandle(builder, blockType: block.blockType) {
209-
_ = builder.processLine(block: block, line: line)
238+
_ = builder.processLine(block: block, line: line, state: &state)
210239
return
211240
}
212241
}
213242
}
214243

215244
/// Create and process a paragraph for this line
216-
private func createAndProcessParagraph(for line: MarkdownLine, context: inout CodeConstructContext<Node, Token>) {
245+
private func createAndProcessParagraph(for line: MarkdownLine, context: inout CodeConstructContext<Node, Token>, state: inout MarkdownConstructState) {
217246
// Create a new paragraph
218247
let paragraph = createParagraphBlock()
219248
context.current.append(paragraph as! MarkdownNodeBase)
220249

221250
// Process the line into the paragraph
222251
for builder in blockBuilders {
223252
if builder is MarkdownParagraphBuilder {
224-
_ = builder.processLine(block: paragraph, line: line)
253+
_ = builder.processLine(block: paragraph, line: line, state: &state)
225254
return
226255
}
227256
}
@@ -235,6 +264,15 @@ public class MarkdownBlockBuilder: CodeNodeBuilder {
235264
return ParagraphNode(range: range)
236265
}
237266

267+
/// Check if a block is a container block that can contain other blocks
268+
private func isContainerBlock(_ block: any MarkdownBlockNode) -> Bool {
269+
switch block.blockType {
270+
case "blockquote": return true
271+
case "list_item": return true
272+
default: return false
273+
}
274+
}
275+
238276
/// Check if a builder can handle a specific block type
239277
private func canBuilderHandle(_ builder: MarkdownBlockBuilderProtocol, blockType: String) -> Bool {
240278
switch blockType {

Sources/CodeParserCollection/Markdown/Nodes/MarkdownBlockBuilderProtocol.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ public protocol MarkdownBlockBuilderProtocol {
2626
/// - Parameters:
2727
/// - block: The existing block to add content to
2828
/// - line: The line tokens to process
29+
/// - state: The construction state that can be modified by the builder
2930
/// - Returns: True if the line was successfully processed
30-
func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool
31+
func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool
3132

3233
/// Close and finalize a block (post-processing)
3334
/// - Parameter block: The block to finalize

Sources/CodeParserCollection/Markdown/Nodes/MarkdownBlockquoteBuilder.swift

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -59,80 +59,33 @@ public class MarkdownBlockquoteBuilder: MarkdownBlockBuilderProtocol {
5959
blockquote.contentColumn = MarkdownIndentation.findContentColumn(tokens: line.tokens, afterMarkerAt: markerColumn)
6060
}
6161

62-
// Process the initial line
63-
_ = processLine(block: blockquote, line: line)
64-
6562
return blockquote
6663
}
6764

68-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
65+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
6966
guard let blockquote = block as? MarkdownBlockquote else { return false }
7067

7168
// Find content tokens after the '>' marker using package-level properties
7269
var contentTokens: [any CodeToken<MarkdownTokenElement>] = []
7370

74-
let (found, _, _) = MarkdownIndentation.findMarkerPosition(tokens: line.tokens, marker: ">", afterWhitespace: true)
71+
let (found, _, _) = MarkdownIndentation.findMarkerPosition(tokens: state.tokens, marker: ">", afterWhitespace: true)
7572
if found {
7673
// Remove content up to the content column (after '> ')
77-
contentTokens = MarkdownIndentation.removeIndentation(from: line.tokens, upToColumn: blockquote.contentColumn)
74+
contentTokens = MarkdownIndentation.removeIndentation(from: state.tokens, upToColumn: blockquote.contentColumn)
7875
} else {
7976
// Lazy continuation - use tokens after the blockquote's indent
80-
contentTokens = MarkdownIndentation.removeIndentation(from: line.tokens, upToColumn: blockquote.indent)
77+
contentTokens = MarkdownIndentation.removeIndentation(from: state.tokens, upToColumn: blockquote.indent)
8178
}
8279

83-
// Add content tokens to a temporary buffer for recursive parsing
84-
// We'll accumulate all blockquote content and then parse it recursively
85-
if !blockquote.children.isEmpty && blockquote.children.last?.element == .content {
86-
// Continue accumulating content
87-
if let contentNode = blockquote.children.last as? ContentNode {
88-
// Add a newline between lines for proper parsing
89-
if !contentNode.tokens.isEmpty {
90-
let syntheticNewline = MarkdownToken(element: .newline, text: "\n", range: "".startIndex..<"".endIndex)
91-
contentNode.tokens.append(syntheticNewline)
92-
}
93-
contentNode.tokens.append(contentsOf: contentTokens)
94-
}
95-
} else {
96-
// Create new content accumulator
97-
let contentNode = ContentNode(tokens: contentTokens)
98-
blockquote.children.append(contentNode)
99-
}
80+
// Update state with remaining content tokens for MarkdownBlockBuilder to process
81+
state.tokens = contentTokens
82+
state.currentLineProcessed = false // Signal that remaining tokens need processing
10083

10184
return true
10285
}
10386

104-
/// Close the block and parse accumulated content recursively
87+
/// Close the block - no special processing needed as content is parsed recursively by MarkdownBlockBuilder
10588
public func closeBlock(block: any MarkdownBlockNode) {
106-
guard let blockquote = block as? MarkdownBlockquote else { return }
107-
108-
// Find all accumulated content
109-
var allContentTokens: [any CodeToken<MarkdownTokenElement>] = []
110-
for child in blockquote.children {
111-
if let contentNode = child as? ContentNode {
112-
allContentTokens.append(contentsOf: contentNode.tokens)
113-
}
114-
}
115-
116-
// Clear the temporary content nodes
117-
blockquote.children.removeAll()
118-
119-
// Create a new parsing context for the blockquote content
120-
if !allContentTokens.isEmpty {
121-
let language = MarkdownLanguage()
122-
let subBuilder = MarkdownBlockBuilder()
123-
124-
// Create parsing context for the content
125-
var state = MarkdownConstructState()
126-
var contentContext = CodeConstructContext<MarkdownNodeElement, MarkdownTokenElement>(
127-
root: blockquote,
128-
current: blockquote,
129-
tokens: allContentTokens,
130-
consuming: 0,
131-
state: state
132-
)
133-
134-
// Parse the content recursively
135-
_ = subBuilder.build(from: &contentContext)
136-
}
89+
// No special closing logic needed - the recursive parsing is handled by MarkdownBlockBuilder
13790
}
13891
}

Sources/CodeParserCollection/Markdown/Nodes/MarkdownFencedCodeBlockBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public class MarkdownFencedCodeBlockBuilder: MarkdownBlockBuilderProtocol {
156156
return codeBlock
157157
}
158158

159-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
159+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
160160
guard let codeBlock = block as? MarkdownFencedCodeBlock else { return false }
161161

162162
// Check if this is a closing fence

Sources/CodeParserCollection/Markdown/Nodes/MarkdownIndentedCodeBlockBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class MarkdownIndentedCodeBlockBuilder: MarkdownBlockBuilderProtocol {
2929
return codeBlock
3030
}
3131

32-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
32+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
3333
guard let codeBlock = block as? CodeBlockNode else { return false }
3434

3535
if line.isBlank {

Sources/CodeParserCollection/Markdown/Nodes/MarkdownListItemBuilder.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,10 @@ public class MarkdownListItemBuilder: MarkdownBlockBuilderProtocol {
154154
// Set the old properties for backward compatibility
155155
listItem.contentIndent = contentColumn
156156

157-
// Process the content after marker
158-
_ = processLine(block: listItem, line: line)
159-
160157
return listItem
161158
}
162159

163-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
160+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
164161
guard let listItem = block as? MarkdownListItem else { return false }
165162

166163
var contentTokens: [any CodeToken<MarkdownTokenElement>] = []

Sources/CodeParserCollection/Markdown/Nodes/MarkdownParagraphBuilder.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ public class MarkdownParagraphBuilder: MarkdownBlockBuilderProtocol {
4949
return paragraph
5050
}
5151

52-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
52+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
5353
guard let paragraph = block as? ParagraphNode else { return false }
5454

55-
// Get content tokens (exclude EOF and newline)
56-
var contentTokens = line.tokens.filter { token in
55+
// Get content tokens from state (these may have been processed by other builders)
56+
var contentTokens = state.tokens.filter { token in
5757
token.element != .eof && token.element != .newline
5858
}
5959

@@ -102,6 +102,9 @@ public class MarkdownParagraphBuilder: MarkdownBlockBuilderProtocol {
102102
}
103103
}
104104

105+
// Mark current line as fully processed since paragraph consumes everything
106+
state.currentLineProcessed = true
107+
105108
return true
106109
}
107110

Sources/CodeParserCollection/Markdown/Nodes/MarkdownSetextHeadingBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public class MarkdownSetextHeadingBuilder: MarkdownBlockBuilderProtocol {
5454
return nil
5555
}
5656

57-
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine) -> Bool {
57+
public func processLine(block: any MarkdownBlockNode, line: MarkdownLine, state: inout MarkdownConstructState) -> Bool {
5858
// Setext headings don't process additional lines
5959
return false
6060
}

0 commit comments

Comments
 (0)