GeminiRenderer: Add Markdown renderer
This commit is contained in:
parent
806149ec1b
commit
d6ff2141dc
|
@ -73,7 +73,7 @@ public class GeminiHTMLRenderer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate extension Document.Line {
|
extension Document.Line {
|
||||||
var isListItem: Bool {
|
var isListItem: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .unorderedListItem(_):
|
case .unorderedListItem(_):
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// GeminiMarkdownRenderer.swift
|
||||||
|
// GeminiRenderer
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GeminiFormat
|
||||||
|
import HTMLEntities
|
||||||
|
|
||||||
|
public class GeminiMarkdownRenderer {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public func renderDocumentToMarkdown(_ doc: Document) -> String {
|
||||||
|
var str = ""
|
||||||
|
|
||||||
|
var inPreformatting = false
|
||||||
|
var inList = false
|
||||||
|
|
||||||
|
for line in doc.lines {
|
||||||
|
if inList && !line.isListItem {
|
||||||
|
str += "\n"
|
||||||
|
inList = false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch line {
|
||||||
|
case let .text(text):
|
||||||
|
if !text.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .link(url, text: maybeText):
|
||||||
|
let text = maybeText ?? url.absoluteString
|
||||||
|
// todo: do ] in the text need to be escaped?
|
||||||
|
str += "[\(text.htmlEscape())](\(url))"
|
||||||
|
str += "\n\n"
|
||||||
|
|
||||||
|
case let .preformattedToggle(alt: alt):
|
||||||
|
inPreformatting = !inPreformatting
|
||||||
|
if inPreformatting {
|
||||||
|
str += "```"
|
||||||
|
if let alt = alt {
|
||||||
|
str += alt
|
||||||
|
}
|
||||||
|
str += "\n"
|
||||||
|
} else {
|
||||||
|
str += "```"
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
case let .preformattedText(text):
|
||||||
|
str += text
|
||||||
|
str += "\n"
|
||||||
|
|
||||||
|
case let .heading(text, level: level):
|
||||||
|
str += String(repeating: "#", count: level.rawValue)
|
||||||
|
str += " "
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
|
||||||
|
case let .unorderedListItem(text):
|
||||||
|
if !inList {
|
||||||
|
inList = true
|
||||||
|
}
|
||||||
|
str += "* \(text.htmlEscape())"
|
||||||
|
str += "\n"
|
||||||
|
|
||||||
|
case let .quote(text):
|
||||||
|
str += "> "
|
||||||
|
str += text.htmlEscape()
|
||||||
|
str += "\n\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
//
|
||||||
|
// GeminiMarkdownRendererTests.swift
|
||||||
|
// GeminiRendererTests
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 10/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import GeminiFormat
|
||||||
|
@testable import GeminiRenderer
|
||||||
|
|
||||||
|
class GeminiMarkdownRendererTests: XCTestCase {
|
||||||
|
|
||||||
|
private var doc: Document!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
doc = Document(url: URL(string: "gemini://example.com/")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEscapeToEntities() {
|
||||||
|
doc.lines = [.text("<b>hello</b>")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "<b>hello</b>\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParagraph() {
|
||||||
|
doc.lines = [.text("Hello, world!")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "Hello, world!\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLink() {
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: "text")]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "[text](gemini://example.com/)\n\n")
|
||||||
|
|
||||||
|
doc.lines = [.link(URL(string: "gemini://example.com/")!, text: nil)]
|
||||||
|
let noText = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(noText, "[gemini://example.com/](gemini://example.com/)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLinksAfterList() {
|
||||||
|
doc.lines = [
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.link(URL(string: "gemini://example.com")!, text: "one"),
|
||||||
|
.link(URL(string: "gemini://example.com")!, text: "two"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "* a\n* b\n\n[one](gemini://example.com)\n\n[two](gemini://example.com)\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPreformatting() {
|
||||||
|
doc.lines = [
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
.preformattedText("foo"),
|
||||||
|
.preformattedText("* bar"),
|
||||||
|
.preformattedToggle(alt: nil),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "```\nfoo\n* bar\n```\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHeading() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("One", level: .h1),
|
||||||
|
.heading("Two", level: .h2),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "# One\n\n## Two\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnorderedList() {
|
||||||
|
doc.lines = [
|
||||||
|
.text("before"),
|
||||||
|
.unorderedListItem("a"),
|
||||||
|
.unorderedListItem("b"),
|
||||||
|
.unorderedListItem("c"),
|
||||||
|
.text("after"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "before\n\n* a\n* b\n* c\n\nafter\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testQuote() {
|
||||||
|
doc.lines = [
|
||||||
|
.quote("quoted")
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "> quoted\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSkipBlankLines() {
|
||||||
|
doc.lines = [
|
||||||
|
.heading("Hello", level: .h1),
|
||||||
|
.text(""),
|
||||||
|
.text("World"),
|
||||||
|
]
|
||||||
|
let markdown = GeminiMarkdownRenderer().renderDocumentToMarkdown(doc)
|
||||||
|
XCTAssertEqual(markdown, "# Hello\n\nWorld\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue