aboutsummaryrefslogtreecommitdiff
path: root/Tests/NorgEditorTests/NorgEditorTests.swift
blob: 6a6caf7251b0027fa0f08e31f48c00be4255fa4a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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") == "  ")
}