postext 1.36.0 → 1.38.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.
Files changed (300) hide show
  1. package/dist/__tests__/comics/frames.test.d.ts +2 -0
  2. package/dist/__tests__/comics/frames.test.d.ts.map +1 -0
  3. package/dist/__tests__/comics/frames.test.js +251 -0
  4. package/dist/__tests__/comics/frames.test.js.map +1 -0
  5. package/dist/__tests__/composed/continuation.test.d.ts +2 -0
  6. package/dist/__tests__/composed/continuation.test.d.ts.map +1 -0
  7. package/dist/__tests__/composed/continuation.test.js +352 -0
  8. package/dist/__tests__/composed/continuation.test.js.map +1 -0
  9. package/dist/__tests__/composed/continuationRules.test.d.ts +2 -0
  10. package/dist/__tests__/composed/continuationRules.test.d.ts.map +1 -0
  11. package/dist/__tests__/composed/continuationRules.test.js +389 -0
  12. package/dist/__tests__/composed/continuationRules.test.js.map +1 -0
  13. package/dist/__tests__/composed/contrast.test.d.ts +2 -0
  14. package/dist/__tests__/composed/contrast.test.d.ts.map +1 -0
  15. package/dist/__tests__/composed/contrast.test.js +124 -0
  16. package/dist/__tests__/composed/contrast.test.js.map +1 -0
  17. package/dist/__tests__/composed/corpus.test.js +7 -1
  18. package/dist/__tests__/composed/corpus.test.js.map +1 -1
  19. package/dist/__tests__/composed/coverImages.test.d.ts +2 -0
  20. package/dist/__tests__/composed/coverImages.test.d.ts.map +1 -0
  21. package/dist/__tests__/composed/coverImages.test.js +75 -0
  22. package/dist/__tests__/composed/coverImages.test.js.map +1 -0
  23. package/dist/__tests__/composed/decorationSize.test.d.ts +2 -0
  24. package/dist/__tests__/composed/decorationSize.test.d.ts.map +1 -0
  25. package/dist/__tests__/composed/decorationSize.test.js +79 -0
  26. package/dist/__tests__/composed/decorationSize.test.js.map +1 -0
  27. package/dist/__tests__/composed/emptyFrames.test.d.ts +2 -0
  28. package/dist/__tests__/composed/emptyFrames.test.d.ts.map +1 -0
  29. package/dist/__tests__/composed/emptyFrames.test.js +129 -0
  30. package/dist/__tests__/composed/emptyFrames.test.js.map +1 -0
  31. package/dist/__tests__/composed/fit.test.js +19 -10
  32. package/dist/__tests__/composed/fit.test.js.map +1 -1
  33. package/dist/__tests__/composed/groups.test.js +34 -0
  34. package/dist/__tests__/composed/groups.test.js.map +1 -1
  35. package/dist/__tests__/composed/headingDesigns.test.d.ts +2 -0
  36. package/dist/__tests__/composed/headingDesigns.test.d.ts.map +1 -0
  37. package/dist/__tests__/composed/headingDesigns.test.js +262 -0
  38. package/dist/__tests__/composed/headingDesigns.test.js.map +1 -0
  39. package/dist/__tests__/composed/labels.test.d.ts +2 -0
  40. package/dist/__tests__/composed/labels.test.d.ts.map +1 -0
  41. package/dist/__tests__/composed/labels.test.js +326 -0
  42. package/dist/__tests__/composed/labels.test.js.map +1 -0
  43. package/dist/__tests__/composed/outsidePrint.test.js +5 -2
  44. package/dist/__tests__/composed/outsidePrint.test.js.map +1 -1
  45. package/dist/__tests__/composed/ruledFrames.test.d.ts +2 -0
  46. package/dist/__tests__/composed/ruledFrames.test.d.ts.map +1 -0
  47. package/dist/__tests__/composed/ruledFrames.test.js +93 -0
  48. package/dist/__tests__/composed/ruledFrames.test.js.map +1 -0
  49. package/dist/__tests__/composed/sourceMap.test.js +26 -0
  50. package/dist/__tests__/composed/sourceMap.test.js.map +1 -1
  51. package/dist/__tests__/composed/spread.test.js +82 -0
  52. package/dist/__tests__/composed/spread.test.js.map +1 -1
  53. package/dist/__tests__/composed/spreadLabels.test.d.ts +2 -0
  54. package/dist/__tests__/composed/spreadLabels.test.d.ts.map +1 -0
  55. package/dist/__tests__/composed/spreadLabels.test.js +131 -0
  56. package/dist/__tests__/composed/spreadLabels.test.js.map +1 -0
  57. package/dist/__tests__/exports.test.js +23 -0
  58. package/dist/__tests__/exports.test.js.map +1 -1
  59. package/dist/__tests__/migrate/fixtures/textbookPort.d.ts +14 -0
  60. package/dist/__tests__/migrate/fixtures/textbookPort.d.ts.map +1 -0
  61. package/dist/__tests__/migrate/fixtures/textbookPort.js +116 -0
  62. package/dist/__tests__/migrate/fixtures/textbookPort.js.map +1 -0
  63. package/dist/__tests__/migrate/labelsOnPictures.test.d.ts +2 -0
  64. package/dist/__tests__/migrate/labelsOnPictures.test.d.ts.map +1 -0
  65. package/dist/__tests__/migrate/labelsOnPictures.test.js +114 -0
  66. package/dist/__tests__/migrate/labelsOnPictures.test.js.map +1 -0
  67. package/dist/__tests__/migrate/textToMarkdown.test.d.ts +2 -0
  68. package/dist/__tests__/migrate/textToMarkdown.test.d.ts.map +1 -0
  69. package/dist/__tests__/migrate/textToMarkdown.test.js +268 -0
  70. package/dist/__tests__/migrate/textToMarkdown.test.js.map +1 -0
  71. package/dist/__tests__/paragraphDropCap.test.js +38 -0
  72. package/dist/__tests__/paragraphDropCap.test.js.map +1 -1
  73. package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts +2 -0
  74. package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts.map +1 -0
  75. package/dist/__tests__/paragraphStyleLetterSpacing.test.js +89 -0
  76. package/dist/__tests__/paragraphStyleLetterSpacing.test.js.map +1 -0
  77. package/dist/__tests__/pictureBelowPrintSize.test.d.ts +2 -0
  78. package/dist/__tests__/pictureBelowPrintSize.test.d.ts.map +1 -0
  79. package/dist/__tests__/pictureBelowPrintSize.test.js +82 -0
  80. package/dist/__tests__/pictureBelowPrintSize.test.js.map +1 -0
  81. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
  82. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  83. package/dist/__tests__/resourceTypePartial.test.d.ts +2 -0
  84. package/dist/__tests__/resourceTypePartial.test.d.ts.map +1 -0
  85. package/dist/__tests__/resourceTypePartial.test.js +60 -0
  86. package/dist/__tests__/resourceTypePartial.test.js.map +1 -0
  87. package/dist/canvas-backend/composed.d.ts +8 -2
  88. package/dist/canvas-backend/composed.d.ts.map +1 -1
  89. package/dist/canvas-backend/composed.js +52 -29
  90. package/dist/canvas-backend/composed.js.map +1 -1
  91. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  92. package/dist/canvas-backend/headerFooter.js +13 -0
  93. package/dist/canvas-backend/headerFooter.js.map +1 -1
  94. package/dist/canvas-backend/index.d.ts.map +1 -1
  95. package/dist/canvas-backend/index.js +53 -24
  96. package/dist/canvas-backend/index.js.map +1 -1
  97. package/dist/canvas-backend/renderResourceBlock.d.ts +5 -1
  98. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  99. package/dist/canvas-backend/renderResourceBlock.js +60 -1
  100. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  101. package/dist/comics/frames.d.ts +82 -0
  102. package/dist/comics/frames.d.ts.map +1 -0
  103. package/dist/comics/frames.js +182 -0
  104. package/dist/comics/frames.js.map +1 -0
  105. package/dist/comics/index.d.ts +4 -1
  106. package/dist/comics/index.d.ts.map +1 -1
  107. package/dist/comics/index.js +3 -1
  108. package/dist/comics/index.js.map +1 -1
  109. package/dist/comics/layoutPage.d.ts +9 -0
  110. package/dist/comics/layoutPage.d.ts.map +1 -1
  111. package/dist/comics/layoutPage.js +53 -68
  112. package/dist/comics/layoutPage.js.map +1 -1
  113. package/dist/comics/page.d.ts.map +1 -1
  114. package/dist/comics/page.js +10 -1
  115. package/dist/comics/page.js.map +1 -1
  116. package/dist/comics/sourceMap.d.ts +60 -0
  117. package/dist/comics/sourceMap.d.ts.map +1 -0
  118. package/dist/comics/sourceMap.js +166 -0
  119. package/dist/comics/sourceMap.js.map +1 -0
  120. package/dist/comics/spread.d.ts.map +1 -1
  121. package/dist/comics/spread.js +6 -0
  122. package/dist/comics/spread.js.map +1 -1
  123. package/dist/comics/textPanels.d.ts +113 -0
  124. package/dist/comics/textPanels.d.ts.map +1 -0
  125. package/dist/comics/textPanels.js +327 -0
  126. package/dist/comics/textPanels.js.map +1 -0
  127. package/dist/comics/transform.d.ts +6 -2
  128. package/dist/comics/transform.d.ts.map +1 -1
  129. package/dist/comics/transform.js +14 -1
  130. package/dist/comics/transform.js.map +1 -1
  131. package/dist/comics/types.d.ts +10 -0
  132. package/dist/comics/types.d.ts.map +1 -1
  133. package/dist/composed/continuation.d.ts +152 -0
  134. package/dist/composed/continuation.d.ts.map +1 -0
  135. package/dist/composed/continuation.js +189 -0
  136. package/dist/composed/continuation.js.map +1 -0
  137. package/dist/composed/groups.d.ts +3 -0
  138. package/dist/composed/groups.d.ts.map +1 -1
  139. package/dist/composed/groups.js +34 -4
  140. package/dist/composed/groups.js.map +1 -1
  141. package/dist/composed/labels.d.ts +154 -0
  142. package/dist/composed/labels.d.ts.map +1 -0
  143. package/dist/composed/labels.js +524 -0
  144. package/dist/composed/labels.js.map +1 -0
  145. package/dist/composed/pageMemo.d.ts +3 -2
  146. package/dist/composed/pageMemo.d.ts.map +1 -1
  147. package/dist/composed/pageMemo.js +3 -1
  148. package/dist/composed/pageMemo.js.map +1 -1
  149. package/dist/composed/paint.d.ts +28 -3
  150. package/dist/composed/paint.d.ts.map +1 -1
  151. package/dist/composed/paint.js +48 -2
  152. package/dist/composed/paint.js.map +1 -1
  153. package/dist/composed/reflowLog.d.ts +3 -1
  154. package/dist/composed/reflowLog.d.ts.map +1 -1
  155. package/dist/composed/reflowLog.js +20 -6
  156. package/dist/composed/reflowLog.js.map +1 -1
  157. package/dist/composed/rules.d.ts +31 -0
  158. package/dist/composed/rules.d.ts.map +1 -0
  159. package/dist/composed/rules.js +45 -0
  160. package/dist/composed/rules.js.map +1 -0
  161. package/dist/composed/slotChecks.d.ts +2 -1
  162. package/dist/composed/slotChecks.d.ts.map +1 -1
  163. package/dist/composed/slotChecks.js +56 -2
  164. package/dist/composed/slotChecks.js.map +1 -1
  165. package/dist/composed/sourceMap.d.ts +0 -21
  166. package/dist/composed/sourceMap.d.ts.map +1 -1
  167. package/dist/composed/sourceMap.js +102 -8
  168. package/dist/composed/sourceMap.js.map +1 -1
  169. package/dist/composed/spread.d.ts +23 -5
  170. package/dist/composed/spread.d.ts.map +1 -1
  171. package/dist/composed/spread.js +52 -27
  172. package/dist/composed/spread.js.map +1 -1
  173. package/dist/composed/templates.d.ts +11 -0
  174. package/dist/composed/templates.d.ts.map +1 -1
  175. package/dist/composed/templates.js +51 -2
  176. package/dist/composed/templates.js.map +1 -1
  177. package/dist/composed/types.d.ts +43 -3
  178. package/dist/composed/types.d.ts.map +1 -1
  179. package/dist/composed/validate.d.ts.map +1 -1
  180. package/dist/composed/validate.js +5 -3
  181. package/dist/composed/validate.js.map +1 -1
  182. package/dist/configWarnings.d.ts +15 -0
  183. package/dist/configWarnings.d.ts.map +1 -1
  184. package/dist/configWarnings.js +40 -18
  185. package/dist/configWarnings.js.map +1 -1
  186. package/dist/contrast.d.ts +5 -3
  187. package/dist/contrast.d.ts.map +1 -1
  188. package/dist/contrast.js +46 -5
  189. package/dist/contrast.js.map +1 -1
  190. package/dist/defaults/headerFooter.d.ts.map +1 -1
  191. package/dist/defaults/headerFooter.js +4 -0
  192. package/dist/defaults/headerFooter.js.map +1 -1
  193. package/dist/defaults/paragraphStyles.d.ts +1 -1
  194. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  195. package/dist/defaults/paragraphStyles.js +5 -1
  196. package/dist/defaults/paragraphStyles.js.map +1 -1
  197. package/dist/defaults/resourceTypes.d.ts +15 -0
  198. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  199. package/dist/defaults/resourceTypes.js +39 -1
  200. package/dist/defaults/resourceTypes.js.map +1 -1
  201. package/dist/design/layout.d.ts +7 -0
  202. package/dist/design/layout.d.ts.map +1 -1
  203. package/dist/design/layout.js +109 -16
  204. package/dist/design/layout.js.map +1 -1
  205. package/dist/design/slots.d.ts +12 -10
  206. package/dist/design/slots.d.ts.map +1 -1
  207. package/dist/design/slots.js +26 -2
  208. package/dist/design/slots.js.map +1 -1
  209. package/dist/fonts/faces.d.ts.map +1 -1
  210. package/dist/fonts/faces.js +6 -0
  211. package/dist/fonts/faces.js.map +1 -1
  212. package/dist/highlightMarks.d.ts.map +1 -1
  213. package/dist/highlightMarks.js +6 -0
  214. package/dist/highlightMarks.js.map +1 -1
  215. package/dist/html-backend.d.ts.map +1 -1
  216. package/dist/html-backend.js +83 -7
  217. package/dist/html-backend.js.map +1 -1
  218. package/dist/htmlComic.d.ts +3 -0
  219. package/dist/htmlComic.d.ts.map +1 -1
  220. package/dist/htmlComic.js +2 -0
  221. package/dist/htmlComic.js.map +1 -1
  222. package/dist/index.d.ts +12 -8
  223. package/dist/index.d.ts.map +1 -1
  224. package/dist/index.js +7 -3
  225. package/dist/index.js.map +1 -1
  226. package/dist/migrate/markdownRuns.d.ts +70 -0
  227. package/dist/migrate/markdownRuns.d.ts.map +1 -0
  228. package/dist/migrate/markdownRuns.js +215 -0
  229. package/dist/migrate/markdownRuns.js.map +1 -0
  230. package/dist/migrate/pageDesign.d.ts +152 -0
  231. package/dist/migrate/pageDesign.d.ts.map +1 -0
  232. package/dist/migrate/pageDesign.js +573 -0
  233. package/dist/migrate/pageDesign.js.map +1 -0
  234. package/dist/migrate/textToMarkdown.d.ts +156 -0
  235. package/dist/migrate/textToMarkdown.d.ts.map +1 -0
  236. package/dist/migrate/textToMarkdown.js +913 -0
  237. package/dist/migrate/textToMarkdown.js.map +1 -0
  238. package/dist/parse/blockParser.js +26 -0
  239. package/dist/parse/blockParser.js.map +1 -1
  240. package/dist/pipeline/build.d.ts.map +1 -1
  241. package/dist/pipeline/build.js +388 -41
  242. package/dist/pipeline/build.js.map +1 -1
  243. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  244. package/dist/pipeline/buildMeasurement.js +12 -1
  245. package/dist/pipeline/buildMeasurement.js.map +1 -1
  246. package/dist/pipeline/calloutLayout.d.ts +25 -1
  247. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  248. package/dist/pipeline/calloutLayout.js +100 -2
  249. package/dist/pipeline/calloutLayout.js.map +1 -1
  250. package/dist/pipeline/composedLayout.d.ts +16 -2
  251. package/dist/pipeline/composedLayout.d.ts.map +1 -1
  252. package/dist/pipeline/composedLayout.js +337 -70
  253. package/dist/pipeline/composedLayout.js.map +1 -1
  254. package/dist/pipeline/contentWarnings.d.ts +15 -0
  255. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  256. package/dist/pipeline/contentWarnings.js +106 -1
  257. package/dist/pipeline/contentWarnings.js.map +1 -1
  258. package/dist/pipeline/dropCap.d.ts +5 -0
  259. package/dist/pipeline/dropCap.d.ts.map +1 -1
  260. package/dist/pipeline/dropCap.js +23 -2
  261. package/dist/pipeline/dropCap.js.map +1 -1
  262. package/dist/pipeline/frameLayout.d.ts +21 -3
  263. package/dist/pipeline/frameLayout.d.ts.map +1 -1
  264. package/dist/pipeline/frameLayout.js +52 -4
  265. package/dist/pipeline/frameLayout.js.map +1 -1
  266. package/dist/pipeline/headerFooter.d.ts +11 -1
  267. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  268. package/dist/pipeline/headerFooter.js +210 -68
  269. package/dist/pipeline/headerFooter.js.map +1 -1
  270. package/dist/pipeline/measureContentBlock.d.ts +1 -1
  271. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  272. package/dist/pipeline/measureContentBlock.js +4 -1
  273. package/dist/pipeline/measureContentBlock.js.map +1 -1
  274. package/dist/pipeline/mirrorFrame.d.ts.map +1 -1
  275. package/dist/pipeline/mirrorFrame.js +25 -2
  276. package/dist/pipeline/mirrorFrame.js.map +1 -1
  277. package/dist/pipeline/resourceLayout.d.ts +28 -0
  278. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  279. package/dist/pipeline/resourceLayout.js +21 -0
  280. package/dist/pipeline/resourceLayout.js.map +1 -1
  281. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  282. package/dist/pipeline/resourceNumbering.js +13 -4
  283. package/dist/pipeline/resourceNumbering.js.map +1 -1
  284. package/dist/pipeline/styles.d.ts +1 -1
  285. package/dist/pipeline/styles.d.ts.map +1 -1
  286. package/dist/pipeline/styles.js +4 -0
  287. package/dist/pipeline/styles.js.map +1 -1
  288. package/dist/preflight.js +5 -1
  289. package/dist/preflight.js.map +1 -1
  290. package/dist/types.d.ts +45 -2
  291. package/dist/types.d.ts.map +1 -1
  292. package/dist/vdt.d.ts +228 -9
  293. package/dist/vdt.d.ts.map +1 -1
  294. package/dist/vdt.js.map +1 -1
  295. package/dist/word/toDocx.d.ts.map +1 -1
  296. package/dist/word/toDocx.js +14 -0
  297. package/dist/word/toDocx.js.map +1 -1
  298. package/dist/word/word.test.js +11 -1
  299. package/dist/word/word.test.js.map +1 -1
  300. package/package.json +1 -1
