@@ -70,7 +70,104 @@ import org.commonmark.node.ThematicBreak
7070import org.commonmark.parser.Parser
7171
7272/* *
73- * Converts common-markdown tree to AstNode tree in a recursive fashion.
73+ * Holds the data for a pending conversion task in the iterative tree traversal.
74+ */
75+ private class ConvertWorkItem (
76+ val startNode : Node ,
77+ val parentAstNode : AstNode ? ,
78+ val initialPrev : AstNode ? ,
79+ val onFirstCreated : (AstNode ? ) -> Unit
80+ )
81+
82+ /* *
83+ * Maps a CommonMark [Node] to its corresponding [AstNodeType].
84+ * Returns null for unrecognized node types (CustomNode, CustomBlock, etc.).
85+ */
86+ private fun convertNodeType (node : Node ): AstNodeType ? = when (node) {
87+ is BlockQuote -> AstBlockQuote
88+ is BulletList -> AstUnorderedList (bulletMarker = node.bulletMarker)
89+ is Code -> AstCode (literal = node.literal)
90+ is Document -> AstDocument
91+ is Emphasis -> AstEmphasis (delimiter = node.openingDelimiter)
92+ is FencedCodeBlock -> AstFencedCodeBlock (
93+ literal = node.literal,
94+ fenceChar = node.fenceChar,
95+ fenceIndent = node.fenceIndent,
96+ fenceLength = node.fenceLength,
97+ info = node.info
98+ )
99+ is HardLineBreak -> AstHardLineBreak
100+ is Heading -> AstHeading (
101+ level = node.level
102+ )
103+ is ThematicBreak -> AstThematicBreak
104+ is HtmlInline -> AstHtmlInline (
105+ literal = node.literal
106+ )
107+ is HtmlBlock -> AstHtmlBlock (
108+ literal = node.literal
109+ )
110+ is Image -> {
111+ if (node.destination == null ) {
112+ null
113+ }
114+ else {
115+ AstImage (
116+ title = node.title ? : " " ,
117+ destination = node.destination
118+ )
119+ }
120+ }
121+ is IndentedCodeBlock -> AstIndentedCodeBlock (
122+ literal = node.literal
123+ )
124+ is Link -> AstLink (
125+ title = node.title ? : " " ,
126+ destination = node.destination
127+ )
128+ is ListItem -> AstListItem
129+ is OrderedList -> AstOrderedList (
130+ startNumber = node.startNumber,
131+ delimiter = node.delimiter
132+ )
133+ is Paragraph -> AstParagraph
134+ is SoftLineBreak -> AstSoftLineBreak
135+ is StrongEmphasis -> AstStrongEmphasis (
136+ delimiter = node.openingDelimiter
137+ )
138+ is Text -> AstText (
139+ literal = node.literal
140+ )
141+ is LinkReferenceDefinition -> AstLinkReferenceDefinition (
142+ title = node.title ? : " " ,
143+ destination = node.destination,
144+ label = node.label
145+ )
146+ is TableBlock -> AstTableRoot
147+ is TableHead -> AstTableHeader
148+ is TableBody -> AstTableBody
149+ is TableRow -> AstTableRow
150+ is TableCell -> AstTableCell (
151+ header = node.isHeader,
152+ alignment = when (node.alignment) {
153+ LEFT -> AstTableCellAlignment .LEFT
154+ CENTER -> AstTableCellAlignment .CENTER
155+ RIGHT -> AstTableCellAlignment .RIGHT
156+ null -> AstTableCellAlignment .LEFT
157+ else -> AstTableCellAlignment .LEFT
158+ }
159+ )
160+ is Strikethrough -> AstStrikethrough (
161+ node.openingDelimiter
162+ )
163+ is CustomNode -> null
164+ is CustomBlock -> null
165+ else -> null
166+ }
167+
168+ /* *
169+ * Converts common-markdown tree to AstNode tree iteratively using an explicit stack,
170+ * avoiding StackOverflowError on deeply nested or long markdown documents.
74171 */
75172internal fun convert (
76173 node : Node ? ,
@@ -79,107 +176,60 @@ internal fun convert(
79176): AstNode ? {
80177 node ? : return null
81178
82- val nodeLinks = AstNodeLinks (
83- parent = parentNode,
84- previous = previousNode,
85- )
179+ var result: AstNode ? = null
180+ val stack = ArrayDeque <ConvertWorkItem >()
181+ stack.addLast(ConvertWorkItem (node, parentNode, previousNode) { result = it })
86182
87- val newNodeType: AstNodeType ? = when (node) {
88- is BlockQuote -> AstBlockQuote
89- is BulletList -> AstUnorderedList (bulletMarker = node.bulletMarker)
90- is Code -> AstCode (literal = node.literal)
91- is Document -> AstDocument
92- is Emphasis -> AstEmphasis (delimiter = node.openingDelimiter)
93- is FencedCodeBlock -> AstFencedCodeBlock (
94- literal = node.literal,
95- fenceChar = node.fenceChar,
96- fenceIndent = node.fenceIndent,
97- fenceLength = node.fenceLength,
98- info = node.info
99- )
100- is HardLineBreak -> AstHardLineBreak
101- is Heading -> AstHeading (
102- level = node.level
103- )
104- is ThematicBreak -> AstThematicBreak
105- is HtmlInline -> AstHtmlInline (
106- literal = node.literal
107- )
108- is HtmlBlock -> AstHtmlBlock (
109- literal = node.literal
110- )
111- is Image -> {
112- if (node.destination == null ) {
113- null
183+ while (stack.isNotEmpty()) {
184+ val item = stack.removeLast()
185+
186+ var prev: AstNode ? = null
187+ var firstCreated: AstNode ? = null
188+ var cmNode: Node ? = item.startNode
189+ var nullTypeNode: Node ? = null
190+
191+ // Iterate through siblings instead of recursing
192+ while (cmNode != null ) {
193+ val nodeType = convertNodeType(cmNode)
194+ val newNode = nodeType?.let {
195+ AstNode (it, AstNodeLinks (
196+ parent = item.parentAstNode,
197+ previous = prev ? : item.initialPrev
198+ ))
114199 }
115- else {
116- AstImage (
117- title = node.title ? : " " ,
118- destination = node.destination
119- )
200+
201+ if (newNode != null ) {
202+ if (firstCreated == null ) firstCreated = newNode
203+ prev?.links?.next = newNode
204+
205+ // Push child processing onto the explicit stack instead of recursing
206+ val child = cmNode.firstChild
207+ if (child != null ) {
208+ stack.addLast(ConvertWorkItem (child, newNode, null ) { newNode.links.firstChild = it })
209+ }
210+
211+ prev = newNode
212+ cmNode = cmNode.next
213+ } else {
214+ // Unrecognized node type — stop sibling chain (preserves original behavior)
215+ nullTypeNode = cmNode
216+ cmNode = null
120217 }
121218 }
122- is IndentedCodeBlock -> AstIndentedCodeBlock (
123- literal = node.literal
124- )
125- is Link -> AstLink (
126- title = node.title ? : " " ,
127- destination = node.destination
128- )
129- is ListItem -> AstListItem
130- is OrderedList -> AstOrderedList (
131- startNumber = node.startNumber,
132- delimiter = node.delimiter
133- )
134- is Paragraph -> AstParagraph
135- is SoftLineBreak -> AstSoftLineBreak
136- is StrongEmphasis -> AstStrongEmphasis (
137- delimiter = node.openingDelimiter
138- )
139- is Text -> AstText (
140- literal = node.literal
141- )
142- is LinkReferenceDefinition -> AstLinkReferenceDefinition (
143- title = node.title ? : " " ,
144- destination = node.destination,
145- label = node.label
146- )
147- is TableBlock -> AstTableRoot
148- is TableHead -> AstTableHeader
149- is TableBody -> AstTableBody
150- is TableRow -> AstTableRow
151- is TableCell -> AstTableCell (
152- header = node.isHeader,
153- alignment = when (node.alignment) {
154- LEFT -> AstTableCellAlignment .LEFT
155- CENTER -> AstTableCellAlignment .CENTER
156- RIGHT -> AstTableCellAlignment .RIGHT
157- null -> AstTableCellAlignment .LEFT
158- else -> AstTableCellAlignment .LEFT
159- }
160- )
161- is Strikethrough -> AstStrikethrough (
162- node.openingDelimiter
163- )
164- is CustomNode -> null
165- is CustomBlock -> null
166- else -> null
167- }
168-
169- val newNode = newNodeType?.let {
170- AstNode (newNodeType, nodeLinks)
171- }
172219
173- if (newNode != null ) {
174- newNode.links.firstChild = convert(node.firstChild, parentNode = newNode, previousNode = null )
175- newNode.links.next = convert(node.next, parentNode = parentNode, previousNode = newNode)
176- }
220+ // Set lastChild on the parent, matching the original recursive behavior
221+ if (nullTypeNode != null ) {
222+ if (nullTypeNode.next == null ) {
223+ item.parentAstNode?.links?.lastChild = null
224+ }
225+ } else {
226+ item.parentAstNode?.links?.lastChild = prev
227+ }
177228
178- if (node.next == null ) {
179- parentNode?.links?.lastChild = newNode
229+ item.onFirstCreated(firstCreated)
180230 }
181231
182- return newNode
232+ return result
183233}
184234
185235public actual class CommonmarkAstNodeParser actual constructor(
0 commit comments