nuartz 0.1.2 → 0.1.3

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.
Files changed (114) hide show
  1. package/dist/__tests__/backlinks.test.d.ts +2 -0
  2. package/dist/__tests__/backlinks.test.d.ts.map +1 -0
  3. package/dist/__tests__/backlinks.test.js +132 -0
  4. package/dist/__tests__/backlinks.test.js.map +1 -0
  5. package/dist/__tests__/fs.test.d.ts +2 -0
  6. package/dist/__tests__/fs.test.d.ts.map +1 -0
  7. package/dist/__tests__/fs.test.js +153 -0
  8. package/dist/__tests__/fs.test.js.map +1 -0
  9. package/dist/__tests__/markdown.test.d.ts +2 -0
  10. package/dist/__tests__/markdown.test.d.ts.map +1 -0
  11. package/dist/__tests__/markdown.test.js +102 -0
  12. package/dist/__tests__/markdown.test.js.map +1 -0
  13. package/dist/__tests__/search.test.d.ts +2 -0
  14. package/dist/__tests__/search.test.d.ts.map +1 -0
  15. package/dist/__tests__/search.test.js +147 -0
  16. package/dist/__tests__/search.test.js.map +1 -0
  17. package/dist/backlinks.d.ts +25 -0
  18. package/dist/backlinks.d.ts.map +1 -0
  19. package/dist/backlinks.js +37 -0
  20. package/dist/backlinks.js.map +1 -0
  21. package/dist/config.d.ts +46 -0
  22. package/dist/config.d.ts.map +1 -0
  23. package/dist/config.js +23 -0
  24. package/dist/config.js.map +1 -0
  25. package/dist/fs.d.ts +33 -0
  26. package/dist/fs.d.ts.map +1 -0
  27. package/dist/fs.js +145 -0
  28. package/dist/fs.js.map +1 -0
  29. package/dist/index.d.ts +17 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +29 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/markdown.d.ts +3 -0
  34. package/dist/markdown.d.ts.map +1 -0
  35. package/dist/markdown.js +119 -0
  36. package/dist/markdown.js.map +1 -0
  37. package/dist/plugins/arrows.d.ts +4 -0
  38. package/dist/plugins/arrows.d.ts.map +1 -0
  39. package/dist/plugins/arrows.js +16 -0
  40. package/dist/plugins/arrows.js.map +1 -0
  41. package/dist/plugins/callout.d.ts +4 -0
  42. package/dist/plugins/callout.d.ts.map +1 -0
  43. package/dist/plugins/callout.js +75 -0
  44. package/dist/plugins/callout.js.map +1 -0
  45. package/dist/plugins/callout.test.d.ts +2 -0
  46. package/dist/plugins/callout.test.d.ts.map +1 -0
  47. package/dist/plugins/callout.test.js +75 -0
  48. package/dist/plugins/callout.test.js.map +1 -0
  49. package/dist/plugins/comment.d.ts +4 -0
  50. package/dist/plugins/comment.d.ts.map +1 -0
  51. package/dist/plugins/comment.js +36 -0
  52. package/dist/plugins/comment.js.map +1 -0
  53. package/dist/plugins/comment.test.d.ts +2 -0
  54. package/dist/plugins/comment.test.d.ts.map +1 -0
  55. package/dist/plugins/comment.test.js +55 -0
  56. package/dist/plugins/comment.test.js.map +1 -0
  57. package/dist/plugins/highlight.d.ts +4 -0
  58. package/dist/plugins/highlight.d.ts.map +1 -0
  59. package/dist/plugins/highlight.js +33 -0
  60. package/dist/plugins/highlight.js.map +1 -0
  61. package/dist/plugins/highlight.test.d.ts +2 -0
  62. package/dist/plugins/highlight.test.d.ts.map +1 -0
  63. package/dist/plugins/highlight.test.js +51 -0
  64. package/dist/plugins/highlight.test.js.map +1 -0
  65. package/dist/plugins/tag.d.ts +11 -0
  66. package/dist/plugins/tag.d.ts.map +1 -0
  67. package/dist/plugins/tag.js +47 -0
  68. package/dist/plugins/tag.js.map +1 -0
  69. package/dist/plugins/tag.test.d.ts +2 -0
  70. package/dist/plugins/tag.test.d.ts.map +1 -0
  71. package/dist/plugins/tag.test.js +75 -0
  72. package/dist/plugins/tag.test.js.map +1 -0
  73. package/dist/plugins/wikilink.d.ts +12 -0
  74. package/dist/plugins/wikilink.d.ts.map +1 -0
  75. package/dist/plugins/wikilink.js +85 -0
  76. package/dist/plugins/wikilink.js.map +1 -0
  77. package/dist/plugins/wikilink.test.d.ts +2 -0
  78. package/dist/plugins/wikilink.test.d.ts.map +1 -0
  79. package/dist/plugins/wikilink.test.js +99 -0
  80. package/dist/plugins/wikilink.test.js.map +1 -0
  81. package/dist/search.d.ts +14 -0
  82. package/dist/search.d.ts.map +1 -0
  83. package/dist/search.js +35 -0
  84. package/dist/search.js.map +1 -0
  85. package/dist/types.d.ts +33 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +3 -0
  88. package/dist/types.js.map +1 -0
  89. package/package.json +3 -1
  90. package/src/__fixtures__/basic.md +0 -20
  91. package/src/__tests__/backlinks.test.ts +0 -156
  92. package/src/__tests__/fs.test.ts +0 -179
  93. package/src/__tests__/markdown.test.ts +0 -113
  94. package/src/__tests__/search.test.ts +0 -167
  95. package/src/backlinks.ts +0 -52
  96. package/src/config.ts +0 -63
  97. package/src/fs.ts +0 -171
  98. package/src/index.ts +0 -16
  99. package/src/markdown.ts +0 -131
  100. package/src/plugins/arrows.ts +0 -14
  101. package/src/plugins/callout.test.ts +0 -80
  102. package/src/plugins/callout.ts +0 -78
  103. package/src/plugins/comment.test.ts +0 -56
  104. package/src/plugins/comment.ts +0 -35
  105. package/src/plugins/highlight.test.ts +0 -52
  106. package/src/plugins/highlight.ts +0 -32
  107. package/src/plugins/tag.test.ts +0 -80
  108. package/src/plugins/tag.ts +0 -58
  109. package/src/plugins/wikilink.test.ts +0 -109
  110. package/src/plugins/wikilink.ts +0 -107
  111. package/src/search.ts +0 -42
  112. package/src/types.ts +0 -35
  113. package/tsconfig.json +0 -17
  114. package/vitest.config.ts +0 -20
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.remarkWikilink = void 0;
4
+ const unist_util_visit_1 = require("unist-util-visit");
5
+ /**
6
+ * Matches:
7
+ * [[target]] → link to target, display "target"
8
+ * [[target|alias]] → link to target, display "alias"
9
+ * [[target#heading]] → link to target#heading
10
+ * [[target#heading|alias]]
11
+ * ![[image.png]] → embed (image or transclusion)
12
+ */
13
+ const WIKILINK_REGEX = /(!?)\[\[([^\[\]|#]+?)(?:#([^\[\]|]+?))?(?:\|([^\[\]]+?))?\]\]/g;
14
+ const remarkWikilink = (options = {}) => {
15
+ const { baseUrl = "/", resolve, knownSlugs } = options;
16
+ const defaultResolve = (target, heading) => {
17
+ const slug = target
18
+ .toLowerCase()
19
+ .replace(/\s+/g, "-")
20
+ .replace(/[^\w-]/g, "");
21
+ const hash = heading ? `#${heading.toLowerCase().replace(/\s+/g, "-")}` : "";
22
+ return `${baseUrl}${slug}${hash}`;
23
+ };
24
+ const resolveUrl = resolve
25
+ ? (t, h) => resolve(t, h)
26
+ : defaultResolve;
27
+ return (tree, file) => {
28
+ const outgoingLinks = [];
29
+ (0, unist_util_visit_1.visit)(tree, "text", (node, index, parent) => {
30
+ if (!parent || index === undefined)
31
+ return;
32
+ const matches = [...node.value.matchAll(WIKILINK_REGEX)];
33
+ if (!matches.length)
34
+ return;
35
+ const nodes = [];
36
+ let lastIndex = 0;
37
+ for (const match of matches) {
38
+ const [full, bang, target, heading, alias] = match;
39
+ const matchIndex = match.index;
40
+ if (matchIndex > lastIndex) {
41
+ nodes.push({ type: "text", value: node.value.slice(lastIndex, matchIndex) });
42
+ }
43
+ const href = resolveUrl(target, heading);
44
+ const displayText = alias ?? (heading ? `${target} > ${heading}` : target);
45
+ if (bang === "!") {
46
+ const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4"]);
47
+ const extMatch = target.match(/\.(\w+)$/);
48
+ const isImage = extMatch ? IMAGE_EXTS.has("." + extMatch[1].toLowerCase()) : false;
49
+ nodes.push({
50
+ type: "image",
51
+ url: isImage ? `/api/content/${target}` : href,
52
+ alt: displayText,
53
+ data: { hProperties: { className: isImage ? "embed-image" : "embed-note", "data-embed": target } },
54
+ });
55
+ }
56
+ else {
57
+ outgoingLinks.push(target);
58
+ const normalized = target.toLowerCase().replace(/\s+/g, "-").replace(/[^\w/-]/g, "");
59
+ const isKnown = !knownSlugs || [...knownSlugs].some((s) => s === normalized || s.endsWith("/" + normalized));
60
+ nodes.push({
61
+ type: "link",
62
+ url: href,
63
+ data: {
64
+ hProperties: {
65
+ className: isKnown ? "wikilink" : "wikilink broken",
66
+ "data-target": target,
67
+ ...(heading ? { "data-heading": heading } : {}),
68
+ },
69
+ },
70
+ children: [{ type: "text", value: displayText }],
71
+ });
72
+ }
73
+ lastIndex = matchIndex + full.length;
74
+ }
75
+ if (lastIndex < node.value.length) {
76
+ nodes.push({ type: "text", value: node.value.slice(lastIndex) });
77
+ }
78
+ parent.children.splice(index, 1, ...nodes);
79
+ });
80
+ const existing = file.data.links ?? [];
81
+ file.data.links = [...new Set([...existing, ...outgoingLinks])];
82
+ };
83
+ };
84
+ exports.remarkWikilink = remarkWikilink;
85
+ //# sourceMappingURL=wikilink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikilink.js","sourceRoot":"","sources":["../../src/plugins/wikilink.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=wikilink.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikilink.test.d.ts","sourceRoot":"","sources":["../../src/plugins/wikilink.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const unified_1 = require("unified");
8
+ const remark_parse_1 = __importDefault(require("remark-parse"));
9
+ const remark_rehype_1 = __importDefault(require("remark-rehype"));
10
+ const rehype_stringify_1 = __importDefault(require("rehype-stringify"));
11
+ const wikilink_js_1 = require("./wikilink.js");
12
+ async function process(md, options) {
13
+ const result = await (0, unified_1.unified)()
14
+ .use(remark_parse_1.default)
15
+ .use(wikilink_js_1.remarkWikilink, options)
16
+ .use(remark_rehype_1.default)
17
+ .use(rehype_stringify_1.default)
18
+ .process(md);
19
+ return String(result);
20
+ }
21
+ async function processWithData(md, options) {
22
+ const file = await (0, unified_1.unified)()
23
+ .use(remark_parse_1.default)
24
+ .use(wikilink_js_1.remarkWikilink, options)
25
+ .use(remark_rehype_1.default)
26
+ .use(rehype_stringify_1.default)
27
+ .process(md);
28
+ return { html: String(file), links: file.data.links ?? [] };
29
+ }
30
+ (0, vitest_1.describe)("remarkWikilink", () => {
31
+ (0, vitest_1.it)("transforms [[Page Name]] into a wikilink", async () => {
32
+ const html = await process("Visit [[Page Name]] for details");
33
+ (0, vitest_1.expect)(html).toContain('href="/page-name"');
34
+ (0, vitest_1.expect)(html).toContain('class="wikilink"');
35
+ (0, vitest_1.expect)(html).toContain("Page Name");
36
+ });
37
+ (0, vitest_1.it)("handles alias [[Page|Alias]]", async () => {
38
+ const html = await process("See [[Page|Custom Display]]");
39
+ (0, vitest_1.expect)(html).toContain('href="/page"');
40
+ (0, vitest_1.expect)(html).toContain("Custom Display");
41
+ });
42
+ (0, vitest_1.it)("handles heading [[Page#Heading]]", async () => {
43
+ const html = await process("Go to [[Page#My Section]]");
44
+ (0, vitest_1.expect)(html).toContain('href="/page#my-section"');
45
+ (0, vitest_1.expect)(html).toContain("Page > My Section");
46
+ });
47
+ (0, vitest_1.it)("handles heading with alias [[Page#Heading|Alias]]", async () => {
48
+ const html = await process("See [[Page#Section|Link Text]]");
49
+ (0, vitest_1.expect)(html).toContain('href="/page#section"');
50
+ (0, vitest_1.expect)(html).toContain("Link Text");
51
+ });
52
+ (0, vitest_1.it)("transforms ![[image.png]] into an embed image", async () => {
53
+ const html = await process("Here: ![[image.png]]");
54
+ (0, vitest_1.expect)(html).toContain("<img");
55
+ (0, vitest_1.expect)(html).toContain('src="/api/content/image.png"');
56
+ (0, vitest_1.expect)(html).toContain('class="embed-image"');
57
+ });
58
+ (0, vitest_1.it)("converts spaces to hyphens in href", async () => {
59
+ const html = await process("[[My Long Page Name]]");
60
+ (0, vitest_1.expect)(html).toContain('href="/my-long-page-name"');
61
+ });
62
+ (0, vitest_1.it)("accumulates links in file.data.links", async () => {
63
+ const { links } = await processWithData("[[Page A]] and [[Page B]]");
64
+ (0, vitest_1.expect)(links).toContain("Page A");
65
+ (0, vitest_1.expect)(links).toContain("Page B");
66
+ });
67
+ (0, vitest_1.it)("deduplicates collected links", async () => {
68
+ const { links } = await processWithData("[[Same]] and [[Same]]");
69
+ (0, vitest_1.expect)(links).toEqual(["Same"]);
70
+ });
71
+ (0, vitest_1.it)("does not add embeds to outgoing links", async () => {
72
+ const { links } = await processWithData("![[image.png]]");
73
+ (0, vitest_1.expect)(links).toEqual([]);
74
+ });
75
+ (0, vitest_1.it)("supports custom resolve function", async () => {
76
+ const html = await process("[[My Page]]", {
77
+ resolve: (target) => `/custom/${target.toLowerCase().replace(/\s+/g, "_")}`,
78
+ });
79
+ (0, vitest_1.expect)(html).toContain('href="/custom/my_page"');
80
+ });
81
+ (0, vitest_1.it)("supports custom baseUrl", async () => {
82
+ const html = await process("[[Page]]", { baseUrl: "/wiki/" });
83
+ (0, vitest_1.expect)(html).toContain('href="/wiki/page"');
84
+ });
85
+ (0, vitest_1.it)("sets data-target attribute", async () => {
86
+ const html = await process("[[My Page]]");
87
+ (0, vitest_1.expect)(html).toContain('data-target="My Page"');
88
+ });
89
+ (0, vitest_1.it)("sets data-heading attribute when heading present", async () => {
90
+ const html = await process("[[Page#Section]]");
91
+ (0, vitest_1.expect)(html).toContain('data-heading="Section"');
92
+ });
93
+ (0, vitest_1.it)("preserves surrounding text", async () => {
94
+ const html = await process("before [[Link]] after");
95
+ (0, vitest_1.expect)(html).toContain("before ");
96
+ (0, vitest_1.expect)(html).toContain(" after");
97
+ });
98
+ });
99
+ //# sourceMappingURL=wikilink.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wikilink.test.js","sourceRoot":"","sources":["../../src/plugins/wikilink.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { MarkdownFile } from "./fs.js";
2
+ export interface SearchEntry {
3
+ slug: string;
4
+ title: string;
5
+ content: string;
6
+ tags: string[];
7
+ description?: string;
8
+ }
9
+ /**
10
+ * Builds a flat search index from a list of MarkdownFile entries.
11
+ * Strip HTML and frontmatter for plain-text indexing.
12
+ */
13
+ export declare function buildSearchIndex(files: MarkdownFile[]): SearchEntry[];
14
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,WAAW,EAAE,CA2BrE"}
package/dist/search.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildSearchIndex = buildSearchIndex;
4
+ /**
5
+ * Builds a flat search index from a list of MarkdownFile entries.
6
+ * Strip HTML and frontmatter for plain-text indexing.
7
+ */
8
+ function buildSearchIndex(files) {
9
+ return files
10
+ .filter((f) => !f.frontmatter.draft)
11
+ .map((file) => {
12
+ // Strip frontmatter block
13
+ const body = file.raw.replace(/^---[\s\S]*?---\n?/, "");
14
+ // Strip markdown syntax (rough pass)
15
+ const text = body
16
+ .replace(/```[\s\S]*?```/g, "") // code blocks
17
+ .replace(/`[^`]+`/g, "") // inline code
18
+ .replace(/!\[.*?\]\(.*?\)/g, "") // images
19
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") // links → text
20
+ .replace(/\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g, "$1") // wikilinks → text
21
+ .replace(/#{1,6}\s/g, "") // headings
22
+ .replace(/[*_~]/g, "") // emphasis
23
+ .replace(/>\s/g, "") // blockquotes
24
+ .replace(/\s+/g, " ")
25
+ .trim();
26
+ return {
27
+ slug: file.slug,
28
+ title: file.frontmatter.title ?? file.slug.split("/").pop() ?? file.slug,
29
+ content: text,
30
+ tags: file.frontmatter.tags ?? [],
31
+ description: file.frontmatter.description,
32
+ };
33
+ });
34
+ }
35
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":";;AAcA,4CA2BC;AA/BD;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,KAAqB;IACpD,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC;SACnC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,0BAA0B;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAA;QACvD,qCAAqC;QACrC,MAAM,IAAI,GAAG,IAAI;aACd,OAAO,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAG,cAAc;aAC/C,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAU,cAAc;aAC/C,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC,CAAE,SAAS;aAC1C,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC,eAAe;aACvD,OAAO,CAAC,oCAAoC,EAAE,IAAI,CAAC,CAAC,mBAAmB;aACvE,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAS,WAAW;aAC5C,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAY,WAAW;aAC5C,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAc,cAAc;aAC/C,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;aACpB,IAAI,EAAE,CAAA;QAET,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,IAAI;YACxE,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,EAAE;YACjC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,WAAW;SAC1C,CAAA;IACH,CAAC,CAAC,CAAA;AACN,CAAC"}
@@ -0,0 +1,33 @@
1
+ export interface Frontmatter {
2
+ title?: string;
3
+ date?: string | Date;
4
+ tags?: string[];
5
+ description?: string;
6
+ draft?: boolean;
7
+ aliases?: string[];
8
+ [key: string]: unknown;
9
+ }
10
+ export interface TocEntry {
11
+ depth: number;
12
+ text: string;
13
+ id: string;
14
+ children: TocEntry[];
15
+ }
16
+ export interface RenderResult {
17
+ html: string;
18
+ frontmatter: Frontmatter;
19
+ toc: TocEntry[];
20
+ links: string[];
21
+ tags: string[];
22
+ }
23
+ export interface RenderOptions {
24
+ /** Base URL for wikilinks (default: '/') */
25
+ baseUrl?: string;
26
+ /** Resolve wikilink target to URL path */
27
+ resolveLink?: (target: string) => string;
28
+ /** Known slugs for dead-link detection */
29
+ knownSlugs?: Set<string>;
30
+ /** Whether to strip draft pages (default: false) */
31
+ stripDrafts?: boolean;
32
+ }
33
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,QAAQ,EAAE,CAAA;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,WAAW,CAAA;IACxB,GAAG,EAAE,QAAQ,EAAE,CAAA;IACf,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,IAAI,EAAE,MAAM,EAAE,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAA;IACxC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,oDAAoD;IACpD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuartz",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Obsidian-compatible markdown pipeline for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,9 +11,11 @@
11
11
  "homepage": "https://github.com/syshin0116/nuartz#readme",
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
+ "files": ["dist"],
14
15
  "exports": {
15
16
  ".": {
16
17
  "types": "./dist/index.d.ts",
18
+ "files": ["dist"],
17
19
  "default": "./dist/index.js"
18
20
  }
19
21
  },
@@ -1,20 +0,0 @@
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.
@@ -1,156 +0,0 @@
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
- })