// // GeminiParserTests.swift // GeminiFormatTests // // Created by Shadowfacts on 7/12/20. // import XCTest @testable import GeminiFormat class GeminiParserTests: XCTestCase { func assertParseLines(text: String, lines expected: [Document.Line], message: String = "", file: StaticString = #filePath, line: UInt = #line) { let doc = GeminiParser.parse(text: text, baseURL: URL(string: "gemini://example.com")!) for (index, (actual, expected)) in zip(doc.lines, expected).enumerated() { XCTAssertEqual(actual, expected, "\(message): index \(index)", file: file, line: line) } } func testParsePlainLines() { assertParseLines(text: "test", lines: [.text("test")], message: "parse a plain text line") assertParseLines(text: "one\ntwo", lines: [ .text("one"), .text("two") ], message: "parse multiple, newline delmited plain lines") assertParseLines(text: "one\r\ntwo", lines: [ .text("one"), .text("two") ], message: "parse multiple, CRLF delmited plain lines") } func testParseLinkLines() { assertParseLines(text: "=> gemini://blah.com", lines: [ .link(URL(string: "gemini://blah.com")!, text: nil) ], message: "parse a bare link line") assertParseLines(text: "=> gemini://blah.com:1234/foo/bar?baz", lines: [ .link(URL(string: "gemini://blah.com:1234/foo/bar?baz")!, text: nil) ], message: "parse a more complex bare link line") assertParseLines(text: "=> gemini://blah.com \t Link to example", lines: [ .link(URL(string: "gemini://blah.com")!, text: "Link to example") ], message: "parse a simple link line with associated text") assertParseLines(text: "=> gemini://blah.com/foo Link to foo", lines: [ .link(URL(string: "gemini://blah.com/foo")!, text: "Link to foo") ], message: "parse a more complex link line with associated text") assertParseLines(text: "=> https://example.com", lines: [ .link(URL(string: "https://example.com")!, text: nil) ], message: "parse a link with a different protocol") assertParseLines(text: "=> /foo/bar/baz", lines: [ .link(URL(string: "gemini://example.com/foo/bar/baz")!, text: nil) ], message: "resolve a relative path link") assertParseLines(text: "=>gemini://blah.com", lines: [ .link(URL(string: "gemini://blah.com")!, text: nil) ], message: "parse link without whitespace after =>") } func testParseHeadingLines() { assertParseLines(text: "# test", lines: [ .heading("test", level: .h1) ], message: "level 1 heading") assertParseLines(text: "#test", lines: [ .heading("test", level: .h1) ], message: "level 1 heading without whitespace") assertParseLines(text: "#test\n## two\r\n###three", lines: [ .heading("test", level: .h1), .heading("two", level: .h2), .heading("three", level: .h3) ], message: "multiples headings with and without whitespace") } func testParseListItemLines() { assertParseLines(text: "* test list item", lines: [ .unorderedListItem("test list item") ], message: "parse simple list item") assertParseLines(text: "*test", lines: [ .text("*test") ], message: "don't parse list item without space after asterisk") assertParseLines(text: "* one\n* two\n*three", lines: [ .unorderedListItem("one"), .unorderedListItem("two"), .text("*three") ], message: "parse multiple list items") } func testParseQuoteLines() { assertParseLines(text: "> quote", lines: [ .quote("quote") ], message: "parse quote line") assertParseLines(text: ">quote", lines: [ .quote("quote") ], message: "parse quote line without space after >") assertParseLines(text: ">one\n> two\n>three", lines: [ .quote("one"), .quote("two"), .quote("three") ], message: "parse multiple quote lines") } func testParsePreformattedLines() { assertParseLines(text: "```\nsomething\n```", lines: [ .preformattedToggle(alt: nil), .preformattedText("something"), .preformattedToggle(alt: nil) ], message: "parse simple preformatted line") assertParseLines(text: "```alt\nsomething\n```", lines: [ .preformattedToggle(alt: "alt"), .preformattedText("something"), .preformattedToggle(alt: nil) ], message: "parse simple preformatted line with alt") assertParseLines(text: "```alt\nsomething\n```other", lines: [ .preformattedToggle(alt: "alt"), .preformattedText("something"), .preformattedToggle(alt: nil) ], message: "ignore extra text after closing ```") assertParseLines(text: "```\n# not a heading\n* not a list item\n>not a quote\n=> /link not a link\n```", lines: [ .preformattedToggle(alt: nil), .preformattedText("# not a heading"), .preformattedText("* not a list item"), .preformattedText(">not a quote"), .preformattedText("=> /link not a link"), .preformattedToggle(alt: nil) ], message: "don't parse special lines inside preformatted") assertParseLines(text: "```a\na line\n```\n```b\nb line\n```", lines: [ .preformattedToggle(alt: "a"), .preformattedText("a line"), .preformattedToggle(alt: nil), .preformattedToggle(alt: "b"), .preformattedText("b line"), .preformattedToggle(alt: nil), ], message: "parse consecutive preformatted blocks") } func testComplexDocument() { let text = """ # Project Gemini ## Overview Gemini is a new internet protocol which: * Is heavier than gopher * Is lighter than the web * Will not replace either * Strives for maximum power to weight ratio * Takes user privacy very seriously ## Resources => docs/ Gemini documentation => software/ Gemini software => servers/ Known Gemini servers => https://lists.orbitalfox.eu/listinfo/gemini Gemini mailing list => gemini://gemini.conman.org/test/torture/ Gemini client torture test ## Web proxies => https://portal.mozz.us/?url=gemini%3A%2F%2Fgemini.circumlunar.space%2F&fmt=fixed Gemini-to-web proxy service => https://proxy.vulpes.one/gemini/gemini.circumlunar.space Another Gemini-to-web proxy service ## Search engines => gemini://gus.guru/ Gemini Universal Search engine => gemini://houston.coder.town Houston search engine ## Geminispace aggregators (experimental!) => capcom/ CAPCOM => gemini://rawtext.club:1965/~sloum/spacewalk.gmi Spacewalk ## Gemini mirrors of web resources => gemini://gempaper.strangled.net/mirrorlist/ A list of mirrored services ## Free Gemini hosting => users/ Users with Gemini content on this server """ let expected: [Document.Line] = [ .heading("Project Gemini", level: .h1), .text(""), .heading("Overview", level: .h2), .text(""), .text("Gemini is a new internet protocol which:"), .text(""), .unorderedListItem("Is heavier than gopher"), .unorderedListItem("Is lighter than the web"), .unorderedListItem("Will not replace either"), .unorderedListItem("Strives for maximum power to weight ratio"), .unorderedListItem("Takes user privacy very seriously"), .text(""), .heading("Resources", level: .h2), .text(""), .link(URL(string: "gemini://example.com/docs/")!, text: "Gemini documentation"), .link(URL(string: "gemini://example.com/software/")!, text: "Gemini software"), .link(URL(string: "gemini://example.com/servers/")!, text: "Known Gemini servers"), .link(URL(string: "https://lists.orbitalfox.eu/listinfo/gemini")!, text: "Gemini mailing list"), .link(URL(string: "gemini://gemini.conman.org/test/torture/")!, text: "Gemini client torture test"), .text(""), .heading("Web proxies", level: .h2), .text(""), .link(URL(string: "https://portal.mozz.us/?url=gemini%3A%2F%2Fgemini.circumlunar.space%2F&fmt=fixed")!, text: "Gemini-to-web proxy service"), .link(URL(string: "https://proxy.vulpes.one/gemini/gemini.circumlunar.space")!, text: "Another Gemini-to-web proxy service"), .text(""), .heading("Search engines", level: .h2), .text(""), .link(URL(string: "gemini://gus.guru/")!, text: "Gemini Universal Search engine"), .link(URL(string: "gemini://houston.coder.town")!, text: "Houston search engine"), .text(""), .heading("Geminispace aggregators (experimental!)", level: .h2), .text(""), .link(URL(string: "gemini://example.com/capcom/")!, text: "CAPCOM"), .link(URL(string: "gemini://rawtext.club:1965/~sloum/spacewalk.gmi")!, text: "Spacewalk"), .text(""), .heading("Gemini mirrors of web resources", level: .h2), .text(""), .link(URL(string: "gemini://gempaper.strangled.net/mirrorlist/")!, text: "A list of mirrored services"), .text(""), .heading("Free Gemini hosting", level: .h2), .text(""), .link(URL(string: "gemini://example.com/users/")!, text: "Users with Gemini content on this server") ] assertParseLines(text: text, lines: expected) } }