312 lines
18 KiB
JavaScript
312 lines
18 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createRequire } from 'node:module';
|
|
import { mkdtemp, readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import * as C from '../src/lib/canvas.js';
|
|
import { resolveLink, refFor } from '../src/lib/links.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const disk = require('../electron/canvas.cjs');
|
|
const links = require('../electron/links.cjs');
|
|
const { Vault } = require('../electron/vault.cjs');
|
|
|
|
const tmp = () => mkdtemp(path.join(tmpdir(), 'ink-canvas-test-'));
|
|
|
|
// A canvas as Obsidian writes it.
|
|
const OBSIDIAN = `{
|
|
\t"nodes":[
|
|
\t\t{"id":"a1b2c3d4e5f60718","type":"group","x":-300,"y":-260,"width":760,"height":520,"label":"Plan","color":"4"},
|
|
\t\t{"id":"0f1e2d3c4b5a6978","type":"text","text":"# Idea\\n\\nSee [[Roadmap]] and [[Roadmap#Goals|goals]]","x":-260,"y":-200,"width":300,"height":160},
|
|
\t\t{"id":"1122334455667788","type":"file","file":"Projects/Roadmap.md","subpath":"#Goals","x":100,"y":-200,"width":320,"height":400},
|
|
\t\t{"id":"99aabbccddeeff00","type":"file","file":"Projects/diagram.png","x":-260,"y":0,"width":300,"height":200},
|
|
\t\t{"id":"abcdefabcdefabcd","type":"link","url":"https://jsoncanvas.org","x":500,"y":0,"width":400,"height":220,"someFutureField":true}
|
|
\t],
|
|
\t"edges":[
|
|
\t\t{"id":"e1e1e1e1e1e1e1e1","fromNode":"0f1e2d3c4b5a6978","fromSide":"right","toNode":"1122334455667788","toSide":"left","label":"details"}
|
|
\t],
|
|
\t"metadata":{"version":"1.0"}
|
|
}`;
|
|
|
|
test('canvas files read and write exactly as Obsidian writes them, on both sides', () => {
|
|
for (const m of [C, disk]) {
|
|
const c = m.parseCanvas(OBSIDIAN);
|
|
assert.equal(c.nodes.length, 5);
|
|
assert.equal(c.edges.length, 1);
|
|
assert.deepEqual(c.extra, { metadata: { version: '1.0' } }, 'unknown top-level keys are kept');
|
|
assert.equal(m.serializeCanvas(c), OBSIDIAN, 'unchanged canvas = unchanged file');
|
|
assert.equal(c.nodes[4].someFutureField, true, 'unknown node fields are kept');
|
|
assert.deepEqual(m.parseCanvas(''), { nodes: [], edges: [], extra: {} }, 'an empty file is an empty canvas');
|
|
assert.deepEqual(m.parseCanvas('{}'), { nodes: [], edges: [], extra: {} });
|
|
assert.throws(() => m.parseCanvas('{ not json'));
|
|
assert.throws(() => m.parseCanvas('[1,2]'));
|
|
}
|
|
assert.equal(C.serializeCanvas({}), disk.EMPTY_CANVAS);
|
|
assert.equal(disk.EMPTY_CANVAS, '{\n\t"nodes":[],\n\t"edges":[]\n}');
|
|
});
|
|
|
|
test('a canvas reads as text with [[links]] to the notes on it (search, backlinks, graph)', () => {
|
|
const t = disk.canvasText(OBSIDIAN);
|
|
assert.ok(t.includes('# Idea') && t.includes('[[Roadmap]]'), 'text cards');
|
|
assert.ok(t.includes('[[Projects/Roadmap#Goals]]'), 'a note on the canvas is a link to it');
|
|
assert.ok(!t.includes('diagram.png'), 'pictures are not page links');
|
|
assert.ok(t.includes('Plan') && t.includes('details') && t.includes('https://jsoncanvas.org'));
|
|
assert.equal(disk.canvasText('{ broken'), '');
|
|
const pages = [
|
|
{ id: 'Projects/Roadmap', title: 'Roadmap', exists: true, body: '## Goals' },
|
|
{ id: 'Ideas.canvas', title: 'Ideas', exists: true, body: t }
|
|
];
|
|
assert.deepEqual(links.backlinks('Projects/Roadmap', pages).map((b) => b.id), ['Ideas.canvas']);
|
|
});
|
|
|
|
test('[[Name.canvas]] finds a canvas; [[Name]] never does', () => {
|
|
const pages = [
|
|
{ id: 'Maps/Plan.canvas', title: 'Plan', exists: true },
|
|
{ id: 'Notes/Plan', title: 'Plan', exists: true },
|
|
{ id: 'Only.canvas', title: 'Only', exists: true }
|
|
];
|
|
for (const resolve of [resolveLink, links.resolveLink]) {
|
|
assert.equal(resolve('Plan.canvas', pages), 'Maps/Plan.canvas');
|
|
assert.equal(resolve('Plan', pages), 'Notes/Plan');
|
|
assert.equal(resolve('Only', pages), null);
|
|
assert.equal(resolve('Only.canvas', pages), 'Only.canvas');
|
|
}
|
|
assert.equal(refFor('Maps/Plan.canvas', pages), 'Plan.canvas');
|
|
assert.equal(links.refFor('Notes/Plan', pages), 'Plan');
|
|
});
|
|
|
|
test('moving and renaming notes keeps canvases pointing at them', () => {
|
|
const before = [{ id: 'Projects/Roadmap', title: 'Roadmap', exists: true }, { id: 'Projects', title: 'Projects', exists: true }];
|
|
const after = [{ id: 'Archive/Projects/Roadmap', title: 'Roadmap', exists: true }, { id: 'Archive/Projects', title: 'Projects', exists: true }];
|
|
const moved = (id) => (id === 'Projects' ? 'Archive/Projects' : id.startsWith('Projects/') ? 'Archive/' + id : null);
|
|
const out = disk.rewriteCanvas(OBSIDIAN, before, after, moved);
|
|
const c = disk.parseCanvas(out);
|
|
assert.equal(c.nodes[2].file, 'Archive/Projects/Roadmap.md', 'note path');
|
|
assert.equal(c.nodes[3].file, 'Archive/Projects/diagram.png', 'a picture in the moved folder');
|
|
assert.equal(c.nodes[1].text, '# Idea\n\nSee [[Roadmap]] and [[Roadmap#Goals|goals]]', 'a link that still resolves by name is left as written');
|
|
assert.equal(disk.rewriteCanvas(OBSIDIAN, before, before, () => null), null, 'nothing moved: no rewrite');
|
|
|
|
const renamed = disk.rewriteCanvas(OBSIDIAN, [{ id: 'Projects/Roadmap', title: 'Roadmap', exists: true }], [{ id: 'Projects/Plan 2027', title: 'Plan 2027', exists: true }], (id) => (id === 'Projects/Roadmap' ? 'Projects/Plan 2027' : null));
|
|
const r = disk.parseCanvas(renamed);
|
|
assert.equal(r.nodes[2].file, 'Projects/Plan 2027.md');
|
|
assert.equal(r.nodes[1].text, '# Idea\n\nSee [[Plan 2027]] and [[Plan 2027#Goals|goals]]');
|
|
});
|
|
|
|
test('renaming a heading updates a note shown at that heading and links in cards', () => {
|
|
const pages = [{ id: 'Projects/Roadmap', title: 'Roadmap', exists: true }];
|
|
const r = disk.renameHeadingInCanvas(OBSIDIAN, pages, 'Projects/Roadmap', 'Goals', 'Aims [2027]');
|
|
assert.equal(r.count, 2);
|
|
const c = disk.parseCanvas(r.body);
|
|
assert.equal(c.nodes[2].subpath, '#Aims 2027');
|
|
assert.equal(c.nodes[1].text, '# Idea\n\nSee [[Roadmap]] and [[Roadmap#Aims 2027|goals]]');
|
|
assert.equal(disk.renameHeadingInCanvas(OBSIDIAN, pages, 'Projects/Roadmap', 'Nope', 'X'), null);
|
|
});
|
|
|
|
test('geometry: sides, paths, fitting', () => {
|
|
const n = { x: 0, y: 0, width: 200, height: 100 };
|
|
assert.deepEqual(C.sidePoint(n, 'right'), [200, 50]);
|
|
assert.deepEqual(C.sidePoint(n, 'top'), [100, 0]);
|
|
assert.equal(C.nearestSide(n, 500, 60), 'right');
|
|
assert.equal(C.nearestSide(n, 90, -300), 'top');
|
|
assert.equal(C.nearestSide(n, -5, 50), 'left');
|
|
assert.deepEqual(C.autoSides(n, { x: 0, y: 400, width: 200, height: 100 }), ['bottom', 'top']);
|
|
const p = C.edgePath([200, 50], 'right', [400, 50], 'left');
|
|
assert.match(p.d, /^M200,50 C/);
|
|
assert.deepEqual(p.mid, [300, 50]);
|
|
assert.deepEqual(p.end.dir, [1, 0]);
|
|
const v = C.fitView({ x: 0, y: 0, width: 1000, height: 500 }, 600, 400, 50);
|
|
assert.equal(v.z, 0.5);
|
|
assert.deepEqual([v.x, v.y], [300 - 500 * 0.5, 200 - 250 * 0.5]);
|
|
assert.equal(C.fitView({ x: 0, y: 0, width: 10, height: 10 }, 800, 600).z, 1, 'never zooms in past 100%');
|
|
assert.equal(C.relPath('A/B', 'A/img/x.png'), '../img/x.png');
|
|
assert.equal(C.relPath('', 'img/x.png'), 'img/x.png');
|
|
assert.equal(C.relPath('A', 'A/x.png'), 'x.png');
|
|
assert.equal(C.fileKind('a/b.md'), 'note');
|
|
assert.equal(C.fileKind('a/b.canvas'), 'canvas');
|
|
assert.equal(C.fileKind('a/b.JPG'), 'image');
|
|
assert.equal(C.colorOf('3'), C.PRESETS[3]);
|
|
assert.equal(C.colorOf('#ff0000'), '#ff0000');
|
|
assert.equal(C.colorOf('url(x)'), null);
|
|
assert.match(C.newId(), /^[0-9a-f]{16}$/);
|
|
});
|
|
|
|
test('the vault lists, writes, renames, moves and trashes canvases as plain .canvas files', async () => {
|
|
const dir = await tmp();
|
|
await mkdir(path.join(dir, 'Maps'));
|
|
await writeFile(path.join(dir, 'Maps', 'Plan.canvas'), OBSIDIAN);
|
|
await writeFile(path.join(dir, 'Note.md'), 'Hello\n');
|
|
const v = new Vault(dir);
|
|
|
|
const list = await v.list();
|
|
const cv = list.find((p) => p.id === 'Maps/Plan.canvas');
|
|
assert.ok(cv, JSON.stringify(list.map((p) => p.id)));
|
|
assert.equal(cv.title, 'Plan');
|
|
assert.equal(cv.kind, 'canvas');
|
|
assert.ok(cv.body.includes('[[Projects/Roadmap#Goals]]'), 'listed with its text');
|
|
|
|
const r = await v.read('Maps/Plan.canvas');
|
|
assert.equal(r.body, OBSIDIAN, 'read gives the file as is');
|
|
assert.deepEqual(r.meta, {});
|
|
await v.write('Maps/Plan.canvas', { tags: ['x'] }, '{\n\t"nodes":[],\n\t"edges":[]\n}');
|
|
assert.equal(await readFile(path.join(dir, 'Maps', 'Plan.canvas'), 'utf8'), '{\n\t"nodes":[],\n\t"edges":[]\n}', 'no front matter in a canvas');
|
|
|
|
const made = await v.createCanvas(null, 'Board ideas');
|
|
assert.equal(made, 'Board ideas.canvas');
|
|
assert.equal(await readFile(path.join(dir, 'Board ideas.canvas'), 'utf8'), disk.EMPTY_CANVAS);
|
|
assert.equal(await v.createCanvas(null, 'Board ideas'), 'Board ideas 2.canvas', 'a free name');
|
|
|
|
assert.equal(await v.rename('Board ideas.canvas', 'Mind map'), 'Mind map.canvas');
|
|
assert.equal(await v.move('Mind map.canvas', 'Maps'), 'Maps/Mind map.canvas');
|
|
await assert.rejects(v.create('Maps/Mind map.canvas', 'Inside'), /canvas cannot hold pages/);
|
|
await assert.rejects(v.move('Note', 'Maps/Mind map.canvas'), /canvas cannot hold pages/);
|
|
|
|
const entry = await v.remove('Maps/Mind map.canvas');
|
|
assert.ok(!(await readdir(path.join(dir, 'Maps'))).includes('Mind map.canvas'));
|
|
assert.equal((await v.listTrash())[0].title, 'Mind map');
|
|
assert.equal(await v.restore(entry), 'Maps/Mind map.canvas');
|
|
assert.equal(await readFile(path.join(dir, 'Maps', 'Mind map.canvas'), 'utf8'), disk.EMPTY_CANVAS);
|
|
});
|
|
|
|
test('aligning a selection: each edge or centre of the group, nothing else moves', () => {
|
|
const a = { id: 'a', x: 0, y: 0, width: 100, height: 50 };
|
|
const b = { id: 'b', x: 200, y: 80, width: 60, height: 40 };
|
|
const c = { id: 'c', x: 50, y: 300, width: 200, height: 100 };
|
|
const all = [a, b, c];
|
|
const at = (how) => Object.fromEntries([...C.alignNodes(all, how)].map(([id, p]) => [id, p]));
|
|
assert.deepEqual(at('left'), { b: { x: 0, y: 80 }, c: { x: 0, y: 300 } });
|
|
assert.deepEqual(at('right'), { a: { x: 160, y: 0 }, c: { x: 60, y: 300 } }, 'b already ends at the right edge (260)');
|
|
assert.deepEqual(at('top'), { b: { x: 200, y: 0 }, c: { x: 50, y: 0 } });
|
|
assert.deepEqual(at('bottom'), { a: { x: 0, y: 350 }, b: { x: 200, y: 360 } });
|
|
assert.deepEqual(at('centerX'), { a: { x: 80, y: 0 }, b: { x: 100, y: 80 }, c: { x: 30, y: 300 } });
|
|
assert.deepEqual(at('centerY'), { a: { x: 0, y: 175 }, b: { x: 200, y: 180 }, c: { x: 50, y: 150 } });
|
|
assert.equal(C.alignNodes([a], 'left').size, 0, 'one node has nothing to line up with');
|
|
assert.equal(C.alignNodes(all, 'sideways').size, 0);
|
|
});
|
|
|
|
test('distributing: equal gaps, the two outermost stay put, order follows position', () => {
|
|
const n = (id, x, w, y = 0, h = 40) => ({ id, x, y, width: w, height: h });
|
|
const row = [n('c', 400, 100), n('a', 0, 100), n('b', 120, 50)]; // out of order, and uneven
|
|
const moved = C.distributeNodes(row, 'x');
|
|
assert.deepEqual([...moved], [['b', { x: 225, y: 0 }]], 'gaps of 125: 100 | 125 | 50 | 125 | 100');
|
|
const col = [n('a', 0, 80, 0, 40), n('b', 0, 80, 50, 60), n('c', 0, 80, 300, 20)];
|
|
const down = C.distributeNodes(col, 'y');
|
|
assert.deepEqual([...down], [['b', { x: 0, y: 140 }]]);
|
|
assert.equal(C.distributeNodes([n('a', 0, 10), n('b', 50, 10)], 'x').size, 0, 'two nodes: nothing to spread');
|
|
// already even: nothing to do
|
|
assert.equal(C.distributeNodes([n('a', 0, 100), n('b', 150, 100), n('c', 300, 100)], 'x').size, 0);
|
|
});
|
|
|
|
test('guides: an edge or centre close to another card snaps onto it, and lines are drawn for what lines up', () => {
|
|
const other = { id: 'o', x: 400, y: 100, width: 200, height: 100 }; // lines: x 400 / 500 / 600, y 100 / 150 / 200
|
|
const box = (x, y, w = 100, h = 60) => ({ x, y, width: w, height: h });
|
|
// left edge 3 off the other's left edge; vertically far away
|
|
let g = C.guideSnap(box(397, 500), [other], 6);
|
|
assert.deepEqual([g.dx, g.sx, g.sy], [3, true, false]);
|
|
// centre (x + 50) 4 left of the other's centre 500: moves right
|
|
g = C.guideSnap(box(446, 500), [other], 6);
|
|
assert.deepEqual([g.dx, g.sx], [4, true]);
|
|
// too far: nothing
|
|
g = C.guideSnap(box(380, 500), [other], 6);
|
|
assert.deepEqual([g.sx, g.sy, g.dx, g.dy], [false, false, 0, 0]);
|
|
// the closest line wins, per axis
|
|
g = C.guideSnap(box(405, 138), [other], 8); // x: left 405 -> 400 (-5); y: centre 168 vs 150 / bottom 198 vs 200 (+2)
|
|
assert.deepEqual([g.dx, g.dy], [-5, 2]);
|
|
// only the lines asked for count (resizing: the right edge)
|
|
g = C.guideSnap({ x: 598, y: 0, width: 0, height: 0 }, [other], 6, { x: [0], y: [0] });
|
|
assert.deepEqual([g.dx, g.sx, g.sy], [2, true, false]);
|
|
|
|
const lines = C.guideLines(box(400, 400, 80), [other]);
|
|
assert.deepEqual(lines, [{ axis: 'x', at: 400, from: 100, to: 460 }], 'the left edges line up; the line spans both cards');
|
|
assert.deepEqual(C.guideLines(box(450, 400), [other]), [{ axis: 'x', at: 500, from: 100, to: 460 }], 'centre on centre');
|
|
assert.deepEqual(C.guideLines(box(0, 0), [other]), []);
|
|
// two cards lined up on the same edge give one line, not two
|
|
const two = [other, { id: 'p', x: 400, y: 700, width: 50, height: 50 }];
|
|
assert.equal(C.guideLines(box(400, 400, 80), two).length, 1);
|
|
assert.deepEqual(C.guideLines(box(400, 400, 80), two)[0], { axis: 'x', at: 400, from: 100, to: 750 });
|
|
});
|
|
|
|
test('arrow styles: straight and right-angled lines, with heads at the right ends', () => {
|
|
const st = C.edgePath([0, 0], 'right', [100, 0], 'left', 'straight');
|
|
assert.equal(st.d, 'M0,0 L100,0');
|
|
assert.deepEqual([st.mid, st.end.dir, st.start.dir], [[50, 0], [1, 0], [-1, 0]]);
|
|
const diag = C.edgePath([0, 0], 'bottom', [30, 40], 'top', 'straight');
|
|
assert.deepEqual(diag.end.dir, [0.6, 0.8]);
|
|
|
|
// right angles: leave sideways, one bend in the middle, arrive sideways; the stubs vanish into the runs
|
|
const z = C.edgePath([0, 0], 'right', [200, 100], 'left', 'orthogonal');
|
|
assert.deepEqual(z.pts, [[0, 0], [100, 0], [100, 100], [200, 100]]);
|
|
assert.deepEqual([z.end.dir, z.start.dir], [[1, 0], [-1, 0]]);
|
|
assert.deepEqual(z.mid, [100, 50]);
|
|
// from the right of one card into the top of another: a single corner
|
|
const l = C.edgePath([0, 0], 'right', [100, 100], 'top', 'orthogonal');
|
|
assert.deepEqual(l.pts, [[0, 0], [100, 0], [100, 100]]);
|
|
assert.deepEqual(l.end.dir, [0, 1]);
|
|
// straight on: no bends at all
|
|
assert.deepEqual(C.edgePath([0, 0], 'right', [200, 0], 'left', 'orthogonal').pts, [[0, 0], [200, 0]]);
|
|
// an arrow still being drawn has a loose end: it arrives along the longer way
|
|
const loose = C.edgePath([0, 0], 'right', [100, 10], null, 'orthogonal');
|
|
assert.deepEqual(loose.pts, [[0, 0], [62, 0], [62, 10], [100, 10]]);
|
|
assert.deepEqual(loose.end.dir, [1, 0]);
|
|
// corners are rounded in the path that is drawn
|
|
assert.match(z.d, /Q/);
|
|
assert.match(z.d, /^M0,0 L/);
|
|
// the default is the curve; unknown styles fall back to it
|
|
assert.match(C.edgePath([0, 0], 'right', [100, 0], 'left').d, / C/);
|
|
assert.equal(C.shapeOf({ style: 'straight' }), 'straight');
|
|
assert.equal(C.shapeOf({ style: 'wavy' }), 'curved');
|
|
assert.equal(C.shapeOf({}), 'curved');
|
|
});
|
|
|
|
test('groups: what a collapsed group hides, how it is drawn, and where a dropped card ends up', () => {
|
|
const g = { id: 'g', type: 'group', x: 0, y: 0, width: 400, height: 300 };
|
|
const a = { id: 'a', type: 'text', x: 40, y: 40, width: 100, height: 60 };
|
|
const b = { id: 'b', type: 'text', x: 200, y: 100, width: 100, height: 60 };
|
|
const out = { id: 'out', type: 'text', x: 500, y: 40, width: 100, height: 60 };
|
|
const nodes = [g, a, b, out];
|
|
assert.equal(C.hiddenMap(nodes).size, 0, 'nothing is hidden while the group is open');
|
|
g.collapsed = true;
|
|
const h = C.hiddenMap(nodes);
|
|
assert.deepEqual([...h.keys()].sort(), ['a', 'b']);
|
|
assert.equal(h.get('a'), g);
|
|
assert.deepEqual(C.displayBox(g), { x: 0, y: 0, width: 400, height: C.COLLAPSED_H });
|
|
assert.equal(C.displayBox(a), a, 'ordinary nodes are drawn as they are');
|
|
// an outer group that is folded hides an inner folded group's members too, and the outer one wins
|
|
const outer = { id: 'o', type: 'group', x: -50, y: -50, width: 600, height: 500, collapsed: true };
|
|
assert.equal(C.hiddenMap([outer, ...nodes]).get('a'), outer);
|
|
assert.equal(C.hiddenMap([outer, ...nodes]).get('g'), outer);
|
|
delete g.collapsed;
|
|
|
|
// which group a card lands in: the smallest one holding its middle; open groups only
|
|
const inner = { id: 'i', type: 'group', x: 20, y: 20, width: 160, height: 120 };
|
|
assert.equal(C.groupFor({ id: 'c', x: 50, y: 50, width: 40, height: 40 }, [g, inner]).id, 'i');
|
|
assert.equal(C.groupFor({ id: 'c', x: 250, y: 200, width: 40, height: 40 }, [g, inner]).id, 'g');
|
|
assert.equal(C.groupFor({ id: 'c', x: 600, y: 600, width: 40, height: 40 }, [g, inner]), null);
|
|
assert.equal(C.groupFor({ id: 'c', x: 50, y: 50, width: 40, height: 40 }, [{ ...g, collapsed: true }]), null, 'folded groups take nothing');
|
|
// dropped half over the edge: pushed in
|
|
assert.equal(C.fitInto({ id: 'c', x: 100, y: 100, width: 80, height: 50 }, g), null, 'already inside');
|
|
assert.deepEqual(C.fitInto({ id: 'c', x: 350, y: 130, width: 100, height: 60 }, g), { x: 280, y: 130 });
|
|
assert.deepEqual(C.fitInto({ id: 'c', x: 10, y: 280, width: 100, height: 60 }, g), { x: 20, y: 220 });
|
|
// too big for the group: the group grows to hold it
|
|
assert.deepEqual(C.fitInto({ id: 'c', x: 100, y: 100, width: 500, height: 100 }, g), { group: { x: 0, y: 0, width: 620, height: 300 } });
|
|
});
|
|
|
|
test('card <-> note: a page name from the text, and the text a page starts with', () => {
|
|
assert.equal(C.titleFromText('# My Plan\n\nSome text'), 'My Plan');
|
|
assert.equal(C.titleFromText('## Sub **heading**'), 'Sub heading');
|
|
assert.equal(C.titleFromText('**Buy** milk and [[Notes/Eggs|eggs]] from [the shop](https://x.y)'), 'Buy milk and eggs from the shop');
|
|
assert.equal(C.titleFromText('- [ ] call the bank\n- [x] other'), 'call the bank');
|
|
assert.equal(C.titleFromText('\n\n---\n \nreal first line'), 'real first line');
|
|
assert.equal(C.titleFromText(''), 'Untitled');
|
|
assert.equal(C.titleFromText('***'), 'Untitled');
|
|
const long = C.titleFromText('word '.repeat(40));
|
|
assert.ok(long.length <= 60 && !long.endsWith(' '), long);
|
|
|
|
assert.equal(C.bodyAfterTitle('# My Plan\n\nSome text\nmore'), 'Some text\nmore\n');
|
|
assert.equal(C.bodyAfterTitle('\n## Only a heading'), '');
|
|
assert.equal(C.bodyAfterTitle('Plain first line\nsecond'), 'Plain first line\nsecond\n', 'a plain first line stays: it is text, not a title');
|
|
assert.equal(C.bodyAfterTitle('#hashtag not a heading\ntext'), '#hashtag not a heading\ntext\n');
|
|
assert.equal(C.bodyAfterTitle(''), '');
|
|
});
|
|
|