nuartz 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "nuartz",
3
+ "version": "0.1.0",
4
+ "description": "Obsidian-compatible markdown pipeline for Next.js",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/syshin0116/nuartz.git"
9
+ },
10
+ "keywords": ["markdown", "obsidian", "nextjs", "blog", "mdx", "remark", "rehype"],
11
+ "homepage": "https://github.com/syshin0116/nuartz#readme",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "test:coverage": "vitest run --coverage"
27
+ },
28
+ "dependencies": {
29
+ "gray-matter": "^4.0.3",
30
+ "hast-util-to-html": "^9.0.0",
31
+ "rehype-autolink-headings": "^7.0.0",
32
+ "rehype-katex": "^7.0.0",
33
+ "rehype-pretty-code": "^0.14.3",
34
+ "rehype-raw": "^7.0.0",
35
+ "rehype-slug": "^6.0.0",
36
+ "rehype-stringify": "^10.0.0",
37
+ "remark-breaks": "^4.0.0",
38
+ "remark-frontmatter": "^5.0.0",
39
+ "remark-gfm": "^4.0.0",
40
+ "remark-math": "^6.0.0",
41
+ "remark-parse": "^11.0.0",
42
+ "remark-rehype": "^11.0.0",
43
+ "shiki": "^4.0.1",
44
+ "unified": "^11.0.0",
45
+ "unist-util-visit": "^5.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20",
49
+ "@vitest/coverage-v8": "^4.0.18",
50
+ "typescript": "^5",
51
+ "vitest": "^4.0.18"
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ # Sample Document
2
+
3
+ This is a paragraph with a [[Wiki Link]] and a #sample-tag.
4
+
5
+ > [!note] Important Note
6
+ > This is a callout with some content.
7
+
8
+ Here is a [[Page|Custom Alias]] and [[Page#Section]] reference.
9
+
10
+ > [!warning]+ Collapsible Warning
11
+ > This warning is collapsible.
12
+
13
+ Tags: #javascript #web-dev
14
+
15
+ ![[diagram.png]]
16
+
17
+ > This is a regular blockquote, not a callout.
18
+
19
+ > [!tip]- Collapsed Tip
20
+ > Hidden by default.
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { buildBacklinkIndex, getBacklinks } from "../backlinks"
3
+ import type { RenderResult } from "../types"
4
+
5
+ function mockPage(
6
+ links: string[],
7
+ title: string,
8
+ raw: string
9
+ ): { result: RenderResult; raw: string } {
10
+ return {
11
+ result: {
12
+ html: "",
13
+ frontmatter: { title },
14
+ toc: [],
15
+ links,
16
+ tags: [],
17
+ },
18
+ raw,
19
+ }
20
+ }
21
+
22
+ describe("buildBacklinkIndex", () => {
23
+ it("builds index from pages with outgoing links", () => {
24
+ const pages = new Map([
25
+ ["page-a", mockPage(["Page B"], "Page A", "Links to Page B")],
26
+ ["page-b", mockPage(["Page A"], "Page B", "Links to Page A")],
27
+ ])
28
+
29
+ const index = buildBacklinkIndex(pages)
30
+ // "Page B" normalizes to "page-b"
31
+ const backlinksForB = index.get("page-b")
32
+ expect(backlinksForB).toBeDefined()
33
+ expect(backlinksForB).toHaveLength(1)
34
+ expect(backlinksForB![0].slug).toBe("page-a")
35
+ expect(backlinksForB![0].title).toBe("Page A")
36
+ })
37
+
38
+ it("normalizes target slugs: lowercase and spaces to hyphens", () => {
39
+ const pages = new Map([
40
+ ["source", mockPage(["My Target Page"], "Source", "Content here")],
41
+ ])
42
+
43
+ const index = buildBacklinkIndex(pages)
44
+ expect(index.has("my-target-page")).toBe(true)
45
+ })
46
+
47
+ it("strips non-word/non-hyphen characters from target slug", () => {
48
+ const pages = new Map([
49
+ ["source", mockPage(["Page (draft)"], "Source", "Content")],
50
+ ])
51
+
52
+ const index = buildBacklinkIndex(pages)
53
+ // "Page (draft)" -> "page-draft" (parens removed)
54
+ expect(index.has("page-draft")).toBe(true)
55
+ })
56
+
57
+ it("collects multiple backlinks for the same target", () => {
58
+ const pages = new Map([
59
+ ["alpha", mockPage(["Target"], "Alpha", "Alpha links to target")],
60
+ ["beta", mockPage(["Target"], "Beta", "Beta links to target")],
61
+ ["gamma", mockPage(["Target"], "Gamma", "Gamma links to target")],
62
+ ])
63
+
64
+ const index = buildBacklinkIndex(pages)
65
+ const backlinks = index.get("target")
66
+ expect(backlinks).toHaveLength(3)
67
+ const slugs = backlinks!.map((b) => b.slug)
68
+ expect(slugs).toContain("alpha")
69
+ expect(slugs).toContain("beta")
70
+ expect(slugs).toContain("gamma")
71
+ })
72
+
73
+ it("generates excerpt from raw content stripping frontmatter", () => {
74
+ const raw = `---
75
+ title: Source Page
76
+ ---
77
+
78
+ This is the body content that should appear in the excerpt.`
79
+
80
+ const pages = new Map([
81
+ ["source", mockPage(["Target"], "Source Page", raw)],
82
+ ])
83
+
84
+ const index = buildBacklinkIndex(pages)
85
+ const backlinks = index.get("target")!
86
+ expect(backlinks[0].excerpt).toContain("This is the body content")
87
+ expect(backlinks[0].excerpt).not.toContain("---")
88
+ })
89
+
90
+ it("truncates excerpt to 160 characters plus ellipsis", () => {
91
+ const longBody = "A".repeat(200)
92
+ const pages = new Map([
93
+ ["source", mockPage(["Target"], "Source", longBody)],
94
+ ])
95
+
96
+ const index = buildBacklinkIndex(pages)
97
+ const backlinks = index.get("target")!
98
+ // 160 chars + ellipsis character
99
+ expect(backlinks[0].excerpt.length).toBeLessThanOrEqual(161)
100
+ expect(backlinks[0].excerpt).toMatch(/\u2026$/)
101
+ })
102
+
103
+ it("uses slug as title fallback when no frontmatter title", () => {
104
+ const pages = new Map([
105
+ [
106
+ "no-title-page",
107
+ {
108
+ result: {
109
+ html: "",
110
+ frontmatter: {},
111
+ toc: [],
112
+ links: ["Target"],
113
+ tags: [],
114
+ } as RenderResult,
115
+ raw: "Some content",
116
+ },
117
+ ],
118
+ ])
119
+
120
+ const index = buildBacklinkIndex(pages)
121
+ const backlinks = index.get("target")!
122
+ expect(backlinks[0].title).toBe("no-title-page")
123
+ })
124
+ })
125
+
126
+ describe("getBacklinks", () => {
127
+ it("returns correct BacklinkEntry[] for known slug", () => {
128
+ const pages = new Map([
129
+ ["source", mockPage(["Target"], "Source Title", "Content about target")],
130
+ ])
131
+
132
+ const index = buildBacklinkIndex(pages)
133
+ const backlinks = getBacklinks(index, "target")
134
+
135
+ expect(backlinks).toHaveLength(1)
136
+ expect(backlinks[0].slug).toBe("source")
137
+ expect(backlinks[0].title).toBe("Source Title")
138
+ expect(backlinks[0].excerpt).toBeTruthy()
139
+ })
140
+
141
+ it("returns empty array for unknown slug", () => {
142
+ const pages = new Map([
143
+ ["source", mockPage(["Target"], "Source", "Content")],
144
+ ])
145
+
146
+ const index = buildBacklinkIndex(pages)
147
+ const backlinks = getBacklinks(index, "nonexistent")
148
+ expect(backlinks).toEqual([])
149
+ })
150
+
151
+ it("returns empty array for empty index", () => {
152
+ const index = buildBacklinkIndex(new Map())
153
+ const backlinks = getBacklinks(index, "anything")
154
+ expect(backlinks).toEqual([])
155
+ })
156
+ })
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest"
2
+ import fs from "node:fs/promises"
3
+ import os from "node:os"
4
+ import path from "node:path"
5
+ import { getAllMarkdownFiles, getMarkdownBySlug, buildFileTree } from "../fs"
6
+ import type { MarkdownFile } from "../fs"
7
+
8
+ describe("getAllMarkdownFiles", () => {
9
+ let tmpDir: string
10
+
11
+ beforeEach(async () => {
12
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "nuartz-fs-test-"))
13
+ })
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tmpDir, { recursive: true, force: true })
17
+ })
18
+
19
+ it("returns all .md files recursively", async () => {
20
+ await fs.writeFile(path.join(tmpDir, "root.md"), "# Root")
21
+ await fs.mkdir(path.join(tmpDir, "sub"), { recursive: true })
22
+ await fs.writeFile(path.join(tmpDir, "sub", "nested.md"), "# Nested")
23
+
24
+ const files = await getAllMarkdownFiles(tmpDir)
25
+ const slugs = files.map((f) => f.slug)
26
+ expect(slugs).toContain("root")
27
+ expect(slugs).toContain("sub/nested")
28
+ })
29
+
30
+ it("excludes files starting with _", async () => {
31
+ await fs.writeFile(path.join(tmpDir, "visible.md"), "# Visible")
32
+ await fs.writeFile(path.join(tmpDir, "_hidden.md"), "# Hidden")
33
+
34
+ const files = await getAllMarkdownFiles(tmpDir)
35
+ const slugs = files.map((f) => f.slug)
36
+ expect(slugs).toContain("visible")
37
+ expect(slugs).not.toContain("_hidden")
38
+ })
39
+
40
+ it("returns MarkdownFile[] with correct slug format", async () => {
41
+ await fs.mkdir(path.join(tmpDir, "notes"), { recursive: true })
42
+ await fs.writeFile(path.join(tmpDir, "notes", "foo.md"), "# Foo")
43
+
44
+ const files = await getAllMarkdownFiles(tmpDir)
45
+ expect(files).toHaveLength(1)
46
+ expect(files[0].slug).toBe("notes/foo")
47
+ expect(files[0].filePath).toBe(path.join(tmpDir, "notes", "foo.md"))
48
+ expect(files[0].raw).toContain("# Foo")
49
+ })
50
+
51
+ it("parses frontmatter", async () => {
52
+ const content = `---
53
+ title: My Note
54
+ tags:
55
+ - test
56
+ ---
57
+
58
+ Body content`
59
+ await fs.writeFile(path.join(tmpDir, "note.md"), content)
60
+
61
+ const files = await getAllMarkdownFiles(tmpDir)
62
+ expect(files[0].frontmatter.title).toBe("My Note")
63
+ expect(files[0].frontmatter.tags).toEqual(["test"])
64
+ })
65
+
66
+ it("returns empty array for non-existent directory", async () => {
67
+ const files = await getAllMarkdownFiles(path.join(tmpDir, "nonexistent"))
68
+ expect(files).toEqual([])
69
+ })
70
+ })
71
+
72
+ describe("getMarkdownBySlug", () => {
73
+ let tmpDir: string
74
+
75
+ beforeEach(async () => {
76
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "nuartz-slug-test-"))
77
+ })
78
+
79
+ afterEach(async () => {
80
+ await fs.rm(tmpDir, { recursive: true, force: true })
81
+ })
82
+
83
+ it("returns MarkdownFile for existing slug", async () => {
84
+ const content = `---
85
+ title: Hello
86
+ ---
87
+
88
+ World`
89
+ await fs.writeFile(path.join(tmpDir, "hello.md"), content)
90
+
91
+ const file = await getMarkdownBySlug(tmpDir, "hello")
92
+ expect(file).not.toBeNull()
93
+ expect(file!.slug).toBe("hello")
94
+ expect(file!.frontmatter.title).toBe("Hello")
95
+ expect(file!.raw).toContain("World")
96
+ })
97
+
98
+ it("returns null for non-existent slug", async () => {
99
+ const file = await getMarkdownBySlug(tmpDir, "does-not-exist")
100
+ expect(file).toBeNull()
101
+ })
102
+
103
+ it("works with nested slugs", async () => {
104
+ await fs.mkdir(path.join(tmpDir, "notes"), { recursive: true })
105
+ await fs.writeFile(path.join(tmpDir, "notes", "deep.md"), "# Deep")
106
+
107
+ const file = await getMarkdownBySlug(tmpDir, "notes/deep")
108
+ expect(file).not.toBeNull()
109
+ expect(file!.slug).toBe("notes/deep")
110
+ })
111
+ })
112
+
113
+ describe("buildFileTree", () => {
114
+ it("builds tree from flat list of files", () => {
115
+ const files: MarkdownFile[] = [
116
+ { slug: "alpha", filePath: "/alpha.md", frontmatter: { title: "Alpha" }, raw: "" },
117
+ { slug: "beta", filePath: "/beta.md", frontmatter: { title: "Beta" }, raw: "" },
118
+ ]
119
+
120
+ const tree = buildFileTree(files)
121
+ expect(tree).toHaveLength(2)
122
+ expect(tree[0].name).toBe("Alpha")
123
+ expect(tree[0].type).toBe("file")
124
+ expect(tree[0].path).toBe("alpha")
125
+ expect(tree[1].name).toBe("Beta")
126
+ })
127
+
128
+ it("creates folder nodes for nested files", () => {
129
+ const files: MarkdownFile[] = [
130
+ { slug: "notes/page1", filePath: "/notes/page1.md", frontmatter: { title: "Page 1" }, raw: "" },
131
+ { slug: "notes/page2", filePath: "/notes/page2.md", frontmatter: { title: "Page 2" }, raw: "" },
132
+ ]
133
+
134
+ const tree = buildFileTree(files)
135
+ expect(tree).toHaveLength(1)
136
+ expect(tree[0].type).toBe("folder")
137
+ expect(tree[0].name).toBe("notes")
138
+ expect(tree[0].children).toHaveLength(2)
139
+ expect(tree[0].children![0].name).toBe("Page 1")
140
+ expect(tree[0].children![1].name).toBe("Page 2")
141
+ })
142
+
143
+ it("uses frontmatter.title for file display name", () => {
144
+ const files: MarkdownFile[] = [
145
+ { slug: "my-file", filePath: "/my-file.md", frontmatter: { title: "Custom Title" }, raw: "" },
146
+ ]
147
+
148
+ const tree = buildFileTree(files)
149
+ expect(tree[0].name).toBe("Custom Title")
150
+ })
151
+
152
+ it("falls back to filename when no title", () => {
153
+ const files: MarkdownFile[] = [
154
+ { slug: "my-file", filePath: "/my-file.md", frontmatter: {}, raw: "" },
155
+ ]
156
+
157
+ const tree = buildFileTree(files)
158
+ expect(tree[0].name).toBe("my-file")
159
+ })
160
+
161
+ it("handles deeply nested structures", () => {
162
+ const files: MarkdownFile[] = [
163
+ { slug: "a/b/c", filePath: "/a/b/c.md", frontmatter: { title: "Deep" }, raw: "" },
164
+ ]
165
+
166
+ const tree = buildFileTree(files)
167
+ expect(tree[0].type).toBe("folder")
168
+ expect(tree[0].name).toBe("a")
169
+ expect(tree[0].children![0].type).toBe("folder")
170
+ expect(tree[0].children![0].name).toBe("b")
171
+ expect(tree[0].children![0].children![0].type).toBe("file")
172
+ expect(tree[0].children![0].children![0].name).toBe("Deep")
173
+ })
174
+
175
+ it("returns empty array for empty input", () => {
176
+ const tree = buildFileTree([])
177
+ expect(tree).toEqual([])
178
+ })
179
+ })
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { renderMarkdown } from "../markdown"
3
+
4
+ describe("renderMarkdown", () => {
5
+ it("returns empty result for empty string", async () => {
6
+ const result = await renderMarkdown("")
7
+ expect(result.html).toBe("")
8
+ expect(result.toc).toEqual([])
9
+ expect(result.tags).toEqual([])
10
+ expect(result.links).toEqual([])
11
+ })
12
+
13
+ it("extracts heading into toc", async () => {
14
+ const result = await renderMarkdown("# Hello World")
15
+ expect(result.toc).toHaveLength(1)
16
+ expect(result.toc[0]).toMatchObject({
17
+ depth: 1,
18
+ text: "Hello World",
19
+ })
20
+ expect(result.toc[0].id).toBeTruthy()
21
+ })
22
+
23
+ it("builds nested toc tree", async () => {
24
+ const md = "# Top\n## Sub\n### Deep"
25
+ const result = await renderMarkdown(md)
26
+ expect(result.toc).toHaveLength(1)
27
+ expect(result.toc[0].text).toBe("Top")
28
+ expect(result.toc[0].children).toHaveLength(1)
29
+ expect(result.toc[0].children[0].text).toBe("Sub")
30
+ expect(result.toc[0].children[0].children).toHaveLength(1)
31
+ expect(result.toc[0].children[0].children[0].text).toBe("Deep")
32
+ })
33
+
34
+ it("parses frontmatter", async () => {
35
+ const md = `---
36
+ title: My Page
37
+ date: 2024-01-01
38
+ ---
39
+
40
+ Some content`
41
+ const result = await renderMarkdown(md)
42
+ expect(result.frontmatter.title).toBe("My Page")
43
+ // gray-matter parses YAML dates as Date objects
44
+ const date = result.frontmatter.date
45
+ const dateStr = date instanceof Date ? date.toISOString().split("T")[0] : date
46
+ expect(dateStr).toBe("2024-01-01")
47
+ })
48
+
49
+ it("extracts wikilinks into links array", async () => {
50
+ const result = await renderMarkdown("Check [[Some Page]] for details")
51
+ expect(result.links).toContain("Some Page")
52
+ })
53
+
54
+ it("extracts wikilink with alias", async () => {
55
+ const result = await renderMarkdown("See [[Target|display text]]")
56
+ expect(result.links).toContain("Target")
57
+ expect(result.html).toContain("display text")
58
+ })
59
+
60
+ it("extracts inline tags", async () => {
61
+ const result = await renderMarkdown("This has #mytag in it")
62
+ expect(result.tags).toContain("mytag")
63
+ })
64
+
65
+ it("merges frontmatter tags with inline tags and deduplicates", async () => {
66
+ const md = `---
67
+ tags:
68
+ - alpha
69
+ - beta
70
+ ---
71
+
72
+ Content with #beta and #gamma`
73
+ const result = await renderMarkdown(md)
74
+ expect(result.tags).toContain("alpha")
75
+ expect(result.tags).toContain("beta")
76
+ expect(result.tags).toContain("gamma")
77
+ // beta should appear only once
78
+ const betaCount = result.tags.filter((t) => t === "beta").length
79
+ expect(betaCount).toBe(1)
80
+ })
81
+
82
+ it("renders callout with correct class", async () => {
83
+ const md = "> [!note]\n> Some note content"
84
+ const result = await renderMarkdown(md)
85
+ expect(result.html).toContain("callout")
86
+ expect(result.html).toContain("callout-note")
87
+ })
88
+
89
+ it("renders LaTeX with KaTeX", async () => {
90
+ const result = await renderMarkdown("Inline math: $x^2$")
91
+ expect(result.html).toContain("katex")
92
+ })
93
+
94
+ it("renders display math with KaTeX", async () => {
95
+ const result = await renderMarkdown("$$\nx^2 + y^2 = z^2\n$$")
96
+ expect(result.html).toContain("katex")
97
+ })
98
+
99
+ it("generates html for basic markdown", async () => {
100
+ const result = await renderMarkdown("Hello **world**")
101
+ expect(result.html).toContain("<strong>world</strong>")
102
+ })
103
+
104
+ it("does not include embed links in outgoing links", async () => {
105
+ const result = await renderMarkdown("![[image.png]]")
106
+ expect(result.links).not.toContain("image.png")
107
+ })
108
+
109
+ it("adds slug ids to headings via rehype-slug", async () => {
110
+ const result = await renderMarkdown("## My Section")
111
+ expect(result.html).toContain('id="my-section"')
112
+ })
113
+ })
@@ -0,0 +1,167 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { buildSearchIndex } from "../search"
3
+ import type { MarkdownFile } from "../fs"
4
+
5
+ function makeFile(overrides: Partial<MarkdownFile> & { slug: string }): MarkdownFile {
6
+ return {
7
+ filePath: `/${overrides.slug}.md`,
8
+ frontmatter: {},
9
+ raw: "",
10
+ ...overrides,
11
+ }
12
+ }
13
+
14
+ describe("buildSearchIndex", () => {
15
+ it("returns SearchEntry[] with slug, title, content, tags", () => {
16
+ const files: MarkdownFile[] = [
17
+ makeFile({
18
+ slug: "hello",
19
+ frontmatter: { title: "Hello", tags: ["greeting"] },
20
+ raw: "---\ntitle: Hello\ntags:\n - greeting\n---\nSome body text",
21
+ }),
22
+ ]
23
+
24
+ const index = buildSearchIndex(files)
25
+ expect(index).toHaveLength(1)
26
+ expect(index[0].slug).toBe("hello")
27
+ expect(index[0].title).toBe("Hello")
28
+ expect(index[0].content).toContain("Some body text")
29
+ expect(index[0].tags).toEqual(["greeting"])
30
+ })
31
+
32
+ it("excludes draft:true files", () => {
33
+ const files: MarkdownFile[] = [
34
+ makeFile({
35
+ slug: "published",
36
+ frontmatter: { title: "Published" },
37
+ raw: "Public content",
38
+ }),
39
+ makeFile({
40
+ slug: "draft",
41
+ frontmatter: { title: "Draft", draft: true },
42
+ raw: "---\ntitle: Draft\ndraft: true\n---\nDraft content",
43
+ }),
44
+ ]
45
+
46
+ const index = buildSearchIndex(files)
47
+ expect(index).toHaveLength(1)
48
+ expect(index[0].slug).toBe("published")
49
+ })
50
+
51
+ it("strips heading markers from content", () => {
52
+ const files: MarkdownFile[] = [
53
+ makeFile({
54
+ slug: "headings",
55
+ raw: "# Heading One\n## Heading Two\nBody",
56
+ }),
57
+ ]
58
+
59
+ const index = buildSearchIndex(files)
60
+ expect(index[0].content).not.toMatch(/^#/)
61
+ expect(index[0].content).toContain("Heading One")
62
+ expect(index[0].content).toContain("Body")
63
+ })
64
+
65
+ it("strips bold and italic markers", () => {
66
+ const files: MarkdownFile[] = [
67
+ makeFile({
68
+ slug: "emphasis",
69
+ raw: "Some **bold** and *italic* and __also__ text",
70
+ }),
71
+ ]
72
+
73
+ const index = buildSearchIndex(files)
74
+ expect(index[0].content).not.toContain("**")
75
+ expect(index[0].content).not.toContain("*")
76
+ expect(index[0].content).not.toContain("__")
77
+ expect(index[0].content).toContain("bold")
78
+ expect(index[0].content).toContain("italic")
79
+ })
80
+
81
+ it("strips inline code", () => {
82
+ const files: MarkdownFile[] = [
83
+ makeFile({
84
+ slug: "code",
85
+ raw: "Use `console.log` to debug",
86
+ }),
87
+ ]
88
+
89
+ const index = buildSearchIndex(files)
90
+ expect(index[0].content).not.toContain("`")
91
+ })
92
+
93
+ it("strips wikilinks keeping display text", () => {
94
+ const files: MarkdownFile[] = [
95
+ makeFile({
96
+ slug: "links",
97
+ raw: "See [[My Page]] and [[Other|display]]",
98
+ }),
99
+ ]
100
+
101
+ const index = buildSearchIndex(files)
102
+ // wikilink regex captures target (first group), not alias
103
+ // [[My Page]] → "My Page", [[Other|display]] → "Other"
104
+ expect(index[0].content).toContain("My Page")
105
+ expect(index[0].content).toContain("Other")
106
+ expect(index[0].content).not.toContain("[[")
107
+ expect(index[0].content).not.toContain("]]")
108
+ })
109
+
110
+ it("uses last slug segment as title fallback", () => {
111
+ const files: MarkdownFile[] = [
112
+ makeFile({
113
+ slug: "notes/deep/my-page",
114
+ frontmatter: {},
115
+ raw: "Content without title",
116
+ }),
117
+ ]
118
+
119
+ const index = buildSearchIndex(files)
120
+ expect(index[0].title).toBe("my-page")
121
+ })
122
+
123
+ it("includes frontmatter tags", () => {
124
+ const files: MarkdownFile[] = [
125
+ makeFile({
126
+ slug: "tagged",
127
+ frontmatter: { tags: ["alpha", "beta"] },
128
+ raw: "---\ntags:\n - alpha\n - beta\n---\nContent",
129
+ }),
130
+ ]
131
+
132
+ const index = buildSearchIndex(files)
133
+ expect(index[0].tags).toEqual(["alpha", "beta"])
134
+ })
135
+
136
+ it("includes description from frontmatter", () => {
137
+ const files: MarkdownFile[] = [
138
+ makeFile({
139
+ slug: "desc",
140
+ frontmatter: { title: "Desc", description: "A short summary" },
141
+ raw: "---\ntitle: Desc\ndescription: A short summary\n---\nBody",
142
+ }),
143
+ ]
144
+
145
+ const index = buildSearchIndex(files)
146
+ expect(index[0].description).toBe("A short summary")
147
+ })
148
+
149
+ it("returns empty array for empty input", () => {
150
+ const index = buildSearchIndex([])
151
+ expect(index).toEqual([])
152
+ })
153
+
154
+ it("strips code blocks from content", () => {
155
+ const files: MarkdownFile[] = [
156
+ makeFile({
157
+ slug: "codeblock",
158
+ raw: "Before\n```js\nconsole.log('hi')\n```\nAfter",
159
+ }),
160
+ ]
161
+
162
+ const index = buildSearchIndex(files)
163
+ expect(index[0].content).not.toContain("console.log")
164
+ expect(index[0].content).toContain("Before")
165
+ expect(index[0].content).toContain("After")
166
+ })
167
+ })