Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions Sources/SwiftParser/Markdown/MarkdownContextState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,21 @@ import Foundation
public class MarkdownContextState: CodeContextState {
public typealias Node = MarkdownNodeElement
public typealias Token = MarkdownTokenElement
/// Stack of open emphasis/strong nodes: the node, its parent, delimiter element, and delimiter length
public var openEmphasis: [(node: MarkdownNodeBase, parent: MarkdownNodeBase, element: MarkdownTokenElement, length: Int)] = []
/// Stack of open emphasis/strong delimiters. Each entry stores the node to
/// be created once closed, its parent container, the index at which the
/// delimiter appeared, the token element (`*` or `_`), and the delimiter
/// length (1 for emphasis, 2 for strong).
public var openEmphasis: [(node: MarkdownNodeBase, parent: MarkdownNodeBase, startIndex: Int, element: MarkdownTokenElement, length: Int)] = []

/// Pending delimiter run that has not yet been processed. We accumulate
/// consecutive `*` or `_` tokens here until a non-delimiter token is
/// encountered.
public var pendingDelimiterElement: MarkdownTokenElement?
public var pendingDelimiterCount: Int = 0

/// Indicates that an emphasis delimiter was just opened. This prevents the
/// next text token from merging with a previous `TextNode`.
public var justOpenedDelimiter: Bool = false

public init() {}
}
70 changes: 70 additions & 0 deletions Sources/SwiftParser/Markdown/MarkdownEmphasisConsumer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

/// Consumer for emphasis and strong emphasis following CommonMark rules
public struct MarkdownEmphasisConsumer: CodeTokenConsumer {
public typealias Node = MarkdownNodeElement
public typealias Token = MarkdownTokenElement

public init() {}

public func consume(token: any CodeToken<MarkdownTokenElement>, context: inout CodeContext<MarkdownNodeElement, MarkdownTokenElement>) -> Bool {
guard let mdState = context.state as? MarkdownContextState else { return false }
guard let mdToken = token as? MarkdownToken else { return false }

// Only handle emphasis delimiters and EOF for flushing
if mdToken.isEmphasisDelimiter {
// Accumulate consecutive delimiters
if mdState.pendingDelimiterElement == mdToken.element {
mdState.pendingDelimiterCount += 1
} else {
flushPending(state: mdState, context: &context)
mdState.pendingDelimiterElement = mdToken.element
mdState.pendingDelimiterCount = 1
}
return true
} else {
flushPending(state: mdState, context: &context)
// EOF is consumed here so other consumers don't process it
if mdToken.element == .eof {
return true
}
return false
}
}

private func flushPending(state: MarkdownContextState, context: inout CodeContext<MarkdownNodeElement, MarkdownTokenElement>) {
guard state.pendingDelimiterCount > 0, let element = state.pendingDelimiterElement else { return }
var remaining = state.pendingDelimiterCount

while remaining > 0 {
if let last = state.openEmphasis.last, last.element == element, last.length <= remaining {
// Close existing delimiter
state.openEmphasis.removeLast()
let parent = last.parent
let start = last.startIndex
guard start <= parent.children.count else { continue }
let children = Array(parent.children[start..<parent.children.count])
parent.children.removeSubrange(start..<parent.children.count)
for child in children {
if let mdChild = child as? MarkdownNodeBase {
last.node.append(mdChild)
}
}
parent.append(last.node)
remaining -= last.length
} else {
// Open new delimiter
let length = remaining >= 2 ? 2 : 1
let newNode: MarkdownNodeBase = length == 2 ? StrongNode(content: "") : EmphasisNode(content: "")
let parent = context.current as! MarkdownNodeBase
let startIndex = parent.children.count
state.openEmphasis.append((node: newNode, parent: parent, startIndex: startIndex, element: element, length: length))
state.justOpenedDelimiter = true
remaining -= length
}
}

state.pendingDelimiterCount = 0
state.pendingDelimiterElement = nil
}
}
1 change: 1 addition & 0 deletions Sources/SwiftParser/Markdown/MarkdownLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class MarkdownLanguage: CodeLanguage {
BlockquoteConsumer(),
InlineCodeConsumer(),
InlineFormulaConsumer(),
MarkdownEmphasisConsumer(),
AutolinkConsumer(),
URLConsumer(),
HTMLInlineConsumer(),
Expand Down
7 changes: 6 additions & 1 deletion Sources/SwiftParser/Markdown/MarkdownTokenConsumer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ public struct TextConsumer: CodeTokenConsumer {
switch token.element {
case .text:
let content = token.text
if let last = context.current.children.last as? TextNode {
let mdState = context.state as? MarkdownContextState
if mdState?.justOpenedDelimiter == true {
mdState?.justOpenedDelimiter = false
let textNode = TextNode(content: content)
context.current.append(textNode)
} else if let last = context.current.children.last as? TextNode {
last.content += content
} else {
let textNode = TextNode(content: content)
Expand Down