postext 1.22.2 → 1.23.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 (288) hide show
  1. package/dist/__tests__/bundle.test.js +1 -1
  2. package/dist/__tests__/codeBlocks.test.d.ts +2 -0
  3. package/dist/__tests__/codeBlocks.test.d.ts.map +1 -0
  4. package/dist/__tests__/codeBlocks.test.js +550 -0
  5. package/dist/__tests__/codeBlocks.test.js.map +1 -0
  6. package/dist/__tests__/exports.test.js +19 -0
  7. package/dist/__tests__/exports.test.js.map +1 -1
  8. package/dist/__tests__/hardBreaks.test.d.ts +2 -0
  9. package/dist/__tests__/hardBreaks.test.d.ts.map +1 -0
  10. package/dist/__tests__/hardBreaks.test.js +226 -0
  11. package/dist/__tests__/hardBreaks.test.js.map +1 -0
  12. package/dist/__tests__/headingInlineMarks.test.js +1 -1
  13. package/dist/__tests__/headingLetterSpacing.test.js +1 -1
  14. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
  15. package/dist/__tests__/lineNumbers.test.d.ts +2 -0
  16. package/dist/__tests__/lineNumbers.test.d.ts.map +1 -0
  17. package/dist/__tests__/lineNumbers.test.js +304 -0
  18. package/dist/__tests__/lineNumbers.test.js.map +1 -0
  19. package/dist/__tests__/mathSizeMigration.test.js +2 -2
  20. package/dist/__tests__/paragraphDropCap.test.d.ts +2 -0
  21. package/dist/__tests__/paragraphDropCap.test.d.ts.map +1 -0
  22. package/dist/__tests__/paragraphDropCap.test.js +442 -0
  23. package/dist/__tests__/paragraphDropCap.test.js.map +1 -0
  24. package/dist/__tests__/paragraphStyleIndent.test.js +26 -2
  25. package/dist/__tests__/paragraphStyleIndent.test.js.map +1 -1
  26. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
  27. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
  28. package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
  29. package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
  30. package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
  31. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
  32. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
  33. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
  34. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +7 -1
  35. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  36. package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
  37. package/dist/__tests__/tabStops.test.d.ts +2 -0
  38. package/dist/__tests__/tabStops.test.d.ts.map +1 -0
  39. package/dist/__tests__/tabStops.test.js +394 -0
  40. package/dist/__tests__/tabStops.test.js.map +1 -0
  41. package/dist/__tests__/verseLines.test.d.ts +2 -0
  42. package/dist/__tests__/verseLines.test.d.ts.map +1 -0
  43. package/dist/__tests__/verseLines.test.js +375 -0
  44. package/dist/__tests__/verseLines.test.js.map +1 -0
  45. package/dist/bundle/configVersion.d.ts +77 -7
  46. package/dist/bundle/configVersion.d.ts.map +1 -1
  47. package/dist/bundle/configVersion.js +239 -5
  48. package/dist/bundle/configVersion.js.map +1 -1
  49. package/dist/bundle/index.d.ts +1 -1
  50. package/dist/bundle/index.d.ts.map +1 -1
  51. package/dist/bundle/index.js +1 -1
  52. package/dist/bundle/index.js.map +1 -1
  53. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  54. package/dist/canvas-backend/blockRender.js +39 -5
  55. package/dist/canvas-backend/blockRender.js.map +1 -1
  56. package/dist/canvas-backend/index.d.ts.map +1 -1
  57. package/dist/canvas-backend/index.js +3 -0
  58. package/dist/canvas-backend/index.js.map +1 -1
  59. package/dist/cjkMarks.js +1 -1
  60. package/dist/cjkMarks.js.map +1 -1
  61. package/dist/code/highlight.d.ts +34 -0
  62. package/dist/code/highlight.d.ts.map +1 -0
  63. package/dist/code/highlight.js +406 -0
  64. package/dist/code/highlight.js.map +1 -0
  65. package/dist/configWarnings.d.ts.map +1 -1
  66. package/dist/configWarnings.js +192 -3
  67. package/dist/configWarnings.js.map +1 -1
  68. package/dist/defaults/bodyText.d.ts.map +1 -1
  69. package/dist/defaults/bodyText.js +25 -0
  70. package/dist/defaults/bodyText.js.map +1 -1
  71. package/dist/defaults/calloutStyles.d.ts.map +1 -1
  72. package/dist/defaults/calloutStyles.js +7 -0
  73. package/dist/defaults/calloutStyles.js.map +1 -1
  74. package/dist/defaults/codeStyle.d.ts +29 -0
  75. package/dist/defaults/codeStyle.d.ts.map +1 -0
  76. package/dist/defaults/codeStyle.js +284 -0
  77. package/dist/defaults/codeStyle.js.map +1 -0
  78. package/dist/defaults/dropCap.d.ts +49 -0
  79. package/dist/defaults/dropCap.d.ts.map +1 -0
  80. package/dist/defaults/dropCap.js +66 -0
  81. package/dist/defaults/dropCap.js.map +1 -0
  82. package/dist/defaults/headings.d.ts +1 -1
  83. package/dist/defaults/headings.d.ts.map +1 -1
  84. package/dist/defaults/headings.js +11 -0
  85. package/dist/defaults/headings.js.map +1 -1
  86. package/dist/defaults/index.d.ts +3 -0
  87. package/dist/defaults/index.d.ts.map +1 -1
  88. package/dist/defaults/index.js +19 -0
  89. package/dist/defaults/index.js.map +1 -1
  90. package/dist/defaults/lineNumbers.d.ts +19 -0
  91. package/dist/defaults/lineNumbers.d.ts.map +1 -0
  92. package/dist/defaults/lineNumbers.js +101 -0
  93. package/dist/defaults/lineNumbers.js.map +1 -0
  94. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  95. package/dist/defaults/paragraphStyles.js +20 -0
  96. package/dist/defaults/paragraphStyles.js.map +1 -1
  97. package/dist/defaults/tabStops.d.ts +29 -0
  98. package/dist/defaults/tabStops.d.ts.map +1 -0
  99. package/dist/defaults/tabStops.js +94 -0
  100. package/dist/defaults/tabStops.js.map +1 -0
  101. package/dist/defaults/verse.d.ts +10 -0
  102. package/dist/defaults/verse.d.ts.map +1 -0
  103. package/dist/defaults/verse.js +52 -0
  104. package/dist/defaults/verse.js.map +1 -0
  105. package/dist/fonts/usage.d.ts +7 -0
  106. package/dist/fonts/usage.d.ts.map +1 -1
  107. package/dist/fonts/usage.js +71 -0
  108. package/dist/fonts/usage.js.map +1 -1
  109. package/dist/html-backend.d.ts.map +1 -1
  110. package/dist/html-backend.js +85 -11
  111. package/dist/html-backend.js.map +1 -1
  112. package/dist/index.d.ts +8 -5
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +4 -2
  115. package/dist/index.js.map +1 -1
  116. package/dist/measure/bidiLines.js +2 -2
  117. package/dist/measure/bidiLines.js.map +1 -1
  118. package/dist/measure/cache.js +2 -2
  119. package/dist/measure/cache.js.map +1 -1
  120. package/dist/measure/cjkCompose.d.ts.map +1 -1
  121. package/dist/measure/cjkCompose.js +5 -5
  122. package/dist/measure/cjkCompose.js.map +1 -1
  123. package/dist/measure/hardBreaks.d.ts +46 -0
  124. package/dist/measure/hardBreaks.d.ts.map +1 -0
  125. package/dist/measure/hardBreaks.js +216 -0
  126. package/dist/measure/hardBreaks.js.map +1 -0
  127. package/dist/measure/index.d.ts +1 -0
  128. package/dist/measure/index.d.ts.map +1 -1
  129. package/dist/measure/index.js +1 -0
  130. package/dist/measure/index.js.map +1 -1
  131. package/dist/measure/leader.d.ts +20 -0
  132. package/dist/measure/leader.d.ts.map +1 -0
  133. package/dist/measure/leader.js +46 -0
  134. package/dist/measure/leader.js.map +1 -0
  135. package/dist/measure/links.js +1 -1
  136. package/dist/measure/links.js.map +1 -1
  137. package/dist/measure/plain.d.ts.map +1 -1
  138. package/dist/measure/plain.js +18 -25
  139. package/dist/measure/plain.js.map +1 -1
  140. package/dist/measure/rich.d.ts +19 -0
  141. package/dist/measure/rich.d.ts.map +1 -1
  142. package/dist/measure/rich.js +211 -38
  143. package/dist/measure/rich.js.map +1 -1
  144. package/dist/measure/tabs.d.ts +103 -0
  145. package/dist/measure/tabs.d.ts.map +1 -0
  146. package/dist/measure/tabs.js +123 -0
  147. package/dist/measure/tabs.js.map +1 -0
  148. package/dist/measure/types.d.ts +26 -2
  149. package/dist/measure/types.d.ts.map +1 -1
  150. package/dist/measure/types.js +23 -3
  151. package/dist/measure/types.js.map +1 -1
  152. package/dist/parse/blockParser.d.ts +13 -6
  153. package/dist/parse/blockParser.d.ts.map +1 -1
  154. package/dist/parse/blockParser.js +457 -87
  155. package/dist/parse/blockParser.js.map +1 -1
  156. package/dist/parse/codeFence.d.ts +18 -0
  157. package/dist/parse/codeFence.d.ts.map +1 -0
  158. package/dist/parse/codeFence.js +52 -0
  159. package/dist/parse/codeFence.js.map +1 -0
  160. package/dist/parse/index.d.ts +1 -1
  161. package/dist/parse/index.d.ts.map +1 -1
  162. package/dist/parse/index.js.map +1 -1
  163. package/dist/parse/indexMarks.d.ts +3 -1
  164. package/dist/parse/indexMarks.d.ts.map +1 -1
  165. package/dist/parse/indexMarks.js +22 -2
  166. package/dist/parse/indexMarks.js.map +1 -1
  167. package/dist/parse/inlineFormatting.d.ts +50 -0
  168. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  169. package/dist/parse/inlineFormatting.js +94 -0
  170. package/dist/parse/inlineFormatting.js.map +1 -1
  171. package/dist/parse/sourceMapping.d.ts.map +1 -1
  172. package/dist/parse/sourceMapping.js +65 -1
  173. package/dist/parse/sourceMapping.js.map +1 -1
  174. package/dist/parse/types.d.ts +144 -9
  175. package/dist/parse/types.d.ts.map +1 -1
  176. package/dist/pipeline/build.d.ts.map +1 -1
  177. package/dist/pipeline/build.js +143 -18
  178. package/dist/pipeline/build.js.map +1 -1
  179. package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
  180. package/dist/pipeline/buildBlockKind.js +60 -1
  181. package/dist/pipeline/buildBlockKind.js.map +1 -1
  182. package/dist/pipeline/buildHelpers.d.ts +3 -0
  183. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  184. package/dist/pipeline/buildHelpers.js +7 -2
  185. package/dist/pipeline/buildHelpers.js.map +1 -1
  186. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  187. package/dist/pipeline/calloutLayout.js +33 -2
  188. package/dist/pipeline/calloutLayout.js.map +1 -1
  189. package/dist/pipeline/chips.d.ts.map +1 -1
  190. package/dist/pipeline/chips.js +4 -0
  191. package/dist/pipeline/chips.js.map +1 -1
  192. package/dist/pipeline/codeBlocks.d.ts +38 -0
  193. package/dist/pipeline/codeBlocks.d.ts.map +1 -0
  194. package/dist/pipeline/codeBlocks.js +146 -0
  195. package/dist/pipeline/codeBlocks.js.map +1 -0
  196. package/dist/pipeline/codeInline.d.ts +22 -0
  197. package/dist/pipeline/codeInline.d.ts.map +1 -0
  198. package/dist/pipeline/codeInline.js +93 -0
  199. package/dist/pipeline/codeInline.js.map +1 -0
  200. package/dist/pipeline/codeLines.d.ts +91 -0
  201. package/dist/pipeline/codeLines.d.ts.map +1 -0
  202. package/dist/pipeline/codeLines.js +399 -0
  203. package/dist/pipeline/codeLines.js.map +1 -0
  204. package/dist/pipeline/config.d.ts.map +1 -1
  205. package/dist/pipeline/config.js +8 -1
  206. package/dist/pipeline/config.js.map +1 -1
  207. package/dist/pipeline/contentWarnings.d.ts +9 -0
  208. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  209. package/dist/pipeline/contentWarnings.js +60 -2
  210. package/dist/pipeline/contentWarnings.js.map +1 -1
  211. package/dist/pipeline/continuation.d.ts.map +1 -1
  212. package/dist/pipeline/continuation.js +9 -2
  213. package/dist/pipeline/continuation.js.map +1 -1
  214. package/dist/pipeline/dropCap.d.ts +128 -0
  215. package/dist/pipeline/dropCap.d.ts.map +1 -0
  216. package/dist/pipeline/dropCap.js +412 -0
  217. package/dist/pipeline/dropCap.js.map +1 -0
  218. package/dist/pipeline/hardBreaks.d.ts +19 -0
  219. package/dist/pipeline/hardBreaks.d.ts.map +1 -0
  220. package/dist/pipeline/hardBreaks.js +36 -0
  221. package/dist/pipeline/hardBreaks.js.map +1 -0
  222. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  223. package/dist/pipeline/headerFooter.js +4 -1
  224. package/dist/pipeline/headerFooter.js.map +1 -1
  225. package/dist/pipeline/lineNumbers.d.ts +37 -0
  226. package/dist/pipeline/lineNumbers.d.ts.map +1 -0
  227. package/dist/pipeline/lineNumbers.js +441 -0
  228. package/dist/pipeline/lineNumbers.js.map +1 -0
  229. package/dist/pipeline/measureContentBlock.d.ts +31 -5
  230. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  231. package/dist/pipeline/measureContentBlock.js +274 -68
  232. package/dist/pipeline/measureContentBlock.js.map +1 -1
  233. package/dist/pipeline/partPalette.d.ts +1 -1
  234. package/dist/pipeline/partPalette.d.ts.map +1 -1
  235. package/dist/pipeline/partPalette.js +24 -2
  236. package/dist/pipeline/partPalette.js.map +1 -1
  237. package/dist/pipeline/styles.d.ts +18 -2
  238. package/dist/pipeline/styles.d.ts.map +1 -1
  239. package/dist/pipeline/styles.js +15 -3
  240. package/dist/pipeline/styles.js.map +1 -1
  241. package/dist/pipeline/tabs.d.ts +35 -0
  242. package/dist/pipeline/tabs.d.ts.map +1 -0
  243. package/dist/pipeline/tabs.js +99 -0
  244. package/dist/pipeline/tabs.js.map +1 -0
  245. package/dist/pipeline/toc.d.ts.map +1 -1
  246. package/dist/pipeline/toc.js +3 -38
  247. package/dist/pipeline/toc.js.map +1 -1
  248. package/dist/pipeline/verseLines.d.ts +78 -0
  249. package/dist/pipeline/verseLines.d.ts.map +1 -0
  250. package/dist/pipeline/verseLines.js +245 -0
  251. package/dist/pipeline/verseLines.js.map +1 -0
  252. package/dist/preflight.d.ts.map +1 -1
  253. package/dist/preflight.js +1 -0
  254. package/dist/preflight.js.map +1 -1
  255. package/dist/types.d.ts +560 -4
  256. package/dist/types.d.ts.map +1 -1
  257. package/dist/vdt.d.ts +240 -7
  258. package/dist/vdt.d.ts.map +1 -1
  259. package/dist/vdt.js +6 -0
  260. package/dist/vdt.js.map +1 -1
  261. package/dist/word/docxRead.d.ts.map +1 -1
  262. package/dist/word/docxRead.js +15 -0
  263. package/dist/word/docxRead.js.map +1 -1
  264. package/dist/word/inline.d.ts +6 -1
  265. package/dist/word/inline.d.ts.map +1 -1
  266. package/dist/word/inline.js +35 -1
  267. package/dist/word/inline.js.map +1 -1
  268. package/dist/word/model.d.ts +14 -0
  269. package/dist/word/model.d.ts.map +1 -1
  270. package/dist/word/model.js.map +1 -1
  271. package/dist/word/template.d.ts +12 -3
  272. package/dist/word/template.d.ts.map +1 -1
  273. package/dist/word/template.js +13 -3
  274. package/dist/word/template.js.map +1 -1
  275. package/dist/word/toDocx.d.ts.map +1 -1
  276. package/dist/word/toDocx.js +52 -8
  277. package/dist/word/toDocx.js.map +1 -1
  278. package/dist/word/toMarkdown.d.ts +5 -1
  279. package/dist/word/toMarkdown.d.ts.map +1 -1
  280. package/dist/word/toMarkdown.js +117 -6
  281. package/dist/word/toMarkdown.js.map +1 -1
  282. package/dist/word/word.test.js +124 -2
  283. package/dist/word/word.test.js.map +1 -1
  284. package/dist/worker/fonts.d.ts +2 -1
  285. package/dist/worker/fonts.d.ts.map +1 -1
  286. package/dist/worker/fonts.js +6 -2
  287. package/dist/worker/fonts.js.map +1 -1
  288. package/package.json +1 -1
