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
@@ -7,7 +7,7 @@
7
7
  import { attachEquationAnchors } from './equationLabels';
8
8
  import { parseAttrBlobStrict, parseDirectiveAttrs } from './attrs';
9
9
  import { extractInlineMath, fixMathSourceMap, injectMathSpans } from './inlineMath';
10
- import { BREAK_PLACEHOLDER, TITLE_BREAK_RE, extractInlineChips, extractInlineFootnotes, extractInlineRefs, injectFootnoteSpans, extractInlineSwatches, injectChipSpans, injectRefSpans, injectSwatchSpans, parseInlineFormatting, protectCodeSpans, titleBreakIndices, trimSpans } from './inlineFormatting';
10
+ import { BREAK_PLACEHOLDER, TITLE_BREAK_RE, extractInlineChips, extractInlineFootnotes, extractInlineRefs, injectFootnoteSpans, extractInlineSwatches, injectChipSpans, injectRefSpans, injectSwatchSpans, parseInlineFormatting, protectCodeSpans, replaceBodyBreaks, BODY_BREAK_MARK, bodyBreakSpans, titleBreakIndices, trimSpans, extractInlineTabs, injectTabSpans } from './inlineFormatting';
11
11
  import { buildBlockMapping } from './sourceMapping';
12
12
  import { extractInlineCitations, injectCitationSpans } from './citations';
13
13
  import { attachIndexMarks, extractIndexMarks, remapParseOffsets } from './indexMarks';
@@ -15,13 +15,18 @@ import { joinEastAsianLines } from './softBreaks';
15
15
  import { asciiDigits } from '../arabicNumerals';
16
16
  import { indentColumn, listDepth, nestListItem } from './listNesting';
17
17
  import { parseComicFence } from '../comics/page';
18
+ import { closesCodeFence, codeFenceOpen } from './codeFence';
18
19
  export { parseDirectiveAttrs, spaceDirectiveLines, MAX_SPACE_LINES } from './attrs';
20
+ export { codeFenceOpen } from './codeFence';
19
21
  const HEADING_RE = /^(#{1,6})\s+(.+)$/;
20
22
  /** `:::name` or `:::name{attrs}` on its own line. Shared by single-line
21
23
  * directives and container opening fences. */
22
24
  const DIRECTIVE_RE = /^:::\s*([a-z][a-z0-9-]*)\s*(?:\{([^}]*)\})?\s*$/;
23
25
  /** A bare `:::` line: closes the innermost open container. */
24
26
  const CONTAINER_CLOSE_RE = /^:::\s*$/;
27
+ /** A bayt's hemistichs cut at a `\\` with a space on each side (the
28
+ * Wikisource markup, #378). */
29
+ const HEMISTICH_WIKI_RE = /\s\\\\\s/;
25
30
  /** Set of directive names recognized today. Unknown names fall through to
26
31
  * paragraph-parsing and downstream warnings flag them. */
27
32
  export const KNOWN_DIRECTIVES = new Set(['pagebreak', 'numbering', 'columnbreak', 'space', 'toc', 'index', 'bibliography', 'references', 'verse', 'page', 'strip']);
@@ -74,28 +79,81 @@ const INTERRUPTING_MATH_SINGLE_RE = /^\s*\$\$((?:(?!\$\$)[\s\S])+)\$\$\s*$/;
74
79
  /** `::resource{id="..."}` block embed on its own line. Malformed variants
75
80
  * (missing/empty id, extra attrs) fall through to paragraph parsing. */
76
81
  const RESOURCE_DIRECTIVE_RE = /^::resource\s*\{id="([^"]+)"\}\s*$/;
77
- // Small LRU memo used by parseMarkdownMemo, keyed by the input string. The
78
- // returned array and its ContentBlocks are treated as read-only by the rest
79
- // of the pipeline. A few slots (not one) because a book is counted chapter
80
- // by chapter — continuation, outline, warnings and the layout itself each
81
- // parse the same few chapters in turn, and a single slot thrashed between
82
- // them.
82
+ /** A fence's info string read (#624): the first word is the language
83
+ * (lower-cased); a `{…}` block after it, or `key=value` pairs, are
84
+ * attributes in the directive grammar; anything else after the language
85
+ * is the title (```` ```console Terminal ````). */
86
+ export function readCodeInfo(info) {
87
+ let rest = info.trim();
88
+ let lang;
89
+ const first = /^([^\s{]+)\s*/.exec(rest);
90
+ if (first) {
91
+ lang = first[1].toLowerCase();
92
+ rest = rest.slice(first[0].length);
93
+ }
94
+ let attrs = {};
95
+ const brace = /\{([^{}]*)\}\s*$/.exec(rest);
96
+ if (brace) {
97
+ attrs = parseDirectiveAttrs(brace[1]);
98
+ rest = rest.slice(0, brace.index).trim();
99
+ }
100
+ else if (/[=\uFF1D]/.test(rest)) {
101
+ attrs = parseDirectiveAttrs(rest);
102
+ rest = '';
103
+ }
104
+ const title = attrs.title !== undefined ? attrs.title : rest !== '' ? rest : undefined;
105
+ return { ...(lang ? { lang } : {}), attrs, ...(title !== undefined && title.trim() !== '' ? { title: title.trim() } : {}) };
106
+ }
107
+ /** How far a line of indented code is indented (#624): the length of the
108
+ * leading whitespace that reaches column 4 (a tab to the next multiple of
109
+ * 4), or -1 when it does not reach it. */
110
+ function indentedCodeCut(line) {
111
+ let col = 0;
112
+ for (let i = 0; i < line.length; i++) {
113
+ const c = line[i];
114
+ if (c === ' ')
115
+ col += 1;
116
+ else if (c === '\t')
117
+ col = col - (col % 4) + 4;
118
+ else
119
+ return -1;
120
+ if (col >= 4)
121
+ return i + 1;
122
+ }
123
+ return -1;
124
+ }
125
+ // Small LRU memo used by parseMarkdownMemo, keyed by the input string (one
126
+ // per way of reading it, see `ParseOptions`). The returned array and its
127
+ // ContentBlocks are treated as read-only by the rest of the pipeline. A few
128
+ // slots (not one) because a book is counted chapter by chapter —
129
+ // continuation, outline, warnings and the layout itself each parse the same
130
+ // few chapters in turn, and a single slot thrashed between them.
83
131
  const PARSE_MEMO_SLOTS = 8;
84
- const _parseMemo = new Map();
85
- function parseMemoLookup(markdown) {
86
- const hit = _parseMemo.get(markdown);
132
+ const _parseMemos = new Map();
133
+ /** The options as one key: the default reading is `''`. */
134
+ function optionsKey(options) {
135
+ const fences = options?.fences !== false;
136
+ const indented = options?.indentedCode === true;
137
+ return fences && !indented ? '' : `${fences ? 'f' : 'n'}${indented ? 'i' : ''}`;
138
+ }
139
+ function parseMemoLookup(markdown, options) {
140
+ const key = optionsKey(options);
141
+ let memo = _parseMemos.get(key);
142
+ if (!memo)
143
+ _parseMemos.set(key, (memo = new Map()));
144
+ const hit = memo.get(markdown);
87
145
  if (hit) {
88
146
  // Refresh recency: a Map iterates in insertion order.
89
- _parseMemo.delete(markdown);
90
- _parseMemo.set(markdown, hit);
147
+ memo.delete(markdown);
148
+ memo.set(markdown, hit);
91
149
  return hit;
92
150
  }
93
- const result = parseMarkdownWithIssues(markdown);
94
- _parseMemo.set(markdown, result);
95
- if (_parseMemo.size > PARSE_MEMO_SLOTS) {
96
- const oldest = _parseMemo.keys().next().value;
151
+ const result = parseMarkdownWithIssues(markdown, options);
152
+ memo.set(markdown, result);
153
+ if (memo.size > PARSE_MEMO_SLOTS) {
154
+ const oldest = memo.keys().next().value;
97
155
  if (oldest !== undefined)
98
- _parseMemo.delete(oldest);
156
+ memo.delete(oldest);
99
157
  }
100
158
  return result;
101
159
  }
@@ -107,30 +165,58 @@ export const PARSE_MEMO_CAPACITY = PARSE_MEMO_SLOTS;
107
165
  * This avoids reparsing a document on each keystroke when upstream
108
166
  * recomputes only because a sibling state changed.
109
167
  */
110
- export function parseMarkdownMemo(markdown) {
111
- return parseMemoLookup(markdown).blocks;
168
+ export function parseMarkdownMemo(markdown, options) {
169
+ return parseMemoLookup(markdown, options).blocks;
112
170
  }
113
- export function parseMarkdownWithIssuesMemo(markdown) {
114
- return parseMemoLookup(markdown);
171
+ export function parseMarkdownWithIssuesMemo(markdown, options) {
172
+ return parseMemoLookup(markdown, options);
115
173
  }
116
- export function parseMarkdown(markdown) {
117
- return parseMarkdownWithIssues(markdown).blocks;
174
+ export function parseMarkdown(markdown, options) {
175
+ return parseMarkdownWithIssues(markdown, options).blocks;
118
176
  }
119
177
  /**
120
178
  * Merge consecutive blockquote lines into a single block, and consecutive
121
179
  * non-blank, non-special lines into paragraphs. Index marks (`:index…`)
122
180
  * are taken out first and attached to the blocks holding them
123
181
  * (`ContentBlock.indexMarks`); every source offset still points into
124
- * `markdown`.
182
+ * `markdown`. `options` picks how code is read (#624): fences on, indented
183
+ * code off by default.
125
184
  */
126
- export function parseMarkdownWithIssues(markdown) {
127
- const marks = extractIndexMarks(markdown);
185
+ export function parseMarkdownWithIssues(markdown, options) {
186
+ const marks = extractIndexMarks(markdown, { fences: options?.fences !== false });
128
187
  if (!marks)
129
- return attachDirections(attachContainerAnchors(attachEquationAnchors(parseBlocks(markdown), markdown)));
130
- const result = parseBlocks(marks.text);
188
+ return attachInlineCode(attachDirections(attachContainerAnchors(attachEquationAnchors(parseBlocks(markdown, options), markdown))), markdown);
189
+ const result = parseBlocks(marks.text, options);
131
190
  attachIndexMarks(result.blocks, marks.marks);
132
191
  remapParseOffsets(result, marks.toOriginal);
133
- return attachDirections(attachContainerAnchors(attachEquationAnchors(result, markdown)));
192
+ return attachInlineCode(attachDirections(attachContainerAnchors(attachEquationAnchors(result, markdown))), markdown);
193
+ }
194
+ /** Inline code (`` `x` ``, outside an escape) between two backticks on
195
+ * one line, as `protectCodeSpans` reads it. */
196
+ const INLINE_CODE_RE = /(?<!\\)`([^`\n]+?)`/g;
197
+ /** Text blocks whose inline code a layout may set in the code face. */
198
+ const INLINE_CODE_HOSTS = new Set(['heading', 'paragraph', 'blockquote', 'listItem']);
199
+ /**
200
+ * The inline code of each text block (#624): the source ranges of the text
201
+ * between the backticks of its code spans (`ContentBlock.inlineCode`), read
202
+ * from the block's own source. Blocks without a backtick are left alone.
203
+ */
204
+ function attachInlineCode(result, markdown) {
205
+ for (const b of result.blocks) {
206
+ if (!INLINE_CODE_HOSTS.has(b.type) || b.verse || b.sourceEnd <= b.sourceStart)
207
+ continue;
208
+ const source = markdown.slice(b.sourceStart, b.sourceEnd);
209
+ if (!source.includes('`'))
210
+ continue;
211
+ const ranges = [];
212
+ INLINE_CODE_RE.lastIndex = 0;
213
+ for (let m = INLINE_CODE_RE.exec(source); m; m = INLINE_CODE_RE.exec(source)) {
214
+ ranges.push({ start: b.sourceStart + m.index + 1, end: b.sourceStart + m.index + 1 + m[1].length });
215
+ }
216
+ if (ranges.length > 0)
217
+ b.inlineCode = ranges;
218
+ }
219
+ return result;
134
220
  }
135
221
  /** The direction a `dir` attribute names (`{dir=rtl}`, any case), or
136
222
  * undefined for none or a value other than `ltr` / `rtl`. */
@@ -179,8 +265,10 @@ function attachDirections(result) {
179
265
  const d = own ?? inherited;
180
266
  if (d)
181
267
  b.direction = d;
182
- if (inheritedLang)
183
- b.lang = inheritedLang;
268
+ // A poem's own `lang` (`:::verse{dir=ltr lang=en}`, #620).
269
+ const lang = (b.verse ? langAttr(b.verse.attrs) : undefined) ?? inheritedLang;
270
+ if (lang)
271
+ b.lang = lang;
184
272
  }
185
273
  return result;
186
274
  }
@@ -220,7 +308,9 @@ function joinedLines(markdown, mapping) {
220
308
  fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
221
309
  return joinEastAsianLines(markdown, mapping.text, mapping.spans, mapping.sourceMap) ?? mapping;
222
310
  }
223
- function parseBlocks(markdown) {
311
+ function parseBlocks(markdown, options) {
312
+ const fences = options?.fences !== false;
313
+ const indentedCode = options?.indentedCode === true;
224
314
  const blocks = [];
225
315
  const issues = [];
226
316
  const rawLines = markdown.split('\n');
@@ -239,31 +329,82 @@ function parseBlocks(markdown) {
239
329
  /** The spans, plain text and source map of a run of inline Markdown
240
330
  * (`raw`, whose source runs from `srcStart` to `srcEnd`): chips,
241
331
  * footnote markers, references, citations, swatches, maths and the
242
- * inline formatting, as a paragraph's text is read. */
243
- const inlineRun = (raw, srcStart, srcEnd) => {
332
+ * inline formatting, as a paragraph's text is read. With `hardBreaks`
333
+ * its forced line breaks (#620) become `BREAK_PLACEHOLDER` (see
334
+ * `replaceBodyBreaks`; `raw` joins its lines with a line feed after a
335
+ * line that ends in a backslash). Maths issues are reported once, by
336
+ * the reading with `report`. */
337
+ const inlineRun = (raw, srcStart, srcEnd, hardBreaks = false, report = true) => {
244
338
  const chipExtract = extractInlineChips(protectCodeSpans(raw), srcStart);
245
339
  const fnExtract = extractInlineFootnotes(chipExtract.cleaned, srcStart);
246
340
  const refExtract = extractInlineRefs(fnExtract.cleaned, srcStart);
247
341
  const citeExtract = extractInlineCitations(refExtract.cleaned, srcStart);
248
342
  const swExtract = extractInlineSwatches(citeExtract.cleaned, srcStart);
249
343
  const mathExtract = extractInlineMath(swExtract.cleaned, null, srcStart, srcEnd);
250
- issues.push(...mathExtract.issues);
251
- const rawSpans = injectChipSpans(injectFootnoteSpans(injectRefSpans(injectCitationSpans(injectSwatchSpans(injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths), swExtract.swatches), citeExtract.citations), refExtract.refs), fnExtract.markers), chipExtract.chips);
344
+ if (report)
345
+ issues.push(...mathExtract.issues);
346
+ // Tabs (#622): `:tab` and tab characters, outside code and maths.
347
+ const tabExtract = extractInlineTabs(mathExtract.cleaned);
348
+ const formatted = hardBreaks
349
+ ? bodyBreakSpans(parseInlineFormatting(replaceBodyBreaks(tabExtract.cleaned, BODY_BREAK_MARK)))
350
+ : parseInlineFormatting(tabExtract.cleaned);
351
+ const rawSpans = injectChipSpans(injectFootnoteSpans(injectRefSpans(injectCitationSpans(injectSwatchSpans(injectMathSpans(injectTabSpans(formatted, tabExtract.tabs), mathExtract.maths), swExtract.swatches), citeExtract.citations), refExtract.refs), fnExtract.markers), chipExtract.chips);
252
352
  return joinedLines(markdown, buildBlockMapping(markdown, srcStart, srcEnd, rawSpans));
253
353
  };
254
354
  /**
255
- * A `:::verse` poem whose fence is line `start` (#378): every non-blank
256
- * line up to the closing `:::` is a bayt, its two hemistichs split at the
257
- * first `||` (or a `\\` with a space on each side, the Wikisource
258
- * markup); a line without one is a single hemistich. The poem is one
259
- * paragraph block (`ContentBlock.verse`) whose text joins the
260
- * hemistichs with a tab and the bayts with a line feed; each hemistich
261
- * is read as inline Markdown on its own, and the tab maps to the
262
- * separator in the source, the line feed to the line's end. An unclosed
263
- * poem runs to the end of the text. A poem with no line is dropped.
355
+ * A block's lines (trimmed) read as one run of inline Markdown with its
356
+ * forced line breaks (#620): joined with a space, or with a line feed
357
+ * after a line that ends in a backslash, for `replaceBodyBreaks`. When
358
+ * the block has a break, `literalBreaks` is the reading postext 1.22
359
+ * made of it (every line joined with a space, the backslashes printed).
360
+ */
361
+ const breakingRun = (lines, srcStart, srcEnd) => {
362
+ let raw = '';
363
+ lines.forEach((l, k) => {
364
+ if (k > 0)
365
+ raw += lines[k - 1].endsWith('\\') ? '\n' : ' ';
366
+ raw += l;
367
+ });
368
+ if (!raw.includes('\\'))
369
+ return inlineRun(raw, srcStart, srcEnd);
370
+ const mapping = inlineRun(raw, srcStart, srcEnd, true);
371
+ if (!mapping.text.includes(BREAK_PLACEHOLDER))
372
+ return mapping;
373
+ const literal = inlineRun(lines.join(' '), srcStart, srcEnd, false, false);
374
+ return { ...mapping, literalBreaks: { text: literal.text, spans: literal.spans, sourceMap: literal.sourceMap } };
375
+ };
376
+ /** Poems in the line layout read so far (`VerseStanza.poem`). */
377
+ let poemCount = 0;
378
+ /**
379
+ * A `:::verse` poem whose fence is line `start` (#378, #620), up to the
380
+ * closing `:::` (an unclosed poem runs to the end of the text). The fence's
381
+ * `layout` picks how its lines are read: `bayt` (see {@link parseBayts}),
382
+ * `lines` (see {@link parseVerseLines}), or, unset or `auto`, `bayt` when
383
+ * any line carries a hemistich separator (`||`, a spaced `\\`) and
384
+ * `lines` otherwise.
264
385
  */
265
386
  const parseVerse = (start, attrsRaw) => {
266
- let k = start + 1;
387
+ const attrs = parseDirectiveAttrs(attrsRaw);
388
+ let end = start + 1;
389
+ while (end < rawLines.length && !CONTAINER_CLOSE_RE.test(rawLines[end].trim()))
390
+ end++;
391
+ const layout = attrs.layout?.trim().toLowerCase();
392
+ const separated = rawLines.slice(start + 1, end).some((raw) => raw.includes('||') || HEMISTICH_WIKI_RE.test(raw));
393
+ if (layout === 'bayt' || (layout !== 'lines' && separated))
394
+ return parseBayts(start, end, attrs);
395
+ return parseVerseLines(start, end, attrs, layout !== 'lines');
396
+ };
397
+ /**
398
+ * A poem in the bayt layout (#378): every non-blank line is a bayt, its
399
+ * two hemistichs split at the first `||` (or a `\\` with a space on
400
+ * each side, the Wikisource markup); a line without one is a single
401
+ * hemistich. The poem is one paragraph block (`ContentBlock.verse`) whose
402
+ * text joins the hemistichs with a tab and the bayts with a line feed;
403
+ * each hemistich is read as inline Markdown on its own, and the tab maps
404
+ * to the separator in the source, the line feed to the line's end. A
405
+ * poem with no line is dropped.
406
+ */
407
+ const parseBayts = (start, end, attrs) => {
267
408
  let text = '';
268
409
  const spans = [];
269
410
  const sourceMap = [];
@@ -284,17 +425,15 @@ function parseBlocks(markdown) {
284
425
  sourceMap.push(...run.sourceMap);
285
426
  };
286
427
  let lastLine = start;
287
- for (; k < rawLines.length; k++) {
428
+ for (let k = start + 1; k < end; k++) {
288
429
  const raw = rawLines[k];
289
- if (CONTAINER_CLOSE_RE.test(raw.trim()))
290
- break;
291
430
  if (raw.trim() === '')
292
431
  continue;
293
432
  if (text !== '')
294
433
  sep('\n', lineOffsets[k] - 1);
295
434
  const lineStart = lineOffsets[k];
296
435
  const bar = raw.indexOf('||');
297
- const wiki = bar < 0 ? /\s\\\\\s/.exec(raw) : null;
436
+ const wiki = bar < 0 ? HEMISTICH_WIKI_RE.exec(raw) : null;
298
437
  const cut = bar >= 0 ? bar : wiki ? wiki.index + 1 : -1;
299
438
  if (cut >= 0) {
300
439
  hemistich(lineStart, lineStart + cut);
@@ -306,24 +445,157 @@ function parseBlocks(markdown) {
306
445
  }
307
446
  lastLine = k;
308
447
  }
309
- const closed = k < rawLines.length;
310
- const next = closed ? k + 1 : k;
448
+ const closed = end < rawLines.length;
449
+ const next = closed ? end + 1 : end;
311
450
  if (text === '')
312
- return { next };
313
- const attrs = parseDirectiveAttrs(attrsRaw);
451
+ return { blocks: [], next };
314
452
  return {
315
- block: {
316
- type: 'paragraph',
317
- text,
318
- spans,
319
- verse: { attrs },
320
- sourceStart: lineOffsets[start],
321
- sourceEnd: closed ? lineEndOffset(k) : lineEndOffset(lastLine),
322
- sourceMap,
323
- },
453
+ blocks: [{
454
+ type: 'paragraph',
455
+ text,
456
+ spans,
457
+ verse: { attrs },
458
+ sourceStart: lineOffsets[start],
459
+ sourceEnd: closed ? lineEndOffset(end) : lineEndOffset(lastLine),
460
+ sourceMap,
461
+ }],
324
462
  next,
325
463
  };
326
464
  };
465
+ /**
466
+ * A poem in the line layout (#620): every non-blank line is a line of
467
+ * verse, read as inline Markdown on its own; a run of blank lines ends a
468
+ * stanza. Each stanza is a paragraph block (`VerseInfo.stanza`) whose text
469
+ * joins its lines with a line feed, mapped to the source's line end. A
470
+ * line's leading whitespace is its indent (`VerseLineInfo.indent`), kept
471
+ * out of the text; a line written `+ …` is a stepped line. With
472
+ * `keepSpaces` on the fence, a run of two or more spaces inside a line (a
473
+ * caesura) is kept as a fixed space of its width. The first stanza opens
474
+ * on the fence line, the last closes on the closing fence.
475
+ */
476
+ const parseVerseLines = (start, end, attrs, auto) => {
477
+ const closed = end < rawLines.length;
478
+ const next = closed ? end + 1 : end;
479
+ const keepSpaces = attrs.keepSpaces !== undefined && attrs.keepSpaces.trim().toLowerCase() !== 'false';
480
+ // The stanzas, as runs of source line indices.
481
+ const groups = [];
482
+ let run = [];
483
+ for (let k = start + 1; k < end; k++) {
484
+ if (rawLines[k].trim() === '') {
485
+ if (run.length > 0)
486
+ groups.push(run);
487
+ run = [];
488
+ }
489
+ else {
490
+ run.push(k);
491
+ }
492
+ }
493
+ if (run.length > 0)
494
+ groups.push(run);
495
+ const stanzas = [];
496
+ for (const rows of groups) {
497
+ const st = { text: '', spans: [], sourceMap: [], lines: [], from: rows[0], to: rows[0] };
498
+ for (const k of rows) {
499
+ const raw = rawLines[k];
500
+ const lineStart = lineOffsets[k];
501
+ let p = 0;
502
+ let indent = 0;
503
+ for (; p < raw.length; p++) {
504
+ const c = raw[p];
505
+ if (c === ' ')
506
+ indent += 1;
507
+ else if (c === '\t')
508
+ indent += 4;
509
+ else if (c === '\u3000')
510
+ indent += 2;
511
+ else
512
+ break;
513
+ }
514
+ let stepped = false;
515
+ // `\+ …`: a line that opens with a plus sign, not a stepped one.
516
+ if (raw[p] === '\\' && raw[p + 1] === '+')
517
+ p++;
518
+ else if (raw[p] === '+' && (raw[p + 1] === ' ' || raw[p + 1] === '\t')) {
519
+ stepped = true;
520
+ p++;
521
+ while (raw[p] === ' ' || raw[p] === '\t')
522
+ p++;
523
+ }
524
+ const bodyEnd = raw.trimEnd().length;
525
+ // The line's pieces of text, and the caesura gaps between them.
526
+ const pieces = [];
527
+ if (keepSpaces) {
528
+ const gapRe = / {2,}/g;
529
+ let at = p;
530
+ for (let m = gapRe.exec(raw); m && m.index < bodyEnd; m = gapRe.exec(raw)) {
531
+ if (m.index < p)
532
+ continue;
533
+ if (m.index > at)
534
+ pieces.push({ from: at, to: m.index, gap: false });
535
+ pieces.push({ from: m.index, to: m.index + m[0].length, gap: true });
536
+ at = m.index + m[0].length;
537
+ }
538
+ if (at < bodyEnd)
539
+ pieces.push({ from: at, to: bodyEnd, gap: false });
540
+ }
541
+ else if (p < bodyEnd) {
542
+ pieces.push({ from: p, to: bodyEnd, gap: false });
543
+ }
544
+ let text = '';
545
+ const spans = [];
546
+ const sourceMap = [];
547
+ for (const piece of pieces) {
548
+ if (piece.gap) {
549
+ if (text === '')
550
+ continue;
551
+ const width = piece.to - piece.from;
552
+ text += ' '.repeat(width);
553
+ spans.push({ text: ' '.repeat(width), bold: false, italic: false, fixedSpace: true });
554
+ for (let c = piece.from; c < piece.to; c++)
555
+ sourceMap.push(lineStart + c);
556
+ continue;
557
+ }
558
+ const r = inlineRun(raw.slice(piece.from, piece.to), lineStart + piece.from, lineStart + piece.to);
559
+ text += r.text;
560
+ spans.push(...r.spans);
561
+ sourceMap.push(...r.sourceMap);
562
+ }
563
+ if (text.trim() === '')
564
+ continue;
565
+ if (st.text !== '') {
566
+ st.text += '\n';
567
+ st.spans.push({ text: '\n', bold: false, italic: false });
568
+ st.sourceMap.push(lineStart - 1);
569
+ }
570
+ st.text += text;
571
+ st.spans.push(...spans);
572
+ st.sourceMap.push(...sourceMap);
573
+ st.lines.push({ indent, ...(stepped ? { stepped: true } : {}) });
574
+ st.to = k;
575
+ }
576
+ if (st.lines.length > 0)
577
+ stanzas.push(st);
578
+ }
579
+ if (stanzas.length === 0)
580
+ return { blocks: [], next };
581
+ const poem = poemCount++;
582
+ let firstLine = 0;
583
+ const blocks = stanzas.map((st, index) => {
584
+ const last = index === stanzas.length - 1;
585
+ const stanza = { poem, index, last, firstLine, lines: st.lines, ...(auto ? { auto: true } : {}) };
586
+ firstLine += st.lines.length;
587
+ return {
588
+ type: 'paragraph',
589
+ text: st.text,
590
+ spans: st.spans,
591
+ verse: { attrs, stanza },
592
+ sourceStart: index === 0 ? lineOffsets[start] : lineOffsets[st.from],
593
+ sourceEnd: last && closed ? lineEndOffset(end) : lineEndOffset(st.to),
594
+ sourceMap: st.sourceMap,
595
+ };
596
+ });
597
+ return { blocks, next };
598
+ };
327
599
  // Open fenced containers, innermost last. Each entry remembers what it
328
600
  // needs to emit the matching `containerEnd` (or an `unclosedContainer`
329
601
  // issue pointing at the opening line when EOF arrives first).
@@ -360,6 +632,98 @@ function parseBlocks(markdown) {
360
632
  * lines (the pipeline numbers adjacent items as one list); any other
361
633
  * block closes it. */
362
634
  let openListItems = [];
635
+ /**
636
+ * A code block (#624) of source lines `from` … `to - 1`, each cut at
637
+ * `cuts[k]` characters from its start (the fence's indentation, the four
638
+ * columns of indented code); the block's range runs from `srcStart` to
639
+ * `srcEnd`. The lines are kept as written (a carriage return before the
640
+ * line feed dropped), joined with a line feed mapped to the source's
641
+ * line end.
642
+ */
643
+ const codeBlock = (from, to, cuts, srcStart, srcEnd, info) => {
644
+ const lines = [];
645
+ const lineStarts = [];
646
+ let text = '';
647
+ const sourceMap = [];
648
+ for (let k = from; k < to; k++) {
649
+ const raw = rawLines[k];
650
+ const cut = Math.min(cuts[k - from] ?? 0, raw.length);
651
+ const body = raw.slice(cut).replace(/\r$/, '');
652
+ const at = lineOffsets[k] + cut;
653
+ if (k > from) {
654
+ text += '\n';
655
+ sourceMap.push(lineEndOffset(k - 1));
656
+ }
657
+ lines.push(body);
658
+ lineStarts.push(at);
659
+ text += body;
660
+ for (let j = 0; j < body.length; j++)
661
+ sourceMap.push(at + j);
662
+ }
663
+ return {
664
+ type: 'code',
665
+ text,
666
+ spans: text.length > 0 ? [{ text, bold: false, italic: false }] : [],
667
+ code: { ...info, lines, lineStarts },
668
+ sourceStart: srcStart,
669
+ sourceEnd: srcEnd,
670
+ sourceMap,
671
+ };
672
+ };
673
+ /** A fenced code block (#624) opening at line `start`, up to its closing
674
+ * fence; a fence left open runs to the end of the text and is reported
675
+ * (`unclosedCodeBlock`). */
676
+ const parseCodeFence = (start, open) => {
677
+ let end = start + 1;
678
+ while (end < rawLines.length && !closesCodeFence(rawLines[end], open.marker))
679
+ end++;
680
+ const closed = end < rawLines.length;
681
+ const cuts = [];
682
+ for (let k = start + 1; k < end; k++) {
683
+ // The fence's indentation comes off each line, as far as it has
684
+ // leading spaces.
685
+ const lead = /^ */.exec(rawLines[k])[0].length;
686
+ cuts.push(Math.min(open.indent, lead));
687
+ }
688
+ const fence = open.marker[0] === '`' ? '```' : '~~~';
689
+ const info = readCodeInfo(open.info);
690
+ const srcStart = lineOffsets[start];
691
+ const srcEnd = closed ? lineEndOffset(end) : lineEndOffset(rawLines.length - 1);
692
+ blocks.push(codeBlock(start + 1, end, cuts, srcStart, srcEnd, { ...info, info: open.info, fence }));
693
+ if (!closed) {
694
+ issues.push({
695
+ kind: 'unclosedCodeBlock',
696
+ delimiter: fence,
697
+ ...(info.lang ? { lang: info.lang } : {}),
698
+ sourceStart: srcStart,
699
+ sourceEnd: lineEndOffset(start),
700
+ });
701
+ }
702
+ return closed ? end + 1 : end;
703
+ };
704
+ /** Indented code (#624, `codeStyle.indentedCode`): the run of lines from
705
+ * `start` indented four columns or blank, its trailing blank lines left
706
+ * out. */
707
+ const parseIndentedCode = (start) => {
708
+ let end = start;
709
+ let last = start;
710
+ while (end < rawLines.length) {
711
+ const raw = rawLines[end];
712
+ if (raw.trim() === '') {
713
+ end++;
714
+ continue;
715
+ }
716
+ if (indentedCodeCut(raw) < 0)
717
+ break;
718
+ last = end;
719
+ end++;
720
+ }
721
+ const cuts = [];
722
+ for (let k = start; k <= last; k++)
723
+ cuts.push(Math.max(0, indentedCodeCut(rawLines[k])));
724
+ blocks.push(codeBlock(start, last + 1, cuts, lineOffsets[start], lineEndOffset(last), { info: '', attrs: {}, fence: 'indent' }));
725
+ return last + 1;
726
+ };
363
727
  let i = 0;
364
728
  while (i < rawLines.length) {
365
729
  const line = rawLines[i];
@@ -369,6 +733,22 @@ function parseBlocks(markdown) {
369
733
  i++;
370
734
  continue;
371
735
  }
736
+ // A code block (#624): a ``` or ~~~ fence, everything up to its
737
+ // closing fence kept as written.
738
+ const fenceOpen = fences ? codeFenceOpen(line) : undefined;
739
+ if (fenceOpen) {
740
+ i = parseCodeFence(i, fenceOpen);
741
+ continue;
742
+ }
743
+ // Indented code, when the reading asks for it: four columns in, after
744
+ // a blank line (or at the start), and not a list item under a list.
745
+ if (indentedCode
746
+ && (i === 0 || rawLines[i - 1].trim() === '')
747
+ && indentedCodeCut(line) >= 0
748
+ && !(blocks[blocks.length - 1]?.type === 'listItem' && (TASK_ITEM_RE.test(line) || ORDERED_LIST_ITEM_RE.test(line) || LIST_ITEM_RE.test(line)))) {
749
+ i = parseIndentedCode(i);
750
+ continue;
751
+ }
372
752
  // Display math — single-line `$$ ... $$`
373
753
  const singleMath = line.match(BLOCK_MATH_SINGLE_RE);
374
754
  if (singleMath) {
@@ -518,11 +898,11 @@ function parseBlocks(markdown) {
518
898
  i++;
519
899
  continue;
520
900
  }
521
- // A poem (#378): `:::verse{attrs}` … `:::`, one bayt a line.
901
+ // A poem (#378, #620): `:::verse{attrs}` … `:::`, a bayt or a line of
902
+ // verse a line.
522
903
  if (refsMatch && refsMatch[1] === 'verse') {
523
904
  const verse = parseVerse(i, refsMatch[2] ?? '');
524
- if (verse.block)
525
- blocks.push(verse.block);
905
+ blocks.push(...verse.blocks);
526
906
  i = verse.next;
527
907
  continue;
528
908
  }
@@ -686,20 +1066,13 @@ function parseBlocks(markdown) {
686
1066
  }
687
1067
  const contentOffset = leading + markerLength;
688
1068
  const itemSrcStart = srcStart + contentOffset;
689
- const chipExtract = extractInlineChips(protectCodeSpans(itemText), itemSrcStart);
690
- const fnExtract = extractInlineFootnotes(chipExtract.cleaned, itemSrcStart);
691
- const refExtract = extractInlineRefs(fnExtract.cleaned, itemSrcStart);
692
- const citeExtract = extractInlineCitations(refExtract.cleaned, itemSrcStart);
693
- const swExtract = extractInlineSwatches(citeExtract.cleaned, itemSrcStart);
694
- const mathExtract = extractInlineMath(swExtract.cleaned, null, itemSrcStart, srcEnd);
695
- issues.push(...mathExtract.issues);
696
- const rawSpans = injectChipSpans(injectFootnoteSpans(injectRefSpans(injectCitationSpans(injectSwatchSpans(injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths), swExtract.swatches), citeExtract.citations), refExtract.refs), fnExtract.markers), chipExtract.chips);
697
- const mapping = buildBlockMapping(markdown, itemSrcStart, srcEnd, rawSpans);
698
- fixMathSourceMap(mapping.text, mapping.spans, mapping.sourceMap);
1069
+ // A `\\` in the item is a forced line break (#620).
1070
+ const mapping = breakingRun([itemText], itemSrcStart, srcEnd);
699
1071
  const block = {
700
1072
  type: 'listItem',
701
1073
  text: mapping.text,
702
1074
  spans: mapping.spans.length > 0 ? mapping.spans : [{ text: '', bold: false, italic: false }],
1075
+ ...(mapping.literalBreaks ? { literalBreaks: mapping.literalBreaks } : {}),
703
1076
  depth,
704
1077
  listKind,
705
1078
  sourceStart: srcStart,
@@ -742,17 +1115,9 @@ function parseBlocks(markdown) {
742
1115
  }
743
1116
  const srcStart = lineOffsets[startIdx];
744
1117
  const srcEnd = lineEndOffset(lastIdx);
745
- const chipExtract = extractInlineChips(protectCodeSpans(quoteLines.join(' ')), srcStart);
746
- const fnExtract = extractInlineFootnotes(chipExtract.cleaned, srcStart);
747
- const refExtract = extractInlineRefs(fnExtract.cleaned, srcStart);
748
- const citeExtract = extractInlineCitations(refExtract.cleaned, srcStart);
749
- const swExtract = extractInlineSwatches(citeExtract.cleaned, srcStart);
750
- const mathExtract = extractInlineMath(swExtract.cleaned, null, srcStart, srcEnd);
751
- issues.push(...mathExtract.issues);
752
- const rawSpans = injectChipSpans(injectFootnoteSpans(injectRefSpans(injectCitationSpans(injectSwatchSpans(injectMathSpans(parseInlineFormatting(mathExtract.cleaned), mathExtract.maths), swExtract.swatches), citeExtract.citations), refExtract.refs), fnExtract.markers), chipExtract.chips);
753
1118
  // Lines join with a space, except between Chinese or Japanese
754
- // characters (#181).
755
- const mapping = joinedLines(markdown, buildBlockMapping(markdown, srcStart, srcEnd, rawSpans));
1119
+ // characters (#181) and at a forced line break (#620).
1120
+ const mapping = breakingRun(quoteLines, srcStart, srcEnd);
756
1121
  blocks.push({
757
1122
  type: 'blockquote',
758
1123
  text: mapping.text,
@@ -760,6 +1125,7 @@ function parseBlocks(markdown) {
760
1125
  sourceStart: srcStart,
761
1126
  sourceEnd: srcEnd,
762
1127
  sourceMap: mapping.sourceMap,
1128
+ ...(mapping.literalBreaks ? { literalBreaks: mapping.literalBreaks } : {}),
763
1129
  });
764
1130
  continue;
765
1131
  }
@@ -777,6 +1143,9 @@ function parseBlocks(markdown) {
777
1143
  break;
778
1144
  if (i > startIdx && isFenceLine(pl))
779
1145
  break;
1146
+ // A code fence interrupts the paragraph (#624).
1147
+ if (i > startIdx && fences && codeFenceOpen(rawLine))
1148
+ break;
780
1149
  // A footnote definition opens a paragraph of its own.
781
1150
  if (i > startIdx && FOOTNOTE_DEF_RE.test(pl))
782
1151
  break;
@@ -804,11 +1173,12 @@ function parseBlocks(markdown) {
804
1173
  srcStart += rawLines[startIdx].indexOf(def[0]) + def[0].length;
805
1174
  }
806
1175
  const srcEnd = lineEndOffset(lastIdx);
807
- const mapping = inlineRun(paraLines.join(' '), srcStart, srcEnd);
1176
+ const mapping = breakingRun(paraLines, srcStart, srcEnd);
808
1177
  blocks.push({
809
1178
  type: 'paragraph',
810
1179
  text: mapping.text,
811
1180
  spans: mapping.spans,
1181
+ ...(mapping.literalBreaks ? { literalBreaks: mapping.literalBreaks } : {}),
812
1182
  ...(startIdx === lastDisplayEnd + 1 && !def ? { continuesParagraph: true } : {}),
813
1183
  ...(def ? { footnoteDef: def[1] } : {}),
814
1184
  sourceStart: srcStart,