Files

261 lines
18 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { readLibrary, parseFrontMatter, LibraryError, MAX_BLOB } from '../src/index.mjs';
import { BASE, git, MARKER, mapping, repo } from './helpers.mjs';
const codes = (result, path) => result.problems.filter(p => p.path === path).map(p => p.code);
const file = (result, path) => result.files.find(f => f.path === path);
test('the fixture reads clean: every resolver step, appears_in and relationships wikilinks indexed', async t => {
const r = repo(t);
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.equal(result.commit, r.commit);
assert.deepEqual(result.files.map(f => [f.path, f.scope]), [
['Book One/Chapters/Chapter 01.md', 'canon'], ['Book One/Characters/Mara Vell.md', 'canon'],
['Book One/Characters/Tomas.md', 'canon'], ['Book One/Places/Harbor Town.md', 'canon'],
['Book One/Playground/What If.md', 'playground'], ['Second World/Chapters/01.md', 'canon']]);
const chapter = file(result, 'Book One/Chapters/Chapter 01.md');
assert.deepEqual(chapter.links.map(l => [l.path, l.step, l.line]), [
['Book One/Characters/Mara Vell.md', 'shortest', 2], ['Book One/Places/Harbor Town.md', 'root', 2],
['Book One/Places/Harbor Town.md', 'relative', 3], ['Book One/Characters/Tomas.md', 'vault-prefix', 3]]);
const mara = file(result, 'Book One/Characters/Mara Vell.md');
assert.deepEqual(mara.frontmatter, { type: 'character', name: 'Mara Vell', appears_in: '[[Book One/Chapters/Chapter 01]]',
relationships: ['[[Tomas]]', 'The Ferryman'] });
assert.deepEqual(mara.links.map(l => [l.field, l.path, l.line]), [
['appears_in', 'Book One/Chapters/Chapter 01.md', 4], ['relationships', 'Book One/Characters/Tomas.md', 6]]);
assert.deepEqual(file(result, 'Book One/Characters/Tomas.md').links.map(l => [l.field, l.path]),
[['appears_in', 'Book One/Chapters/Chapter 01.md']]);
assert.equal(mara.bodyLine, 9);
assert.equal(mara.lines[8][0], 9);
assert.equal(mara.type, 'character');
// Only the unmapped paths are problems: the top-level note and both .canvas files.
assert.deepEqual(result.problems.map(p => [p.code, p.path]), [
['R2', 'Book One/Board.canvas'], ['R2', 'Book One/Playground/Sketch.canvas'], ['R2', 'Inbox Note.md']]);
assert.ok(Object.isFrozen(result.files[0].links));
});
test('R1: executables, symlinks, gitlinks, blobs over 1 MiB and non-UTF-8 text are not indexed', async t => {
const r = repo(t, { ...BASE,
'Book One/Places/Run.md': { text: 'x\n', mode: 0o755 },
'Book One/Places/Link.md': { link: 'Harbor Town.md' },
'Book One/Places/Big.md': 'x'.repeat(MAX_BLOB + 1),
'Book One/Places/Edge.md': 'y'.repeat(MAX_BLOB),
'Book One/Places/Latin.md': { bytes: Buffer.from([0x63, 0x61, 0x66, 0xe9, 0x0a]) },
'Book One/Places/Nul.md': 'a\0b\n' });
git(r.repo, 'update-index', '--add', '--cacheinfo', `160000,${r.commit},Book One/Places/Sub.md`);
git(r.repo, 'commit', '-qm', 'gitlink');
const result = await readLibrary({ repo: r.repo, commit: git(r.repo, 'rev-parse', 'HEAD'), mapping: mapping() });
for (const name of ['Run', 'Link', 'Big', 'Latin', 'Nul', 'Sub']) {
assert.deepEqual(codes(result, `Book One/Places/${name}.md`), ['R1'], name);
assert.equal(file(result, `Book One/Places/${name}.md`), undefined, name);
}
assert.ok(file(result, 'Book One/Places/Edge.md'), 'exactly 1 MiB is read');
});
test('R2: unmapped paths are reported and not indexed', async t => {
const r = repo(t);
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
for (const path of ['Inbox Note.md', 'Book One/Board.canvas', 'Book One/Playground/Sketch.canvas']) {
assert.deepEqual(codes(result, path), ['R2']);
assert.equal(file(result, path), undefined);
}
});
test('R3: front matter not fully parsed stays raw, with the line where parsing stopped', async t => {
const r = repo(t, { ...BASE,
'Book One/Places/Nested.md': ['---', 'type: place', 'coords:', ' lat: 1', '---', 'Body [[Tomas]]', ''].join('\n'),
'Book One/Places/Block.md': ['---', 'summary: |', ' text', '---', ''].join('\n'),
'Book One/Places/Open.md': ['---', 'type: place', 'never closed', ''].join('\n') });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
const nested = file(result, 'Book One/Places/Nested.md');
assert.deepEqual(result.problems.filter(p => p.path === 'Book One/Places/Nested.md'), [{ code: 'R3', path: 'Book One/Places/Nested.md', line: 4, message: 'front matter not fully parsed' }]);
assert.equal(nested.frontmatter, null);
assert.equal(nested.frontmatterRaw, 'type: place\ncoords:\n lat: 1');
assert.deepEqual(nested.links.map(l => l.path), ['Book One/Characters/Tomas.md'], 'still indexed by body');
assert.equal(result.problems.find(p => p.path === 'Book One/Places/Block.md').line, 2);
assert.equal(result.problems.find(p => p.path === 'Book One/Places/Open.md').line, 1);
assert.equal(file(result, 'Book One/Places/Open.md').bodyLine, 1);
});
test('front matter forms the reader accepts and refuses', () => {
const parse = text => parseFrontMatter(text.split('\n'));
assert.deepEqual(parse('---\na: plain words\nb: "quoted: yes"\nc: \'it\'\'s\'\nd: [x, "y z", 3]\ne:\n- one\n- "[[Two]]"\nf: [[Wiki]]\ng: true\nh:\n---').frontmatter,
{ a: 'plain words', b: 'quoted: yes', c: "it's", d: ['x', 'y z', 3], e: ['one', '[[Two]]'], f: '[[Wiki]]', g: true, h: null });
for (const bad of ['a: {x: 1}', 'a: &anchor x', 'a: [x, [y]]', 'a: x: y', 'a: 1\na: 2', ' a: 1', 'a: "open', '- loose'])
assert.notEqual(parse(`---\n${bad}\n---`).stop, null, bad);
assert.deepEqual(parse('no front matter'), { frontmatter: null, raw: null, bodyLine: 1, stop: null });
});
test('R4: a link to no file, to more than one, or into another repo is reported and not followed', async t => {
const r = repo(t, { ...BASE,
'Book One/Research/Notes.md': 'a\n', 'Book One/Places/Notes.md': 'b\n',
'Book One/Chapters/Links.md': ['[[Nobody]]', '[[Notes]]', '[[bean-brain/Journal/Today]]', '[[Tomas]]', ''].join('\n') });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.deepEqual(result.problems.filter(p => p.path === 'Book One/Chapters/Links.md').map(p => [p.code, p.line]), [['R4', 1], ['R4', 2], ['R4', 3]]);
assert.deepEqual(file(result, 'Book One/Chapters/Links.md').links.map(l => l.path), ['Book One/Characters/Tomas.md']);
});
test('a block list link carries its item line, a scalar or flow link its key line, and problems follow', async t => {
const r = repo(t, { ...BASE, 'Book One/Characters/Ana.md': ['---', 'appears_in: ["[[Nobody]]"]', 'relationships:', ' - "[[Tomas]]"',
' - Plain Name', ' - "[[Ghost]]"', ' - "[[What If]]"', '---', ''].join('\n') });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.deepEqual(file(result, 'Book One/Characters/Ana.md').links.map(l => [l.field, l.path, l.line]), [
['relationships', 'Book One/Characters/Tomas.md', 4], ['relationships', 'Book One/Playground/What If.md', 7]]);
assert.deepEqual(result.problems.filter(p => p.path === 'Book One/Characters/Ana.md').map(p => [p.code, p.line]),
[['R4', 2], ['R4', 6], ['R5', 7]]);
});
// sage-1900: only an unresolved body embed with an extension other than .md is an attachment. A plain
// link to one and a front matter value stay R4 when they resolve to nothing.
test('an unresolved attachment embed gives no problem; a link or front matter value to one is R4; a note embed resolves', async t => {
const r = repo(t, { ...BASE, 'Book One/Places/note.md': 'n\n', 'Book One/Places/Shore.md': ['---',
'relationships:', ' - "![[portrait.JPG]]"', '---', '![[cover.png]] [[Board.canvas]] ![[map.pdf|Map]]',
'![[missing-note]] ![[note.md]] ![[Harbor Town]]', ''].join('\n') });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.deepEqual(result.problems.filter(p => p.path === 'Book One/Places/Shore.md').map(p => [p.code, p.line]),
[['R4', 3], ['R4', 5], ['R4', 6]]);
assert.deepEqual(file(result, 'Book One/Places/Shore.md').links.map(l => [l.path, l.embed]),
[['Book One/Places/note.md', true], ['Book One/Places/Harbor Town.md', true]]);
});
// webby-0070 B1: a note whose name looks like it has an extension resolves before the attachment test.
test('an indexed Dr.Quinn.md resolves as a body link, a block list link and an embed', async t => {
const r = repo(t, { ...BASE, 'Book One/Characters/Dr.Quinn.md': 'q\n', 'Book One/Characters/Ana.md': ['---',
'relationships:', ' - "[[Dr.Quinn]]"', '---', '[[Dr.Quinn]] ![[Dr.Quinn]]', ''].join('\n') });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.deepEqual(file(result, 'Book One/Characters/Ana.md').links.map(l => [l.field ?? null, l.path, l.line, l.embed]), [
[null, 'Book One/Characters/Dr.Quinn.md', 5, false], [null, 'Book One/Characters/Dr.Quinn.md', 5, true],
['relationships', 'Book One/Characters/Dr.Quinn.md', 3, false]]);
assert.deepEqual(result.problems.filter(p => p.path === 'Book One/Characters/Ana.md'), []);
});
test('R5: a canon file that refers into the playground is reported, and the link still goes by path', async t => {
const r = repo(t, { ...BASE, 'Book One/Places/Dock.md': 'See [[What If]].\n' });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.deepEqual(codes(result, 'Book One/Places/Dock.md'), ['R5']);
assert.deepEqual(file(result, 'Book One/Places/Dock.md').links.map(l => l.path), ['Book One/Playground/What If.md']);
// The playground file links to canon: no problem, R5 is only about canon.
assert.deepEqual(codes(result, 'Book One/Playground/What If.md'), []);
});
test('R6: a canon file that refers into another universe is reported; a hybrid may refer into its sources', async t => {
const r = repo(t, { ...BASE, 'Book One/Places/Far.md': 'Like [[Second World/Chapters/01]].\n',
'Crossover/Chapters/A.md': 'From [[Mara Vell]] and [[Second World/Chapters/01]].\n' });
const hybrid = mapping(e => { e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'] }; return e; });
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: hybrid });
assert.deepEqual(codes(result, 'Book One/Places/Far.md'), ['R6']);
assert.deepEqual(file(result, 'Book One/Places/Far.md').links.map(l => l.path), ['Second World/Chapters/01.md']);
assert.deepEqual(codes(result, 'Crossover/Chapters/A.md'), []);
});
// C2-15: a hybrid over harbor and second with one exclude and, optionally, a pin on Mara's eyes.
const crossWith = (...constraints) => mapping(e => {
e.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'], constraints };
return e;
});
const EYES = { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'eyes', value: 'grey' } };
const character = (name, extra = []) => ['---', 'type: character', ...(name ? [`name: ${name}`] : []), ...extra, '---', MARKER, ''].join('\n');
const MARA_GREY = character('Mara Vell', ['eyes: grey']);
test('C2-15 R6: a hybrid file that links into an excluded source path is reported, in both exclude forms', async t => {
const r = repo(t, { ...BASE, 'Second World/Characters/Ilse.md': character('Ilse'), 'Book One/Research/Logs.md': '# Logs\n',
'Crossover/Chapters/A.md': 'From [[Mara Vell]] and [[Ilse]].\nThen [[Logs]].\n' });
for (const [exclude, line] of [['second/Characters', 1], ['Book One/Research', 2]]) {
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith({ exclude }) });
assert.deepEqual(result.problems.filter(p => p.path === 'Crossover/Chapters/A.md'),
[{ code: 'R6', path: 'Crossover/Chapters/A.md', line, message: 'canon file refers into another universe' }], exclude);
// The link stays in the file's links, as for every R-rule; the index's sees() is what hides it.
assert.equal(file(result, 'Crossover/Chapters/A.md').links.length, 3);
// A source's own link into the same path is not the hybrid's and stays clean.
assert.deepEqual(codes(result, 'Book One/Chapters/Chapter 01.md'), []);
}
});
test('C2-15 R8: the pinned entity reads clean when its field holds the value, and is reported when missing or changed', async t => {
const at = async files => {
const r = repo(t, { ...BASE, ...files });
return (await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8');
};
assert.deepEqual(await at({ 'Book One/Characters/Mara Vell.md': MARA_GREY }), []);
// Missing: no line.
const base = { ...BASE }; delete base['Book One/Characters/Mara Vell.md'];
const r = repo(t, base);
assert.deepEqual((await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8'),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', message: 'pinned value changed' }]);
// Two pins on the missing entity give one entry (Filbert N1).
const twoPins = crossWith(EYES, { pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'name', value: 'Mara Vell' } });
assert.deepEqual((await readLibrary({ repo: r.repo, commit: r.commit, mapping: twoPins })).problems.filter(p => p.code === 'R8'),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', message: 'pinned value changed' }]);
// Changed, with the key's line; a missing key has no line.
assert.deepEqual(await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['eyes: green']) }),
[{ code: 'R8', path: 'Book One/Characters/Mara Vell.md', line: 4, message: 'pinned value changed' }]);
assert.deepEqual((await at({})).map(p => [p.path, p.line]), [['Book One/Characters/Mara Vell.md', undefined]]);
// A list holding the value, or null, is not the value (String(['grey']) would be 'grey').
for (const eyes of ['eyes: [grey]', 'eyes:', 'eyes: ~'])
assert.deepEqual((await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', [eyes]) })).map(p => p.line), [4], eyes);
// Unparsed (R3) front matter counts as changed.
assert.equal((await at({ 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['eyes: grey', 'bad: {a: 1}']) })).length, 1);
// A number pin compares as text.
const r2 = repo(t, { ...BASE, 'Book One/Characters/Mara Vell.md': character('Mara Vell', ['age: 30']) });
const age = crossWith({ pin: { entity: 'Book One/Characters/Mara Vell.md', field: 'age', value: 30 } });
assert.deepEqual((await readLibrary({ repo: r2.repo, commit: r2.commit, mapping: age })).problems.filter(p => p.code === 'R8'), []);
});
test('C2-15 R8: a hybrid version of the entity that sets the field to another value is reported on that line; a link alone is not a version', async t => {
const read = async files => {
const r = repo(t, { ...BASE, 'Book One/Characters/Mara Vell.md': MARA_GREY, ...files });
return (await readLibrary({ repo: r.repo, commit: r.commit, mapping: crossWith(EYES) })).problems.filter(p => p.code === 'R8');
};
// Same type and name, no link to the entity: a version.
assert.deepEqual(await read({ 'Crossover/Characters/Older Mara.md': character('Mara Vell', ['eyes: blue']) }),
[{ code: 'R8', path: 'Crossover/Characters/Older Mara.md', line: 4, message: 'pinned value changed' }]);
// No name on the version: the file names compare.
assert.deepEqual((await read({ 'Crossover/Characters/Mara Vell.md': character(null, ['eyes: blue']) })).map(p => [p.path, p.line]),
[['Crossover/Characters/Mara Vell.md', 3]]);
// A version that keeps the value, or doesn't set the field, is clean.
assert.deepEqual(await read({ 'Crossover/Characters/Older Mara.md': character('Mara Vell', ['eyes: grey']),
'Crossover/Characters/Young Mara.md': character('Mara Vell') }), []);
// Not versions: a place with the field, a different character that links her, a playground copy, a source file.
assert.deepEqual(await read({
'Crossover/Places/Mara Vell.md': ['---', 'type: place', 'name: Mara Vell', 'eyes: blue', '---', '', ''].join('\n'),
'Crossover/Characters/Ilse.md': character('Ilse', ['eyes: blue', 'relationships:', ' - "[[Mara Vell]]"']),
'Second World/Characters/Mara Vell.md': character('Mara Vell', ['eyes: blue']),
}), []);
});
test('R7: two paths that differ only in case are both left out', async t => {
const r = repo(t, BASE);
writeFileSync(join(r.root, 'twin'), 'twin\n');
const blob = git(r.repo, 'hash-object', '-w', join(r.root, 'twin'));
git(r.repo, 'update-index', '--add', '--cacheinfo', `100644,${blob},Book One/Characters/tomas.md`);
git(r.repo, 'commit', '-qm', 'case twin');
const result = await readLibrary({ repo: r.repo, commit: git(r.repo, 'rev-parse', 'HEAD'), mapping: mapping() });
for (const path of ['Book One/Characters/Tomas.md', 'Book One/Characters/tomas.md']) {
assert.deepEqual(codes(result, path), ['R7'], path);
assert.equal(file(result, path), undefined);
}
});
test('the reader reads the commit, never the working tree, and refuses a bad commit id', async t => {
const r = repo(t);
writeFileSync(join(r.repo, 'Book One/Places/Harbor Town.md'), 'unsaved [[Nobody]]\n');
writeFileSync(join(r.repo, 'Book One/Places/Untracked.md'), 'x\n');
const result = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
assert.equal(file(result, 'Book One/Places/Harbor Town.md').lines[0][1], '---');
assert.equal(file(result, 'Book One/Places/Untracked.md'), undefined);
for (const commit of ['HEAD', r.commit.slice(0, 12), '0'.repeat(40), git(r.repo, 'rev-parse', 'HEAD^{tree}')])
await assert.rejects(readLibrary({ repo: r.repo, commit, mapping: mapping() }), e => e instanceof LibraryError, commit);
await assert.rejects(readLibrary({ repo: r.repo, commit: r.commit, mapping: {} }), /library-mapping-invalid/);
});
test('a bare mirror reads the same as the clone', async t => {
const r = repo(t);
const bare = join(r.root, 'mirror.git');
git(r.root, 'clone', '-q', '--bare', r.repo, bare);
const a = await readLibrary({ repo: r.repo, commit: r.commit, mapping: mapping() });
const b = await readLibrary({ repo: bare, commit: r.commit, mapping: mapping() });
assert.deepEqual(b, a);
});