@@ -592,7 +592,7 @@ describe('bundles written by postext 1.4 or earlier', () => {
592
592
  const pageCounts = (docs) => docs.map((d) => d.pages.length);
593
593
  it('stamps every manifest it writes with the configuration rules', async () => {
594
594
  const { manifest } = await sample();
595
- expect(CONFIG_VERSION).toBe(8);
595
+ expect(CONFIG_VERSION).toBe(9);
596
596
  expect(manifest.configVersion).toBe(CONFIG_VERSION);
597
597
  });
598
598
  it('reads a 1.4 bundle with the H1 break it laid out, and lays it out as 1.4 did', async () => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=codeBlocks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codeBlocks.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/codeBlocks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,550 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { parseMarkdown, parseMarkdownWithIssues } from '../parse';
3
+ import { buildDocument } from '../pipeline';
4
+ import { renderToHtml } from '../html-backend';
5
+ import { CONFIG_VERSION, migrateConfig, migrateBundleConfig, pinLegacyCodeBlocks } from '../bundle/configVersion';
6
+ import { resolveCodeStyleConfig, stripCodeStyleDefaults, DEFAULT_CODE_STYLE } from '../defaults/codeStyle';
7
+ import { resolveBodyTextConfig } from '../defaults/bodyText';
8
+ import { stripConfigDefaults } from '../defaults';
9
+ import { highlightCode, registerCodeHighlighter } from '../code/highlight';
10
+ import { fencedCodeLines } from '../parse/codeFence';
11
+ import { collectConfigWarnings } from '../configWarnings';
12
+ import { configFontFamilies, collectFontUsage } from '../fonts/usage';
13
+ import { formatWarning } from '../pipeline/contentWarnings';
14
+ // #624: code blocks. The stub font: 7 px a character, a space 4 px; the
15
+ // 'Shrinky' face scales with its size (7 px at 17 px).
16
+ class StubCtx {
17
+ font = '';
18
+ measureText(s) {
19
+ let w = 0;
20
+ for (const c of s)
21
+ w += c === ' ' ? 4 : 7;
22
+ const size = /(\d+(?:\.\d+)?)px/.exec(this.font);
23
+ return { width: this.font.includes('Shrinky') && size ? w * Number(size[1]) / 17 : w };
24
+ }
25
+ }
26
+ globalThis.OffscreenCanvas = class {
27
+ getContext() {
28
+ return new StubCtx();
29
+ }
30
+ };
31
+ const pt = (value) => ({ value, unit: 'pt' });
32
+ const em = (value) => ({ value, unit: 'em' });
33
+ const hex = (h) => ({ hex: h, model: 'hex' });
34
+ // 72 dpi, a 20 pt body on 30 pt leading, a 360 pt measure from x = 20; the
35
+ // code at 0.85 em (17 px) in a box padded 0.6 em of it.
36
+ const config = (extra = {}, width = 400, height = 600) => ({
37
+ page: { dpi: 72, width: pt(width), height: pt(height), margins: { top: pt(20), bottom: pt(20), left: pt(20), right: pt(20) } },
38
+ layout: { layoutType: 'single' },
39
+ header: { elements: [] },
40
+ footer: { elements: [] },
41
+ ...extra,
42
+ bodyText: { fontSize: pt(20), lineHeight: { value: 1.5, unit: 'em' }, firstLineIndent: pt(0), hyphenation: { enabled: false }, ...extra.bodyText },
43
+ });
44
+ const codeBlocks = (doc) => doc.blocks.filter((b) => b.type === 'code');
45
+ const frames = (doc) => doc.blocks.filter((b) => b.type === 'callout');
46
+ const codeLines = (doc) => codeBlocks(doc).flatMap((b) => b.lines);
47
+ const pageOf = (doc, block) => doc.pages.findIndex((p) => p.columns.some((c) => c.blocks.includes(block)));
48
+ /** Copied text of a block in the HTML viewer: its markup with tags and
49
+ * hidden spans (leaders, numbers) left out. */
50
+ function copiedText(html, blockId) {
51
+ const start = html.indexOf(`data-block-id="${blockId}"`);
52
+ const open = html.lastIndexOf('<div', start);
53
+ let depth = 0;
54
+ let end = open;
55
+ const re = /<(\/?)div\b[^>]*>/g;
56
+ re.lastIndex = open;
57
+ for (let m = re.exec(html); m; m = re.exec(html)) {
58
+ depth += m[1] ? -1 : 1;
59
+ if (depth === 0) {
60
+ end = m.index;
61
+ break;
62
+ }
63
+ }
64
+ const inner = html.slice(open, end).replace(/<span aria-hidden="true"[^>]*>[^<]*(?:<span[^>]*>[^<]*<\/span>)?<\/span>/g, '');
65
+ return inner.replace(/<[^>]+>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&');
66
+ }
67
+ afterEach(() => {
68
+ registerCodeHighlighter('js', null);
69
+ registerCodeHighlighter('*', null);
70
+ });
71
+ describe('code fences: the markup', () => {
72
+ const md = ['Before.', '```js', ' # not a heading', '- not a list', '1. not a list', '> no quote', '$x$ **no** `x` :::callout', ':::', '', '\tTab and spaces', '```', 'After.'].join('\n');
73
+ it('keeps every character of the listing, nothing read as Markdown', () => {
74
+ const blocks = parseMarkdown(md);
75
+ expect(blocks.map((b) => b.type)).toEqual(['paragraph', 'code', 'paragraph']);
76
+ const code = blocks[1];
77
+ expect(code.code.lang).toBe('js');
78
+ expect(code.code.fence).toBe('```');
79
+ expect(code.code.lines).toEqual([' # not a heading', '- not a list', '1. not a list', '> no quote', '$x$ **no** `x` :::callout', ':::', '', '\tTab and spaces']);
80
+ expect(code.text).toBe(code.code.lines.join('\n'));
81
+ expect(code.spans).toEqual([{ text: code.text, bold: false, italic: false }]);
82
+ });
83
+ it('maps every character to its source, a line feed to the line end', () => {
84
+ const code = parseMarkdown(md)[1];
85
+ for (let i = 0; i < code.text.length; i++) {
86
+ if (code.text[i] === '\n')
87
+ expect(md[code.sourceMap[i]]).toBe('\n');
88
+ else
89
+ expect(md[code.sourceMap[i]]).toBe(code.text[i]);
90
+ }
91
+ expect(code.sourceStart).toBe(md.indexOf('```js'));
92
+ expect(md.slice(code.sourceEnd - 3, code.sourceEnd)).toBe('```');
93
+ });
94
+ it('interrupts a paragraph, and the text after the closing fence is a new paragraph', () => {
95
+ const blocks = parseMarkdown(['One', 'two', '~~~', 'x', '~~~', 'three'].join('\n'));
96
+ expect(blocks.map((b) => [b.type, b.text])).toEqual([['paragraph', 'One two'], ['code', 'x'], ['paragraph', 'three']]);
97
+ });
98
+ it('closes on a fence of its own character at least as long', () => {
99
+ const four = parseMarkdown(['````md', '```js', 'inner', '```', '````'].join('\n'));
100
+ expect(four.map((b) => b.type)).toEqual(['code']);
101
+ expect(four[0].code.lines).toEqual(['```js', 'inner', '```']);
102
+ const tilde = parseMarkdown(['~~~', '```', 'x', '~~~'].join('\n'));
103
+ expect(tilde[0].code.lines).toEqual(['```', 'x']);
104
+ expect(tilde[0].code.fence).toBe('~~~');
105
+ });
106
+ it('takes the fence indentation off the lines', () => {
107
+ const [code] = parseMarkdown([' ```', ' a', ' b', 'c', ' ```'].join('\n'));
108
+ expect(code.code.lines).toEqual(['a', ' b', 'c']);
109
+ });
110
+ it('reports a fence left open and runs it to the end', () => {
111
+ const { blocks, issues } = parseMarkdownWithIssues(['Text.', '', '```python', 'print(1)', '', '# end'].join('\n'));
112
+ expect(blocks.map((b) => b.type)).toEqual(['paragraph', 'code']);
113
+ expect(blocks[1].code.lines).toEqual(['print(1)', '', '# end']);
114
+ expect(issues).toEqual([expect.objectContaining({ kind: 'unclosedCodeBlock', delimiter: '```', lang: 'python' })]);
115
+ });
116
+ it('reads the info string: language, attributes, a bare title', () => {
117
+ const read = (info) => parseMarkdown(['```' + info, 'x', '```'].join('\n'))[0].code;
118
+ expect(read('bash {title="backup.sh" lineNumbers start=12 highlight="3,5-7"}')).toMatchObject({ lang: 'bash', title: 'backup.sh', attrs: { title: 'backup.sh', lineNumbers: '', start: '12', highlight: '3,5-7' } });
119
+ expect(read('console Terminal')).toMatchObject({ lang: 'console', title: 'Terminal' });
120
+ expect(read('python normalize.py')).toMatchObject({ lang: 'python', title: 'normalize.py' });
121
+ expect(read('JS title="a b"')).toMatchObject({ lang: 'js', title: 'a b' });
122
+ expect(read('')).toEqual(expect.objectContaining({ info: '', attrs: {} }));
123
+ // A backtick fence's info string holds no backtick: a line of text.
124
+ expect(parseMarkdown('```js `x` here').map((b) => b.type)).toEqual(['paragraph']);
125
+ });
126
+ it('reads indented code only when asked', () => {
127
+ const md2 = ['Text.', '', ' let a = 1;', '', '\tlet b = 2;', '', 'After.'].join('\n');
128
+ expect(parseMarkdown(md2).map((b) => b.type)).toEqual(['paragraph', 'paragraph', 'paragraph', 'paragraph']);
129
+ const on = parseMarkdown(md2, { indentedCode: true });
130
+ expect(on.map((b) => b.type)).toEqual(['paragraph', 'code', 'paragraph']);
131
+ expect(on[1].code.lines).toEqual(['let a = 1;', '', 'let b = 2;']);
132
+ expect(on[1].code.fence).toBe('indent');
133
+ // A nested list keeps its items.
134
+ expect(parseMarkdown(['- a', '', ' - b'].join('\n'), { indentedCode: true }).map((b) => b.type)).toEqual(['listItem', 'listItem']);
135
+ });
136
+ it('reads fences as Markdown when asked (postext 1.22)', () => {
137
+ const blocks = parseMarkdown(md, { fences: false });
138
+ expect(blocks.some((b) => b.type === 'code')).toBe(false);
139
+ expect(blocks.some((b) => b.type === 'heading')).toBe(true);
140
+ });
141
+ it('works inside a box and closes a list', () => {
142
+ const blocks = parseMarkdown([':::callout', '```', ':::', '```', ':::', '', '- item', '```', 'x', '```'].join('\n'));
143
+ expect(blocks.map((b) => b.type)).toEqual(['containerStart', 'code', 'containerEnd', 'listItem', 'code']);
144
+ });
145
+ it('keeps index marks and anchors in a listing as written', () => {
146
+ const blocks = parseMarkdown(['A :index[term] mark.', '', '```', 'B :index[x] and :anchor{#a}', '```'].join('\n'));
147
+ expect(blocks[1].code.lines).toEqual(['B :index[x] and :anchor{#a}']);
148
+ expect(blocks[0].indexMarks?.length).toBe(1);
149
+ expect(blocks[1].indexMarks).toBeUndefined();
150
+ });
151
+ it('tells editors which lines a listing takes', () => {
152
+ expect(fencedCodeLines(['a', '```js', '# x', '```', 'b', '~~~', 'c'])).toEqual([false, true, true, true, false, true, true]);
153
+ });
154
+ it('records the inline code of text blocks', () => {
155
+ const md3 = 'Run `ls -l` and `pwd`.';
156
+ const [p] = parseMarkdown(md3);
157
+ expect(p.inlineCode.map((r) => md3.slice(r.start, r.end))).toEqual(['ls -l', 'pwd']);
158
+ });
159
+ });
160
+ describe('code listings: the layout', () => {
161
+ const LISTING = ['Text.', '', '```', 'one', ' two three', '', 'four', '```', '', 'After.'].join('\n');
162
+ it('sets one line per source line, as written, in the code face, never justified', () => {
163
+ const doc = buildDocument({ markdown: LISTING }, config({ bodyText: { textAlign: 'justify' } }));
164
+ const [code] = codeBlocks(doc);
165
+ expect(code.lines.map((l) => l.text)).toEqual(['one', ' two three', '', 'four']);
166
+ for (const line of code.lines) {
167
+ expect(line.hyphenated).toBe(false);
168
+ expect(line.ragged).toBe(true);
169
+ for (const seg of line.segments ?? [])
170
+ expect(seg.fontString).toContain('Source Code Pro');
171
+ }
172
+ // Spaces keep their advance: two spaces then "two".
173
+ const second = code.lines[1];
174
+ expect(second.segments.reduce((s, g) => s + g.width, 0)).toBe(4 * 4 + 3 * 7 + 5 * 7);
175
+ expect(code.textAlign).toBe('left');
176
+ });
177
+ it('sets the box of codeStyle: background, border, radius, padding', () => {
178
+ const doc = buildDocument({ markdown: LISTING }, config({ codeStyle: {
179
+ background: hex('#101010'), border: { enabled: true, color: hex('#ff0000'), width: pt(1) },
180
+ borderRadius: pt(4), padding: { top: pt(5), right: pt(6), bottom: pt(7), left: pt(8) },
181
+ } }));
182
+ const [frame] = frames(doc);
183
+ const [code] = codeBlocks(doc);
184
+ expect(frame.callout.styleId).toBe('__postext-code');
185
+ const box = frame.designOverlay.blocks.find((b) => b.kind === 'box');
186
+ expect(box && box.kind === 'box' ? box.box : undefined).toEqual({ backgroundColor: '#101010', borderColor: '#ff0000', borderWidthPx: 1, borderRadiusPx: 4 });
187
+ expect(code.bbox.x - frame.bbox.x).toBeCloseTo(8, 5);
188
+ expect(code.bbox.y - frame.bbox.y).toBeCloseTo(5, 5);
189
+ expect(frame.bbox.height).toBeCloseTo(5 + 4 * 30 + 7, 5);
190
+ });
191
+ it('keeps tabs as tab segments at the next stop', () => {
192
+ const doc = buildDocument({ markdown: ['```', 'a\tb', '\tc', '```'].join('\n') }, config());
193
+ const [code] = codeBlocks(doc);
194
+ const [first, second] = code.lines;
195
+ const tab = first.segments.find((s) => s.text === '\t');
196
+ expect(tab.kind).toBe('space');
197
+ // 'a' takes one cell, the tab the three to the stop at 4.
198
+ expect(tab.width).toBe(3 * 4);
199
+ expect(second.segments[0].width).toBe(4 * 4);
200
+ expect(first.text).toBe('a\tb');
201
+ });
202
+ it('a title row from the fence, or a label tab when codeStyle.label is set', () => {
203
+ const md = ['```js app.js', 'x', '```'].join('\n');
204
+ const titled = frames(buildDocument({ markdown: md }, config()))[0];
205
+ expect(titled.designOverlay.blocks.some((b) => b.kind === 'text' && b.lines[0].text === 'app.js')).toBe(true);
206
+ const labelled = frames(buildDocument({ markdown: md }, config({ codeStyle: { label: { background: hex('#000000'), color: hex('#ffffff') } } })))[0];
207
+ const texts = labelled.designOverlay.blocks.filter((b) => b.kind === 'text');
208
+ expect(texts.map((b) => b.kind === 'text' ? b.lines[0].text : '')).toEqual(['app.js']);
209
+ expect(labelled.designOverlay.blocks.some((b) => b.kind === 'box' && b.box.backgroundColor === '#000000')).toBe(true);
210
+ });
211
+ it('spans the page with span=page in a two-column layout', () => {
212
+ const doc = buildDocument({ markdown: ['Text.', '', '```js {span=page}', 'wide()', '```'].join('\n') }, config({ layout: { layoutType: 'double', gutterWidth: pt(20) } }));
213
+ const [frame] = frames(doc);
214
+ expect(frame.bbox.width).toBeCloseTo(360, 0);
215
+ });
216
+ it('sits in a box nested in a callout', () => {
217
+ const doc = buildDocument({ markdown: [':::callout', 'Note.', '', '```', 'code', '```', ':::'].join('\n') }, config());
218
+ const fs = frames(doc);
219
+ expect(fs.map((f) => f.callout.styleId)).toEqual(['note', '__postext-code']);
220
+ const [code] = codeBlocks(doc);
221
+ expect(code.lines[0].text).toBe('code');
222
+ expect(code.bbox.x).toBeGreaterThan(fs[1].bbox.x);
223
+ expect(fs[1].bbox.x).toBeGreaterThan(fs[0].bbox.x);
224
+ });
225
+ it('keeps the code style in a :::paragraphs group', () => {
226
+ const doc = buildDocument({ markdown: [':::paragraphs{style="small"}', 'Para.', '', '```', 'code', '```', ':::'].join('\n') }, config({ paragraphStyles: [{ id: 'small', fontSize: pt(10) }] }));
227
+ const [code] = codeBlocks(doc);
228
+ expect(code.lines[0].segments[0].fontString).toContain('17px');
229
+ });
230
+ it('follows the flow of a vertical book, with no line numbers', () => {
231
+ const doc = buildDocument({ markdown: ['本文。', '', '```js {lineNumbers}', 'let a = 1;', '```'].join('\n') }, config({ locale: 'ja', layout: { layoutType: 'single', writingMode: 'vertical-rl' } }));
232
+ const [code] = codeBlocks(doc);
233
+ expect(code.lines.map((l) => l.text)).toEqual(['let a = 1;']);
234
+ expect(doc.pages.every((p) => !p.lineNumbers)).toBe(true);
235
+ });
236
+ it('is laid out in a right-to-left document left to right, from the far side', () => {
237
+ const doc = buildDocument({ markdown: ['نص.', '', '```', 'let a = 1;', '```'].join('\n') }, config({ locale: 'ar' }));
238
+ const [code] = codeBlocks(doc);
239
+ expect(code.lines[0].text).toBe('let a = 1;');
240
+ expect(code.direction).toBe('ltr');
241
+ expect(code.lines[0].measure).toBeDefined();
242
+ });
243
+ });
244
+ describe('code listings: lines wider than the box', () => {
245
+ const long = 'x'.repeat(60) + ' ' + 'y'.repeat(59);
246
+ const md = ['```js', long, 'short', '```'].join('\n');
247
+ // A 300 pt page: 260 pt measure, the code 17 px a character? The stub
248
+ // sets every character 7 px: 239 px of code room, 34 characters.
249
+ const narrow = (cs) => buildDocument({ markdown: md }, config({ codeStyle: cs }, 300));
250
+ it('wraps with the marker in the hang, and warns', () => {
251
+ const doc = narrow({});
252
+ const lines = codeLines(doc);
253
+ expect(lines.length).toBeGreaterThan(3);
254
+ const conts = lines.filter((l) => l.codeLine?.continued);
255
+ expect(conts.length).toBeGreaterThan(0);
256
+ expect(lines[0].codeLine.wrapped).toBe(true);
257
+ for (const c of conts) {
258
+ expect(c.segments[0].leader).toBe('text');
259
+ expect(c.segments[0].text).toBe('»');
260
+ expect(c.text.includes('»')).toBe(false);
261
+ }
262
+ // The pieces join into the line as written.
263
+ const joined = lines.slice(0, lines.findIndex((l) => l.text === 'short')).map((l) => l.text).join('');
264
+ expect(joined).toBe(long);
265
+ // Every visual line fits.
266
+ const room = codeBlocks(doc)[0].bbox.width;
267
+ for (const l of lines)
268
+ expect(l.segments.reduce((s, g) => s + g.width, 0)).toBeLessThanOrEqual(room + 0.01);
269
+ expect(doc.contentWarnings).toEqual(expect.arrayContaining([expect.objectContaining({ kind: 'codeOverflow', mode: 'wrap', lines: 1, lang: 'js' })]));
270
+ });
271
+ it('breaks after a space when one fits', () => {
272
+ const doc = buildDocument({ markdown: ['```', 'aaaa '.repeat(20), '```'].join('\n') }, config({}, 300));
273
+ for (const l of codeLines(doc).slice(0, -1))
274
+ expect(l.text.endsWith(' ')).toBe(true);
275
+ });
276
+ it('shrinks the listing down to minFontScale, then wraps, and warns', () => {
277
+ const shrink = { overflow: 'shrink', minFontScale: 0.5, fontFamily: 'Shrinky' };
278
+ const fits = buildDocument({ markdown: ['```js', 'x'.repeat(50), 'short', '```'].join('\n') }, config({ codeStyle: shrink }, 300));
279
+ const [code] = codeBlocks(fits);
280
+ expect(code.lines.map((l) => l.text)).toEqual(['x'.repeat(50), 'short']);
281
+ const scale = code.code.fit.scale;
282
+ expect(scale).toBeLessThan(1);
283
+ expect(scale).toBeGreaterThanOrEqual(0.5);
284
+ expect(code.lines[0].segments[0].fontString).toContain(`${17 * scale}px`);
285
+ expect(code.lines[0].segments.reduce((s, g) => s + g.width, 0)).toBeLessThanOrEqual(code.bbox.width + 0.01);
286
+ const w = fits.contentWarnings.find((x) => x.kind === 'codeOverflow');
287
+ expect(w).toMatchObject({ mode: 'shrink', lines: 1, scale });
288
+ expect(formatWarning(w)).toContain('%');
289
+ // Past the floor the line turns over too.
290
+ const floor = codeBlocks(narrow(shrink))[0];
291
+ expect(floor.code.fit.scale).toBe(0.5);
292
+ expect(floor.lines.some((l) => l.codeLine?.continued)).toBe(true);
293
+ });
294
+ it('clips at the box edge, and warns', () => {
295
+ const doc = narrow({ overflow: 'clip' });
296
+ const [code] = codeBlocks(doc);
297
+ expect(code.lines.length).toBe(2);
298
+ expect(code.lines[0].codeLine.clipped).toBe(true);
299
+ expect(code.lines[0].segments.reduce((s, g) => s + g.width, 0)).toBeLessThanOrEqual(code.bbox.width + 0.01);
300
+ expect(doc.contentWarnings).toEqual(expect.arrayContaining([expect.objectContaining({ kind: 'codeOverflow', mode: 'clip' })]));
301
+ });
302
+ });
303
+ describe('code listings: across columns and pages', () => {
304
+ const many = (n) => ['Intro.', '', '```', ...Array.from({ length: n }, (_, i) => `line ${i + 1}`), '```', '', 'After.'].join('\n');
305
+ it('splits between lines across pages, each part framed, no part of one line', () => {
306
+ const doc = buildDocument({ markdown: many(40) }, config({}, 400, 400));
307
+ const parts = codeBlocks(doc);
308
+ expect(parts.length).toBeGreaterThan(1);
309
+ expect(new Set(parts.map((p) => pageOf(doc, p))).size).toBe(parts.length);
310
+ for (const p of parts)
311
+ expect(p.lines.length).toBeGreaterThanOrEqual(2);
312
+ expect(frames(doc).length).toBe(parts.length);
313
+ expect(parts.flatMap((p) => p.lines.map((l) => l.text))).toEqual(Array.from({ length: 40 }, (_, i) => `line ${i + 1}`));
314
+ });
315
+ it('splits across columns', () => {
316
+ const doc = buildDocument({ markdown: many(30) }, config({ layout: { layoutType: 'double', gutterWidth: pt(20) } }, 400, 400));
317
+ const parts = codeBlocks(doc);
318
+ expect(parts.length).toBeGreaterThan(1);
319
+ const cols = parts.map((p) => doc.pages.flatMap((pg) => pg.columns).findIndex((c) => c.blocks.includes(p)));
320
+ expect(new Set(cols).size).toBe(parts.length);
321
+ });
322
+ it('repeats the title only when asked', () => {
323
+ const md = ['```js long.js', ...Array.from({ length: 40 }, (_, i) => `line ${i + 1}`), '```'].join('\n');
324
+ const titles = (doc) => frames(doc).map((f) => f.designOverlay.blocks.filter((b) => b.kind === 'text').map((b) => b.kind === 'text' ? b.lines[0].text : '').join());
325
+ const plain = titles(buildDocument({ markdown: md }, config({}, 400, 400)));
326
+ expect(plain[0]).toBe('long.js');
327
+ expect(plain.slice(1).every((t) => t === '')).toBe(true);
328
+ const repeated = titles(buildDocument({ markdown: md }, config({ codeStyle: { repeatTitle: true } }, 400, 400)));
329
+ expect(repeated.slice(1).every((t) => t.startsWith('long.js'))).toBe(true);
330
+ });
331
+ it('keeps a wrapped line with its continuations when another cut fits', () => {
332
+ const long = 'z'.repeat(80);
333
+ const lines = Array.from({ length: 30 }, (_, i) => (i % 3 === 2 ? long : `line ${i + 1}`));
334
+ const doc = buildDocument({ markdown: ['```', ...lines, '```'].join('\n') }, config({}, 300, 400));
335
+ const parts = codeBlocks(doc);
336
+ expect(parts.length).toBeGreaterThan(1);
337
+ for (const p of parts.slice(1))
338
+ expect(p.lines[0].codeLine.continued).toBeUndefined();
339
+ });
340
+ });
341
+ describe('code listings: line numbers and highlighted lines', () => {
342
+ const md = ['```js {lineNumbers start=9 highlight="2"}', 'a();', 'b();', 'c();', '```'].join('\n');
343
+ it('numbers the lines in the page slot, out of the text', () => {
344
+ const doc = buildDocument({ markdown: md }, config());
345
+ const [code] = codeBlocks(doc);
346
+ expect(code.lines.map((l) => l.codeLine.number)).toEqual(['9', '10', '11']);
347
+ const slot = doc.pages[0].lineNumbers;
348
+ const nums = slot.blocks.filter((b) => b.kind === 'text');
349
+ expect(nums.map((b) => b.kind === 'text' ? b.lines[0].text : '')).toEqual(['9', '10', '11']);
350
+ for (const b of nums)
351
+ expect(b.kind === 'text' && b.artifact).toBe(true);
352
+ // Right-aligned against the gutter, left of the code.
353
+ const rights = nums.map((b) => b.bbox.x + b.bbox.width);
354
+ expect(rights[0]).toBeCloseTo(rights[1], 5);
355
+ expect(rights[0]).toBeLessThan(code.lines[0].bbox.x);
356
+ // The gutter: two digits and the gap.
357
+ const frame = frames(doc)[0];
358
+ expect(code.lines[0].bbox.x - frame.callout.innerRect.x - frame.bbox.x).toBeCloseTo(2 * 7 + 17, 5);
359
+ // The highlighted line has a band behind it.
360
+ const band = frame.designOverlay.blocks.find((b) => b.kind === 'box' && b.box.backgroundColor === DEFAULT_CODE_STYLE.highlightBackground.hex);
361
+ expect(band).toBeDefined();
362
+ expect(band.bbox.y).toBeCloseTo(code.lines[1].bbox.y, 5);
363
+ });
364
+ it('numbers every listing with codeStyle.lineNumbers, a fence turning it off', () => {
365
+ const doc = buildDocument({ markdown: ['```', 'a', '```', '', '```js {lineNumbers=false}', 'b', '```'].join('\n') }, config({ codeStyle: { lineNumbers: true } }));
366
+ expect(codeBlocks(doc).map((b) => b.lines[0].codeLine.number)).toEqual(['1', undefined]);
367
+ });
368
+ it('copies the listing with its indentation and no numbers or markers from the HTML viewer', () => {
369
+ const doc = buildDocument({ markdown: ['```py {lineNumbers}', 'def f():', ' return 1 # one', '', 'x = f()', '```'].join('\n') }, config());
370
+ const html = renderToHtml(doc);
371
+ const [code] = codeBlocks(doc);
372
+ expect(copiedText(html, code.id)).toBe('def f():\n return 1 # one\n\nx = f()\n');
373
+ expect(html).toContain('pt-line-numbers');
374
+ });
375
+ it('copies a wrapped line whole', () => {
376
+ const long = 'w'.repeat(70);
377
+ const doc = buildDocument({ markdown: ['```', long, 'end', '```'].join('\n') }, config({}, 300));
378
+ const [code] = codeBlocks(doc);
379
+ expect(copiedText(renderToHtml(doc), code.id)).toBe(`${long}\nend\n`);
380
+ });
381
+ });
382
+ describe('code listings: syntax colouring', () => {
383
+ const kinds = (code, lang) => highlightCode(code, lang, true).flat().filter((t) => t.token).map((t) => [t.token, t.text]);
384
+ it('names the tokens of js, python, bash and console', () => {
385
+ expect(kinds('const a = "s"; // c\nf(42);', 'js')).toEqual([
386
+ ['keyword', 'const'], ['operator', '='], ['string', '"s"'], ['punctuation', ';'], ['comment', '// c'],
387
+ ['function', 'f'], ['punctuation', '('], ['number', '42'], ['punctuation', ');'],
388
+ ]);
389
+ expect(kinds('def f(x):\n return "a" # c', 'python')).toEqual([
390
+ ['keyword', 'def'], ['function', 'f'], ['punctuation', '('], ['punctuation', ')'], ['operator', ':'],
391
+ ['keyword', 'return'], ['string', '"a"'], ['comment', '# c'],
392
+ ]);
393
+ expect(kinds('if [ -f "$F" ]; then echo $HOME; fi # done', 'bash')).toEqual([
394
+ ['keyword', 'if'], ['punctuation', '['], ['meta', '-f'], ['string', '"$F"'], ['punctuation', ']'], ['operator', ';'],
395
+ ['keyword', 'then'], ['function', 'echo'], ['variable', '$HOME'], ['operator', ';'], ['keyword', 'fi'], ['comment', '# done'],
396
+ ]);
397
+ expect(kinds('$ ls -l\ntotal 8\n% pwd', 'console')).toEqual([['prompt', '$ ls -l'], ['output', 'total 8'], ['prompt', '% pwd']]);
398
+ });
399
+ it('keeps a comment or a string that runs over lines', () => {
400
+ const lines = highlightCode('a /* one\ntwo */ b\n"""x\ny"""', 'js', true);
401
+ expect(lines[1][0]).toEqual({ text: 'two */', token: 'comment' });
402
+ const py = highlightCode('s = """x\ny"""', 'python', true);
403
+ expect(py[1][0]).toEqual({ text: 'y"""', token: 'string' });
404
+ });
405
+ it('reads json, css, html, markdown and sql', () => {
406
+ expect(kinds('{"a": 1, "b": true}', 'json')).toEqual(expect.arrayContaining([['variable', '"a"'], ['number', '1'], ['number', 'true']]));
407
+ expect(kinds('a { color: #fff; }', 'css')).toEqual(expect.arrayContaining([['type', 'a'], ['variable', 'color'], ['number', '#fff']]));
408
+ expect(kinds('<a href="x">t</a>', 'html')).toEqual(expect.arrayContaining([['keyword', 'a'], ['variable', 'href'], ['string', '"x"']]));
409
+ expect(kinds('# Head\n- item `c`', 'markdown')).toEqual(expect.arrayContaining([['keyword', '# Head'], ['punctuation', '-'], ['string', '`c`']]));
410
+ expect(kinds('SELECT a FROM t -- c', 'sql')).toEqual(expect.arrayContaining([['keyword', 'SELECT'], ['keyword', 'FROM'], ['comment', '-- c']]));
411
+ });
412
+ it('sets an unknown language plain, and none with highlight: none', () => {
413
+ expect(highlightCode('x', 'cobol', true)).toBeUndefined();
414
+ expect(highlightCode('const a', 'js', false)).toBeUndefined();
415
+ });
416
+ it('colours the tokens in codeStyle.tokens colours, palette links included', () => {
417
+ const md = ['```js', 'const a = 1; // note', '```'].join('\n');
418
+ const cfg = config({
419
+ colorPalette: [{ id: 'kw', name: 'kw', value: hex('#aa0000') }],
420
+ codeStyle: { tokens: { keyword: { color: { hex: '#000000', model: 'hex', paletteId: 'kw' }, bold: true }, comment: { color: hex('#00aa00') } } },
421
+ });
422
+ const segs = codeLines(buildDocument({ markdown: md }, cfg)).flatMap((l) => l.segments ?? []);
423
+ const kw = segs.find((s) => s.text === 'const');
424
+ expect(kw.color).toBe('#aa0000');
425
+ expect(kw.bold).toBe(true);
426
+ expect(kw.fontString).toContain('700');
427
+ const comment = segs.find((s) => s.text.startsWith('//'));
428
+ expect(comment.color).toBe('#00aa00');
429
+ expect(comment.italic).toBe(true);
430
+ });
431
+ it('follows a part palette', () => {
432
+ const md = [':::part{palette="kw=#0000ff"}', '# Part', ':::', '', '```js', 'const a', '```'].join('\n');
433
+ const cfg = config({
434
+ colorPalette: [{ id: 'kw', name: 'kw', value: hex('#aa0000') }],
435
+ codeStyle: { tokens: { keyword: { color: { hex: '#aa0000', model: 'hex', paletteId: 'kw' } } } },
436
+ });
437
+ const kw = codeLines(buildDocument({ markdown: md }, cfg)).flatMap((l) => l.segments ?? []).find((s) => s.text === 'const');
438
+ expect(kw.color?.toLowerCase()).toBe('#0000ff');
439
+ });
440
+ it('lets a registered highlighter replace the built-in one for its language', () => {
441
+ registerCodeHighlighter('js', (code) => code.split('\n').map((line) => [{ text: line, color: '#123456' }]));
442
+ const segs = codeLines(buildDocument({ markdown: ['```js', 'const a', '```'].join('\n') }, config())).flatMap((l) => l.segments ?? []);
443
+ expect(segs.map((s) => [s.text, s.color])).toEqual([['const a', '#123456']]);
444
+ // One whose lines do not match the listing is ignored.
445
+ registerCodeHighlighter('js', () => [[{ text: 'other' }]]);
446
+ expect(highlightCode('const a', 'js', true)[0][0]).toEqual({ text: 'const', token: 'keyword' });
447
+ // A catch-all one takes every language.
448
+ registerCodeHighlighter('js', null);
449
+ registerCodeHighlighter('*', (code) => code.split('\n').map((line) => [{ text: line, token: 'string' }]));
450
+ expect(highlightCode('x', 'cobol', true)).toEqual([[{ text: 'x', token: 'string' }]]);
451
+ });
452
+ });
453
+ describe('inline code', () => {
454
+ const md = 'Run `ls -l` now.';
455
+ it('stays in the body face unless codeStyle.inline is set', () => {
456
+ const doc = buildDocument({ markdown: md }, config());
457
+ const p = doc.blocks.find((b) => b.type === 'paragraph');
458
+ expect(p.lines.flatMap((l) => l.segments ?? []).some((s) => s.chip)).toBe(false);
459
+ expect(p.lines[0].text).toBe('Run ls -l now.');
460
+ });
461
+ it('is set as a chip in the code face with codeStyle.inline', () => {
462
+ const doc = buildDocument({ markdown: md }, config({ codeStyle: { inline: { background: hex('#eeeeee'), color: hex('#333333') } } }));
463
+ const p = doc.blocks.find((b) => b.type === 'paragraph');
464
+ const chip = p.lines.flatMap((l) => l.segments ?? []).find((s) => s.chip);
465
+ expect(chip.text).toBe('ls -l');
466
+ expect(chip.chip.runs[0].fontString).toContain('Source Code Pro');
467
+ expect(chip.chip.runs[0].fontString).toContain('18px');
468
+ expect(chip.chip.background).toBe('#eeeeee');
469
+ expect(chip.chip.color).toBe('#333333');
470
+ expect(p.lines[0].text).toBe('Run ls -l now.');
471
+ expect(p.lines[0].sourceEnd).toBe(md.length);
472
+ });
473
+ });
474
+ describe('codeStyle: settings', () => {
475
+ const body = resolveBodyTextConfig(undefined);
476
+ it('resolves and strips', () => {
477
+ const r = resolveCodeStyleConfig({ tabSize: 2, overflow: 'clip', tokens: { keyword: { bold: true } }, inline: {} }, body);
478
+ expect(r.tabSize).toBe(2);
479
+ expect(r.overflow).toBe('clip');
480
+ expect(r.tokens.keyword).toMatchObject({ bold: true, color: { hex: '#8b2c8f' } });
481
+ expect(r.inline).toMatchObject({ fontFamily: 'Source Code Pro', fontSize: em(0.9), paddingX: em(0) });
482
+ expect(r.color).toEqual(body.color);
483
+ expect(stripCodeStyleDefaults({ tabSize: 4, overflow: 'wrap', fontFamily: 'Source Code Pro' })).toBeUndefined();
484
+ expect(stripCodeStyleDefaults({ tabSize: 2, tokens: { keyword: { color: hex('#8b2c8f'), bold: true } } })).toEqual({ tabSize: 2, tokens: { keyword: { bold: true } } });
485
+ expect(stripConfigDefaults({ codeStyle: { blocks: true } }).codeStyle).toBeUndefined();
486
+ });
487
+ it('warns of unknown keys and values', () => {
488
+ const w = collectConfigWarnings({ codeStyle: { overflow: 'warp', tabsize: 2, tokens: { keywrd: {} }, inline: { colour: hex('#000000') } } });
489
+ expect(w).toEqual(expect.arrayContaining([
490
+ expect.objectContaining({ kind: 'unknownConfigValue', path: 'codeStyle.overflow', suggestion: 'wrap' }),
491
+ expect.objectContaining({ kind: 'unknownConfigKey', path: 'codeStyle.tabsize', suggestion: 'tabSize' }),
492
+ expect.objectContaining({ kind: 'unknownConfigKey', path: 'codeStyle.tokens.keywrd', suggestion: 'keyword' }),
493
+ expect.objectContaining({ kind: 'unknownConfigKey', path: 'codeStyle.inline.colour', suggestion: 'color' }),
494
+ ]));
495
+ });
496
+ it('asks for the code face when the text holds a fence', () => {
497
+ expect(configFontFamilies({}, 'no code')).not.toContain('Source Code Pro');
498
+ expect(configFontFamilies({}, ['# A', '```\nx\n```'])).toContain('Source Code Pro');
499
+ expect(configFontFamilies({ codeStyle: { fontFamily: 'JetBrains Mono' } })).toContain('JetBrains Mono');
500
+ expect(configFontFamilies({ codeStyle: { blocks: false } }, '```\nx\n```')).not.toContain('Source Code Pro');
501
+ expect(collectFontUsage({}, { markdown: '```\nx\n```' }).get('Source Code Pro')).toEqual(expect.arrayContaining([{ weight: 700, style: 'italic' }]));
502
+ });
503
+ });
504
+ /** A stable signature of a layout: every page's blocks and lines. */
505
+ function signature(doc) {
506
+ const r = (n) => Math.round(n * 100) / 100;
507
+ return JSON.stringify(doc.pages.map((p) => p.columns.map((c) => c.blocks.map((b) => [b.type, r(b.bbox.x), r(b.bbox.y), r(b.bbox.height), b.lines.map((l) => [l.text, r(l.bbox.x), r(l.baseline)])]))));
508
+ }
509
+ function fnv(text) {
510
+ let h = 0x811c9dc5;
511
+ for (let i = 0; i < text.length; i++) {
512
+ h ^= text.charCodeAt(i);
513
+ h = Math.imul(h, 0x01000193) >>> 0;
514
+ }
515
+ return h.toString(16);
516
+ }
517
+ describe('stored documents (CONFIG_VERSION 9)', () => {
518
+ // A chapter written before #624: fences read as Markdown then.
519
+ const LEGACY = [
520
+ '# Shell', '',
521
+ 'Some text before a listing,', '```bash', '# a comment that was a heading', 'echo "hi" && ls -l', '- a list item', '```', 'and text after it.', '',
522
+ '~~~', '1. numbered', '`inline` code', '~~~', '',
523
+ 'The end.',
524
+ ].join('\n');
525
+ it('pins the 1.22 reading of fences for a configuration with fences in its text', () => {
526
+ expect(CONFIG_VERSION).toBe(9);
527
+ expect(pinLegacyCodeBlocks({})).toEqual({ codeStyle: { blocks: false } });
528
+ const own = { codeStyle: { blocks: true } };
529
+ expect(pinLegacyCodeBlocks(own)).toBe(own);
530
+ expect(migrateConfig({}, 8, { content: LEGACY }).codeStyle).toEqual({ blocks: false });
531
+ expect(migrateConfig({}, 8, { content: 'no fences here' }).codeStyle).toBeUndefined();
532
+ expect(migrateConfig({}, 8).codeStyle).toEqual({ blocks: false });
533
+ expect(migrateConfig({}, 9, { content: LEGACY }).codeStyle).toBeUndefined();
534
+ expect(migrateBundleConfig({}, [{}], 8, { content: [LEGACY] }).codeStyle).toEqual({ blocks: false });
535
+ });
536
+ it('lays a pinned document out exactly as postext 1.22 did', () => {
537
+ const doc = buildDocument({ markdown: LEGACY }, config({ codeStyle: { blocks: false } }));
538
+ expect(codeBlocks(doc)).toEqual([]);
539
+ // The hash of the same layout built by the engine before #624
540
+ // (c784c63b, with the same stub font).
541
+ expect(fnv(signature(doc))).toBe(LEGACY_HASH);
542
+ // Unpinned, the fences are code blocks: another layout.
543
+ const today = buildDocument({ markdown: LEGACY }, config());
544
+ expect(codeBlocks(today).length).toBe(2);
545
+ expect(fnv(signature(today))).not.toBe(LEGACY_HASH);
546
+ });
547
+ });
548
+ /** See 'lays a pinned document out exactly as postext 1.22 did'. */
549
+ const LEGACY_HASH = 'c50e5ec7';
550
+ //# sourceMappingURL=codeBlocks.test.js.map