forked from yvv/decision
99 lines
4.0 KiB
TypeScript
99 lines
4.0 KiB
TypeScript
// A space at the start or the end of a block ({#if}, {:else}, {#each}…) is dropped by Svelte,
|
|
// in the browser as on the server: glued to what comes before, « {n} contre{#if s} <span>dont
|
|
// 1 simulé</span>{/if} » reads « 0 contredont 1 simulé » (review of M6b). Every component is
|
|
// read here, and a space a block would drop is pointed at: `{' '}` keeps it.
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
import { join, relative } from 'node:path';
|
|
import { parse } from 'svelte/compiler';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const SRC = join(__dirname, '../../src');
|
|
|
|
function components(dir: string): string[] {
|
|
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
const path = join(dir, e.name);
|
|
return e.isDirectory() ? components(path) : e.name.endsWith('.svelte') ? [path] : [];
|
|
});
|
|
}
|
|
|
|
interface Node {
|
|
type: string;
|
|
start: number;
|
|
data?: string;
|
|
elseif?: boolean;
|
|
[key: string]: unknown;
|
|
}
|
|
interface Fragment {
|
|
nodes: Node[];
|
|
}
|
|
|
|
/** The fragments a block shows, its `{:else if}` chain included. */
|
|
function branches(b: Node): Fragment[] {
|
|
const out: Fragment[] = [];
|
|
for (const key of ['consequent', 'alternate', 'body', 'fallback', 'pending', 'then', 'catch', 'fragment']) {
|
|
const f = b[key] as Fragment | null | undefined;
|
|
if (!f?.nodes) continue;
|
|
const only = f.nodes.length === 1 ? f.nodes[0] : undefined;
|
|
if (key === 'alternate' && only?.type === 'IfBlock' && only.elseif) out.push(...branches(only));
|
|
else out.push(f);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const BLOCKS = new Set(['IfBlock', 'EachBlock', 'AwaitBlock', 'KeyBlock']);
|
|
/** A space or a tab, not the newline that only lays the markup out. */
|
|
const LEADS = /^[ \t]/;
|
|
const TRAILS = /[ \t]$/;
|
|
|
|
/** Each space a block drops while something is glued to it, as `line: text`. */
|
|
function dropped(source: string): string[] {
|
|
const ast = parse(source, { modern: true }) as unknown as { fragment: Fragment };
|
|
const out: string[] = [];
|
|
const line = (at: number) => source.slice(0, at).split('\n').length;
|
|
const walk = (f: Fragment) => {
|
|
f.nodes.forEach((n, i) => {
|
|
if (BLOCKS.has(n.type)) {
|
|
const before = f.nodes[i - 1];
|
|
const after = f.nodes[i + 1];
|
|
const gluedBefore = before !== undefined && !(before.type === 'Text' && /\s$/.test(before.data ?? ''));
|
|
const gluedAfter = after !== undefined && !(after.type === 'Text' && /^\s/.test(after.data ?? ''));
|
|
for (const b of branches(n)) {
|
|
const first = b.nodes[0];
|
|
const last = b.nodes.at(-1);
|
|
if (gluedBefore && first?.type === 'Text' && LEADS.test(first.data ?? '')) {
|
|
out.push(`${line(first.start)}: « ${(first.data ?? '').slice(0, 30)} »`);
|
|
}
|
|
if (gluedAfter && last?.type === 'Text' && TRAILS.test(last.data ?? '')) {
|
|
out.push(`${line(last.start)}: « ${(last.data ?? '').slice(-30)} »`);
|
|
}
|
|
}
|
|
}
|
|
for (const value of Object.values(n)) {
|
|
const child = value as Fragment | null;
|
|
if (child && typeof child === 'object' && Array.isArray(child.nodes)) walk(child);
|
|
}
|
|
});
|
|
};
|
|
walk(ast.fragment);
|
|
return out;
|
|
}
|
|
|
|
describe('a space a block would drop', () => {
|
|
it('is seen where it is glued', () => {
|
|
expect(dropped('<p>{a}{#if s} <span>x</span>{/if}</p>')).toHaveLength(1);
|
|
expect(dropped('<p>{a}{#if s} · x{:else if t} · y{:else} · z{/if}</p>')).toHaveLength(3);
|
|
expect(dropped('<p>{#each xs as x}{x} {/each}<b>y</b></p>')).toHaveLength(1);
|
|
// Kept: `{' '}`, a space outside the block, or a newline that lays the markup out.
|
|
expect(dropped("<p>{a}{#if s}{' '}<span>x</span>{/if}</p>")).toEqual([]);
|
|
expect(dropped('<p>{a} {#if s} · x{/if}</p>')).toEqual([]);
|
|
expect(dropped('<div>{a}{#if s}\n <p>x</p>\n{/if}</div>')).toEqual([]);
|
|
});
|
|
|
|
it('is never dropped in a component of the interface', () => {
|
|
const found = components(SRC).flatMap((path) =>
|
|
dropped(readFileSync(path, 'utf8')).map((d) => `${relative(SRC, path)}:${d}`),
|
|
);
|
|
expect(found).toEqual([]);
|
|
});
|
|
});
|