import Foundation import Testing @testable import NorgKit struct NorgParserTests { @Test func parsesHeadingLevelAndStatus() { let doc = NorgParser.parse("** (x) Ship it") guard case .heading(let level, let status, let content, let line) = doc.blocks.first else { Issue.record("Expected a heading") return } #expect(level == 2) #expect(status == .done) #expect(content.plainText == "Ship it") #expect(line == 0) } @Test func parsesPlainHeading() { let doc = NorgParser.parse("* Title") guard case .heading(_, let status, let content, _) = doc.blocks.first else { Issue.record("Expected a heading") return } #expect(status == nil) #expect(content.plainText == "Title") } @Test func parsesUnorderedAndNestedList() { let doc = NorgParser.parse("- ( ) todo\n-- nested") guard case .unorderedListItem(let level1, let status, _, _) = doc.blocks.first else { Issue.record("Expected list item") return } #expect(level1 == 1) #expect(status == .undone) guard case .unorderedListItem(let level2, _, let content, _) = doc.blocks.last else { Issue.record("Expected nested list item") return } #expect(level2 == 2) #expect(content.plainText == "nested") } @Test func parsesOrderedList() { let doc = NorgParser.parse("~ first\n~ second") #expect(doc.blocks.count == 2) if case .orderedListItem = doc.blocks.first {} else { Issue.record("Expected ordered item") } } @Test func parsesQuote() { let doc = NorgParser.parse("> a wise quote") guard case .quote(let level, let status, let content, _) = doc.blocks.first else { Issue.record("Expected quote") return } #expect(level == 1) #expect(status == nil) #expect(content.plainText == "a wise quote") } @Test func quoteCarriesStatus() { let doc = NorgParser.parse("> (x) a settled matter") guard case .quote(_, let status, let content, _) = doc.blocks.first else { Issue.record("Expected quote") return } #expect(status == .done) #expect(content.plainText == "a settled matter") } @Test func parsesUnknownRangedTagGenerically() { let doc = NorgParser.parse("@math\nx^2 + y^2 = z^2\n@end") guard case .rangedTag(let name, let parameters, let content, _) = doc.blocks.first else { Issue.record("Expected ranged tag") return } #expect(name == ["math"]) #expect(parameters.isEmpty) #expect(content == "x^2 + y^2 = z^2") } @Test func rangedTagSplitsNameAndParameters() { let doc = NorgParser.parse("@image.png alt text\n/img/cat.png\n@end") guard case .rangedTag(let name, let parameters, _, _) = doc.blocks.first else { Issue.record("Expected ranged tag") return } #expect(name == ["image", "png"]) #expect(parameters == ["alt", "text"]) } @Test func parsesCrlfDocument() { let doc = NorgParser.parse("* One\r\n\r\n- (x) two\r\n@code swift\r\nlet x = 1\r\n@end\r\n") #expect(doc.blocks.count == 3) guard case .heading(_, _, let headingContent, _) = doc.blocks[0] else { Issue.record("Expected heading") return } #expect(headingContent.plainText == "One") guard case .unorderedListItem(_, let status, let listContent, let line) = doc.blocks[1] else { Issue.record("Expected list item") return } #expect(status == .done) #expect(listContent.plainText == "two") #expect(line == 2) guard case .codeBlock(let language, let code, _) = doc.blocks[2] else { Issue.record("Expected code block") return } #expect(language == "swift") #expect(code == "let x = 1") } @Test func parsesCodeBlockWithLanguage() { let doc = NorgParser.parse("@code swift\nlet x = 1\nlet y = 2\n@end") guard case .codeBlock(let language, let code, _) = doc.blocks.first else { Issue.record("Expected code block") return } #expect(language == "swift") #expect(code == "let x = 1\nlet y = 2") } @Test func emptyVerbatimBodyProducesEmptyCode() { // No body lines means no common indentation to compute. let doc = NorgParser.parse("@code\n@end") guard case .codeBlock(_, let code, _) = doc.blocks.first else { Issue.record("Expected code block") return } #expect(code.isEmpty) } @Test func tagWithEmptyHeaderParsesWithEmptyName() { // A bare `@` opener has no name field; it still forms a ranged tag. let doc = NorgParser.parse("@\nbody\n@end") guard case .rangedTag(let name, let parameters, let content, _) = doc.blocks.first else { Issue.record("Expected ranged tag") return } #expect(name.isEmpty) #expect(parameters.isEmpty) #expect(content == "body") } @Test func dedentsCommonLeadingIndentationInVerbatimBody() { // All body lines share four leading spaces, which are stripped while the // relative indentation of the nested line is preserved. A blank line does // not contribute to the common indent. let doc = NorgParser.parse("@code\n one\n\n two\n three\n@end") guard case .codeBlock(_, let code, _) = doc.blocks.first else { Issue.record("Expected code block") return } #expect(code == "one\n\n two\nthree") } @Test func strayEndTagIsNoOpAndDoesNotSwallowDocument() { let doc = NorgParser.parse("* One\n@end\n* Two") #expect(doc.blocks.count == 2) if case .heading = doc.blocks.first {} else { Issue.record("Expected first heading") } if case .heading = doc.blocks.last {} else { Issue.record("Expected second heading") } } @Test func hidesDocumentMetadata() { let doc = NorgParser.parse("@document.meta\ntitle: Test\n@end\n* Heading") #expect(doc.blocks.count == 1) if case .heading = doc.blocks.first {} else { Issue.record("Expected only the heading") } } } // MARK: - Delimiters, paragraphs, and range-able blocks extension NorgParserTests { @Test func parsesDelimitingModifiers() { if case .horizontalRule = NorgParser.parse("___").blocks.first { } else { Issue.record("Expected horizontal rule") } if case .weakDelimiter = NorgParser.parse("---").blocks.first { } else { Issue.record("Expected weak delimiter") } if case .strongDelimiter = NorgParser.parse("===").blocks.first { } else { Issue.record("Expected strong delimiter") } } @Test func mergesSoftWrappedParagraph() { let doc = NorgParser.parse("line one\nline two") #expect(doc.blocks.count == 1) guard case .paragraph(let content, _) = doc.blocks.first else { Issue.record("Expected paragraph") return } #expect(content.plainText == "line one line two") } @Test func boldAtLineStartIsParagraphNotHeading() { let doc = NorgParser.parse("*bold* word") guard case .paragraph(let content, _) = doc.blocks.first else { Issue.record("Expected paragraph") return } #expect(content.plainText == "bold word") } @Test func tracksLineNumbersAcrossBlankLines() { let doc = NorgParser.parse("* One\n\n* Two") #expect(doc.blocks.count == 2) #expect(doc.blocks[1].line == 2) } @Test func listItemMergesSoftWrappedContinuation() { // Nestable detached modifiers consume a whole paragraph as content, so a // continuation line with no marker joins the item (Norg 1.0 §"Nestable // Detached Modifiers"). let doc = NorgParser.parse("- ( ) buy milk\n and eggs") #expect(doc.blocks.count == 1) guard case .unorderedListItem(_, let status, let content, _) = doc.blocks.first else { Issue.record("Expected a single list item") return } #expect(status == .undone) #expect(content.plainText == "buy milk and eggs") } @Test func quoteMergesSoftWrappedContinuation() { let doc = NorgParser.parse("> a wise quote\nthat spans lines") #expect(doc.blocks.count == 1) guard case .quote(_, _, let content, _) = doc.blocks.first else { Issue.record("Expected a single quote") return } #expect(content.plainText == "a wise quote that spans lines") } @Test func headingTitleDoesNotMergeContinuation() { // Headings are structural: the title is a single paragraph segment, and // the following line becomes its own block. let doc = NorgParser.parse("* A heading\nbody text below") #expect(doc.blocks.count == 2) guard case .heading(_, _, let content, _) = doc.blocks.first else { Issue.record("Expected heading") return } #expect(content.plainText == "A heading") guard case .paragraph(let body, let line) = doc.blocks.last else { Issue.record("Expected paragraph") return } #expect(body.plainText == "body text below") #expect(line == 1) } @Test func blankLineTerminatesListItemContent() { let doc = NorgParser.parse("- item one\n\nseparate paragraph") #expect(doc.blocks.count == 2) guard case .unorderedListItem(_, _, let content, _) = doc.blocks.first else { Issue.record("Expected list item") return } #expect(content.plainText == "item one") if case .paragraph = doc.blocks.last {} else { Issue.record("Expected paragraph") } } @Test func nextListItemBreaksContinuation() { let doc = NorgParser.parse("- first\n- second") #expect(doc.blocks.count == 2) } @Test func parsesSingleDefinition() { let doc = NorgParser.parse("$ Norg\nA structured note format.\nplain follow-up") // The definition owns only the immediately following paragraph; the // blank-free continuation merges into that one body paragraph. guard case .definition(let title, let status, let body, let line) = doc.blocks.first else { Issue.record("Expected a definition") return } #expect(title.plainText == "Norg") #expect(status == nil) #expect(line == 0) #expect(body.count == 1) guard case .paragraph(let content, let bodyLine) = body.first else { Issue.record("Expected a paragraph body") return } #expect(content.plainText == "A structured note format. plain follow-up") #expect(bodyLine == 1) // absolute line number is preserved } @Test func parsesRangedDefinitionWithNestedBlocks() { let doc = NorgParser.parse("$$ Term\n- one\n- two\n$$\nafter") #expect(doc.blocks.count == 2) guard case .definition(let title, _, let body, _) = doc.blocks.first else { Issue.record("Expected a definition") return } #expect(title.plainText == "Term") #expect(body.count == 2) if case .unorderedListItem = body.first {} else { Issue.record("Expected a list body") } // The block after the closer is back at the top level on its own line. guard case .paragraph(let after, let line) = doc.blocks.last else { Issue.record("Expected trailing paragraph") return } #expect(after.plainText == "after") #expect(line == 4) } @Test func nestedRangeDoesNotCloseOuter() { // An inner `$$ … $$` must not be mistaken for the outer range's closer. let doc = NorgParser.parse("$$ outer\n$$ inner\nbody\n$$\nstill outer\n$$") #expect(doc.blocks.count == 1) guard case .definition(_, _, let body, _) = doc.blocks.first else { Issue.record("Expected outer definition") return } // Body holds the inner definition and the trailing paragraph. #expect(body.count == 2) if case .definition = body.first {} else { Issue.record("Expected nested definition") } #expect(body.last?.content?.plainText == "still outer") } @Test func parsesFootnoteAndTableCell() { let doc = NorgParser.parse("^ source\nthe reference\n: A1\nthe cell") guard case .footnote(let footTitle, _, _, _) = doc.blocks.first else { Issue.record("Expected a footnote") return } #expect(footTitle.plainText == "source") guard case .tableCell(let cellTitle, _, _, _) = doc.blocks.last else { Issue.record("Expected a table cell") return } #expect(cellTitle.plainText == "A1") } @Test func definitionCarriesStatus() { let doc = NorgParser.parse("$ (x) Settled term\nits meaning") guard case .definition(_, let status, _, _) = doc.blocks.first else { Issue.record("Expected a definition") return } #expect(status == .done) } @Test func documentRoundTripsThroughJSON() throws { // Includes a ranged definition so the recursive `body` encodes/decodes. let doc = NorgParser.parse( "* (x) Done\n- ( ) todo {https://example.com}[link]\n$$ Term\n- nested\n$$\n@code swift\nlet x = 1\n@end" ) let data = try JSONEncoder().encode(doc) let decoded = try JSONDecoder().decode(NorgDocument.self, from: data) #expect(decoded == doc) } }