Skip to content
Closed
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
92 changes: 86 additions & 6 deletions Sources/SwiftParser/Languages/MarkdownLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public struct MarkdownLanguage: CodeLanguage {
case linkReferenceDefinition
case footnoteDefinition
case footnoteReference
case formulaBlock
}

public enum Token: CodeToken {
Expand All @@ -53,6 +54,9 @@ public struct MarkdownLanguage: CodeLanguage {
case rparen(Range<String.Index>)
case dot(Range<String.Index>)
case number(String, Range<String.Index>)
case doubleDollar(Range<String.Index>)
case backslashLbracket(Range<String.Index>)
case backslashRbracket(Range<String.Index>)
case hardBreak(Range<String.Index>)
case newline(Range<String.Index>)
case eof(Range<String.Index>)
Expand Down Expand Up @@ -80,6 +84,9 @@ public struct MarkdownLanguage: CodeLanguage {
case .rparen: return ")"
case .dot: return "."
case .number: return "number"
case .doubleDollar: return "$$"
case .backslashLbracket: return "\\["
case .backslashRbracket: return "\\]"
case .hardBreak: return "hardBreak"
case .newline: return "newline"
case .eof: return "eof"
Expand Down Expand Up @@ -109,6 +116,9 @@ public struct MarkdownLanguage: CodeLanguage {
case .rparen: return ")"
case .dot: return "."
case .number(let s, _): return s
case .doubleDollar: return "$$"
case .backslashLbracket: return "\\["
case .backslashRbracket: return "\\]"
case .hardBreak, .newline: return "\n"
case .eof: return ""
}
Expand All @@ -120,7 +130,8 @@ public struct MarkdownLanguage: CodeLanguage {
.plus(let r), .backtick(let r), .greaterThan(let r), .exclamation(let r), .tilde(let r),
.equal(let r), .lessThan(let r), .ampersand(let r), .semicolon(let r), .pipe(let r),
.lbracket(let r), .rbracket(let r), .lparen(let r), .rparen(let r), .dot(let r),
.number(_, let r), .hardBreak(let r), .newline(let r), .eof(let r):
.number(_, let r), .doubleDollar(let r), .backslashLbracket(let r), .backslashRbracket(let r),
.hardBreak(let r), .newline(let r), .eof(let r):
return r
}
}
Expand All @@ -140,12 +151,27 @@ public struct MarkdownLanguage: CodeLanguage {
let start = index
advance()
if index < input.endIndex {
let escaped = input[index]
let next = input[index]
advance()
add(.text(String(escaped), start..<index))
if next == "[" {
add(.backslashLbracket(start..<index))
} else if next == "]" {
add(.backslashRbracket(start..<index))
} else {
add(.text(String(next), start..<index))
}
} else {
add(.text("\\", start..<index))
}
} else if ch == "$" {
let start = index
advance()
if index < input.endIndex && input[index] == "$" {
advance()
add(.doubleDollar(start..<index))
} else {
add(.text("$", start..<index))
}
} else if ch == "#" {
let start = index
advance()
Expand Down Expand Up @@ -901,6 +927,59 @@ public struct MarkdownLanguage: CodeLanguage {
}
}

public class FormulaBlockBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if context.index > 0 {
if let prev = context.tokens[context.index - 1] as? Token, case .newline = prev {
// ok
} else if context.index != 0 {
return false
}
}
switch tok {
case .doubleDollar, .backslashLbracket:
return true
default:
return false
}
}
public func build(context: inout CodeContext) {
guard context.index < context.tokens.count else { return }
let token = context.tokens[context.index]
context.index += 1
guard let start = token as? Token else { return }
switch start {
case .doubleDollar, .backslashLbracket:
break
default:
context.errors.append(CodeError("Unexpected token \(start.kindDescription) for formula block", range: start.range))
return
}
var text = ""
var closed = false
while context.index < context.tokens.count {
guard let tok = context.tokens[context.index] as? Token else { context.index += 1; continue }
switch tok {
case .doubleDollar where start.kindDescription == "$$",
.backslashRbracket where start.kindDescription == "\\[":
context.index += 1
closed = true
break
default:
text += tok.text
context.index += 1
}
if closed { break }
}
if !closed {
context.errors.append(CodeError("Unterminated formula block", range: start.range))
}
context.currentNode.addChild(MarkdownFormulaBlockNode(value: text))
}
}

public class HTMLBlockBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
Expand Down Expand Up @@ -1545,7 +1624,7 @@ public struct MarkdownLanguage: CodeLanguage {
while context.index < context.tokens.count {
guard let tok = context.tokens[context.index] as? Token else { context.index += 1; continue }
switch tok {
case .text, .star, .underscore, .backtick:
case .text, .star, .underscore, .backtick, .backslashRbracket:
tokens.append(tok)
context.index += 1
case .hardBreak:
Expand All @@ -1558,7 +1637,8 @@ public struct MarkdownLanguage: CodeLanguage {
context.index += 1
ended = true
case .dash, .hash, .plus, .lbracket,
.greaterThan, .exclamation, .tilde, .equal, .lessThan, .ampersand, .semicolon, .pipe:
.greaterThan, .exclamation, .tilde, .equal, .lessThan, .ampersand, .semicolon, .pipe,
.doubleDollar, .backslashLbracket:
ended = true
case .number:
if context.index + 1 < context.tokens.count,
Expand Down Expand Up @@ -1589,7 +1669,7 @@ public struct MarkdownLanguage: CodeLanguage {

public var tokenizer: CodeTokenizer { Tokenizer() }
public var builders: [CodeElementBuilder] {
[HeadingBuilder(), SetextHeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListBuilder(), UnorderedListBuilder(), ImageBuilder(), HTMLBlockBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), BareAutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkReferenceDefinitionBuilder(), LinkBuilder(), ParagraphBuilder()]
[HeadingBuilder(), SetextHeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListBuilder(), UnorderedListBuilder(), ImageBuilder(), FormulaBlockBuilder(), HTMLBlockBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), BareAutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkReferenceDefinitionBuilder(), LinkBuilder(), ParagraphBuilder()]
}
public var expressionBuilders: [CodeExpressionBuilder] { [] }
public var rootElement: any CodeElement { Element.root }
Expand Down
6 changes: 6 additions & 0 deletions Sources/SwiftParser/Languages/MarkdownNodes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,9 @@ public final class MarkdownFootnoteReferenceNode: CodeNode {
return hasher.finalize()
}
}

public final class MarkdownFormulaBlockNode: CodeNode {
public init(value: String = "", range: Range<String.Index>? = nil) {
super.init(type: MarkdownLanguage.Element.formulaBlock, value: value, range: range)
}
}
17 changes: 17 additions & 0 deletions Tests/SwiftParserTests/SwiftParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -544,4 +544,21 @@ tilde
XCTAssertTrue(elements.contains(e), "Missing \(e)")
}
}

func testInvalidBlockFormulaStart() {
let parser = SwiftParser()
let source = "$ invalid"
let result = parser.parse(source, language: MarkdownLanguage())
XCTAssertGreaterThan(result.root.children.count, 0)
}

func testFormulaBlockParsing() {
let parser = SwiftParser()
let source = "$$\nE=mc^2\n$$"
let result = parser.parse(source, language: MarkdownLanguage())
XCTAssertEqual(result.errors.count, 0)
XCTAssertEqual(result.root.children.count, 1)
let block = result.root.children.first as? MarkdownFormulaBlockNode
XCTAssertEqual(block?.value, "\nE=mc^2\n")
}
}