@@ -0,0 +1,913 @@
1
+ import { collectConfigWarnings, contentInConfigRule } from '../configWarnings';
2
+ import { resolveAllConfig } from '../pipeline/config';
3
+ import { bitmapLayoutSize } from '../bitmapResolution';
4
+ import { stableStringify } from '../util/stableHash';
5
+ import { scanChapter } from './markdownRuns';
6
+ import { joinLines, planDesign, remapDecorationAnchors, toMm } from './pageDesign';
7
+ const ENCODER = new TextEncoder();
8
+ const bytes = (v) => ENCODER.encode(JSON.stringify(v ?? {})).length;
9
+ function countContent(config, metadata) {
10
+ const ws = collectConfigWarnings({ ...config, checks: { ...config.checks, contentInConfig: { ...config.checks?.contentInConfig, enabled: true } } }, metadata ? { metadata } : {})
11
+ .filter((w) => w.kind === 'contentInConfig');
12
+ return { warnings: ws.length, elements: ws.reduce((n, w) => n + (w.count ?? 0), 0), chars: ws.reduce((n, w) => n + (w.chars ?? 0), 0) };
13
+ }
14
+ const slotOf = (v) => v && typeof v === 'object' && Array.isArray(v.elements) ? v : undefined;
15
+ /** A readable id: a style's part after its last `--` (`tema--p12` → `p12`),
16
+ * free among `taken`. */
17
+ function freeId(base, taken) {
18
+ const clean = base.replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '') || 'page';
19
+ let id = clean;
20
+ for (let n = 2; taken.has(id); n++)
21
+ id = `${clean}-${n}`;
22
+ taken.add(id);
23
+ return id;
24
+ }
25
+ const slug = (s) => s.normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
26
+ const ALIGN = { left: 'left', center: 'center', right: 'right', justify: 'justify', start: 'start', end: 'end' };
27
+ /** The paragraph style of a look and spacing, as the migration writes it. */
28
+ function styleFields(look, gap) {
29
+ return {
30
+ ...(look.fontFamily ? { fontFamily: look.fontFamily } : {}),
31
+ fontSize: look.fontSize,
32
+ lineHeight: { value: look.lineHeight, unit: 'em' },
33
+ ...(look.color ? { color: look.color, boldColor: look.color, italicColor: look.color } : {}),
34
+ textAlign: ALIGN[look.align],
35
+ fontWeight: look.fontWeight,
36
+ boldFontWeight: Math.max(700, look.fontWeight),
37
+ ...(look.italic ? { italic: true } : {}),
38
+ ...(look.uppercase ? { textTransform: 'uppercase' } : {}),
39
+ // The text's tracking, which paragraph styles carry since #907.
40
+ ...(look.letterSpacing ? { letterSpacing: look.letterSpacing } : {}),
41
+ hyphenation: look.hyphenate,
42
+ firstLineIndent: { value: 0, unit: 'mm' },
43
+ spaceBetween: { value: gap, unit: 'mm' },
44
+ marginTop: { value: 0, unit: 'mm' },
45
+ marginBottom: { value: 0, unit: 'mm' },
46
+ snapToGrid: false,
47
+ };
48
+ }
49
+ /** Whether a book's paragraph style sets a look exactly (the fields the
50
+ * migration writes, and nothing that would change the lines). */
51
+ function sameStyle(own, want) {
52
+ const harmless = new Set(['id', 'name']);
53
+ const ownKeys = Object.keys(own).filter((k) => !harmless.has(k));
54
+ const wantKeys = Object.keys(want);
55
+ if (ownKeys.some((k) => !wantKeys.includes(k)))
56
+ return false;
57
+ const norm = (v) => stableStringify(v, new Set(['paletteId', 'model']));
58
+ return wantKeys.every((k) => {
59
+ const a = own[k];
60
+ const b = want[k];
61
+ if (a === undefined) {
62
+ // Unset values the migration only spells out.
63
+ return (k === 'italic' && b === undefined) || (k === 'spaceBetween' && b.value === 0)
64
+ || ((k === 'marginTop' || k === 'marginBottom') && b.value === 0);
65
+ }
66
+ return norm(a) === norm(b);
67
+ });
68
+ }
69
+ /** Run the migration. */
70
+ export function moveTextToMarkdown(input, options = {}) {
71
+ const config = input.config;
72
+ const merge = options.merge !== false;
73
+ const share = options.shareTemplates !== false;
74
+ const highlights = options.highlightsToChips !== false;
75
+ const metadata = input.metadata;
76
+ const verdict = contentInConfigRule(config, metadata ? { metadata } : {});
77
+ const isContent = (c) => verdict(c).content;
78
+ /** Text worth moving once a slot is migrated: any with a letter left
79
+ * after the allowed phrases. */
80
+ const movable = (c) => /\p{L}/u.test(verdict(c).rest);
81
+ const resolved = resolveAllConfig(config);
82
+ const page = { width: toMm(resolved.page.width) ?? 0, height: toMm(resolved.page.height) ?? 0 };
83
+ const before = countContent(config, metadata);
84
+ const bodyMm = toMm(resolved.bodyText.fontSize) ?? 4.2;
85
+ const labelOpt = options.labelsOnPictures;
86
+ const labelMaxMm = !labelOpt ? undefined : (typeof labelOpt === 'object' && labelOpt.maxFontSize ? toMm(labelOpt.maxFontSize, bodyMm) : undefined) ?? bodyMm * 0.75;
87
+ const resourceById = new Map((input.resources ?? []).map((r) => [r.id, r]));
88
+ const pictureSize = (id) => {
89
+ const r = resourceById.get(id);
90
+ if (!r || (r.kind !== 'bitmap' && r.kind !== 'svg'))
91
+ return undefined;
92
+ const dims = r.kind === 'bitmap' ? r.bitmap : r.svg;
93
+ return { width: dims?.width ?? 0, height: dims?.height ?? 0 };
94
+ };
95
+ // 1. Page designs: heading styles whose running heads hold content,
96
+ // grouped by what those running heads draw.
97
+ const styles = Array.isArray(config.headingStyles) ? config.headingStyles : [];
98
+ const designs = new Map();
99
+ const designOfStyle = new Map();
100
+ const left = [];
101
+ styles.forEach((s, i) => {
102
+ if (!s || typeof s !== 'object')
103
+ return;
104
+ const header = slotOf(s.header);
105
+ const footer = slotOf(s.footer);
106
+ const holds = (slot) => !!slot && slot.elements.some((e) => e?.kind === 'text' && typeof e.content === 'string' && isContent(e.content));
107
+ if (!holds(header) && !holds(footer)) {
108
+ if (slotOf(s.advancedDesign?.slot) && holds(slotOf(s.advancedDesign?.slot)))
109
+ left.push({ path: `headingStyles[${i}].advancedDesign.slot`, reason: 'notPage' });
110
+ return;
111
+ }
112
+ const key = stableStringify({ header: header ?? null, footer: footer ?? null });
113
+ let d = designs.get(key);
114
+ if (!d) {
115
+ const headerEls = header?.elements ?? [];
116
+ const headerIds = new Set(headerEls.map((e) => e.id));
117
+ // Footer elements whose id the header already uses are renamed.
118
+ const footerEls = (footer?.elements ?? []).map((e) => (headerIds.has(e.id) ? { ...e, id: `${e.id}-footer` } : e));
119
+ const plan = planDesign({
120
+ key,
121
+ elements: [...headerEls, ...footerEls],
122
+ origins: [...headerEls.map(() => 'header'), ...footerEls.map(() => 'footer')],
123
+ page,
124
+ isContent: movable,
125
+ chipStyles: config.chipStyles ?? [],
126
+ highlightsToChips: highlights,
127
+ merge,
128
+ ...(labelMaxMm !== undefined ? { labelsOnPictures: { maxSizeMm: labelMaxMm, pictureSize } } : {}),
129
+ });
130
+ const roleFiltered = [...headerEls, ...footerEls].some((e) => typeof e.pages === 'string' && e.pages !== 'all');
131
+ d = { key, styleIds: [], ownHeader: !!header, ownFooter: !!footer, plan, pages: 0, ...(roleFiltered ? { roleFiltered } : {}) };
132
+ designs.set(key, d);
133
+ for (const k of plan.kept)
134
+ left.push({ path: `headingStyles[${i}] #${k.id}`, reason: k.reason });
135
+ }
136
+ d.styleIds.push(s.id);
137
+ designOfStyle.set(s.id, d);
138
+ });
139
+ const styleById = new Map(styles.map((s) => [s.id, s]));
140
+ const breakBefore = (h) => {
141
+ const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
142
+ const level = resolved.headings.levels[h.level - 1];
143
+ const own = st?.breakBefore;
144
+ return {
145
+ enabled: own?.enabled ?? level?.breakBefore.enabled ?? h.level === 1,
146
+ parity: own?.parity ?? level?.breakBefore.parity ?? 'any',
147
+ };
148
+ };
149
+ // 2. The runs of each chapter.
150
+ const scans = input.chapters.map((ch) => ({
151
+ chapter: ch,
152
+ scan: scanChapter(ch.markdown, {
153
+ designOf: (id) => designOfStyle.get(id)?.key,
154
+ breaksPage: (h) => breakBefore(h).enabled,
155
+ }),
156
+ }));
157
+ for (const { scan } of scans)
158
+ for (const r of scan.runs)
159
+ designs.get(r.design).pages += r.pages.length;
160
+ const unused = [...designOfStyle.entries()].filter(([, d]) => d.pages === 0).map(([id]) => id);
161
+ const usedDesigns = [...designs.values()].filter((d) => d.pages > 0);
162
+ if (usedDesigns.length === 0 && (unused.length === 0 || options.unusedStyles !== 'remove')) {
163
+ return {
164
+ config,
165
+ chapters: input.chapters.map((c) => ({ ...c })),
166
+ unchanged: true,
167
+ report: emptyReport(before, bytes(config), left, unused),
168
+ ...(input.resources ? { resources: input.resources.map((r) => ({ ...r })) } : {}),
169
+ };
170
+ }
171
+ // 3. Templates.
172
+ const takenTemplates = new Set((config.pageTemplates ?? []).map((t) => t.id));
173
+ const templates = buildTemplates(usedDesigns, share, takenTemplates);
174
+ // A design whose elements show on some page roles only (`pages:
175
+ // 'opener'`) keeps showing them where they showed: its pages' templates
176
+ // report the role each page had (a composed page reports `'composed'`).
177
+ const pageRole = (p) => {
178
+ const h = p.headings[0];
179
+ if (!h || h.line !== p.startLine)
180
+ return 'body';
181
+ const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
182
+ const span = st?.span ?? resolved.headings.levels[h.level - 1]?.span;
183
+ return span === 'page' || breakBefore(h).enabled ? 'opener' : 'body';
184
+ };
185
+ const roleTemplate = new Map();
186
+ const templateFor = (d, p) => {
187
+ if (!d.roleFiltered)
188
+ return d.template;
189
+ const role = pageRole(p);
190
+ const key = `${d.template}\n${role}`;
191
+ const hit = roleTemplate.get(key);
192
+ if (hit)
193
+ return hit;
194
+ const own = templates.list.find((t) => t.id === d.template);
195
+ const sharedBy = usedDesigns.filter((x) => x.template === d.template).length;
196
+ const extended = templates.list.some((t) => t.extends === d.template);
197
+ let id;
198
+ if (own.role === undefined && sharedBy === 1 && !extended && ![...roleTemplate.keys()].some((k) => k.startsWith(`${d.template}\n`))) {
199
+ own.role = role;
200
+ id = own.id;
201
+ }
202
+ else if (own.role === role) {
203
+ id = own.id;
204
+ }
205
+ else {
206
+ id = freeId(`${own.id}-${role}`, takenTemplates);
207
+ templates.list.push({ id, name: `${own.id} (${role})`, extends: own.id, role });
208
+ }
209
+ roleTemplate.set(key, id);
210
+ return id;
211
+ };
212
+ // 4. Paragraph and chip styles.
213
+ const paragraphStyles = (config.paragraphStyles ?? []).slice();
214
+ const takenParagraph = new Set(paragraphStyles.map((p) => p.id));
215
+ const createdParagraph = [];
216
+ const reusedParagraph = new Set();
217
+ const styleCache = new Map();
218
+ const bodyColour = config.bodyText?.color?.hex?.toLowerCase();
219
+ const paragraphStyleFor = (f) => {
220
+ const want = styleFields(f.look, f.gap);
221
+ const key = stableStringify(want);
222
+ const hit = styleCache.get(key);
223
+ if (hit)
224
+ return hit;
225
+ const own = paragraphStyles.find((p) => sameStyle(p, want));
226
+ if (own) {
227
+ reusedParagraph.add(own.id);
228
+ styleCache.set(key, own.id);
229
+ return own.id;
230
+ }
231
+ const size = toMm(f.look.fontSize);
232
+ const pt = size !== undefined ? Math.round((size * 72) / 25.4 * 10) / 10 : undefined;
233
+ const hex = f.look.color?.hex?.toLowerCase();
234
+ const parts = [
235
+ slug(f.look.fontFamily ?? 'text') || 'text',
236
+ pt !== undefined ? String(pt).replace('.', '-') : '',
237
+ f.look.fontWeight >= 600 ? 'bold' : '',
238
+ f.look.italic ? 'italic' : '',
239
+ f.look.align !== 'left' && f.look.align !== 'start' ? f.look.align : '',
240
+ hex && hex !== bodyColour ? hex.replace('#', '') : '',
241
+ ].filter(Boolean);
242
+ const id = freeId(parts.join('-'), takenParagraph);
243
+ const name = [f.look.fontFamily ?? '', pt !== undefined ? `${pt} pt` : '', f.look.fontWeight >= 600 ? 'bold' : '', f.look.italic ? 'italic' : '', f.look.align, hex ?? '']
244
+ .filter(Boolean).join(' ');
245
+ paragraphStyles.push({ id, name, ...want });
246
+ createdParagraph.push(id);
247
+ styleCache.set(key, id);
248
+ return id;
249
+ };
250
+ const chipStyles = (config.chipStyles ?? []).slice();
251
+ const createdChips = [];
252
+ for (const d of usedDesigns) {
253
+ for (const [id, st] of d.plan.newChipStyles) {
254
+ if (chipStyles.some((c) => c.id === id))
255
+ continue;
256
+ chipStyles.push(st);
257
+ createdChips.push(id);
258
+ }
259
+ }
260
+ // 4b. Labels (#918): each label an anchor of its picture's resource, its
261
+ // look a label style.
262
+ const labelStyles = (config.labelStyles ?? []).slice();
263
+ const labels = placeLabels(usedDesigns, input.resources ?? [], labelStyles, resolved.page.dpi);
264
+ // 5. The Markdown.
265
+ const pages = [];
266
+ let headingsDropped = 0;
267
+ const chapters = scans.map(({ chapter, scan }) => {
268
+ const { lines } = scan;
269
+ const lineStart = [];
270
+ for (let i = 0, at = 0; i < lines.length; i++) {
271
+ lineStart[i] = at;
272
+ at += lines[i].length + 1;
273
+ }
274
+ const drop = new Set(scan.dropBreaks);
275
+ const out = [];
276
+ let fence = 0;
277
+ let cursor = 0;
278
+ const runsAt = new Map(scan.runs.map((r) => [r.startLine, r]));
279
+ while (cursor < lines.length) {
280
+ const run = runsAt.get(cursor);
281
+ if (!run) {
282
+ if (!drop.has(cursor))
283
+ out.push(lines[cursor]);
284
+ cursor++;
285
+ continue;
286
+ }
287
+ const d = designs.get(run.design);
288
+ const firstBreak = breakBefore(run.pages[0].headings[0]);
289
+ run.pages.forEach((p, pi) => {
290
+ const side = options.sideOf?.(chapter.id, lineStart[p.startLine]) ?? heuristicSide(firstBreak.parity, pi);
291
+ const written = writePage(d, templateFor(d, p), p, lines, side, styleById, paragraphStyleFor, labels.anchorOf);
292
+ headingsDropped += written.dropped;
293
+ if (out.length > 0 && out[out.length - 1].trim() !== '')
294
+ out.push('');
295
+ // A composed page starts a page but reads no heading's parity: the
296
+ // side the opening heading asked for is a page break's.
297
+ const parity = pi === 0 && firstBreak.enabled ? sideParity(firstBreak.parity) : undefined;
298
+ if (parity)
299
+ out.push(`:::pagebreak{parity="${parity}"}`, '');
300
+ out.push(...written.lines, '');
301
+ pages.push({
302
+ chapterId: chapter.id,
303
+ fence: fence++,
304
+ template: written.template,
305
+ styles: [...new Set(p.headings.map((h) => h.attrs.style).filter((s) => s !== undefined))],
306
+ ...(side ? { side } : {}),
307
+ frames: written.frames,
308
+ elements: written.elements,
309
+ chars: written.chars,
310
+ differences: written.differences,
311
+ });
312
+ });
313
+ cursor = run.endLine + 1;
314
+ // The blank line after the fence is written; skip the source's.
315
+ while (cursor < lines.length && lines[cursor].trim() === '' && out[out.length - 1] === '')
316
+ cursor++;
317
+ }
318
+ return { id: chapter.id, markdown: out.join('\n') };
319
+ });
320
+ // 6. Heading styles: the running heads are gone; styles left the same
321
+ // are merged; unused ones go when asked.
322
+ const migrated = new Set(usedDesigns.flatMap((d) => d.styleIds));
323
+ const removeUnused = options.unusedStyles === 'remove';
324
+ const removed = [];
325
+ const stripped = [];
326
+ for (const s of styles) {
327
+ if (removeUnused && unused.includes(s.id)) {
328
+ removed.push(s.id);
329
+ continue;
330
+ }
331
+ if (!migrated.has(s.id)) {
332
+ stripped.push(s);
333
+ continue;
334
+ }
335
+ const d = designOfStyle.get(s.id);
336
+ const copy = { ...s };
337
+ if (d.ownHeader)
338
+ delete copy.header;
339
+ if (d.ownFooter)
340
+ delete copy.footer;
341
+ stripped.push(copy);
342
+ }
343
+ const { styles: mergedStyles, renames, merged } = mergeStyles(stripped, migrated);
344
+ const finalChapters = chapters.map((c) => ({ ...c, markdown: renameStyles(c.markdown, renames) }));
345
+ for (const p of pages)
346
+ p.styles = p.styles.map((s) => renames.get(s) ?? s);
347
+ const next = {
348
+ ...config,
349
+ headingStyles: mergedStyles,
350
+ paragraphStyles,
351
+ ...(chipStyles.length > 0 ? { chipStyles } : {}),
352
+ ...(labelStyles.length > 0 ? { labelStyles } : {}),
353
+ pageTemplates: [...(config.pageTemplates ?? []), ...templates.list],
354
+ };
355
+ for (const id of unused)
356
+ if (!removeUnused)
357
+ left.push({ path: `headingStyles[id=${id}]`, reason: 'unused' });
358
+ const report = {
359
+ pages,
360
+ templates: templates.list.map((t) => ({
361
+ id: t.id,
362
+ ...(t.extends ? { extends: t.extends } : {}),
363
+ frames: (t.frames?.length ?? 0) + (t.variants?.recto?.frames?.length ?? 0) + (t.variants?.verso?.frames?.length ?? 0),
364
+ pages: pages.filter((p) => p.template === t.id).length,
365
+ })),
366
+ movedElements: usedDesigns.reduce((n, d) => n + d.plan.movedElements, 0),
367
+ movedChars: usedDesigns.reduce((n, d) => n + d.plan.movedChars, 0),
368
+ chips: usedDesigns.reduce((n, d) => n + d.plan.chips, 0),
369
+ frames: usedDesigns.reduce((n, d) => n + d.plan.frames.length, 0),
370
+ paragraphStylesCreated: createdParagraph,
371
+ paragraphStylesReused: [...reusedParagraph],
372
+ chipStylesCreated: createdChips,
373
+ headingStylesMerged: merged,
374
+ headingStylesRemoved: removed,
375
+ unusedStyles: unused,
376
+ headingsDropped,
377
+ left,
378
+ configBytesBefore: bytes(config),
379
+ configBytesAfter: bytes(next),
380
+ contentInConfigBefore: before,
381
+ contentInConfigAfter: countContent(next, metadata),
382
+ labels: labels.report,
383
+ };
384
+ return {
385
+ config: next, chapters: finalChapters, report, unchanged: false,
386
+ ...(input.resources ? { resources: labels.resources } : {}),
387
+ };
388
+ }
389
+ function emptyReport(before, size, left, unused) {
390
+ return {
391
+ pages: [], templates: [], movedElements: 0, movedChars: 0, chips: 0, frames: 0,
392
+ paragraphStylesCreated: [], paragraphStylesReused: [], chipStylesCreated: [],
393
+ headingStylesMerged: [], headingStylesRemoved: [], unusedStyles: unused, headingsDropped: 0, left,
394
+ configBytesBefore: size, configBytesAfter: size, contentInConfigBefore: before, contentInConfigAfter: before,
395
+ labels: { pictures: 0, labels: 0, anchors: 0, labelStylesCreated: [], labelStylesReused: [], resolutionSet: [] },
396
+ };
397
+ }
398
+ /** The page break parity of a heading's `breakBefore.parity`. */
399
+ function sideParity(parity) {
400
+ return parity === 'even' || parity === 'always-even' ? 'even' : parity === 'odd' || parity === 'always-odd' ? 'odd' : undefined;
401
+ }
402
+ /** The side of a run's `index`-th page from its opening heading's parity. */
403
+ function heuristicSide(parity, index) {
404
+ const first = parity === 'even' || parity === 'always-even' ? 'verso' : parity === 'odd' || parity === 'always-odd' ? 'recto' : undefined;
405
+ if (!first)
406
+ return undefined;
407
+ return index % 2 === 0 ? first : first === 'recto' ? 'verso' : 'recto';
408
+ }
409
+ /** Attributes of the opening heading the page fence carries: those the
410
+ * template reads (`{attr.x}`). */
411
+ const FENCE_RESERVED = new Set(['style', 'id', 'toc', 'hidden', 'numbered', 'slot', 'template', 'spread', 'background']);
412
+ function writePage(d, template, p, lines, side, styleById, paragraphStyleFor, anchorOf) {
413
+ const differences = new Set();
414
+ const attrs = [`template="${template}"`];
415
+ if (d.plan.background)
416
+ attrs.push(`background="${d.plan.background.replace(/["\\]/g, '')}"`);
417
+ const opening = p.headings[0];
418
+ for (const name of d.attrNames ?? []) {
419
+ if (FENCE_RESERVED.has(name))
420
+ continue;
421
+ const token = opening?.attrTokens[name];
422
+ if (token !== undefined)
423
+ attrs.push(token);
424
+ }
425
+ // The page's own lines, without the hidden headings that only switched
426
+ // the running head (no id, nothing in the contents, no words).
427
+ const dropLines = new Set();
428
+ for (const h of p.headings) {
429
+ const st = h.attrs.style !== undefined ? styleById.get(h.attrs.style) : undefined;
430
+ if (h.attrs.id === undefined && st?.hidden === true && st.toc === false && !/[\p{L}\p{N}]/u.test(h.title))
431
+ dropLines.add(h.line);
432
+ }
433
+ const body = [];
434
+ for (let i = p.startLine; i <= p.endLine; i++)
435
+ if (!dropLines.has(i))
436
+ body.push(lines[i]);
437
+ while (body.length > 0 && body[body.length - 1].trim() === '')
438
+ body.pop();
439
+ while (body.length > 0 && body[0].trim() === '')
440
+ body.shift();
441
+ const flowText = body.some((l) => l.trim() !== '' && !/^#{1,6}[ \t]/.test(l.trim()));
442
+ if (flowText)
443
+ differences.add('flow');
444
+ const out = [`::::page{${attrs.join(' ')}}`, ...body];
445
+ let frames = 0;
446
+ let elements = 0;
447
+ let chars = 0;
448
+ for (const f of d.plan.frames) {
449
+ if (f.side && side && f.side !== side)
450
+ continue;
451
+ if (f.side && !side)
452
+ differences.add('side');
453
+ frames++;
454
+ if (f.picture) {
455
+ // A picture with its labels (#918): the figure in its frame.
456
+ const pic = f.picture;
457
+ out.push('', `:::figure{resource="${pic.element.resourceId.replace(/["\\]/g, '')}" slot="${f.name}"}`);
458
+ for (const l of pic.labels) {
459
+ out.push(`::label[${l.lines[0]}]{at="${anchorOf(d, l)}"}`);
460
+ elements++;
461
+ chars += l.chars;
462
+ }
463
+ out.push(':::');
464
+ for (const n of pic.notes)
465
+ differences.add(n);
466
+ continue;
467
+ }
468
+ out.push('', `:::frame{slot="${f.name}"}`, `:::paragraphs{style="${paragraphStyleFor(f)}"}`);
469
+ f.pieces.forEach((piece, i) => {
470
+ if (i > 0)
471
+ out.push('');
472
+ out.push(joinLines(piece.lines));
473
+ elements++;
474
+ chars += piece.chars;
475
+ for (const n of piece.notes)
476
+ differences.add(n);
477
+ });
478
+ out.push(':::', ':::');
479
+ }
480
+ if (d.pages > 1 && d.plan.frames.some((f) => !f.side))
481
+ differences.add('repeated');
482
+ out.push('::::');
483
+ return { template, lines: out, frames, elements, chars, differences: [...differences].sort(), dropped: dropLines.size };
484
+ }
485
+ /** A frame of the template, from a planned one. */
486
+ function frameConfig(f) {
487
+ if (f.picture) {
488
+ // The picture's box, over the decoration and under the text frames;
489
+ // the picture keeps its size.
490
+ const pic = f.picture;
491
+ return {
492
+ name: f.name,
493
+ placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: round3(pic.x), unit: 'mm' }, y: { value: round3(pic.y), unit: 'mm' } } },
494
+ width: { value: round3(pic.width), unit: 'mm' },
495
+ height: { value: round3(pic.height), unit: 'mm' },
496
+ layer: 0,
497
+ fit: { scaleImages: false },
498
+ };
499
+ }
500
+ const first = f.pieces[0];
501
+ const el = first.element;
502
+ const placement = first.x !== undefined && first.y !== undefined
503
+ ? { anchor: { to: 'page', edge: 'top-left' }, offset: { x: { value: round3(first.x), unit: 'mm' }, y: { value: round3(first.y), unit: 'mm' } } }
504
+ : { anchor: el.placement.anchor, ...(el.placement.offset ? { offset: el.placement.offset } : {}) };
505
+ const width = f.width !== undefined ? { value: round3(f.width), unit: 'mm' }
506
+ : typeof el.placement.size?.width === 'object' && 'value' in el.placement.size.width ? el.placement.size.width : undefined;
507
+ return {
508
+ name: f.name,
509
+ placement,
510
+ ...(width ? { width } : {}),
511
+ ...(f.look.direction ? { direction: f.look.direction } : {}),
512
+ ...(f.look.writingMode ? { writingMode: f.look.writingMode } : {}),
513
+ };
514
+ }
515
+ const round3 = (v) => Math.round(v * 1000) / 1000;
516
+ /** The frames of a template, those of one side in its variant. */
517
+ function framesPatch(frames) {
518
+ const own = frames.filter((f) => !f.side).map(frameConfig);
519
+ const recto = frames.filter((f) => f.side === 'recto').map(frameConfig);
520
+ const verso = frames.filter((f) => f.side === 'verso').map(frameConfig);
521
+ const variants = {};
522
+ if (recto.length > 0)
523
+ variants.recto = { frames: recto };
524
+ if (verso.length > 0)
525
+ variants.verso = { frames: verso };
526
+ return { frames: own, ...(recto.length > 0 || verso.length > 0 ? { variants } : {}) };
527
+ }
528
+ /** The running text's frame: the content area, under the decoration as
529
+ * the running text was under the running head that drew the page. */
530
+ const FLOW_FRAME = { name: 'flow', flow: true, optional: true, layer: -1 };
531
+ /** The running heads a design's template sets: its styles' own (what
532
+ * stayed in them), hidden when nothing did; unset when the styles had
533
+ * none of their own. */
534
+ function runningHeads(d) {
535
+ return {
536
+ ...(d.ownHeader ? { header: d.plan.header.length > 0 ? { elements: d.plan.header } : false } : {}),
537
+ ...(d.ownFooter ? { footer: d.plan.footer.length > 0 ? { elements: d.plan.footer } : false } : {}),
538
+ };
539
+ }
540
+ /** `{attr.x}` names a template's decoration and running heads read. */
541
+ function attrNamesOf(t) {
542
+ const names = new Set();
543
+ const text = stableStringify(t);
544
+ for (const m of text.matchAll(/\{attr\.([\w-]+)\}/g))
545
+ names.add(m[1]);
546
+ return names;
547
+ }
548
+ /** Templates for the designs: one each, or (sharing) a base per family of
549
+ * designs whose frames stand in the same places, with the pages' own
550
+ * templates extending it. Names frames on the way. */
551
+ function buildTemplates(designs, share, taken) {
552
+ const list = [];
553
+ const idFor = (d) => {
554
+ const first = d.styleIds[0];
555
+ const cut = first.lastIndexOf('--');
556
+ return freeId(cut >= 0 ? first.slice(cut + 2) : first, taken);
557
+ };
558
+ // Keys of frames that more than one design has.
559
+ const occurrences = new Map();
560
+ const keysOf = (d) => {
561
+ const seen = new Map();
562
+ return d.plan.frames.map((f) => {
563
+ const n = (seen.get(f.key) ?? 0) + 1;
564
+ seen.set(f.key, n);
565
+ return `${f.key}#${n}`;
566
+ });
567
+ };
568
+ const designKeys = new Map(designs.map((d) => [d, keysOf(d)]));
569
+ for (const d of designs)
570
+ for (const k of designKeys.get(d)) {
571
+ const set = occurrences.get(k) ?? new Set();
572
+ set.add(d.key);
573
+ occurrences.set(k, set);
574
+ }
575
+ const families = new Map();
576
+ for (const d of designs) {
577
+ const common = share ? designKeys.get(d).filter((k) => occurrences.get(k).size > 1).sort().join('\n') : '';
578
+ const fam = common ? `${common}\n${stableStringify(runningHeads(d))}` : `solo:${d.key}`;
579
+ const arr = families.get(fam) ?? [];
580
+ arr.push(d);
581
+ families.set(fam, arr);
582
+ }
583
+ let baseCount = 0;
584
+ const childBySpec = new Map();
585
+ for (const [fam, members] of families) {
586
+ if (fam.startsWith('solo:') || members.length < 2) {
587
+ for (const d of members) {
588
+ let n = 0;
589
+ let pics = 0;
590
+ for (const f of d.plan.frames.slice().sort(byPosition))
591
+ f.name = f.picture ? `picture-${++pics}` : `text-${++n}`;
592
+ const id = idFor(d);
593
+ const decoration = remapDecorationAnchors(d.plan.decoration, d.plan.movedTo, d.plan.frames);
594
+ const { frames, variants } = framesPatch(d.plan.frames);
595
+ const t = {
596
+ id,
597
+ name: `${id} (${d.styleIds.join(', ')})`,
598
+ ...runningHeads(d),
599
+ decoration: { elements: decoration },
600
+ frames: [FLOW_FRAME, ...frames],
601
+ ...(variants ? { variants } : {}),
602
+ };
603
+ list.push(t);
604
+ d.template = id;
605
+ d.attrNames = attrNamesOf([t]);
606
+ }
607
+ continue;
608
+ }
609
+ // A base with the shared frames and the decoration every member has.
610
+ const sharedKeys = new Set(fam.split('\n').slice(0, -1));
611
+ const first = members[0];
612
+ const firstKeys = designKeys.get(first);
613
+ const baseFrames = first.plan.frames.filter((_, i) => sharedKeys.has(firstKeys[i])).slice().sort(byPosition);
614
+ const nameOfKey = new Map();
615
+ baseFrames.forEach((f, i) => nameOfKey.set(firstKeys[first.plan.frames.indexOf(f)], `shared-${i + 1}`));
616
+ for (const d of members) {
617
+ const keys = designKeys.get(d);
618
+ d.plan.frames.forEach((f, i) => {
619
+ const shared = nameOfKey.get(keys[i]);
620
+ if (shared)
621
+ f.name = shared;
622
+ });
623
+ let n = 0;
624
+ let pics = 0;
625
+ for (const f of d.plan.frames.slice().sort(byPosition))
626
+ if (!f.name)
627
+ f.name = f.picture ? `picture-${++pics}` : `text-${++n}`;
628
+ }
629
+ // Decoration every member draws (ids aside): the base's, under the
630
+ // first member's ids.
631
+ const NO_ID = new Set(['id']);
632
+ const decos = members.map((d) => remapDecorationAnchors(d.plan.decoration, d.plan.movedTo, d.plan.frames));
633
+ const decoStrings = decos.map((list) => list.map((e) => stableStringify(e, NO_ID)));
634
+ const countOf = (list) => list.reduce((m, k) => m.set(k, (m.get(k) ?? 0) + 1), new Map());
635
+ const counts = decoStrings.map(countOf);
636
+ const shared = new Map();
637
+ for (const [k, n] of counts[0])
638
+ shared.set(k, Math.min(n, ...counts.slice(1).map((c) => c.get(k) ?? 0)));
639
+ const commonIdx = decoStrings.map((list) => {
640
+ const left = new Map(shared);
641
+ const picked = new Set();
642
+ list.forEach((k, i) => {
643
+ const n = left.get(k) ?? 0;
644
+ if (n > 0) {
645
+ picked.add(i);
646
+ left.set(k, n - 1);
647
+ }
648
+ });
649
+ return picked;
650
+ });
651
+ const common = decos[0].filter((_, i) => commonIdx[0].has(i));
652
+ const baseId = freeId(baseCount++ === 0 ? 'page-base' : `page-base-${baseCount}`, taken);
653
+ const baseFramesCfg = framesPatch(baseFrames);
654
+ const base = {
655
+ id: baseId,
656
+ name: `Shared frames (${members.length} pages)`,
657
+ ...runningHeads(first),
658
+ decoration: { elements: common },
659
+ frames: [FLOW_FRAME, ...baseFramesCfg.frames],
660
+ ...(baseFramesCfg.variants ? { variants: baseFramesCfg.variants } : {}),
661
+ };
662
+ list.push(base);
663
+ members.forEach((d, mi) => {
664
+ const keys = designKeys.get(d);
665
+ const own = d.plan.frames.filter((_, i) => !sharedKeys.has(keys[i]));
666
+ // A shared element goes by the base's id: anchors to it follow.
667
+ const renamed = new Map();
668
+ const firstShared = decos[0].filter((_, i) => commonIdx[0].has(i));
669
+ decos[mi].filter((_, i) => commonIdx[mi].has(i)).forEach((e, j) => { if (firstShared[j] && firstShared[j].id !== e.id)
670
+ renamed.set(e.id, firstShared[j].id); });
671
+ const ownDeco = decos[mi].filter((_, i) => !commonIdx[mi].has(i)).map((e) => {
672
+ const to = e.placement?.anchor?.to;
673
+ const target = typeof to === 'string' && to.startsWith('#') ? renamed.get(to.slice(1)) : undefined;
674
+ return target ? { ...e, placement: { ...e.placement, anchor: { ...e.placement.anchor, to: `#${target}` } } } : e;
675
+ });
676
+ if (own.length === 0 && ownDeco.length === 0) {
677
+ d.template = baseId;
678
+ d.attrNames = attrNamesOf([base]);
679
+ return;
680
+ }
681
+ const patch = framesPatch(own);
682
+ const spec = stableStringify({ patch, ownDeco, own: own.map((f) => f.key) });
683
+ const known = childBySpec.get(spec);
684
+ if (known) {
685
+ d.template = known;
686
+ d.attrNames = attrNamesOf([base, list.find((t) => t.id === known)]);
687
+ return;
688
+ }
689
+ const id = idFor(d);
690
+ const child = {
691
+ id,
692
+ name: `${id} (${d.styleIds.join(', ')})`,
693
+ extends: baseId,
694
+ ...(ownDeco.length > 0 ? { decoration: { elements: ownDeco } } : {}),
695
+ ...(patch.frames.length > 0 ? { frames: patch.frames } : {}),
696
+ ...(patch.variants ? { variants: patch.variants } : {}),
697
+ };
698
+ list.push(child);
699
+ childBySpec.set(spec, id);
700
+ d.template = id;
701
+ d.attrNames = attrNamesOf([base, child]);
702
+ });
703
+ }
704
+ return { list };
705
+ }
706
+ function byPosition(a, b) {
707
+ const at = (f) => (f.picture ? { x: f.picture.x, y: f.picture.y } : { x: f.pieces[0].x ?? 0, y: f.pieces[0].y ?? Infinity });
708
+ const pa = at(a);
709
+ const pb = at(b);
710
+ return pa.y - pb.y || pa.x - pb.x;
711
+ }
712
+ /** Migrated heading styles that came out the same are one: the id their
713
+ * ids share before `--` when it is free (`tema--p04`, `tema--p05` →
714
+ * `tema`), else the first one's. */
715
+ function mergeStyles(styles, migrated) {
716
+ const groups = new Map();
717
+ for (const s of styles) {
718
+ if (!migrated.has(s.id))
719
+ continue;
720
+ const prefix = s.id.includes('--') ? s.id.slice(0, s.id.lastIndexOf('--')) : s.id;
721
+ const key = `${prefix}\n${stableStringify(s, new Set(['id', 'name']))}`;
722
+ const g = groups.get(key) ?? [];
723
+ g.push(s);
724
+ groups.set(key, g);
725
+ }
726
+ const ids = new Set(styles.map((s) => s.id));
727
+ const renames = new Map();
728
+ const merged = [];
729
+ const keepAs = new Map();
730
+ for (const g of groups.values()) {
731
+ if (g.length < 2)
732
+ continue;
733
+ const prefixes = new Set(g.map((s) => (s.id.includes('--') ? s.id.slice(0, s.id.lastIndexOf('--')) : null)));
734
+ const prefix = prefixes.size === 1 ? [...prefixes][0] : null;
735
+ const into = prefix && (!ids.has(prefix) || g.some((s) => s.id === prefix)) ? prefix : g[0].id;
736
+ ids.add(into);
737
+ const kept = { ...g[0], id: into, name: into === g[0].id ? (g[0].name ?? into) : into };
738
+ keepAs.set(g[0].id, kept);
739
+ const from = g.map((s) => s.id).filter((id) => id !== into);
740
+ for (const s of g)
741
+ if (s.id !== into)
742
+ renames.set(s.id, into);
743
+ merged.push({ into, from });
744
+ }
745
+ const grouped = new Set(merged.flatMap((m) => [m.into, ...m.from]));
746
+ const out = [];
747
+ for (const s of styles) {
748
+ const kept = keepAs.get(s.id);
749
+ if (kept)
750
+ out.push(kept);
751
+ else if (!grouped.has(s.id))
752
+ out.push(s);
753
+ }
754
+ return { styles: out, renames, merged };
755
+ }
756
+ /** `style="old"` on a heading line names the merged style. */
757
+ function renameStyles(markdown, renames) {
758
+ if (renames.size === 0)
759
+ return markdown;
760
+ return markdown.split('\n').map((line) => {
761
+ if (!/^#{1,6}[ \t]/.test(line) || !line.trimEnd().endsWith('}'))
762
+ return line;
763
+ return line.replace(/(\bstyle\s*=\s*)("([^"]*)"|'([^']*)'|([^\s}"']+))/, (m, pre, _v, a, b, c) => {
764
+ const id = a ?? b ?? c ?? '';
765
+ const to = renames.get(id);
766
+ return to ? `${pre}"${to}"` : m;
767
+ });
768
+ }).join('\n');
769
+ }
770
+ /** The pitch of a label's lines in ems when its style lends none
771
+ * (composed/labels.ts `LABEL_LINE_HEIGHT`). */
772
+ const LABEL_PITCH = 1.15;
773
+ /** Where a line's baseline stands in its line box, from the top: design
774
+ * texts and labels alike (`lineHeight × 0.8`). */
775
+ const BASELINE = 0.8;
776
+ const round5 = (v) => Math.round(v * 1e5) / 1e5;
777
+ /** The label style of a design text's look: the book's own when one sets
778
+ * exactly that, else a new one. */
779
+ function labelStyleFields(piece) {
780
+ const el = piece.element;
781
+ const sizeMm = toMm(el.fontSize) ?? 4.2;
782
+ const tracking = el.letterSpacing ? (toMm(el.letterSpacing, sizeMm) ?? 0) / sizeMm : 0;
783
+ const stroke = el.stroke && !el.stroke.hollow && (toMm(el.stroke.width, sizeMm) ?? 0) > 0 ? el.stroke : undefined;
784
+ return {
785
+ ...(piece.look.fontFamily ? { fontFamily: piece.look.fontFamily } : {}),
786
+ fontSize: piece.look.fontSize,
787
+ fontWeight: piece.look.fontWeight,
788
+ ...(piece.look.italic ? { fontStyle: 'italic' } : {}),
789
+ ...(piece.look.color ? { color: piece.look.color } : {}),
790
+ ...(piece.look.uppercase ? { textTransform: 'uppercase' } : {}),
791
+ ...(Math.abs(tracking) > 1e-6 ? { letterSpacing: round5(tracking) } : {}),
792
+ ...(stroke ? { halo: { color: stroke.color ?? piece.look.color ?? { hex: '#ffffff', model: 'hex' }, width: stroke.width } } : {}),
793
+ };
794
+ }
795
+ /**
796
+ * The anchors of the labels (#918): one per label on its picture's
797
+ * resource, at the point the design text stood (its alignment's side,
798
+ * its first baseline where the design set it), as wide as its box, with a
799
+ * label style of its look. A picture shown on several pages shares its
800
+ * anchors: the same text at the same place is one anchor.
801
+ */
802
+ function placeLabels(designs, resources, labelStyles, dpi) {
803
+ const out = resources.map((r) => ({ ...r }));
804
+ const byId = new Map(out.map((r) => [r.id, r]));
805
+ const ids = new Map();
806
+ const created = [];
807
+ const reused = new Set();
808
+ const takenStyles = new Set(labelStyles.map((st) => st.id));
809
+ const styleCache = new Map();
810
+ const NO_ID = new Set(['id', 'name', 'paletteId', 'model']);
811
+ const styleFor = (piece) => {
812
+ const want = labelStyleFields(piece);
813
+ const key = stableStringify(want, NO_ID);
814
+ const hit = styleCache.get(key);
815
+ if (hit)
816
+ return hit;
817
+ const own = labelStyles.find((st) => stableStringify(st, NO_ID) === key);
818
+ if (own) {
819
+ reused.add(own.id);
820
+ styleCache.set(key, own.id);
821
+ return own.id;
822
+ }
823
+ const size = toMm(piece.look.fontSize);
824
+ const pt = size !== undefined ? Math.round((size * 72) / 25.4 * 10) / 10 : undefined;
825
+ const hex = piece.look.color?.hex?.toLowerCase();
826
+ const parts = ['label', slug(piece.look.fontFamily ?? '') || '', pt !== undefined ? String(pt).replace('.', '-') : '', piece.look.fontWeight >= 600 ? 'bold' : '', piece.look.italic ? 'italic' : '', hex ? hex.replace('#', '') : ''].filter(Boolean);
827
+ const id = freeId(parts.join('-'), takenStyles);
828
+ labelStyles.push({ id, name: ['Label', piece.look.fontFamily ?? '', pt !== undefined ? `${pt} pt` : '', hex ?? ''].filter(Boolean).join(' '), ...want });
829
+ created.push(id);
830
+ styleCache.set(key, id);
831
+ return id;
832
+ };
833
+ let pictures = 0;
834
+ let labels = 0;
835
+ let anchors = 0;
836
+ /** The widest box each bitmap is drawn at, mm. */
837
+ const widest = new Map();
838
+ for (const d of designs) {
839
+ for (const f of d.plan.frames) {
840
+ const pic = f.picture;
841
+ if (!pic)
842
+ continue;
843
+ const res = byId.get(pic.element.resourceId);
844
+ if (!res)
845
+ continue;
846
+ pictures++;
847
+ widest.set(res.id, Math.max(widest.get(res.id) ?? 0, pic.width));
848
+ const list = res.anchors ? res.anchors.slice() : [];
849
+ for (const l of pic.labels) {
850
+ labels++;
851
+ const el = l.element;
852
+ const sizeMm = toMm(el.fontSize) ?? 4.2;
853
+ const width = l.width ?? 0;
854
+ const rtl = l.look.direction === 'rtl';
855
+ const a = l.look.align;
856
+ const side = a === 'center' ? 'center' : a === 'right' || (a === 'end' && !rtl) || (a === 'start' && rtl) ? 'right' : 'left';
857
+ const px = side === 'center' ? l.x + width / 2 : side === 'right' ? l.x + width : l.x;
858
+ // The design text's first baseline, where the label's goes.
859
+ const h = el.placement?.size?.height;
860
+ // A height sized to another box's edge (#928) states no length here.
861
+ const boxH = toMm(typeof h === 'object' && 'value' in h ? h : undefined);
862
+ const textH = l.lines.length * l.lineMm;
863
+ const va = el.verticalAlign;
864
+ const top = l.y + (boxH !== undefined && boxH > textH ? (va === 'middle' ? (boxH - textH) / 2 : va === 'bottom' ? boxH - textH : 0) : 0);
865
+ const baseline = top + BASELINE * l.lineMm;
866
+ const labelTop = baseline - BASELINE * LABEL_PITCH * sizeMm;
867
+ const anchor = {
868
+ id: '',
869
+ x: round5((px - pic.x) / pic.width),
870
+ y: round5((labelTop - pic.y) / pic.height),
871
+ ...(width > 0 ? { width: round5(width / pic.width) } : {}),
872
+ align: side === 'center' ? 'center' : (side === 'left') !== rtl ? 'start' : 'end',
873
+ verticalAlign: 'top',
874
+ style: styleFor(l),
875
+ };
876
+ const plain = l.lines[0].replace(/\\(.)/g, '$1').replace(/\*\*|[*_^~]/g, '');
877
+ const base = slug(plain) || 'label';
878
+ const same = (b) => Math.abs(b.x - anchor.x) < 1e-4 && Math.abs(b.y - anchor.y) < 1e-4 && b.width === anchor.width && b.align === anchor.align && b.style === anchor.style;
879
+ let id = list.find((b) => (b.id === base || b.id.startsWith(`${base}-`)) && same(b))?.id;
880
+ if (!id) {
881
+ id = base;
882
+ for (let n = 2; list.some((b) => b.id === id); n++)
883
+ id = `${base}-${n}`;
884
+ list.push({ ...anchor, id });
885
+ anchors++;
886
+ }
887
+ ids.set(`${d.key}\n${l.id}`, id);
888
+ }
889
+ res.anchors = list;
890
+ }
891
+ }
892
+ // A bitmap is set at its print size and never enlarged, where the design
893
+ // stretched it over its box: one smaller than its box takes the
894
+ // resolution that makes it that wide.
895
+ const resolutionSet = [];
896
+ for (const [id, widthMm] of widest) {
897
+ const res = byId.get(id);
898
+ if (res.kind !== 'bitmap' || !res.bitmap || !(res.bitmap.width > 0) || !(widthMm > 0))
899
+ continue;
900
+ const natural = bitmapLayoutSize(res.bitmap, dpi).width;
901
+ const wantPx = (widthMm / 25.4) * dpi;
902
+ if (natural >= wantPx - 0.5)
903
+ continue;
904
+ res.bitmap = { ...res.bitmap, resolution: round5((res.bitmap.width * 25.4) / widthMm) };
905
+ resolutionSet.push(id);
906
+ }
907
+ return {
908
+ anchorOf: (d, piece) => ids.get(`${d.key}\n${piece.id}`) ?? '',
909
+ resources: out,
910
+ report: { pictures, labels, anchors, labelStylesCreated: created, labelStylesReused: [...reused], resolutionSet },
911
+ };
912
+ }
913
+ //# sourceMappingURL=textToMarkdown.js.map