postext 1.36.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/composed/continuation.test.d.ts +2 -0
- package/dist/__tests__/composed/continuation.test.d.ts.map +1 -0
- package/dist/__tests__/composed/continuation.test.js +352 -0
- package/dist/__tests__/composed/continuation.test.js.map +1 -0
- package/dist/__tests__/composed/contrast.test.d.ts +2 -0
- package/dist/__tests__/composed/contrast.test.d.ts.map +1 -0
- package/dist/__tests__/composed/contrast.test.js +124 -0
- package/dist/__tests__/composed/contrast.test.js.map +1 -0
- package/dist/__tests__/composed/corpus.test.js +7 -1
- package/dist/__tests__/composed/corpus.test.js.map +1 -1
- package/dist/__tests__/composed/fit.test.js +9 -6
- package/dist/__tests__/composed/fit.test.js.map +1 -1
- package/dist/__tests__/composed/groups.test.js +14 -0
- package/dist/__tests__/composed/groups.test.js.map +1 -1
- package/dist/__tests__/composed/headingDesigns.test.d.ts +2 -0
- package/dist/__tests__/composed/headingDesigns.test.d.ts.map +1 -0
- package/dist/__tests__/composed/headingDesigns.test.js +262 -0
- package/dist/__tests__/composed/headingDesigns.test.js.map +1 -0
- package/dist/__tests__/composed/labels.test.d.ts +2 -0
- package/dist/__tests__/composed/labels.test.d.ts.map +1 -0
- package/dist/__tests__/composed/labels.test.js +326 -0
- package/dist/__tests__/composed/labels.test.js.map +1 -0
- package/dist/__tests__/composed/outsidePrint.test.js +5 -2
- package/dist/__tests__/composed/outsidePrint.test.js.map +1 -1
- package/dist/__tests__/composed/sourceMap.test.js +26 -0
- package/dist/__tests__/composed/sourceMap.test.js.map +1 -1
- package/dist/__tests__/composed/spreadLabels.test.d.ts +2 -0
- package/dist/__tests__/composed/spreadLabels.test.d.ts.map +1 -0
- package/dist/__tests__/composed/spreadLabels.test.js +131 -0
- package/dist/__tests__/composed/spreadLabels.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +11 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/migrate/fixtures/textbookPort.d.ts +14 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.d.ts.map +1 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.js +116 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.js.map +1 -0
- package/dist/__tests__/migrate/textToMarkdown.test.d.ts +2 -0
- package/dist/__tests__/migrate/textToMarkdown.test.d.ts.map +1 -0
- package/dist/__tests__/migrate/textToMarkdown.test.js +268 -0
- package/dist/__tests__/migrate/textToMarkdown.test.js.map +1 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts +2 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts.map +1 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.js +89 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.js.map +1 -0
- package/dist/__tests__/pictureBelowPrintSize.test.d.ts +2 -0
- package/dist/__tests__/pictureBelowPrintSize.test.d.ts.map +1 -0
- package/dist/__tests__/pictureBelowPrintSize.test.js +82 -0
- package/dist/__tests__/pictureBelowPrintSize.test.js.map +1 -0
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
- package/dist/canvas-backend/composed.d.ts +5 -2
- package/dist/canvas-backend/composed.d.ts.map +1 -1
- package/dist/canvas-backend/composed.js +5 -3
- package/dist/canvas-backend/composed.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +37 -19
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts +5 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +60 -1
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/composed/continuation.d.ts +102 -0
- package/dist/composed/continuation.d.ts.map +1 -0
- package/dist/composed/continuation.js +87 -0
- package/dist/composed/continuation.js.map +1 -0
- package/dist/composed/labels.d.ts +154 -0
- package/dist/composed/labels.d.ts.map +1 -0
- package/dist/composed/labels.js +524 -0
- package/dist/composed/labels.js.map +1 -0
- package/dist/composed/pageMemo.d.ts +3 -2
- package/dist/composed/pageMemo.d.ts.map +1 -1
- package/dist/composed/pageMemo.js +3 -1
- package/dist/composed/pageMemo.js.map +1 -1
- package/dist/composed/paint.d.ts +8 -2
- package/dist/composed/paint.d.ts.map +1 -1
- package/dist/composed/paint.js +8 -2
- package/dist/composed/paint.js.map +1 -1
- package/dist/composed/reflowLog.d.ts +3 -1
- package/dist/composed/reflowLog.d.ts.map +1 -1
- package/dist/composed/reflowLog.js +20 -6
- package/dist/composed/reflowLog.js.map +1 -1
- package/dist/composed/sourceMap.d.ts +0 -21
- package/dist/composed/sourceMap.d.ts.map +1 -1
- package/dist/composed/sourceMap.js +87 -7
- package/dist/composed/sourceMap.js.map +1 -1
- package/dist/composed/spread.d.ts +3 -0
- package/dist/composed/spread.d.ts.map +1 -1
- package/dist/composed/spread.js +1 -1
- package/dist/composed/spread.js.map +1 -1
- package/dist/configWarnings.d.ts +15 -0
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +29 -17
- package/dist/configWarnings.js.map +1 -1
- package/dist/contrast.d.ts +5 -3
- package/dist/contrast.d.ts.map +1 -1
- package/dist/contrast.js +46 -5
- package/dist/contrast.js.map +1 -1
- package/dist/defaults/paragraphStyles.d.ts +1 -1
- package/dist/defaults/paragraphStyles.d.ts.map +1 -1
- package/dist/defaults/paragraphStyles.js +5 -1
- package/dist/defaults/paragraphStyles.js.map +1 -1
- package/dist/design/slots.d.ts +12 -10
- package/dist/design/slots.d.ts.map +1 -1
- package/dist/design/slots.js +26 -2
- package/dist/design/slots.js.map +1 -1
- package/dist/fonts/faces.d.ts.map +1 -1
- package/dist/fonts/faces.js +6 -0
- package/dist/fonts/faces.js.map +1 -1
- package/dist/highlightMarks.d.ts.map +1 -1
- package/dist/highlightMarks.js +6 -0
- package/dist/highlightMarks.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +40 -2
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate/markdownRuns.d.ts +70 -0
- package/dist/migrate/markdownRuns.d.ts.map +1 -0
- package/dist/migrate/markdownRuns.js +215 -0
- package/dist/migrate/markdownRuns.js.map +1 -0
- package/dist/migrate/pageDesign.d.ts +119 -0
- package/dist/migrate/pageDesign.d.ts.map +1 -0
- package/dist/migrate/pageDesign.js +446 -0
- package/dist/migrate/pageDesign.js.map +1 -0
- package/dist/migrate/textToMarkdown.d.ts +124 -0
- package/dist/migrate/textToMarkdown.d.ts.map +1 -0
- package/dist/migrate/textToMarkdown.js +716 -0
- package/dist/migrate/textToMarkdown.js.map +1 -0
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +192 -24
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +12 -1
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +20 -1
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +30 -2
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/composedLayout.d.ts +16 -2
- package/dist/pipeline/composedLayout.d.ts.map +1 -1
- package/dist/pipeline/composedLayout.js +107 -37
- package/dist/pipeline/composedLayout.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts +15 -0
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +85 -0
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/frameLayout.d.ts +4 -1
- package/dist/pipeline/frameLayout.d.ts.map +1 -1
- package/dist/pipeline/frameLayout.js +5 -2
- package/dist/pipeline/frameLayout.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +11 -1
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +169 -66
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/mirrorFrame.d.ts.map +1 -1
- package/dist/pipeline/mirrorFrame.js +25 -2
- package/dist/pipeline/mirrorFrame.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +28 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +21 -0
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/pipeline/styles.d.ts +1 -1
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +4 -0
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +138 -7
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/dist/word/toDocx.d.ts.map +1 -1
- package/dist/word/toDocx.js +14 -0
- package/dist/word/toDocx.js.map +1 -1
- package/dist/word/word.test.js +11 -1
- package/dist/word/word.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { collectConfigWarnings, contentInConfigRule } from '../configWarnings';
|
|
2
|
+
import { resolveAllConfig } from '../pipeline/config';
|
|
3
|
+
import { stableStringify } from '../util/stableHash';
|
|
4
|
+
import { scanChapter } from './markdownRuns';
|
|
5
|
+
import { joinLines, planDesign, remapDecorationAnchors, toMm } from './pageDesign';
|
|
6
|
+
const ENCODER = new TextEncoder();
|
|
7
|
+
const bytes = (v) => ENCODER.encode(JSON.stringify(v ?? {})).length;
|
|
8
|
+
function countContent(config, metadata) {
|
|
9
|
+
const ws = collectConfigWarnings({ ...config, checks: { ...config.checks, contentInConfig: { ...config.checks?.contentInConfig, enabled: true } } }, metadata ? { metadata } : {})
|
|
10
|
+
.filter((w) => w.kind === 'contentInConfig');
|
|
11
|
+
return { warnings: ws.length, elements: ws.reduce((n, w) => n + (w.count ?? 0), 0), chars: ws.reduce((n, w) => n + (w.chars ?? 0), 0) };
|
|
12
|
+
}
|
|
13
|
+
const slotOf = (v) => v && typeof v === 'object' && Array.isArray(v.elements) ? v : undefined;
|
|
14
|
+
/** A readable id: a style's part after its last `--` (`tema--p12` → `p12`),
|
|
15
|
+
* free among `taken`. */
|
|
16
|
+
function freeId(base, taken) {
|
|
17
|
+
const clean = base.replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '') || 'page';
|
|
18
|
+
let id = clean;
|
|
19
|
+
for (let n = 2; taken.has(id); n++)
|
|
20
|
+
id = `${clean}-${n}`;
|
|
21
|
+
taken.add(id);
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
const slug = (s) => s.normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
25
|
+
const ALIGN = { left: 'left', center: 'center', right: 'right', justify: 'justify', start: 'start', end: 'end' };
|
|
26
|
+
/** The paragraph style of a look and spacing, as the migration writes it. */
|
|
27
|
+
function styleFields(look, gap) {
|
|
28
|
+
return {
|
|
29
|
+
...(look.fontFamily ? { fontFamily: look.fontFamily } : {}),
|
|
30
|
+
fontSize: look.fontSize,
|
|
31
|
+
lineHeight: { value: look.lineHeight, unit: 'em' },
|
|
32
|
+
...(look.color ? { color: look.color, boldColor: look.color, italicColor: look.color } : {}),
|
|
33
|
+
textAlign: ALIGN[look.align],
|
|
34
|
+
fontWeight: look.fontWeight,
|
|
35
|
+
boldFontWeight: Math.max(700, look.fontWeight),
|
|
36
|
+
...(look.italic ? { italic: true } : {}),
|
|
37
|
+
...(look.uppercase ? { textTransform: 'uppercase' } : {}),
|
|
38
|
+
// The text's tracking, which paragraph styles carry since #907.
|
|
39
|
+
...(look.letterSpacing ? { letterSpacing: look.letterSpacing } : {}),
|
|
40
|
+
hyphenation: look.hyphenate,
|
|
41
|
+
firstLineIndent: { value: 0, unit: 'mm' },
|
|
42
|
+
spaceBetween: { value: gap, unit: 'mm' },
|
|
43
|
+
marginTop: { value: 0, unit: 'mm' },
|
|
44
|
+
marginBottom: { value: 0, unit: 'mm' },
|
|
45
|
+
snapToGrid: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Whether a book's paragraph style sets a look exactly (the fields the
|
|
49
|
+
* migration writes, and nothing that would change the lines). */
|
|
50
|
+
function sameStyle(own, want) {
|
|
51
|
+
const harmless = new Set(['id', 'name']);
|
|
52
|
+
const ownKeys = Object.keys(own).filter((k) => !harmless.has(k));
|
|
53
|
+
const wantKeys = Object.keys(want);
|
|
54
|
+
if (ownKeys.some((k) => !wantKeys.includes(k)))
|
|
55
|
+
return false;
|
|
56
|
+
const norm = (v) => stableStringify(v, new Set(['paletteId', 'model']));
|
|
57
|
+
return wantKeys.every((k) => {
|
|
58
|
+
const a = own[k];
|
|
59
|
+
const b = want[k];
|
|
60
|
+
if (a === undefined) {
|
|
61
|
+
// Unset values the migration only spells out.
|
|
62
|
+
return (k === 'italic' && b === undefined) || (k === 'spaceBetween' && b.value === 0)
|
|
63
|
+
|| ((k === 'marginTop' || k === 'marginBottom') && b.value === 0);
|
|
64
|
+
}
|
|
65
|
+
return norm(a) === norm(b);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Run the migration. */
|
|
69
|
+
export function moveTextToMarkdown(input, options = {}) {
|
|
70
|
+
const config = input.config;
|
|
71
|
+
const merge = options.merge !== false;
|
|
72
|
+
const share = options.shareTemplates !== false;
|
|
73
|
+
const highlights = options.highlightsToChips !== false;
|
|
74
|
+
const metadata = input.metadata;
|
|
75
|
+
const verdict = contentInConfigRule(config, metadata ? { metadata } : {});
|
|
76
|
+
const isContent = (c) => verdict(c).content;
|
|
77
|
+
/** Text worth moving once a slot is migrated: any with a letter left
|
|
78
|
+
* after the allowed phrases. */
|
|
79
|
+
const movable = (c) => /\p{L}/u.test(verdict(c).rest);
|
|
80
|
+
const resolved = resolveAllConfig(config);
|
|
81
|
+
const page = { width: toMm(resolved.page.width) ?? 0, height: toMm(resolved.page.height) ?? 0 };
|
|
82
|
+
const before = countContent(config, metadata);
|
|
83
|
+
// 1. Page designs: heading styles whose running heads hold content,
|
|
84
|
+
// grouped by what those running heads draw.
|
|
85
|
+
const styles = Array.isArray(config.headingStyles) ? config.headingStyles : [];
|
|
86
|
+
const designs = new Map();
|
|
87
|
+
const designOfStyle = new Map();
|
|
88
|
+
const left = [];
|
|
89
|
+
styles.forEach((s, i) => {
|
|
90
|
+
if (!s || typeof s !== 'object')
|
|
91
|
+
return;
|
|
92
|
+
const header = slotOf(s.header);
|
|
93
|
+
const footer = slotOf(s.footer);
|
|
94
|
+
const holds = (slot) => !!slot && slot.elements.some((e) => e?.kind === 'text' && typeof e.content === 'string' && isContent(e.content));
|
|
95
|
+
if (!holds(header) && !holds(footer)) {
|
|
96
|
+
if (slotOf(s.advancedDesign?.slot) && holds(slotOf(s.advancedDesign?.slot)))
|
|
97
|
+
left.push({ path: `headingStyles[${i}].advancedDesign.slot`, reason: 'notPage' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const key = stableStringify({ header: header ?? null, footer: footer ?? null });
|
|
101
|
+
let d = designs.get(key);
|
|
102
|
+
if (!d) {
|
|
103
|
+
const headerEls = header?.elements ?? [];
|
|
104
|
+
const headerIds = new Set(headerEls.map((e) => e.id));
|
|
105
|
+
// Footer elements whose id the header already uses are renamed.
|
|
106
|
+
const footerEls = (footer?.elements ?? []).map((e) => (headerIds.has(e.id) ? { ...e, id: `${e.id}-footer` } : e));
|
|
107
|
+
const plan = planDesign({
|
|
108
|
+
key,
|
|
109
|
+
elements: [...headerEls, ...footerEls],
|
|
110
|
+
origins: [...headerEls.map(() => 'header'), ...footerEls.map(() => 'footer')],
|
|
111
|
+
page,
|
|
112
|
+
isContent: movable,
|
|
113
|
+
chipStyles: config.chipStyles ?? [],
|
|
114
|
+
highlightsToChips: highlights,
|
|
115
|
+
merge,
|
|
116
|
+
});
|
|
117
|
+
const roleFiltered = [...headerEls, ...footerEls].some((e) => typeof e.pages === 'string' && e.pages !== 'all');
|
|
118
|
+
d = { key, styleIds: [], ownHeader: !!header, ownFooter: !!footer, plan, pages: 0, ...(roleFiltered ? { roleFiltered } : {}) };
|
|
119
|
+
designs.set(key, d);
|
|
120
|
+
for (const k of plan.kept)
|
|
121
|
+
left.push({ path: `headingStyles[${i}] #${k.id}`, reason: k.reason });
|
|
122
|
+
}
|
|
123
|
+
d.styleIds.push(s.id);
|
|
124
|
+
designOfStyle.set(s.id, d);
|
|
125
|
+
});
|
|
126
|
+
const styleById = new Map(styles.map((s) => [s.id, s]));
|
|
127
|
+
const breakBefore = (h) => {
|
|
128
|
+
const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
|
|
129
|
+
const level = resolved.headings.levels[h.level - 1];
|
|
130
|
+
const own = st?.breakBefore;
|
|
131
|
+
return {
|
|
132
|
+
enabled: own?.enabled ?? level?.breakBefore.enabled ?? h.level === 1,
|
|
133
|
+
parity: own?.parity ?? level?.breakBefore.parity ?? 'any',
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
// 2. The runs of each chapter.
|
|
137
|
+
const scans = input.chapters.map((ch) => ({
|
|
138
|
+
chapter: ch,
|
|
139
|
+
scan: scanChapter(ch.markdown, {
|
|
140
|
+
designOf: (id) => designOfStyle.get(id)?.key,
|
|
141
|
+
breaksPage: (h) => breakBefore(h).enabled,
|
|
142
|
+
}),
|
|
143
|
+
}));
|
|
144
|
+
for (const { scan } of scans)
|
|
145
|
+
for (const r of scan.runs)
|
|
146
|
+
designs.get(r.design).pages += r.pages.length;
|
|
147
|
+
const unused = [...designOfStyle.entries()].filter(([, d]) => d.pages === 0).map(([id]) => id);
|
|
148
|
+
const usedDesigns = [...designs.values()].filter((d) => d.pages > 0);
|
|
149
|
+
if (usedDesigns.length === 0 && (unused.length === 0 || options.unusedStyles !== 'remove')) {
|
|
150
|
+
return {
|
|
151
|
+
config,
|
|
152
|
+
chapters: input.chapters.map((c) => ({ ...c })),
|
|
153
|
+
unchanged: true,
|
|
154
|
+
report: emptyReport(before, bytes(config), left, unused),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// 3. Templates.
|
|
158
|
+
const takenTemplates = new Set((config.pageTemplates ?? []).map((t) => t.id));
|
|
159
|
+
const templates = buildTemplates(usedDesigns, share, takenTemplates);
|
|
160
|
+
// A design whose elements show on some page roles only (`pages:
|
|
161
|
+
// 'opener'`) keeps showing them where they showed: its pages' templates
|
|
162
|
+
// report the role each page had (a composed page reports `'composed'`).
|
|
163
|
+
const pageRole = (p) => {
|
|
164
|
+
const h = p.headings[0];
|
|
165
|
+
if (!h || h.line !== p.startLine)
|
|
166
|
+
return 'body';
|
|
167
|
+
const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
|
|
168
|
+
const span = st?.span ?? resolved.headings.levels[h.level - 1]?.span;
|
|
169
|
+
return span === 'page' || breakBefore(h).enabled ? 'opener' : 'body';
|
|
170
|
+
};
|
|
171
|
+
const roleTemplate = new Map();
|
|
172
|
+
const templateFor = (d, p) => {
|
|
173
|
+
if (!d.roleFiltered)
|
|
174
|
+
return d.template;
|
|
175
|
+
const role = pageRole(p);
|
|
176
|
+
const key = `${d.template}\n${role}`;
|
|
177
|
+
const hit = roleTemplate.get(key);
|
|
178
|
+
if (hit)
|
|
179
|
+
return hit;
|
|
180
|
+
const own = templates.list.find((t) => t.id === d.template);
|
|
181
|
+
const sharedBy = usedDesigns.filter((x) => x.template === d.template).length;
|
|
182
|
+
const extended = templates.list.some((t) => t.extends === d.template);
|
|
183
|
+
let id;
|
|
184
|
+
if (own.role === undefined && sharedBy === 1 && !extended && ![...roleTemplate.keys()].some((k) => k.startsWith(`${d.template}\n`))) {
|
|
185
|
+
own.role = role;
|
|
186
|
+
id = own.id;
|
|
187
|
+
}
|
|
188
|
+
else if (own.role === role) {
|
|
189
|
+
id = own.id;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
id = freeId(`${own.id}-${role}`, takenTemplates);
|
|
193
|
+
templates.list.push({ id, name: `${own.id} (${role})`, extends: own.id, role });
|
|
194
|
+
}
|
|
195
|
+
roleTemplate.set(key, id);
|
|
196
|
+
return id;
|
|
197
|
+
};
|
|
198
|
+
// 4. Paragraph and chip styles.
|
|
199
|
+
const paragraphStyles = (config.paragraphStyles ?? []).slice();
|
|
200
|
+
const takenParagraph = new Set(paragraphStyles.map((p) => p.id));
|
|
201
|
+
const createdParagraph = [];
|
|
202
|
+
const reusedParagraph = new Set();
|
|
203
|
+
const styleCache = new Map();
|
|
204
|
+
const bodyColour = config.bodyText?.color?.hex?.toLowerCase();
|
|
205
|
+
const paragraphStyleFor = (f) => {
|
|
206
|
+
const want = styleFields(f.look, f.gap);
|
|
207
|
+
const key = stableStringify(want);
|
|
208
|
+
const hit = styleCache.get(key);
|
|
209
|
+
if (hit)
|
|
210
|
+
return hit;
|
|
211
|
+
const own = paragraphStyles.find((p) => sameStyle(p, want));
|
|
212
|
+
if (own) {
|
|
213
|
+
reusedParagraph.add(own.id);
|
|
214
|
+
styleCache.set(key, own.id);
|
|
215
|
+
return own.id;
|
|
216
|
+
}
|
|
217
|
+
const size = toMm(f.look.fontSize);
|
|
218
|
+
const pt = size !== undefined ? Math.round((size * 72) / 25.4 * 10) / 10 : undefined;
|
|
219
|
+
const hex = f.look.color?.hex?.toLowerCase();
|
|
220
|
+
const parts = [
|
|
221
|
+
slug(f.look.fontFamily ?? 'text') || 'text',
|
|
222
|
+
pt !== undefined ? String(pt).replace('.', '-') : '',
|
|
223
|
+
f.look.fontWeight >= 600 ? 'bold' : '',
|
|
224
|
+
f.look.italic ? 'italic' : '',
|
|
225
|
+
f.look.align !== 'left' && f.look.align !== 'start' ? f.look.align : '',
|
|
226
|
+
hex && hex !== bodyColour ? hex.replace('#', '') : '',
|
|
227
|
+
].filter(Boolean);
|
|
228
|
+
const id = freeId(parts.join('-'), takenParagraph);
|
|
229
|
+
const name = [f.look.fontFamily ?? '', pt !== undefined ? `${pt} pt` : '', f.look.fontWeight >= 600 ? 'bold' : '', f.look.italic ? 'italic' : '', f.look.align, hex ?? '']
|
|
230
|
+
.filter(Boolean).join(' ');
|
|
231
|
+
paragraphStyles.push({ id, name, ...want });
|
|
232
|
+
createdParagraph.push(id);
|
|
233
|
+
styleCache.set(key, id);
|
|
234
|
+
return id;
|
|
235
|
+
};
|
|
236
|
+
const chipStyles = (config.chipStyles ?? []).slice();
|
|
237
|
+
const createdChips = [];
|
|
238
|
+
for (const d of usedDesigns) {
|
|
239
|
+
for (const [id, st] of d.plan.newChipStyles) {
|
|
240
|
+
if (chipStyles.some((c) => c.id === id))
|
|
241
|
+
continue;
|
|
242
|
+
chipStyles.push(st);
|
|
243
|
+
createdChips.push(id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 5. The Markdown.
|
|
247
|
+
const pages = [];
|
|
248
|
+
let headingsDropped = 0;
|
|
249
|
+
const chapters = scans.map(({ chapter, scan }) => {
|
|
250
|
+
const { lines } = scan;
|
|
251
|
+
const lineStart = [];
|
|
252
|
+
for (let i = 0, at = 0; i < lines.length; i++) {
|
|
253
|
+
lineStart[i] = at;
|
|
254
|
+
at += lines[i].length + 1;
|
|
255
|
+
}
|
|
256
|
+
const drop = new Set(scan.dropBreaks);
|
|
257
|
+
const out = [];
|
|
258
|
+
let fence = 0;
|
|
259
|
+
let cursor = 0;
|
|
260
|
+
const runsAt = new Map(scan.runs.map((r) => [r.startLine, r]));
|
|
261
|
+
while (cursor < lines.length) {
|
|
262
|
+
const run = runsAt.get(cursor);
|
|
263
|
+
if (!run) {
|
|
264
|
+
if (!drop.has(cursor))
|
|
265
|
+
out.push(lines[cursor]);
|
|
266
|
+
cursor++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const d = designs.get(run.design);
|
|
270
|
+
const firstBreak = breakBefore(run.pages[0].headings[0]);
|
|
271
|
+
run.pages.forEach((p, pi) => {
|
|
272
|
+
const side = options.sideOf?.(chapter.id, lineStart[p.startLine]) ?? heuristicSide(firstBreak.parity, pi);
|
|
273
|
+
const written = writePage(d, templateFor(d, p), p, lines, side, styleById, paragraphStyleFor);
|
|
274
|
+
headingsDropped += written.dropped;
|
|
275
|
+
if (out.length > 0 && out[out.length - 1].trim() !== '')
|
|
276
|
+
out.push('');
|
|
277
|
+
// A composed page starts a page but reads no heading's parity: the
|
|
278
|
+
// side the opening heading asked for is a page break's.
|
|
279
|
+
const parity = pi === 0 && firstBreak.enabled ? sideParity(firstBreak.parity) : undefined;
|
|
280
|
+
if (parity)
|
|
281
|
+
out.push(`:::pagebreak{parity="${parity}"}`, '');
|
|
282
|
+
out.push(...written.lines, '');
|
|
283
|
+
pages.push({
|
|
284
|
+
chapterId: chapter.id,
|
|
285
|
+
fence: fence++,
|
|
286
|
+
template: written.template,
|
|
287
|
+
styles: [...new Set(p.headings.map((h) => h.attrs.style).filter((s) => s !== undefined))],
|
|
288
|
+
...(side ? { side } : {}),
|
|
289
|
+
frames: written.frames,
|
|
290
|
+
elements: written.elements,
|
|
291
|
+
chars: written.chars,
|
|
292
|
+
differences: written.differences,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
cursor = run.endLine + 1;
|
|
296
|
+
// The blank line after the fence is written; skip the source's.
|
|
297
|
+
while (cursor < lines.length && lines[cursor].trim() === '' && out[out.length - 1] === '')
|
|
298
|
+
cursor++;
|
|
299
|
+
}
|
|
300
|
+
return { id: chapter.id, markdown: out.join('\n') };
|
|
301
|
+
});
|
|
302
|
+
// 6. Heading styles: the running heads are gone; styles left the same
|
|
303
|
+
// are merged; unused ones go when asked.
|
|
304
|
+
const migrated = new Set(usedDesigns.flatMap((d) => d.styleIds));
|
|
305
|
+
const removeUnused = options.unusedStyles === 'remove';
|
|
306
|
+
const removed = [];
|
|
307
|
+
const stripped = [];
|
|
308
|
+
for (const s of styles) {
|
|
309
|
+
if (removeUnused && unused.includes(s.id)) {
|
|
310
|
+
removed.push(s.id);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (!migrated.has(s.id)) {
|
|
314
|
+
stripped.push(s);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const d = designOfStyle.get(s.id);
|
|
318
|
+
const copy = { ...s };
|
|
319
|
+
if (d.ownHeader)
|
|
320
|
+
delete copy.header;
|
|
321
|
+
if (d.ownFooter)
|
|
322
|
+
delete copy.footer;
|
|
323
|
+
stripped.push(copy);
|
|
324
|
+
}
|
|
325
|
+
const { styles: mergedStyles, renames, merged } = mergeStyles(stripped, migrated);
|
|
326
|
+
const finalChapters = chapters.map((c) => ({ ...c, markdown: renameStyles(c.markdown, renames) }));
|
|
327
|
+
for (const p of pages)
|
|
328
|
+
p.styles = p.styles.map((s) => renames.get(s) ?? s);
|
|
329
|
+
const next = {
|
|
330
|
+
...config,
|
|
331
|
+
headingStyles: mergedStyles,
|
|
332
|
+
paragraphStyles,
|
|
333
|
+
...(chipStyles.length > 0 ? { chipStyles } : {}),
|
|
334
|
+
pageTemplates: [...(config.pageTemplates ?? []), ...templates.list],
|
|
335
|
+
};
|
|
336
|
+
for (const id of unused)
|
|
337
|
+
if (!removeUnused)
|
|
338
|
+
left.push({ path: `headingStyles[id=${id}]`, reason: 'unused' });
|
|
339
|
+
const report = {
|
|
340
|
+
pages,
|
|
341
|
+
templates: templates.list.map((t) => ({
|
|
342
|
+
id: t.id,
|
|
343
|
+
...(t.extends ? { extends: t.extends } : {}),
|
|
344
|
+
frames: (t.frames?.length ?? 0) + (t.variants?.recto?.frames?.length ?? 0) + (t.variants?.verso?.frames?.length ?? 0),
|
|
345
|
+
pages: pages.filter((p) => p.template === t.id).length,
|
|
346
|
+
})),
|
|
347
|
+
movedElements: usedDesigns.reduce((n, d) => n + d.plan.movedElements, 0),
|
|
348
|
+
movedChars: usedDesigns.reduce((n, d) => n + d.plan.movedChars, 0),
|
|
349
|
+
chips: usedDesigns.reduce((n, d) => n + d.plan.chips, 0),
|
|
350
|
+
frames: usedDesigns.reduce((n, d) => n + d.plan.frames.length, 0),
|
|
351
|
+
paragraphStylesCreated: createdParagraph,
|
|
352
|
+
paragraphStylesReused: [...reusedParagraph],
|
|
353
|
+
chipStylesCreated: createdChips,
|
|
354
|
+
headingStylesMerged: merged,
|
|
355
|
+
headingStylesRemoved: removed,
|
|
356
|
+
unusedStyles: unused,
|
|
357
|
+
headingsDropped,
|
|
358
|
+
left,
|
|
359
|
+
configBytesBefore: bytes(config),
|
|
360
|
+
configBytesAfter: bytes(next),
|
|
361
|
+
contentInConfigBefore: before,
|
|
362
|
+
contentInConfigAfter: countContent(next, metadata),
|
|
363
|
+
};
|
|
364
|
+
return { config: next, chapters: finalChapters, report, unchanged: false };
|
|
365
|
+
}
|
|
366
|
+
function emptyReport(before, size, left, unused) {
|
|
367
|
+
return {
|
|
368
|
+
pages: [], templates: [], movedElements: 0, movedChars: 0, chips: 0, frames: 0,
|
|
369
|
+
paragraphStylesCreated: [], paragraphStylesReused: [], chipStylesCreated: [],
|
|
370
|
+
headingStylesMerged: [], headingStylesRemoved: [], unusedStyles: unused, headingsDropped: 0, left,
|
|
371
|
+
configBytesBefore: size, configBytesAfter: size, contentInConfigBefore: before, contentInConfigAfter: before,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** The page break parity of a heading's `breakBefore.parity`. */
|
|
375
|
+
function sideParity(parity) {
|
|
376
|
+
return parity === 'even' || parity === 'always-even' ? 'even' : parity === 'odd' || parity === 'always-odd' ? 'odd' : undefined;
|
|
377
|
+
}
|
|
378
|
+
/** The side of a run's `index`-th page from its opening heading's parity. */
|
|
379
|
+
function heuristicSide(parity, index) {
|
|
380
|
+
const first = parity === 'even' || parity === 'always-even' ? 'verso' : parity === 'odd' || parity === 'always-odd' ? 'recto' : undefined;
|
|
381
|
+
if (!first)
|
|
382
|
+
return undefined;
|
|
383
|
+
return index % 2 === 0 ? first : first === 'recto' ? 'verso' : 'recto';
|
|
384
|
+
}
|
|
385
|
+
/** Attributes of the opening heading the page fence carries: those the
|
|
386
|
+
* template reads (`{attr.x}`). */
|
|
387
|
+
const FENCE_RESERVED = new Set(['style', 'id', 'toc', 'hidden', 'numbered', 'slot', 'template', 'spread', 'background']);
|
|
388
|
+
function writePage(d, template, p, lines, side, styleById, paragraphStyleFor) {
|
|
389
|
+
const differences = new Set();
|
|
390
|
+
const attrs = [`template="${template}"`];
|
|
391
|
+
if (d.plan.background)
|
|
392
|
+
attrs.push(`background="${d.plan.background.replace(/["\\]/g, '')}"`);
|
|
393
|
+
const opening = p.headings[0];
|
|
394
|
+
for (const name of d.attrNames ?? []) {
|
|
395
|
+
if (FENCE_RESERVED.has(name))
|
|
396
|
+
continue;
|
|
397
|
+
const token = opening?.attrTokens[name];
|
|
398
|
+
if (token !== undefined)
|
|
399
|
+
attrs.push(token);
|
|
400
|
+
}
|
|
401
|
+
// The page's own lines, without the hidden headings that only switched
|
|
402
|
+
// the running head (no id, nothing in the contents, no words).
|
|
403
|
+
const dropLines = new Set();
|
|
404
|
+
for (const h of p.headings) {
|
|
405
|
+
const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
|
|
406
|
+
if (h.attrs.id === undefined && st?.hidden === true && st.toc === false && !/[\p{L}\p{N}]/u.test(h.title))
|
|
407
|
+
dropLines.add(h.line);
|
|
408
|
+
}
|
|
409
|
+
const body = [];
|
|
410
|
+
for (let i = p.startLine; i <= p.endLine; i++)
|
|
411
|
+
if (!dropLines.has(i))
|
|
412
|
+
body.push(lines[i]);
|
|
413
|
+
while (body.length > 0 && body[body.length - 1].trim() === '')
|
|
414
|
+
body.pop();
|
|
415
|
+
while (body.length > 0 && body[0].trim() === '')
|
|
416
|
+
body.shift();
|
|
417
|
+
const flowText = body.some((l) => l.trim() !== '' && !/^#{1,6}[ \t]/.test(l.trim()));
|
|
418
|
+
if (flowText)
|
|
419
|
+
differences.add('flow');
|
|
420
|
+
const out = [`::::page{${attrs.join(' ')}}`, ...body];
|
|
421
|
+
let frames = 0;
|
|
422
|
+
let elements = 0;
|
|
423
|
+
let chars = 0;
|
|
424
|
+
for (const f of d.plan.frames) {
|
|
425
|
+
if (f.side && side && f.side !== side)
|
|
426
|
+
continue;
|
|
427
|
+
if (f.side && !side)
|
|
428
|
+
differences.add('side');
|
|
429
|
+
frames++;
|
|
430
|
+
out.push('', `:::frame{slot="${f.name}"}`, `:::paragraphs{style="${paragraphStyleFor(f)}"}`);
|
|
431
|
+
f.pieces.forEach((piece, i) => {
|
|
432
|
+
if (i > 0)
|
|
433
|
+
out.push('');
|
|
434
|
+
out.push(joinLines(piece.lines));
|
|
435
|
+
elements++;
|
|
436
|
+
chars += piece.chars;
|
|
437
|
+
for (const n of piece.notes)
|
|
438
|
+
differences.add(n);
|
|
439
|
+
});
|
|
440
|
+
out.push(':::', ':::');
|
|
441
|
+
}
|
|
442
|
+
if (d.pages > 1 && d.plan.frames.some((f) => !f.side))
|
|
443
|
+
differences.add('repeated');
|
|
444
|
+
out.push('::::');
|
|
445
|
+
return { template, lines: out, frames, elements, chars, differences: [...differences].sort(), dropped: dropLines.size };
|
|
446
|
+
}
|
|
447
|
+
/** A frame of the template, from a planned one. */
|
|
448
|
+
function frameConfig(f) {
|
|
449
|
+
const first = f.pieces[0];
|
|
450
|
+
const el = first.element;
|
|
451
|
+
const placement = first.x !== undefined && first.y !== undefined
|
|
452
|
+
? { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: round3(first.x), unit: 'mm' }, y: { value: round3(first.y), unit: 'mm' } } }
|
|
453
|
+
: { anchor: el.placement.anchor, ...(el.placement.offset ? { offset: el.placement.offset } : {}) };
|
|
454
|
+
const width = f.width !== undefined ? { value: round3(f.width), unit: 'mm' }
|
|
455
|
+
: typeof el.placement.size?.width === 'object' ? el.placement.size.width : undefined;
|
|
456
|
+
return {
|
|
457
|
+
name: f.name,
|
|
458
|
+
placement,
|
|
459
|
+
...(width ? { width } : {}),
|
|
460
|
+
...(f.look.direction ? { direction: f.look.direction } : {}),
|
|
461
|
+
...(f.look.writingMode ? { writingMode: f.look.writingMode } : {}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const round3 = (v) => Math.round(v * 1000) / 1000;
|
|
465
|
+
/** The frames of a template, those of one side in its variant. */
|
|
466
|
+
function framesPatch(frames) {
|
|
467
|
+
const own = frames.filter((f) => !f.side).map(frameConfig);
|
|
468
|
+
const recto = frames.filter((f) => f.side === 'recto').map(frameConfig);
|
|
469
|
+
const verso = frames.filter((f) => f.side === 'verso').map(frameConfig);
|
|
470
|
+
const variants = {};
|
|
471
|
+
if (recto.length > 0)
|
|
472
|
+
variants.recto = { frames: recto };
|
|
473
|
+
if (verso.length > 0)
|
|
474
|
+
variants.verso = { frames: verso };
|
|
475
|
+
return { frames: own, ...(recto.length > 0 || verso.length > 0 ? { variants } : {}) };
|
|
476
|
+
}
|
|
477
|
+
/** The running text's frame: the content area, under the decoration as
|
|
478
|
+
* the running text was under the running head that drew the page. */
|
|
479
|
+
const FLOW_FRAME = { name: 'flow', flow: true, optional: true, layer: -1 };
|
|
480
|
+
/** The running heads a design's template sets: its styles' own (what
|
|
481
|
+
* stayed in them), hidden when nothing did; unset when the styles had
|
|
482
|
+
* none of their own. */
|
|
483
|
+
function runningHeads(d) {
|
|
484
|
+
return {
|
|
485
|
+
...(d.ownHeader ? { header: d.plan.header.length > 0 ? { elements: d.plan.header } : false } : {}),
|
|
486
|
+
...(d.ownFooter ? { footer: d.plan.footer.length > 0 ? { elements: d.plan.footer } : false } : {}),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** `{attr.x}` names a template's decoration and running heads read. */
|
|
490
|
+
function attrNamesOf(t) {
|
|
491
|
+
const names = new Set();
|
|
492
|
+
const text = stableStringify(t);
|
|
493
|
+
for (const m of text.matchAll(/\{attr\.([\w-]+)\}/g))
|
|
494
|
+
names.add(m[1]);
|
|
495
|
+
return names;
|
|
496
|
+
}
|
|
497
|
+
/** Templates for the designs: one each, or (sharing) a base per family of
|
|
498
|
+
* designs whose frames stand in the same places, with the pages' own
|
|
499
|
+
* templates extending it. Names frames on the way. */
|
|
500
|
+
function buildTemplates(designs, share, taken) {
|
|
501
|
+
const list = [];
|
|
502
|
+
const idFor = (d) => {
|
|
503
|
+
const first = d.styleIds[0];
|
|
504
|
+
const cut = first.lastIndexOf('--');
|
|
505
|
+
return freeId(cut >= 0 ? first.slice(cut + 2) : first, taken);
|
|
506
|
+
};
|
|
507
|
+
// Keys of frames that more than one design has.
|
|
508
|
+
const occurrences = new Map();
|
|
509
|
+
const keysOf = (d) => {
|
|
510
|
+
const seen = new Map();
|
|
511
|
+
return d.plan.frames.map((f) => {
|
|
512
|
+
const n = (seen.get(f.key) ?? 0) + 1;
|
|
513
|
+
seen.set(f.key, n);
|
|
514
|
+
return `${f.key}#${n}`;
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
const designKeys = new Map(designs.map((d) => [d, keysOf(d)]));
|
|
518
|
+
for (const d of designs)
|
|
519
|
+
for (const k of designKeys.get(d)) {
|
|
520
|
+
const set = occurrences.get(k) ?? new Set();
|
|
521
|
+
set.add(d.key);
|
|
522
|
+
occurrences.set(k, set);
|
|
523
|
+
}
|
|
524
|
+
const families = new Map();
|
|
525
|
+
for (const d of designs) {
|
|
526
|
+
const common = share ? designKeys.get(d).filter((k) => occurrences.get(k).size > 1).sort().join('\n') : '';
|
|
527
|
+
const fam = common ? `${common}\n${stableStringify(runningHeads(d))}` : `solo:${d.key}`;
|
|
528
|
+
const arr = families.get(fam) ?? [];
|
|
529
|
+
arr.push(d);
|
|
530
|
+
families.set(fam, arr);
|
|
531
|
+
}
|
|
532
|
+
let baseCount = 0;
|
|
533
|
+
const childBySpec = new Map();
|
|
534
|
+
for (const [fam, members] of families) {
|
|
535
|
+
if (fam.startsWith('solo:') || members.length < 2) {
|
|
536
|
+
for (const d of members) {
|
|
537
|
+
let n = 0;
|
|
538
|
+
for (const f of d.plan.frames.slice().sort(byPosition))
|
|
539
|
+
f.name = `text-${++n}`;
|
|
540
|
+
const id = idFor(d);
|
|
541
|
+
const decoration = remapDecorationAnchors(d.plan.decoration, d.plan.movedTo, d.plan.frames);
|
|
542
|
+
const { frames, variants } = framesPatch(d.plan.frames);
|
|
543
|
+
const t = {
|
|
544
|
+
id,
|
|
545
|
+
name: `${id} (${d.styleIds.join(', ')})`,
|
|
546
|
+
...runningHeads(d),
|
|
547
|
+
decoration: { elements: decoration },
|
|
548
|
+
frames: [FLOW_FRAME, ...frames],
|
|
549
|
+
...(variants ? { variants } : {}),
|
|
550
|
+
};
|
|
551
|
+
list.push(t);
|
|
552
|
+
d.template = id;
|
|
553
|
+
d.attrNames = attrNamesOf([t]);
|
|
554
|
+
}
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
// A base with the shared frames and the decoration every member has.
|
|
558
|
+
const sharedKeys = new Set(fam.split('\n').slice(0, -1));
|
|
559
|
+
const first = members[0];
|
|
560
|
+
const firstKeys = designKeys.get(first);
|
|
561
|
+
const baseFrames = first.plan.frames.filter((_, i) => sharedKeys.has(firstKeys[i])).slice().sort(byPosition);
|
|
562
|
+
const nameOfKey = new Map();
|
|
563
|
+
baseFrames.forEach((f, i) => nameOfKey.set(firstKeys[first.plan.frames.indexOf(f)], `shared-${i + 1}`));
|
|
564
|
+
for (const d of members) {
|
|
565
|
+
const keys = designKeys.get(d);
|
|
566
|
+
d.plan.frames.forEach((f, i) => {
|
|
567
|
+
const shared = nameOfKey.get(keys[i]);
|
|
568
|
+
if (shared)
|
|
569
|
+
f.name = shared;
|
|
570
|
+
});
|
|
571
|
+
let n = 0;
|
|
572
|
+
for (const f of d.plan.frames.slice().sort(byPosition))
|
|
573
|
+
if (!f.name)
|
|
574
|
+
f.name = `text-${++n}`;
|
|
575
|
+
}
|
|
576
|
+
// Decoration every member draws (ids aside): the base's, under the
|
|
577
|
+
// first member's ids.
|
|
578
|
+
const NO_ID = new Set(['id']);
|
|
579
|
+
const decos = members.map((d) => remapDecorationAnchors(d.plan.decoration, d.plan.movedTo, d.plan.frames));
|
|
580
|
+
const decoStrings = decos.map((list) => list.map((e) => stableStringify(e, NO_ID)));
|
|
581
|
+
const countOf = (list) => list.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), new Map());
|
|
582
|
+
const counts = decoStrings.map(countOf);
|
|
583
|
+
const shared = new Map();
|
|
584
|
+
for (const [k, n] of counts[0])
|
|
585
|
+
shared.set(k, Math.min(n, ...counts.slice(1).map((c) => c.get(k) ?? 0)));
|
|
586
|
+
const commonIdx = decoStrings.map((list) => {
|
|
587
|
+
const left = new Map(shared);
|
|
588
|
+
const picked = new Set();
|
|
589
|
+
list.forEach((k, i) => {
|
|
590
|
+
const n = left.get(k) ?? 0;
|
|
591
|
+
if (n > 0) {
|
|
592
|
+
picked.add(i);
|
|
593
|
+
left.set(k, n - 1);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
return picked;
|
|
597
|
+
});
|
|
598
|
+
const common = decos[0].filter((_, i) => commonIdx[0].has(i));
|
|
599
|
+
const baseId = freeId(baseCount++ === 0 ? 'page-base' : `page-base-${baseCount}`, taken);
|
|
600
|
+
const baseFramesCfg = framesPatch(baseFrames);
|
|
601
|
+
const base = {
|
|
602
|
+
id: baseId,
|
|
603
|
+
name: `Shared frames (${members.length} pages)`,
|
|
604
|
+
...runningHeads(first),
|
|
605
|
+
decoration: { elements: common },
|
|
606
|
+
frames: [FLOW_FRAME, ...baseFramesCfg.frames],
|
|
607
|
+
...(baseFramesCfg.variants ? { variants: baseFramesCfg.variants } : {}),
|
|
608
|
+
};
|
|
609
|
+
list.push(base);
|
|
610
|
+
members.forEach((d, mi) => {
|
|
611
|
+
const keys = designKeys.get(d);
|
|
612
|
+
const own = d.plan.frames.filter((_, i) => !sharedKeys.has(keys[i]));
|
|
613
|
+
// A shared element goes by the base's id: anchors to it follow.
|
|
614
|
+
const renamed = new Map();
|
|
615
|
+
const firstShared = decos[0].filter((_, i) => commonIdx[0].has(i));
|
|
616
|
+
decos[mi].filter((_, i) => commonIdx[mi].has(i)).forEach((e, j) => { if (firstShared[j] && firstShared[j].id !== e.id)
|
|
617
|
+
renamed.set(e.id, firstShared[j].id); });
|
|
618
|
+
const ownDeco = decos[mi].filter((_, i) => !commonIdx[mi].has(i)).map((e) => {
|
|
619
|
+
const to = e.placement?.anchor?.to;
|
|
620
|
+
const target = typeof to === 'string' && to.startsWith('#') ? renamed.get(to.slice(1)) : undefined;
|
|
621
|
+
return target ? { ...e, placement: { ...e.placement, anchor: { ...e.placement.anchor, to: `#${target}` } } } : e;
|
|
622
|
+
});
|
|
623
|
+
if (own.length === 0 && ownDeco.length === 0) {
|
|
624
|
+
d.template = baseId;
|
|
625
|
+
d.attrNames = attrNamesOf([base]);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const patch = framesPatch(own);
|
|
629
|
+
const spec = stableStringify({ patch, ownDeco, own: own.map((f) => f.key) });
|
|
630
|
+
const known = childBySpec.get(spec);
|
|
631
|
+
if (known) {
|
|
632
|
+
d.template = known;
|
|
633
|
+
d.attrNames = attrNamesOf([base, list.find((t) => t.id === known)]);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const id = idFor(d);
|
|
637
|
+
const child = {
|
|
638
|
+
id,
|
|
639
|
+
name: `${id} (${d.styleIds.join(', ')})`,
|
|
640
|
+
extends: baseId,
|
|
641
|
+
...(ownDeco.length > 0 ? { decoration: { elements: ownDeco } } : {}),
|
|
642
|
+
...(patch.frames.length > 0 ? { frames: patch.frames } : {}),
|
|
643
|
+
...(patch.variants ? { variants: patch.variants } : {}),
|
|
644
|
+
};
|
|
645
|
+
list.push(child);
|
|
646
|
+
childBySpec.set(spec, id);
|
|
647
|
+
d.template = id;
|
|
648
|
+
d.attrNames = attrNamesOf([base, child]);
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
return { list };
|
|
652
|
+
}
|
|
653
|
+
function byPosition(a, b) {
|
|
654
|
+
const ay = a.pieces[0].y ?? Infinity;
|
|
655
|
+
const by = b.pieces[0].y ?? Infinity;
|
|
656
|
+
return ay - by || (a.pieces[0].x ?? 0) - (b.pieces[0].x ?? 0);
|
|
657
|
+
}
|
|
658
|
+
/** Migrated heading styles that came out the same are one: the id their
|
|
659
|
+
* ids share before `--` when it is free (`tema--p04`, `tema--p05` →
|
|
660
|
+
* `tema`), else the first one's. */
|
|
661
|
+
function mergeStyles(styles, migrated) {
|
|
662
|
+
const groups = new Map();
|
|
663
|
+
for (const s of styles) {
|
|
664
|
+
if (!migrated.has(s.id))
|
|
665
|
+
continue;
|
|
666
|
+
const prefix = s.id.includes('--') ? s.id.slice(0, s.id.lastIndexOf('--')) : s.id;
|
|
667
|
+
const key = `${prefix}\n${stableStringify(s, new Set(['id', 'name']))}`;
|
|
668
|
+
const g = groups.get(key) ?? [];
|
|
669
|
+
g.push(s);
|
|
670
|
+
groups.set(key, g);
|
|
671
|
+
}
|
|
672
|
+
const ids = new Set(styles.map((s) => s.id));
|
|
673
|
+
const renames = new Map();
|
|
674
|
+
const merged = [];
|
|
675
|
+
const keepAs = new Map();
|
|
676
|
+
for (const g of groups.values()) {
|
|
677
|
+
if (g.length < 2)
|
|
678
|
+
continue;
|
|
679
|
+
const prefixes = new Set(g.map((s) => (s.id.includes('--') ? s.id.slice(0, s.id.lastIndexOf('--')) : null)));
|
|
680
|
+
const prefix = prefixes.size === 1 ? [...prefixes][0] : null;
|
|
681
|
+
const into = prefix && (!ids.has(prefix) || g.some((s) => s.id === prefix)) ? prefix : g[0].id;
|
|
682
|
+
ids.add(into);
|
|
683
|
+
const kept = { ...g[0], id: into, name: into === g[0].id ? (g[0].name ?? into) : into };
|
|
684
|
+
keepAs.set(g[0].id, kept);
|
|
685
|
+
const from = g.map((s) => s.id).filter((id) => id !== into);
|
|
686
|
+
for (const s of g)
|
|
687
|
+
if (s.id !== into)
|
|
688
|
+
renames.set(s.id, into);
|
|
689
|
+
merged.push({ into, from });
|
|
690
|
+
}
|
|
691
|
+
const grouped = new Set(merged.flatMap((m) => [m.into, ...m.from]));
|
|
692
|
+
const out = [];
|
|
693
|
+
for (const s of styles) {
|
|
694
|
+
const kept = keepAs.get(s.id);
|
|
695
|
+
if (kept)
|
|
696
|
+
out.push(kept);
|
|
697
|
+
else if (!grouped.has(s.id))
|
|
698
|
+
out.push(s);
|
|
699
|
+
}
|
|
700
|
+
return { styles: out, renames, merged };
|
|
701
|
+
}
|
|
702
|
+
/** `style="old"` on a heading line names the merged style. */
|
|
703
|
+
function renameStyles(markdown, renames) {
|
|
704
|
+
if (renames.size === 0)
|
|
705
|
+
return markdown;
|
|
706
|
+
return markdown.split('\n').map((line) => {
|
|
707
|
+
if (!/^#{1,6}[ \t]/.test(line) || !line.trimEnd().endsWith('}'))
|
|
708
|
+
return line;
|
|
709
|
+
return line.replace(/(\bstyle\s*=\s*)("([^"]*)"|'([^']*)'|([^\s}"']+))/, (m, pre, _v, a, b, c) => {
|
|
710
|
+
const id = a ?? b ?? c ?? '';
|
|
711
|
+
const to = renames.get(id);
|
|
712
|
+
return to ? `${pre}"${to}"` : m;
|
|
713
|
+
});
|
|
714
|
+
}).join('\n');
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=textToMarkdown.js.map
|