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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

extension MarkdownLanguage {
public class AutoLinkBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if case .lessThan = tok { return true }
return false
}
public func build(context: inout CodeContext) {
context.index += 1
var text = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
if case .greaterThan = tok { context.index += 1; break }
else { text += tok.text; context.index += 1 }
} else { context.index += 1 }
}
context.currentNode.addChild(MarkdownAutoLinkNode(url: text))
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

extension MarkdownLanguage {
public class BareAutoLinkBuilder: CodeElementBuilder {
private static let regex: NSRegularExpression = {
let pattern = #"^((https?|ftp)://[^\s<>]+|www\.[^\s<>]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})"#
return try! NSRegularExpression(pattern: pattern, options: [])
}()

public init() {}

public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
let start = tok.range.lowerBound
let text = String(context.input[start...])
let range = NSRange(location: 0, length: text.utf16.count)
if let m = Self.regex.firstMatch(in: text, range: range), m.range.location == 0 {
return true
}
return false
}

public func build(context: inout CodeContext) {
guard let tok = context.tokens[context.index] as? Token else { return }
let start = tok.range.lowerBound
let text = String(context.input[start...])
let range = NSRange(location: 0, length: text.utf16.count)
guard let m = Self.regex.firstMatch(in: text, range: range) else { return }
let endPos = context.input.index(start, offsetBy: m.range.length)
let url = String(context.input[start..<endPos])
context.currentNode.addChild(MarkdownAutoLinkNode(url: url))
while context.index < context.tokens.count {
if let t = context.tokens[context.index] as? Token, t.range.upperBound <= endPos {
context.index += 1
} else {
break
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

extension MarkdownLanguage {
public class BlockQuoteBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if case .greaterThan = tok {
if context.index == 0 { return true }
if let prev = context.tokens[context.index - 1] as? Token, case .newline = prev { return true }
}
return false
}
public func build(context: inout CodeContext) {
context.index += 1 // skip '>'
var text = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
switch tok {
case .newline:
context.index += 1
let node = MarkdownBlockQuoteNode(value: text.trimmingCharacters(in: .whitespaces))
context.currentNode.addChild(node)
return
case .eof:
let node = MarkdownBlockQuoteNode(value: text.trimmingCharacters(in: .whitespaces))
context.currentNode.addChild(node)
context.index += 1
return
default:
text += tok.text
context.index += 1
}
} else { context.index += 1 }
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Foundation

extension MarkdownLanguage {
public class CodeBlockBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let first = token as? Token else { return false }
let fenceKind: String
switch first {
case .backtick: fenceKind = "`"
case .tilde: fenceKind = "~"
default: return false
}
var idx = context.index
var count = 0
while idx < context.tokens.count, let t = context.tokens[idx] as? Token, t.kindDescription == fenceKind {
count += 1; idx += 1
}
guard count >= 3 else { return false }
if context.index == 0 { return true }
if let prev = context.tokens[context.index - 1] as? Token, case .newline = prev {
return true
}
return false
}
public func build(context: inout CodeContext) {
guard let startTok = context.tokens[context.index] as? Token else { return }
let fenceKind = startTok.kindDescription
var fenceLength = 0
while context.index < context.tokens.count, let t = context.tokens[context.index] as? Token, t.kindDescription == fenceKind {
fenceLength += 1
context.index += 1
}
// capture info string until end of line and trim whitespace
var info = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
if case .newline = tok {
context.index += 1
break
} else {
info += tok.text
context.index += 1
}
} else {
context.index += 1
}
}
info = info.trimmingCharacters(in: .whitespaces)
let lang = info.split(whereSeparator: { $0.isWhitespace }).first.map(String.init)

let blockStart = context.index
var text = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
// check for closing fence at start of line
if tok.kindDescription == fenceKind && (context.index == blockStart || (context.index > blockStart && (context.tokens[context.index - 1] as? Token)?.kindDescription == "newline")) {
var idx = context.index
var count = 0
while idx < context.tokens.count, let t = context.tokens[idx] as? Token, t.kindDescription == fenceKind {
count += 1; idx += 1
}
if count >= fenceLength {
context.index = idx
if context.index < context.tokens.count, let nl = context.tokens[context.index] as? Token, case .newline = nl { context.index += 1 }
context.currentNode.addChild(MarkdownCodeBlockNode(lang: lang, content: text))
return
}
}
text += tok.text
context.index += 1
} else { context.index += 1 }
}
context.currentNode.addChild(MarkdownCodeBlockNode(lang: lang, content: text))
}
}

}
34 changes: 34 additions & 0 deletions Sources/SwiftParser/Languages/MarkdownLanguage+Element.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

extension MarkdownLanguage {
public enum Element: String, CodeElement {
case root
case paragraph
case heading
case text
case listItem
case orderedListItem
case unorderedList
case orderedList
case emphasis
case strong
case codeBlock
case inlineCode
case link
case blockQuote
case thematicBreak
case image
case html
case entity
case strikethrough
case table
case tableHeader
case tableRow
case tableCell
case autoLink
case linkReferenceDefinition
case footnoteDefinition
case footnoteReference
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

extension MarkdownLanguage {
public class EmphasisBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if case .star = tok { return true }
if case .underscore = tok { return true }
return false
}
public func build(context: inout CodeContext) {
let snap = context.snapshot()
guard let open = context.tokens[context.index] as? Token else { return }
context.index += 1
let (children, ok) = MarkdownLanguage.parseInline(context: &context, closing: open, count: 1)
if ok {
let node = MarkdownEmphasisNode(value: "")
children.forEach { node.addChild($0) }
context.currentNode.addChild(node)
} else {
context.restore(snap)
context.currentNode.addChild(MarkdownTextNode(value: open.text))
context.index += 1
}
}
}

}
48 changes: 48 additions & 0 deletions Sources/SwiftParser/Languages/MarkdownLanguage+EntityBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

extension MarkdownLanguage {
public class EntityBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if case .ampersand = tok { return true }
return false
}
public func build(context: inout CodeContext) {
context.index += 1
var text = ""
while context.index < context.tokens.count {
if let tok = context.tokens[context.index] as? Token {
if case .semicolon = tok { context.index += 1; break }
else { text += tok.text; context.index += 1 }
} else { context.index += 1 }
}
let decoded = decode(text)
context.currentNode.addChild(MarkdownEntityNode(value: decoded))
}

private func decode(_ entity: String) -> String {
switch entity {
case "amp": return "&"
case "lt": return "<"
case "gt": return ">"
case "quot": return "\""
case "apos": return "'"
default:
if entity.hasPrefix("#x") || entity.hasPrefix("#X") {
let hex = entity.dropFirst(2)
if let value = UInt32(hex, radix: 16), let scalar = UnicodeScalar(value) {
return String(Character(scalar))
}
} else if entity.hasPrefix("#") {
let num = entity.dropFirst()
if let value = UInt32(num), let scalar = UnicodeScalar(value) {
return String(Character(scalar))
}
}
return "&" + entity + ";"
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation

extension MarkdownLanguage {
public class FootnoteBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let lb = token as? Token, case .lbracket = lb else { return false }
guard context.index + 2 < context.tokens.count else { return false }
guard let first = context.tokens[context.index + 1] as? Token else { return false }
if case .text(let s, _) = first, s.starts(with: "^") {
var idx = context.index + 2
while idx < context.tokens.count {
if let t = context.tokens[idx] as? Token {
if case .rbracket = t { return true }
if case .text = t {
idx += 1; continue
}
if case .number = t {
idx += 1; continue
}
}
break
}
}
return false
}
public func build(context: inout CodeContext) {
context.index += 1 // skip [
var id = ""
while context.index < context.tokens.count {
guard let tok = context.tokens[context.index] as? Token else { context.index += 1; continue }
if case .rbracket = tok { break }
id += tok.text
context.index += 1
}
if id.hasPrefix("^") { id.removeFirst() }
if context.index < context.tokens.count { context.index += 1 } // skip ]

if context.index < context.tokens.count,
let colon = context.tokens[context.index] as? Token,
case .text(let s, _) = colon,
s.trimmingCharacters(in: .whitespaces).hasPrefix(":") {
var text = s
context.index += 1
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 }
}
if text.hasPrefix(":") { text.removeFirst() }
let trimmed = text.trimmingCharacters(in: .whitespaces)
context.currentNode.addChild(MarkdownFootnoteDefinitionNode(identifier: id, text: trimmed))
} else {
context.currentNode.addChild(MarkdownFootnoteReferenceNode(identifier: id))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

extension MarkdownLanguage {
public class HTMLBlockBuilder: CodeElementBuilder {
public init() {}
public func accept(context: CodeContext, token: any CodeToken) -> Bool {
guard let tok = token as? Token else { return false }
if case .lessThan = tok, context.index == 0 {
let rest = String(context.input[tok.range.upperBound...]).lowercased()
return rest.hasPrefix("!doctype") || rest.hasPrefix("html")
}
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 { text += tok.text }
context.index += 1
}
let closed = MarkdownLanguage.isHTMLClosed(text)
context.currentNode.addChild(MarkdownHtmlNode(value: text, closed: closed))
}
}

}
Loading