Files
Steffen Becker 99f10c9399 docs: each showcase shot opens the tab its feature lives in
A shot names a ribbon tab and, optionally, where the caret goes: the tables
one lands in a cell, so the contextual Table Layout tab is up. The floating
table toolbar is hidden while shooting — it covered the table the ribbon is
already showing. `data-tab` on the tab buttons is what the script clicks.
2026-09-06 19:07:24 +02:00

123 lines
6.4 KiB
TypeScript

// What the showcase documents share: the node builders, a styled table, pictures as
// data-URIs and the shape run.mjs hands to buildOdt/buildDocx. Runs in the browser,
// served by the dev server (see run.mjs).
import type { PageMargins } from '../../src/lib/storage/pageMargins';
import type { PageFormat } from '../../src/lib/storage/pageFormat';
import type { HfExport } from '../../src/lib/export/odt';
import type { StyleSheet } from '../../src/lib/styles/styleSheet';
import type { NoteSettings } from '../../src/lib/storage/noteSettings';
import type { DocProperties } from '../../src/lib/storage/docProperties';
import type { PageNumbering } from '../../src/lib/storage/pageNumbering';
import type { PageDecor } from '../../src/lib/storage/pageDecor';
import type { LineNumbering } from '../../src/lib/storage/lineNumbering';
import {
DEFAULT_TABLE_LOOK, builtinTableStyles, resolveTableCell, tableLookAttr,
} from '../../src/lib/styles/tableStyles';
export { T, P, H, TBL, ROW, TC, THC, BOLD } from '../../src/lib/templates/builders';
import { P, T } from '../../src/lib/templates/builders';
export type N = Record<string, unknown>;
export const ITALIC = { type: 'italic' };
export const LI = (...content: N[]): N => ({ type: 'listItem', content });
export const UL = (...items: N[]): N => ({ type: 'bulletList', content: items });
export const OL = (...items: N[]): N => ({ type: 'orderedList', content: items });
export const BR: N = { type: 'hardBreak' };
export const FORMULA = (latex: string, display = false): N => ({ type: 'formula', attrs: { latex, display } });
export const SEQ = (category: 'figure' | 'table', number: number): N =>
({ type: 'sequenceField', attrs: { category, format: '1', number } });
export const CITE = (identifier: string, type: string, fields: Record<string, string>): N =>
({ type: 'bibliographyEntry', attrs: { identifier, type, fields, text: '' } });
export const IDX = (term: string): N => ({ type: 'indexEntry', attrs: { term } });
export const FOOTNOTE = (id: string, n: number): N =>
({ type: 'noteRef', attrs: { id, kind: 'footnote', text: String(n) } });
export const NOTE = (id: string, ...content: N[]): N =>
({ type: 'note', attrs: { id, kind: 'footnote', label: null, text: '' }, content });
export const INDEX = (index: string, title: string, extra: N = {}): N =>
({ type: 'tableOfContents', attrs: { entries: [], title, index, leader: '.', tabPosCm: null, maxLevel: 3, ...extra } });
export const COLUMNS = (count: number, gapCm: number, ...content: N[]): N =>
({ type: 'columns', attrs: { count, gapCm }, content });
export const PAGE_NUMBER: N = { type: 'pageNumber' };
export const HF = (attrs: N | null, ...content: N[]): N => ({ type: 'doc', content: [P(attrs, ...content)] });
/** A block-level picture in its own paragraph, or floating with the text around it. */
export const IMAGE = (src: string, width: number, height: number, attrs: N = {}): N =>
({ type: 'image', attrs: { src, width, height, ...attrs } });
// A picture served by the dev server (this folder's img/), as the data-URI the editor stores.
export async function picture(file: string): Promise<string> {
const blob = await (await fetch(`/scripts/showcase/img/${file}`)).blob();
return new Promise((resolve) => {
const r = new FileReader();
r.onload = () => resolve(String(r.result));
r.readAsDataURL(blob);
});
}
export const svgDataUrl = (svg: string): string =>
`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`;
// A table in one of the built-in table styles, its look resolved per cell the way the
// editor stores it. Cells are text, or ready-made node lists; `numeric` right-aligns.
export function styledTable(
name: string, rows: (string | N[])[][],
opts: { header?: boolean; widths?: number[]; formulas?: Record<string, string>; cellFormat?: string; numeric?: number[]; attrs?: N } = {},
): N {
const style = builtinTableStyles()[name];
if (!style) throw new Error(`no table style ${name}`);
const look = { ...DEFAULT_TABLE_LOOK, ...(style.look ?? {}) };
const nRows = rows.length, cols = rows[0].length;
const header = opts.header ?? true;
const content = rows.map((row, ri) => ({
type: 'tableRow',
content: row.map((cell, ci) => {
const paint = resolveTableCell(style, { row: ri, col: ci, rows: nRows, cols }, look);
const attrs: N = { colspan: 1, rowspan: 1, colwidth: opts.widths ? [opts.widths[ci]] : null };
if (paint.fill) attrs.backgroundColor = paint.fill;
for (const [k, v] of Object.entries(paint.borders)) if (v !== null) attrs[k] = v;
if (paint.regions.length) attrs.region = paint.regions.join(' ');
const ref = `${String.fromCharCode(65 + ci)}${ri + 1}`;
if (opts.formulas?.[ref]) { attrs.formula = opts.formulas[ref]; if (opts.cellFormat) attrs.cellFormat = opts.cellFormat; }
const para = typeof cell === 'string'
? P(opts.numeric?.includes(ci) && ri > 0 ? { textAlign: 'right' } : null, ...(cell ? [T(cell)] : []))
: P(null, ...cell);
return { type: ri === 0 && header ? 'tableHeader' : 'tableCell', attrs, content: [para] };
}),
}));
return { type: 'table', attrs: { tableStyle: name, tableLook: tableLookAttr(look), repeatHeader: header, ...(opts.attrs ?? {}) }, content };
}
/** One screenshot: which page sits at the top of the window, at which zoom and theme. */
export type Shot = {
/** The page at the top of the window: by number, or the one holding this heading. */
file: string; page?: number; at?: string; zoom?: number; theme?: 'light' | 'dark';
/** Pages side by side (View ▸ page columns). */
columns?: number; markup?: boolean; spelling?: boolean;
/** The ribbon tab to open, and where to put the caret (first match in view). */
tab?: string; caret?: string;
};
export type Showcase = {
name: string;
doc: N;
margins?: PageMargins;
orientation?: 'portrait' | 'landscape';
hf?: HfExport;
language?: { language: string; country: string };
pageFormat?: PageFormat;
styles?: StyleSheet;
notes?: NoteSettings;
props?: DocProperties;
hyphenate?: boolean;
pageNumbering?: PageNumbering;
decor?: PageDecor;
lineNumbering?: LineNumbering;
shots: Shot[];
};
/** The positional argument list of buildOdt/buildDocx; `undefined` takes the default. */
export function exportArgs(s: Showcase): unknown[] {
return [s.doc, s.margins, s.orientation, s.hf, s.language, s.pageFormat, s.styles, undefined, undefined,
false, s.notes, s.props, s.hyphenate ?? false, s.pageNumbering, s.decor, s.lineNumbering];
}