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
98 changes: 97 additions & 1 deletion Sources/SwiftParser/Languages/MarkdownLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,102 @@ public struct MarkdownLanguage: CodeLanguage {
}
}

public class SetextHeadingBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard token is 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
}
}

var idx = context.index
var sawText = false
while idx < context.tokens.count {
guard let t = context.tokens[idx] as? Token else { return false }
if case .newline = t { break }
if case .eof = t { return false }
sawText = true
idx += 1
}
guard sawText else { return false }
guard idx < context.tokens.count, let nl = context.tokens[idx] as? Token, case .newline = nl else { return false }
idx += 1
guard idx < context.tokens.count else { return false }

var kind: Token?
var count = 0
while idx < context.tokens.count {
guard let tok = context.tokens[idx] as? Token else { return false }
switch tok {
case .dash:
if kind == nil { kind = tok }
if case .dash = kind! { count += 1; idx += 1 } else { return false }
case .equal:
if kind == nil { kind = tok }
if case .equal = kind! { count += 1; idx += 1 } else { return false }
case .text(let s, _):
if s.trimmingCharacters(in: .whitespaces).isEmpty { idx += 1 } else { return false }
case .newline, .eof:
break
default:
return false
}
if idx < context.tokens.count, let next = context.tokens[idx] as? Token, case .newline = next { break }
}
if count == 0 { return false }
if idx < context.tokens.count, let endTok = context.tokens[idx] as? Token {
switch endTok {
case .newline, .eof:
return true
default:
return false
}
}
return false
}
public func build(context: inout CodeContext) {
var text = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
if case .newline = tok {
context.index += 1
break
} else {
text += tok.text
context.index += 1
}
} else { context.index += 1 }
}
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
switch tok {
case .dash, .equal:
context.index += 1
case .text(let s, _) where s.trimmingCharacters(in: .whitespaces).isEmpty:
context.index += 1
case .newline:
context.index += 1
let node = CodeNode(type: Element.heading, value: text.trimmingCharacters(in: .whitespaces))
context.currentNode.addChild(node)
return
case .eof:
context.index += 1
let node = CodeNode(type: Element.heading, value: text.trimmingCharacters(in: .whitespaces))
context.currentNode.addChild(node)
return
default:
context.index += 1
}
} else { context.index += 1 }
}
context.currentNode.addChild(CodeNode(type: Element.heading, value: text.trimmingCharacters(in: .whitespaces)))
}
}

public class ListItemBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
Expand Down Expand Up @@ -855,7 +951,7 @@ public struct MarkdownLanguage: CodeLanguage {

public var tokenizer: CodeTokenizer { Tokenizer() }
public var builders: [CodeElementBuilder] {
[HeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListItemBuilder(), ListItemBuilder(), ImageBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkBuilder(), StrongBuilder(), EmphasisBuilder(), InlineCodeBuilder(), ParagraphBuilder()]
[HeadingBuilder(), SetextHeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListItemBuilder(), ListItemBuilder(), ImageBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkBuilder(), StrongBuilder(), EmphasisBuilder(), InlineCodeBuilder(), ParagraphBuilder()]
}
public var expressionBuilders: [CodeExpressionBuilder] { [] }
public var rootElement: any CodeElement { Element.root }
Expand Down
8 changes: 8 additions & 0 deletions Tests/SwiftParserTests/SwiftParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ final class SwiftParserTests: XCTestCase {
XCTAssertEqual(result.root.children.count, 2)
}

func testMarkdownSetextHeading() {
let parser = SwiftParser()
let source = "Title\n----\n"
let result = parser.parse(source, language: MarkdownLanguage())
XCTAssertEqual(result.errors.count, 0)
XCTAssertEqual(result.root.children.first?.type as? MarkdownLanguage.Element, .heading)
}

func testMarkdownListItem() {
let parser = SwiftParser()
let source = "- item1\n- item2"
Expand Down