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 {
|
||||
switch self {
|
||||
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