From 93c6797d120ca6d7747e0e88f618f392f06f16a8 Mon Sep 17 00:00:00 2001 From: Dongyu Zhao Date: Tue, 15 Jul 2025 01:49:21 +0800 Subject: [PATCH] Add bare autolink support --- .../Languages/MarkdownLanguage.swift | 40 ++++++++++++++++++- Tests/SwiftParserTests/SwiftParserTests.swift | 9 +++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftParser/Languages/MarkdownLanguage.swift b/Sources/SwiftParser/Languages/MarkdownLanguage.swift index b739ea5..ceb470c 100644 --- a/Sources/SwiftParser/Languages/MarkdownLanguage.swift +++ b/Sources/SwiftParser/Languages/MarkdownLanguage.swift @@ -710,6 +710,44 @@ public struct MarkdownLanguage: CodeLanguage { } } + 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.. Bool { @@ -1016,7 +1054,7 @@ public struct MarkdownLanguage: CodeLanguage { public var tokenizer: CodeTokenizer { Tokenizer() } public var builders: [CodeElementBuilder] { - [HeadingBuilder(), SetextHeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListItemBuilder(), ListItemBuilder(), ImageBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkReferenceDefinitionBuilder(), LinkBuilder(), StrongBuilder(), EmphasisBuilder(), InlineCodeBuilder(), ParagraphBuilder()] + [HeadingBuilder(), SetextHeadingBuilder(), CodeBlockBuilder(), IndentedCodeBlockBuilder(), BlockQuoteBuilder(), ThematicBreakBuilder(), OrderedListItemBuilder(), ListItemBuilder(), ImageBuilder(), HTMLBuilder(), EntityBuilder(), StrikethroughBuilder(), AutoLinkBuilder(), BareAutoLinkBuilder(), TableBuilder(), FootnoteBuilder(), LinkReferenceDefinitionBuilder(), LinkBuilder(), StrongBuilder(), EmphasisBuilder(), InlineCodeBuilder(), ParagraphBuilder()] } public var expressionBuilders: [CodeExpressionBuilder] { [] } public var rootElement: any CodeElement { Element.root } diff --git a/Tests/SwiftParserTests/SwiftParserTests.swift b/Tests/SwiftParserTests/SwiftParserTests.swift index a50f974..4333387 100644 --- a/Tests/SwiftParserTests/SwiftParserTests.swift +++ b/Tests/SwiftParserTests/SwiftParserTests.swift @@ -76,6 +76,15 @@ final class SwiftParserTests: XCTestCase { XCTAssertEqual(result.root.children.first?.type as? MarkdownLanguage.Element, .link) } + func testMarkdownAutoLinkWithoutBrackets() { + let parser = SwiftParser() + let source = "https://example.com" + let result = parser.parse(source, language: MarkdownLanguage()) + XCTAssertEqual(result.errors.count, 0) + XCTAssertEqual(result.root.children.first?.type as? MarkdownLanguage.Element, .autoLink) + XCTAssertEqual(result.root.children.first?.value, "https://example.com") + } + func testMarkdownReferenceLink() { let parser = SwiftParser() let source = "[title][ref]\n[ref]: http://example.com"