Gemini/GeminiFormat/GeminiParser.swift

107 lines
4.4 KiB
Swift
Raw Normal View History

2020-07-13 03:09:22 +00:00
//
// GeminiParser.swift
// GeminiFormat
//
// Created by Shadowfacts on 7/12/20.
//
import Foundation
2020-07-13 03:25:20 +00:00
public struct GeminiParser {
2020-07-13 03:09:22 +00:00
private init() {}
2020-07-13 03:25:20 +00:00
public static func parse(text: String, baseURL: URL) -> Document {
2020-07-13 03:09:22 +00:00
var doc = Document(url: baseURL)
2020-07-13 04:12:31 +00:00
var inPreformattingBlock = false
2020-07-13 03:09:22 +00:00
text.enumerateLines { (line, stop) in
if line.starts(with: "```") {
2020-07-13 04:12:31 +00:00
if inPreformattingBlock {
inPreformattingBlock = false
// todo: should the toggle off line be a separate line type?
doc.lines.append(.preformattedToggle(alt: nil))
} else {
2020-07-13 03:09:22 +00:00
let alt: String?
if line.count > 3 {
alt = String(line[line.index(line.startIndex, offsetBy: 3)...])
} else {
alt = nil
}
2020-07-13 04:12:31 +00:00
inPreformattingBlock = true
2020-07-13 03:52:38 +00:00
doc.lines.append(.preformattedToggle(alt: alt))
2020-07-13 03:09:22 +00:00
}
2020-07-13 04:12:31 +00:00
} else if inPreformattingBlock {
2020-07-13 03:52:38 +00:00
doc.lines.append(.preformattedText(line))
2020-07-13 03:09:22 +00:00
} else if line.starts(with: "=>") {
// Link line
let urlStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: 2))
let urlEnd = line.firstWhitespaceIndex(after: urlStart)
let textStart = line.firstNonWhitespaceIndex(after: urlEnd)
var urlString = String(line[urlStart..<urlEnd])
if urlString.hasPrefix("//") {
// URL(string:relativeTo:) does not handle // meaning the same protocol as the base URL
urlString = baseURL.scheme! + ":" + urlString
}
2020-07-13 03:09:22 +00:00
// todo: if the URL initializer fails, should there be a .link line with a nil URL?
let url = URL(string: urlString, relativeTo: baseURL)!.absoluteURL
let text: String?
if textStart < line.endIndex {
text = String(line[textStart..<line.endIndex])
} else {
text = nil
}
doc.lines.append(.link(url, text: text))
} else if line.starts(with: "#") {
let level: Document.HeadingLevel
if line.starts(with: "###") {
level = .h3
} else if line.starts(with: "##") {
level = .h2
} else {
level = .h1
}
let headingStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: level.rawValue))
let headingText = String(line[headingStart...])
doc.lines.append(.heading(headingText, level: level))
} else if line.starts(with: "* ") {
let listItemStart = line.firstNonWhitespaceIndex(after: line.index(line.startIndex, offsetBy: 2))
let listItemText = String(line[listItemStart...])
doc.lines.append(.unorderedListItem(listItemText))
} else if line.starts(with: ">") {
let quoteStartIndex = line.firstNonWhitespaceIndex(after: line.index(after: line.startIndex))
let quoteText = String(line[quoteStartIndex...])
doc.lines.append(.quote(quoteText))
} else {
doc.lines.append(.text(line))
}
}
return doc
}
}
fileprivate extension String {
func firstNonWhitespaceIndex(after index: String.Index) -> String.Index {
var index = index
// using .unicodeScalars.first should be fine, since all whitespace characters are single scalars
while index < self.endIndex, CharacterSet.whitespaces.contains(self[index].unicodeScalars.first!) {
index = self.index(after: index)
}
return index
}
func firstWhitespaceIndex(after index: String.Index) -> String.Index {
var index = index
// todo: could the first unicode scalar of a character be whitespace even though the whole character is not?
while index < self.endIndex, !CharacterSet.whitespaces.contains(self[index].unicodeScalars.first!) {
index = self.index(after: index)
}
return index
}
}