benjivers/notes Public Public: anyone can read this; nobody can change it.

Files otel
127 lines · 4.7 KB · ~1.4k tok Raw History
1
/**
2
 * The vault's own sense of itself — links, backlinks, tags, the graph —
3
 * and the markdown it is written in, which the browser renders with the
4
 * very same file.
5
 */
6
import { expect, test } from "bun:test";
7
// @ts-ignore — plain JS, shared with the browser.
8
import { render } from "../static/md.js";
9
import {
10
  backlinks,
11
  excerpt,
12
  graph,
13
  headings,
14
  links,
15
  outgoing,
16
  relink,
17
  renderNote,
18
  search,
19
  tagCounts,
20
  tags,
21
  type VaultNote,
22
} from "./vault.ts";
23
24
const note = (id: number, title: string, body: string, updated_at = `2026-10-0${id}`): VaultNote => ({
25
  id,
26
  title,
27
  body,
28
  pinned: 0,
29
  updated_at,
30
});
31
32
test("links and tags are found in prose, never in code", () => {
33
  const body = "See [[Alpha]], [[beta|B]] and [[Alpha#Part]].\n`[[Not]] #nope`\n```\n[[Fenced]] #fenced\n```\n#real and #nested/tag, not #123 or a#b";
34
  expect(links(body)).toEqual(["Alpha", "beta"]);
35
  expect(tags(body)).toEqual(["real", "nested/tag"]);
36
  expect(headings("# One\ntext\n## Two ##\n```\n# not\n```")).toEqual([
37
    { level: 1, text: "One", id: "one" },
38
    { level: 2, text: "Two", id: "two" },
39
  ]);
40
});
41
42
test("backlinks, links out, tag counts and the graph agree", () => {
43
  const a = note(1, "Alpha", "to [[Beta]] and [[Missing]] #x");
44
  const b = note(2, "Beta", "back to [[alpha]] #x #y");
45
  const c = note(3, "Gamma", "to [[BETA]]");
46
  const vault = [a, b, c];
47
  expect(backlinks(b, vault).map((n) => n.id)).toEqual([1, 3]);
48
  expect(backlinks(c, vault)).toEqual([]);
49
  expect(outgoing(a, vault)).toEqual({ found: [b], missing: ["Missing"] });
50
  expect(tagCounts(vault)).toEqual([
51
    { tag: "x", count: 2 },
52
    { tag: "y", count: 1 },
53
  ]);
54
  const g = graph(vault);
55
  expect(g.edges).toEqual([
56
    [1, 2],
57
    [3, 2],
58
  ]);
59
  expect(g.nodes.find((n) => n.id === 2)?.links).toBe(2);
60
});
61
62
test("relink rewrites only links to the old title", () => {
63
  expect(relink("[[Old]] [[old|alias]] [[Old#h]] ![[Old]] [[Older]] Old", "Old", "New")).toBe(
64
    "[[New]] [[New|alias]] [[New#h]] ![[New]] [[Older]] Old",
65
  );
66
});
67
68
test("search ranks title matches first, then the most recent", () => {
69
  const vault = [note(1, "Plant", "water"), note(2, "Garden", "plant beds"), note(3, "Shed", "plant pots")];
70
  expect(search(vault, "plant").map((h) => h.note.id)).toEqual([1, 3, 2]);
71
  expect(search(vault, "")).toEqual([]);
72
  expect(excerpt("# Head\n**bold** [[A|alias]] text")).toBe("Head bold alias text");
73
});
74
75
test("markdown renders the everyday core", () => {
76
  const html = render(
77
    "# Title\n\nSome **bold**, *em*, ~~gone~~, ==lit== and `code`.\n\n- one\n  - nested\n- [x] done\n- [ ] todo\n\n1. first\n2. second\n\n> quoted\n\n---\n\n| a | b |\n|---|--:|\n| 1 | 2 |\n\n```js\nlet x = 1 < 2;\n```",
78
  );
79
  for (const piece of [
80
    '<h1 id="title">Title</h1>',
81
    "<strong>bold</strong>",
82
    "<em>em</em>",
83
    "<del>gone</del>",
84
    "<mark>lit</mark>",
85
    "<code>code</code>",
86
    "<ul><li>one<ul><li>nested</li></ul></li>",
87
    'aria-checked="true"',
88
    'aria-checked="false"',
89
    "<ol><li>first</li><li>second</li></ol>",
90
    "<blockquote><p>quoted</p>",
91
    "<hr>",
92
    '<td style="text-align:right">2</td>',
93
    '<pre><code class="lang-js">let x = 1 &lt; 2;</code></pre>',
94
  ])
95
    expect([piece, html.includes(piece)]).toEqual([piece, true]);
96
});
97
98
test("wikilinks point at notes, or at making them; callouts and tags are Obsidian's", () => {
99
  const vault = [note(7, "Known", "")];
100
  const html = renderNote("[[Known]] [[known#Part B|that part]] [[Unknown]]\n\n> [!warning] Careful\n> body\n\n#tag", vault);
101
  expect(html).toContain('<a class="wikilink" href="/n/7" data-title="Known">Known</a>');
102
  expect(html).toContain('href="/n/7#part-b"');
103
  expect(html).toContain(">that part</a>");
104
  expect(html).toContain('class="wikilink unresolved" href="/open?title=Unknown"');
105
  expect(html).toContain('<div class="callout callout-warning"><p class="callout-title">Careful</p><p>body</p></div>');
106
  expect(html).toContain('<a class="tag" href="/search?q=%23tag">#tag</a>');
107
});
108
109
test("nothing a note says becomes markup the renderer did not write", () => {
110
  const evil = [
111
    '<script>alert(1)</script>',
112
    '<img src=x onerror=alert(1)>',
113
    "[click](javascript:alert(1))",
114
    '[x](https://a.example/" onmouseover="alert(1))',
115
    '[[x" onmouseover="alert(1)]]',
116
    "[[<b>bold</b>]]",
117
    "| <i>a</i> |\n|---|\n| <svg onload=alert(1)> |",
118
    "> [!<img src=x onerror=1>] t",
119
    "```\n</code></pre><script>x</script>\n```",
120
  ].join("\n\n");
121
  const html = render(evil);
122
  expect(html).not.toMatch(/<(script|img|svg|i|b)\b/i);
123
  expect(html).not.toMatch(/href="(?!https?:|\/|mailto:)/);
124
  expect(html).not.toMatch(/\son\w+="/i);
125
  // The safe link kinds still work.
126
  expect(render("[a](https://x.example) [b](/n/1) [c](mailto:[email protected])")).toContain('href="https://x.example"');
127
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts