import Foundation public enum HybridMarkdownLineKind: Hashable, Sendable { case paragraph case heading(level: Int, markerRange: NSRange, textRange: NSRange) } public enum HybridMarkdownSpanKind: Hashable, Sendable { case bold case italic case inlineCode case markdownDelimiter } public struct HybridMarkdownSpan: Hashable, Sendable { public var range: NSRange public var kind: HybridMarkdownSpanKind public init(range: NSRange, kind: HybridMarkdownSpanKind) { self.range = range self.kind = kind } } public struct HybridMarkdownLineRenderPlan: Hashable, Sendable { public var line: EditorLine public var kind: HybridMarkdownLineKind public var spans: [HybridMarkdownSpan] public init(line: EditorLine, kind: HybridMarkdownLineKind, spans: [HybridMarkdownSpan]) { self.line = line self.kind = kind self.spans = spans } } public struct HybridMarkdownLineRenderer: Sendable { public init() {} public func renderPlan(for line: EditorLine) -> HybridMarkdownLineRenderPlan { let kind = lineKind(for: line) let spans = inlineSpans(in: line) return HybridMarkdownLineRenderPlan(line: line, kind: kind, spans: spans) } private func lineKind(for line: EditorLine) -> HybridMarkdownLineKind { let markerCount = line.source.prefix { $0 == "#" }.count guard (1...6).contains(markerCount), line.source.dropFirst(markerCount).first == " " else { return .paragraph } let textOffset = markerCount + 1 return .heading( level: markerCount, markerRange: NSRange(location: line.range.location, length: markerCount), textRange: NSRange( location: line.range.location + textOffset, length: max(0, line.range.length - textOffset) ) ) } private func inlineSpans(in line: EditorLine) -> [HybridMarkdownSpan] { guard line.range.length > 0 else { return [] } var spans: [HybridMarkdownSpan] = [] var excludedRanges: [NSRange] = [] collectMatches("`([^`\\n]+)`", in: line, excluding: excludedRanges).forEach { match in spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .inlineCode)) spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 1, trailing: 1)) excludedRanges.append(match.fullRange) } collectMatches("\\*\\*([^*\\n]+)\\*\\*", in: line, excluding: excludedRanges).forEach { match in spans.append(HybridMarkdownSpan(range: match.contentRange, kind: .bold)) spans.append(contentsOf: delimiterRanges(match.fullRange, leading: 2, trailing: 2)) excludedRanges.append(match.fullRange) } collectMatches("(? [InlineMatch] { guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } return regex.matches(in: line.source, range: NSRange(location: 0, length: line.source.utf16.count)) .compactMap { match in guard match.numberOfRanges > 1 else { return nil } let shiftedRange = NSRange( location: line.range.location + match.range.location, length: match.range.length ) guard !excludedRanges.contains(where: { $0.intersects(shiftedRange) }) else { return nil } return InlineMatch( fullRange: shiftedRange, contentRange: NSRange( location: line.range.location + match.range(at: 1).location, length: match.range(at: 1).length ) ) } } private func delimiterRanges( _ fullRange: NSRange, leading: Int, trailing: Int ) -> [HybridMarkdownSpan] { [ HybridMarkdownSpan(range: NSRange(location: fullRange.location, length: leading), kind: .markdownDelimiter), HybridMarkdownSpan( range: NSRange(location: fullRange.upperBound - trailing, length: trailing), kind: .markdownDelimiter ) ] } } private struct InlineMatch { var fullRange: NSRange var contentRange: NSRange } private extension NSRange { var upperBound: Int { location + length } func intersects(_ other: NSRange) -> Bool { location < other.upperBound && other.location < upperBound } }