import Foundation import NorgKit import Testing @testable import NorgEditor @Test func plainTextYieldsNoHighlights() { #expect(NorgHighlighter.highlights(in: "just some prose").isEmpty) #expect(NorgHighlighter.highlights(in: "").isEmpty) } @Test func headingMarkerIsHighlighted() { let highlights = NorgHighlighter.highlights(in: "** Title") let heading = highlights.first #expect(heading?.kind == .heading(level: 2)) // The marker run `**` spans the first two UTF-16 units; the title is plain. #expect(heading?.range == NSRange(location: 0, length: 2)) } @Test func taskStatusIsHighlightedWithMarkerAndStatus() { let highlights = NorgHighlighter.highlights(in: "- (x) buy milk") #expect(highlights.contains { $0.kind == .unorderedList(level: 1) }) let status = highlights.first { highlight in if case .taskStatus = highlight.kind { return true } return false } #expect(status?.kind == .taskStatus(.done)) // `(x)` — open paren, status char, close paren. #expect(status?.range == NSRange(location: 2, length: 3)) } @Test func inlineVerbatimReportsDelimitersAndStyledText() { let highlights = NorgHighlighter.highlights(in: "a `code` b") let styled = highlights.first { highlight in if case .styledText(let style) = highlight.kind { return style.contains(.verbatim) } return false } #expect(styled?.range == NSRange(location: 3, length: 4)) // "code" #expect(highlights.contains { $0.kind == .modifierDelimiter(.verbatim) }) } @Test func rangesAreValidForMultibyteSource() { // An emoji is two UTF-16 units; the bold run must land on the right span. let source = "🌟 *bold*" let highlights = NorgHighlighter.highlights(in: source) let nsSource = source as NSString let styled = highlights.first { highlight in if case .styledText(let style) = highlight.kind { return style.contains(.bold) } return false } let range = styled?.range #expect(range.map { nsSource.substring(with: $0) } == "bold") } @Test func linkTargetIsHighlighted() { let highlights = NorgHighlighter.highlights(in: "see {https://example.com}") #expect(highlights.contains { $0.kind == .linkTarget }) #expect(highlights.contains { $0.kind == .linkDelimiter }) } // MARK: - Auto-indentation (Return key) /// Indentation at the end of `source` (where Return would land). private func indent(after source: String) -> String { NorgAutoIndent.indentation(for: source, newlineAt: (source as NSString).length) } @Test func indentsUnderTheOpenHeading() { // `*** ` is three markers plus a space, so the text starts at column 4. #expect(indent(after: "*** Headline") == " ") #expect(indent(after: "* Top") == " ") } @Test func plainTextWithoutAHeadingHasNoIndent() { #expect(indent(after: "just a paragraph") == "") #expect(indent(after: "") == "") } @Test func indentPersistsAcrossContentLines() { // A paragraph under the heading keeps the heading's indent on the next line. #expect(indent(after: "* A\nsome prose") == " ") } @Test func indentFollowsTheDeepestOpenHeading() { #expect(indent(after: "* A\n** B\n*** C") == " ") } @Test func strongDelimiterClosesAllHeadings() { #expect(indent(after: "* A\n** B\n===\nmore") == "") } @Test func weakDelimiterReturnsToTheParentHeading() { // `---` closes `** B`, leaving `* A` open → align under its text (column 2). #expect(indent(after: "* A\n** B\n---\nmore") == " ") } @Test func horizontalRuleLeavesHeadingScopeIntact() { #expect(indent(after: "* A\n___\nmore") == " ") }