postext 1.15.1 → 1.16.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 (421) hide show
  1. package/dist/__tests__/arabic/harakatProfile.test.d.ts +2 -0
  2. package/dist/__tests__/arabic/harakatProfile.test.d.ts.map +1 -0
  3. package/dist/__tests__/arabic/harakatProfile.test.js +105 -0
  4. package/dist/__tests__/arabic/harakatProfile.test.js.map +1 -0
  5. package/dist/__tests__/bundleLocaleAndBytes.test.js +9 -9
  6. package/dist/__tests__/bundleLocaleAndBytes.test.js.map +1 -1
  7. package/dist/__tests__/chineseLocale.test.js +10 -5
  8. package/dist/__tests__/chineseLocale.test.js.map +1 -1
  9. package/dist/__tests__/cjk/annotationsRender.test.js +5 -2
  10. package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -1
  11. package/dist/__tests__/cjk/chipTracking.test.d.ts +2 -0
  12. package/dist/__tests__/cjk/chipTracking.test.d.ts.map +1 -0
  13. package/dist/__tests__/cjk/chipTracking.test.js +52 -0
  14. package/dist/__tests__/cjk/chipTracking.test.js.map +1 -0
  15. package/dist/__tests__/cjk/japaneseHtml.test.d.ts +2 -0
  16. package/dist/__tests__/cjk/japaneseHtml.test.d.ts.map +1 -0
  17. package/dist/__tests__/cjk/japaneseHtml.test.js +163 -0
  18. package/dist/__tests__/cjk/japaneseHtml.test.js.map +1 -0
  19. package/dist/__tests__/cjk/japaneseMarks.test.d.ts +2 -0
  20. package/dist/__tests__/cjk/japaneseMarks.test.d.ts.map +1 -0
  21. package/dist/__tests__/cjk/japaneseMarks.test.js +289 -0
  22. package/dist/__tests__/cjk/japaneseMarks.test.js.map +1 -0
  23. package/dist/__tests__/cjk/japaneseMarksRender.test.d.ts +2 -0
  24. package/dist/__tests__/cjk/japaneseMarksRender.test.d.ts.map +1 -0
  25. package/dist/__tests__/cjk/japaneseMarksRender.test.js +161 -0
  26. package/dist/__tests__/cjk/japaneseMarksRender.test.js.map +1 -0
  27. package/dist/__tests__/cjk/japaneseRuby.test.d.ts +2 -0
  28. package/dist/__tests__/cjk/japaneseRuby.test.d.ts.map +1 -0
  29. package/dist/__tests__/cjk/japaneseRuby.test.js +457 -0
  30. package/dist/__tests__/cjk/japaneseRuby.test.js.map +1 -0
  31. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.d.ts +2 -0
  32. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.d.ts.map +1 -0
  33. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js +50 -0
  34. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js.map +1 -0
  35. package/dist/__tests__/cjk/japaneseSpacing.test.d.ts +2 -0
  36. package/dist/__tests__/cjk/japaneseSpacing.test.d.ts.map +1 -0
  37. package/dist/__tests__/cjk/japaneseSpacing.test.js +417 -0
  38. package/dist/__tests__/cjk/japaneseSpacing.test.js.map +1 -0
  39. package/dist/__tests__/cjk/kanbunKunten.test.d.ts +2 -0
  40. package/dist/__tests__/cjk/kanbunKunten.test.d.ts.map +1 -0
  41. package/dist/__tests__/cjk/kanbunKunten.test.js +230 -0
  42. package/dist/__tests__/cjk/kanbunKunten.test.js.map +1 -0
  43. package/dist/__tests__/cjk/kanbunKuntenRender.test.d.ts +2 -0
  44. package/dist/__tests__/cjk/kanbunKuntenRender.test.d.ts.map +1 -0
  45. package/dist/__tests__/cjk/kanbunKuntenRender.test.js +127 -0
  46. package/dist/__tests__/cjk/kanbunKuntenRender.test.js.map +1 -0
  47. package/dist/__tests__/cjk/kinsokuJa.test.d.ts +2 -0
  48. package/dist/__tests__/cjk/kinsokuJa.test.d.ts.map +1 -0
  49. package/dist/__tests__/cjk/kinsokuJa.test.js +388 -0
  50. package/dist/__tests__/cjk/kinsokuJa.test.js.map +1 -0
  51. package/dist/__tests__/cjk/kinsokuJaBuild.test.d.ts +2 -0
  52. package/dist/__tests__/cjk/kinsokuJaBuild.test.d.ts.map +1 -0
  53. package/dist/__tests__/cjk/kinsokuJaBuild.test.js +38 -0
  54. package/dist/__tests__/cjk/kinsokuJaBuild.test.js.map +1 -0
  55. package/dist/__tests__/cjk/resourceAnnotations.test.d.ts +2 -0
  56. package/dist/__tests__/cjk/resourceAnnotations.test.d.ts.map +1 -0
  57. package/dist/__tests__/cjk/resourceAnnotations.test.js +223 -0
  58. package/dist/__tests__/cjk/resourceAnnotations.test.js.map +1 -0
  59. package/dist/__tests__/defaults/cjk.test.js +10 -2
  60. package/dist/__tests__/defaults/cjk.test.js.map +1 -1
  61. package/dist/__tests__/exports.test.js +36 -0
  62. package/dist/__tests__/exports.test.js.map +1 -1
  63. package/dist/__tests__/headingLetterSpacing.test.js +2 -1
  64. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
  65. package/dist/__tests__/indexChinese.test.js +1 -1
  66. package/dist/__tests__/indexJapanese.test.d.ts +2 -0
  67. package/dist/__tests__/indexJapanese.test.d.ts.map +1 -0
  68. package/dist/__tests__/indexJapanese.test.js +242 -0
  69. package/dist/__tests__/indexJapanese.test.js.map +1 -0
  70. package/dist/__tests__/japaneseHeadings.test.d.ts +2 -0
  71. package/dist/__tests__/japaneseHeadings.test.d.ts.map +1 -0
  72. package/dist/__tests__/japaneseHeadings.test.js +319 -0
  73. package/dist/__tests__/japaneseHeadings.test.js.map +1 -0
  74. package/dist/__tests__/japaneseLocale.test.d.ts +2 -0
  75. package/dist/__tests__/japaneseLocale.test.d.ts.map +1 -0
  76. package/dist/__tests__/japaneseLocale.test.js +201 -0
  77. package/dist/__tests__/japaneseLocale.test.js.map +1 -0
  78. package/dist/__tests__/japaneseNotes.test.d.ts +2 -0
  79. package/dist/__tests__/japaneseNotes.test.d.ts.map +1 -0
  80. package/dist/__tests__/japaneseNotes.test.js +336 -0
  81. package/dist/__tests__/japaneseNotes.test.js.map +1 -0
  82. package/dist/__tests__/japaneseNumbering.test.d.ts +2 -0
  83. package/dist/__tests__/japaneseNumbering.test.d.ts.map +1 -0
  84. package/dist/__tests__/japaneseNumbering.test.js +292 -0
  85. package/dist/__tests__/japaneseNumbering.test.js.map +1 -0
  86. package/dist/__tests__/pipeline/citationsJapanese.test.d.ts +2 -0
  87. package/dist/__tests__/pipeline/citationsJapanese.test.d.ts.map +1 -0
  88. package/dist/__tests__/pipeline/citationsJapanese.test.js +110 -0
  89. package/dist/__tests__/pipeline/citationsJapanese.test.js.map +1 -0
  90. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
  91. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  92. package/dist/__tests__/pipeline/safeArea.test.d.ts +2 -0
  93. package/dist/__tests__/pipeline/safeArea.test.d.ts.map +1 -0
  94. package/dist/__tests__/pipeline/safeArea.test.js +207 -0
  95. package/dist/__tests__/pipeline/safeArea.test.js.map +1 -0
  96. package/dist/__tests__/vertical/compositionPaint.test.js +6 -5
  97. package/dist/__tests__/vertical/compositionPaint.test.js.map +1 -1
  98. package/dist/__tests__/vertical/japaneseVertical.test.d.ts +2 -0
  99. package/dist/__tests__/vertical/japaneseVertical.test.d.ts.map +1 -0
  100. package/dist/__tests__/vertical/japaneseVertical.test.js +247 -0
  101. package/dist/__tests__/vertical/japaneseVertical.test.js.map +1 -0
  102. package/dist/__tests__/vertical/verticalSpans.d.ts +2 -1
  103. package/dist/__tests__/vertical/verticalSpans.d.ts.map +1 -1
  104. package/dist/__tests__/vertical/verticalSpans.js +3 -2
  105. package/dist/__tests__/vertical/verticalSpans.js.map +1 -1
  106. package/dist/__tests__/video.test.d.ts +2 -0
  107. package/dist/__tests__/video.test.d.ts.map +1 -0
  108. package/dist/__tests__/video.test.js +305 -0
  109. package/dist/__tests__/video.test.js.map +1 -0
  110. package/dist/arabicMarks.d.ts +11 -1
  111. package/dist/arabicMarks.d.ts.map +1 -1
  112. package/dist/arabicMarks.js +72 -5
  113. package/dist/arabicMarks.js.map +1 -1
  114. package/dist/bundle/adapters.d.ts +6 -0
  115. package/dist/bundle/adapters.d.ts.map +1 -1
  116. package/dist/bundle/adapters.js +30 -0
  117. package/dist/bundle/adapters.js.map +1 -1
  118. package/dist/bundle/codec.d.ts.map +1 -1
  119. package/dist/bundle/codec.js +67 -3
  120. package/dist/bundle/codec.js.map +1 -1
  121. package/dist/bundle/index.d.ts +2 -2
  122. package/dist/bundle/index.d.ts.map +1 -1
  123. package/dist/bundle/index.js +2 -2
  124. package/dist/bundle/index.js.map +1 -1
  125. package/dist/bundle/manifest.d.ts +8 -0
  126. package/dist/bundle/manifest.d.ts.map +1 -1
  127. package/dist/bundle/manifest.js +48 -1
  128. package/dist/bundle/manifest.js.map +1 -1
  129. package/dist/bundle/types.d.ts +5 -0
  130. package/dist/bundle/types.d.ts.map +1 -1
  131. package/dist/bundle/zip.js +1 -1
  132. package/dist/bundle/zip.js.map +1 -1
  133. package/dist/canvas-backend/annotations.d.ts +20 -5
  134. package/dist/canvas-backend/annotations.d.ts.map +1 -1
  135. package/dist/canvas-backend/annotations.js +59 -6
  136. package/dist/canvas-backend/annotations.js.map +1 -1
  137. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  138. package/dist/canvas-backend/blockRender.js +29 -3
  139. package/dist/canvas-backend/blockRender.js.map +1 -1
  140. package/dist/canvas-backend/renderResourceBlock.d.ts +6 -1
  141. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  142. package/dist/canvas-backend/renderResourceBlock.js +124 -5
  143. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  144. package/dist/canvas-backend/verticalText.d.ts +9 -4
  145. package/dist/canvas-backend/verticalText.d.ts.map +1 -1
  146. package/dist/canvas-backend/verticalText.js +20 -9
  147. package/dist/canvas-backend/verticalText.js.map +1 -1
  148. package/dist/chineseNumerals.d.ts +3 -2
  149. package/dist/chineseNumerals.d.ts.map +1 -1
  150. package/dist/chineseNumerals.js +11 -8
  151. package/dist/chineseNumerals.js.map +1 -1
  152. package/dist/citations/bibtex.d.ts.map +1 -1
  153. package/dist/citations/bibtex.js +4 -0
  154. package/dist/citations/bibtex.js.map +1 -1
  155. package/dist/citations/data.d.ts.map +1 -1
  156. package/dist/citations/data.js +6 -1
  157. package/dist/citations/data.js.map +1 -1
  158. package/dist/citations/types.d.ts +8 -0
  159. package/dist/citations/types.d.ts.map +1 -1
  160. package/dist/cjkMarks.d.ts +23 -14
  161. package/dist/cjkMarks.d.ts.map +1 -1
  162. package/dist/cjkMarks.js +251 -49
  163. package/dist/cjkMarks.js.map +1 -1
  164. package/dist/configWarnings.js +9 -3
  165. package/dist/configWarnings.js.map +1 -1
  166. package/dist/defaults/cjk.d.ts +64 -14
  167. package/dist/defaults/cjk.d.ts.map +1 -1
  168. package/dist/defaults/cjk.js +203 -29
  169. package/dist/defaults/cjk.js.map +1 -1
  170. package/dist/defaults/footnotes.d.ts +27 -4
  171. package/dist/defaults/footnotes.d.ts.map +1 -1
  172. package/dist/defaults/footnotes.js +95 -36
  173. package/dist/defaults/footnotes.js.map +1 -1
  174. package/dist/defaults/headings.d.ts +5 -0
  175. package/dist/defaults/headings.d.ts.map +1 -1
  176. package/dist/defaults/headings.js +46 -1
  177. package/dist/defaults/headings.js.map +1 -1
  178. package/dist/defaults/index.d.ts +3 -2
  179. package/dist/defaults/index.d.ts.map +1 -1
  180. package/dist/defaults/index.js +12 -3
  181. package/dist/defaults/index.js.map +1 -1
  182. package/dist/defaults/indexConfig.d.ts.map +1 -1
  183. package/dist/defaults/indexConfig.js +7 -4
  184. package/dist/defaults/indexConfig.js.map +1 -1
  185. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  186. package/dist/defaults/paragraphStyles.js +4 -0
  187. package/dist/defaults/paragraphStyles.js.map +1 -1
  188. package/dist/defaults/resourceTypes.d.ts +16 -3
  189. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  190. package/dist/defaults/resourceTypes.js +56 -8
  191. package/dist/defaults/resourceTypes.js.map +1 -1
  192. package/dist/defaults/shared.d.ts.map +1 -1
  193. package/dist/defaults/shared.js +21 -0
  194. package/dist/defaults/shared.js.map +1 -1
  195. package/dist/defaults/tableStyle.d.ts.map +1 -1
  196. package/dist/defaults/tableStyle.js +4 -1
  197. package/dist/defaults/tableStyle.js.map +1 -1
  198. package/dist/defaults/videoStyle.d.ts +9 -0
  199. package/dist/defaults/videoStyle.d.ts.map +1 -0
  200. package/dist/defaults/videoStyle.js +130 -0
  201. package/dist/defaults/videoStyle.js.map +1 -0
  202. package/dist/design/placeholders.d.ts.map +1 -1
  203. package/dist/design/placeholders.js +5 -3
  204. package/dist/design/placeholders.js.map +1 -1
  205. package/dist/html-backend.d.ts +13 -0
  206. package/dist/html-backend.d.ts.map +1 -1
  207. package/dist/html-backend.js +233 -26
  208. package/dist/html-backend.js.map +1 -1
  209. package/dist/htmlAnnotations.d.ts +47 -9
  210. package/dist/htmlAnnotations.d.ts.map +1 -1
  211. package/dist/htmlAnnotations.js +89 -17
  212. package/dist/htmlAnnotations.js.map +1 -1
  213. package/dist/index.d.ts +13 -7
  214. package/dist/index.d.ts.map +1 -1
  215. package/dist/index.js +8 -4
  216. package/dist/index.js.map +1 -1
  217. package/dist/japaneseNumerals.d.ts +44 -0
  218. package/dist/japaneseNumerals.d.ts.map +1 -0
  219. package/dist/japaneseNumerals.js +100 -0
  220. package/dist/japaneseNumerals.js.map +1 -0
  221. package/dist/knuthPlass/richAdapter.d.ts +5 -1
  222. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  223. package/dist/knuthPlass/richAdapter.js +2 -0
  224. package/dist/knuthPlass/richAdapter.js.map +1 -1
  225. package/dist/locale.d.ts +19 -12
  226. package/dist/locale.d.ts.map +1 -1
  227. package/dist/locale.js +24 -12
  228. package/dist/locale.js.map +1 -1
  229. package/dist/measure/cache.d.ts.map +1 -1
  230. package/dist/measure/cache.js +17 -8
  231. package/dist/measure/cache.js.map +1 -1
  232. package/dist/measure/canvas.d.ts +21 -0
  233. package/dist/measure/canvas.d.ts.map +1 -1
  234. package/dist/measure/canvas.js +82 -0
  235. package/dist/measure/canvas.js.map +1 -1
  236. package/dist/measure/cjk.d.ts +3 -1
  237. package/dist/measure/cjk.d.ts.map +1 -1
  238. package/dist/measure/cjk.js +4 -2
  239. package/dist/measure/cjk.js.map +1 -1
  240. package/dist/measure/cjkAnnotate.d.ts +62 -3
  241. package/dist/measure/cjkAnnotate.d.ts.map +1 -1
  242. package/dist/measure/cjkAnnotate.js +99 -2
  243. package/dist/measure/cjkAnnotate.js.map +1 -1
  244. package/dist/measure/cjkClasses.d.ts +93 -13
  245. package/dist/measure/cjkClasses.d.ts.map +1 -1
  246. package/dist/measure/cjkClasses.js +204 -14
  247. package/dist/measure/cjkClasses.js.map +1 -1
  248. package/dist/measure/cjkCompose.d.ts +10 -4
  249. package/dist/measure/cjkCompose.d.ts.map +1 -1
  250. package/dist/measure/cjkCompose.js +715 -75
  251. package/dist/measure/cjkCompose.js.map +1 -1
  252. package/dist/measure/cjkPunctuation.d.ts +68 -14
  253. package/dist/measure/cjkPunctuation.d.ts.map +1 -1
  254. package/dist/measure/cjkPunctuation.js +96 -21
  255. package/dist/measure/cjkPunctuation.js.map +1 -1
  256. package/dist/measure/plain.js +2 -2
  257. package/dist/measure/plain.js.map +1 -1
  258. package/dist/measure/rich.d.ts +12 -2
  259. package/dist/measure/rich.d.ts.map +1 -1
  260. package/dist/measure/rich.js +46 -8
  261. package/dist/measure/rich.js.map +1 -1
  262. package/dist/measure/rubyJis.d.ts +96 -0
  263. package/dist/measure/rubyJis.d.ts.map +1 -0
  264. package/dist/measure/rubyJis.js +245 -0
  265. package/dist/measure/rubyJis.js.map +1 -0
  266. package/dist/measure/vertical.d.ts +13 -5
  267. package/dist/measure/vertical.d.ts.map +1 -1
  268. package/dist/measure/vertical.js +16 -6
  269. package/dist/measure/vertical.js.map +1 -1
  270. package/dist/numberWords.d.ts +4 -3
  271. package/dist/numberWords.d.ts.map +1 -1
  272. package/dist/numberWords.js +15 -5
  273. package/dist/numberWords.js.map +1 -1
  274. package/dist/numbering.d.ts +24 -12
  275. package/dist/numbering.d.ts.map +1 -1
  276. package/dist/numbering.js +57 -11
  277. package/dist/numbering.js.map +1 -1
  278. package/dist/parse/annotations.d.ts +21 -18
  279. package/dist/parse/annotations.d.ts.map +1 -1
  280. package/dist/parse/annotations.js +140 -51
  281. package/dist/parse/annotations.js.map +1 -1
  282. package/dist/parse/index.d.ts +1 -1
  283. package/dist/parse/index.d.ts.map +1 -1
  284. package/dist/parse/index.js.map +1 -1
  285. package/dist/parse/indexMarks.d.ts.map +1 -1
  286. package/dist/parse/indexMarks.js +42 -0
  287. package/dist/parse/indexMarks.js.map +1 -1
  288. package/dist/parse/inlineFormatting.d.ts +4 -3
  289. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  290. package/dist/parse/inlineFormatting.js +9 -8
  291. package/dist/parse/inlineFormatting.js.map +1 -1
  292. package/dist/parse/types.d.ts +93 -5
  293. package/dist/parse/types.d.ts.map +1 -1
  294. package/dist/pipeline/annotations.d.ts +25 -15
  295. package/dist/pipeline/annotations.d.ts.map +1 -1
  296. package/dist/pipeline/annotations.js +102 -16
  297. package/dist/pipeline/annotations.js.map +1 -1
  298. package/dist/pipeline/build.d.ts.map +1 -1
  299. package/dist/pipeline/build.js +284 -31
  300. package/dist/pipeline/build.js.map +1 -1
  301. package/dist/pipeline/buildBlockKind.js +2 -1
  302. package/dist/pipeline/buildBlockKind.js.map +1 -1
  303. package/dist/pipeline/buildHelpers.d.ts +2 -0
  304. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  305. package/dist/pipeline/buildHelpers.js +17 -0
  306. package/dist/pipeline/buildHelpers.js.map +1 -1
  307. package/dist/pipeline/buildMeasurement.d.ts +3 -0
  308. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  309. package/dist/pipeline/buildMeasurement.js +1 -0
  310. package/dist/pipeline/buildMeasurement.js.map +1 -1
  311. package/dist/pipeline/calloutLayout.d.ts +2 -0
  312. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  313. package/dist/pipeline/calloutLayout.js +5 -5
  314. package/dist/pipeline/calloutLayout.js.map +1 -1
  315. package/dist/pipeline/citations.d.ts +1 -1
  316. package/dist/pipeline/citations.d.ts.map +1 -1
  317. package/dist/pipeline/citations.js +15 -7
  318. package/dist/pipeline/citations.js.map +1 -1
  319. package/dist/pipeline/columnBalancing.d.ts +22 -0
  320. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  321. package/dist/pipeline/columnBalancing.js +80 -3
  322. package/dist/pipeline/columnBalancing.js.map +1 -1
  323. package/dist/pipeline/config.d.ts.map +1 -1
  324. package/dist/pipeline/config.js +3 -2
  325. package/dist/pipeline/config.js.map +1 -1
  326. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  327. package/dist/pipeline/contentWarnings.js +19 -0
  328. package/dist/pipeline/contentWarnings.js.map +1 -1
  329. package/dist/pipeline/continuation.d.ts.map +1 -1
  330. package/dist/pipeline/continuation.js +3 -3
  331. package/dist/pipeline/continuation.js.map +1 -1
  332. package/dist/pipeline/crossRefs.d.ts.map +1 -1
  333. package/dist/pipeline/crossRefs.js +3 -0
  334. package/dist/pipeline/crossRefs.js.map +1 -1
  335. package/dist/pipeline/footnotes.d.ts +20 -12
  336. package/dist/pipeline/footnotes.d.ts.map +1 -1
  337. package/dist/pipeline/footnotes.js +62 -16
  338. package/dist/pipeline/footnotes.js.map +1 -1
  339. package/dist/pipeline/headingStyles.js +1 -1
  340. package/dist/pipeline/headingStyles.js.map +1 -1
  341. package/dist/pipeline/indexDirective.d.ts.map +1 -1
  342. package/dist/pipeline/indexDirective.js +89 -12
  343. package/dist/pipeline/indexDirective.js.map +1 -1
  344. package/dist/pipeline/indexGroups.d.ts +5 -3
  345. package/dist/pipeline/indexGroups.d.ts.map +1 -1
  346. package/dist/pipeline/indexGroups.js +9 -4
  347. package/dist/pipeline/indexGroups.js.map +1 -1
  348. package/dist/pipeline/indexJapanese.d.ts +75 -0
  349. package/dist/pipeline/indexJapanese.d.ts.map +1 -0
  350. package/dist/pipeline/indexJapanese.js +264 -0
  351. package/dist/pipeline/indexJapanese.js.map +1 -0
  352. package/dist/pipeline/lineSpan.d.ts +36 -0
  353. package/dist/pipeline/lineSpan.d.ts.map +1 -0
  354. package/dist/pipeline/lineSpan.js +36 -0
  355. package/dist/pipeline/lineSpan.js.map +1 -0
  356. package/dist/pipeline/lists.js +2 -2
  357. package/dist/pipeline/lists.js.map +1 -1
  358. package/dist/pipeline/measureContentBlock.d.ts +4 -0
  359. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  360. package/dist/pipeline/measureContentBlock.js +54 -5
  361. package/dist/pipeline/measureContentBlock.js.map +1 -1
  362. package/dist/pipeline/outline.d.ts.map +1 -1
  363. package/dist/pipeline/outline.js +3 -1
  364. package/dist/pipeline/outline.js.map +1 -1
  365. package/dist/pipeline/paragraphContainers.d.ts +25 -2
  366. package/dist/pipeline/paragraphContainers.d.ts.map +1 -1
  367. package/dist/pipeline/paragraphContainers.js +82 -2
  368. package/dist/pipeline/paragraphContainers.js.map +1 -1
  369. package/dist/pipeline/parts.d.ts +2 -2
  370. package/dist/pipeline/parts.js +3 -3
  371. package/dist/pipeline/parts.js.map +1 -1
  372. package/dist/pipeline/placement.d.ts.map +1 -1
  373. package/dist/pipeline/placement.js +2 -2
  374. package/dist/pipeline/placement.js.map +1 -1
  375. package/dist/pipeline/resourceLayout.d.ts +6 -0
  376. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  377. package/dist/pipeline/resourceLayout.js +85 -15
  378. package/dist/pipeline/resourceLayout.js.map +1 -1
  379. package/dist/pipeline/resourceNumbering.d.ts +5 -2
  380. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  381. package/dist/pipeline/resourceNumbering.js +12 -7
  382. package/dist/pipeline/resourceNumbering.js.map +1 -1
  383. package/dist/pipeline/safeArea.d.ts +29 -0
  384. package/dist/pipeline/safeArea.d.ts.map +1 -0
  385. package/dist/pipeline/safeArea.js +89 -0
  386. package/dist/pipeline/safeArea.js.map +1 -0
  387. package/dist/pipeline/styles.d.ts +12 -0
  388. package/dist/pipeline/styles.d.ts.map +1 -1
  389. package/dist/pipeline/styles.js +7 -1
  390. package/dist/pipeline/styles.js.map +1 -1
  391. package/dist/pipeline/verse.d.ts +3 -0
  392. package/dist/pipeline/verse.d.ts.map +1 -1
  393. package/dist/pipeline/verse.js +1 -1
  394. package/dist/pipeline/verse.js.map +1 -1
  395. package/dist/pipeline/verticalMetrics.d.ts.map +1 -1
  396. package/dist/pipeline/verticalMetrics.js +4 -0
  397. package/dist/pipeline/verticalMetrics.js.map +1 -1
  398. package/dist/pipeline/videoOverlay.d.ts +24 -0
  399. package/dist/pipeline/videoOverlay.d.ts.map +1 -0
  400. package/dist/pipeline/videoOverlay.js +134 -0
  401. package/dist/pipeline/videoOverlay.js.map +1 -0
  402. package/dist/types.d.ts +567 -50
  403. package/dist/types.d.ts.map +1 -1
  404. package/dist/uprightArabic.js +1 -1
  405. package/dist/uprightArabic.js.map +1 -1
  406. package/dist/vdt.d.ts +221 -17
  407. package/dist/vdt.d.ts.map +1 -1
  408. package/dist/vdt.js.map +1 -1
  409. package/dist/video/qr.d.ts +21 -0
  410. package/dist/video/qr.d.ts.map +1 -0
  411. package/dist/video/qr.js +326 -0
  412. package/dist/video/qr.js.map +1 -0
  413. package/dist/video/url.d.ts +62 -0
  414. package/dist/video/url.d.ts.map +1 -0
  415. package/dist/video/url.js +206 -0
  416. package/dist/video/url.js.map +1 -0
  417. package/dist/writingMode.d.ts +74 -20
  418. package/dist/writingMode.d.ts.map +1 -1
  419. package/dist/writingMode.js +146 -27
  420. package/dist/writingMode.js.map +1 -1
  421. package/package.json +1 -1
@@ -21,7 +21,11 @@
21
21
  * segment of its own. A line that needs more than the cap (½ em, or
22
22
  * `bodyText.maxJustifyTracking` when set) is set with the cap and flagged
23
23
  * `cjkLoose` and `ragged`; a line with no CJK character on it (the head of
24
- * a long web address) is set `ragged` without the flag.
24
+ * a long web address) is set `ragged` without the flag. Japanese
25
+ * full-width text (`isJlreqSpacing`) follows JLReq §3.8 instead where it
26
+ * differs: a line takes one more character by giving up blank in Table 3's
27
+ * order (`fitLine`), and is never spread inside a bracket or next to
28
+ * 、。・?!, a hyphen or U+3000 (`gapStretches`, JLReq §3.1.11).
25
29
  *
26
30
  * Segments: a Western run keeps one of its own, and single characters
27
31
  * share one only when they have one style, link and spacing and advance
@@ -29,18 +33,20 @@
29
33
  * and a Markdown link covers only its own.
30
34
  */
31
35
  import { createBoundingBox } from '../vdt';
36
+ import { isJapaneseLanguage } from '../locale';
32
37
  import { lineMeasure } from './types';
33
38
  import { measureInkBox, measureInkExtent, measureTextWidth, normalSpaceWidthFor } from './canvas';
34
39
  import { atomicSpanToken, setLabelTabs, emergencySplit, expandSmallCaps, pickSpanFont, scriptMetrics, setsObject, spanScriptFields, stackedScriptPairs, textWidth, tokenSegment, URL_LIKE_RE, urlBreakIndices, } from './rich';
35
- import { cjkBreakAllowed, cjkClassOf, getCjkLineBreak, isCjkGrapheme, isFullwidthAlnum, isFullwidthDigit, isLineEndProhibited, isLineStartProhibited, isWordInnerMark, } from './cjkClasses';
40
+ import { cjkBreakAllowed, cjkClassOf, getCjkLineBreak, isCjkGrapheme, isFullwidthAlnum, isFullwidthDigit, isJapaneseLineBreak, isLineEndProhibited, isLineStartProhibited, isWordInnerMark, } from './cjkClasses';
36
41
  import { hasCJK } from './cjk';
37
42
  import { graphemeCount, graphemesOf, lastGrapheme } from './graphemes';
38
43
  import { isBreakingSpace } from './spaces';
39
44
  import { trimChipLineEdges } from './chipEdges';
40
- import { cellAdvance, fontEm, fontFamilyOf, getMeasureRegion, getMeasureUprightDigits, getMeasureWritingMode, lineBaselineOffset, measureCentralBaseline, verticalTrackCount, withMeasureWritingMode } from './vertical';
41
- import { verticalRuns } from '../writingMode';
42
- import { foldWidth, isZhuyin, noteRowBaselines, readingAdvance, rubyGeometry, splitNote, withFontSize, ZHUYIN_SIZE_RATIO } from './cjkAnnotate';
43
- import { applyLineEdges, boxCut, carryStopBlank, compositionFor, compressPair, getCjkComposition, isLatinSpacingHan, isLatinSpacingLatin, isPlainComposition, latinSpacingPx, lineEdgeCut, mayHang, punctuationBox, punctuationShrink, routesSharedMarks, shrinkPunctuation, shrinkStep, } from './cjkPunctuation';
45
+ import { cellAdvance, flowTextWidth, fontEm, fontFamilyOf, getMeasureRegion, getMeasureUprightDigits, getMeasureWritingMode, lineBaselineOffset, measureCentralBaseline, verticalTrackCount, withMeasureWritingMode } from './vertical';
46
+ import { isUprightMarkPair, verticalRuns } from '../writingMode';
47
+ import { foldWidth, isZhuyin, kuntenGeometry, noteRowBaselines, readingAdvance, readingBaseline, rubyGeometry, splitNote, withFontSize, ZHUYIN_SIZE_RATIO } from './cjkAnnotate';
48
+ import { boxesWidth, fullSizeKana, layoutJisRuby, layoutJukugo } from './rubyJis';
49
+ import { applyLineEdges, blankEnd, blankStart, boxCut, carryStopBlank, compositionFor, compressPair, getCjkComposition, isLatinSpacingHan, isLatinSpacingLatin, isJlreqSpacing, isPlainComposition, jlreqShrinkStep, latinSpacingPx, lineEdgeCut, mayHang, punctuationBox, punctuationShrink, routesSharedMarks, shrinkPunctuation, shrinkStep, } from './cjkPunctuation';
44
50
  /** Fit tolerance of the breaker (px): running sums of many widths may
45
51
  * land a hair past a measure they fill exactly. */
46
52
  const FIT_EPS = 1e-6;
@@ -97,11 +103,13 @@ export function isCjkParagraph(text) {
97
103
  export function composesAsCjk(text) {
98
104
  return hasCJK(text) && isCjkParagraph(text);
99
105
  }
100
- /** The marks a span sets on its characters (#193), the emphasis dots'
101
- * defaults filled in: a filled dot (an open circle), under the text, or
102
- * right of it in vertical text. */
106
+ /** The marks a span sets on its characters (#193, #421), the defaults
107
+ * of what the span leaves unset filled in (`cjk.emphasisMark` has filled
108
+ * the region's in already, `pipeline/annotations.ts`): emphasis dots a
109
+ * filled dot (an open circle), a side line solid; both under the text,
110
+ * or right of it in vertical text. */
103
111
  export function spanMarks(span, vertical) {
104
- if (!span.emphasisMark && span.properName === undefined && !span.bookTitle)
112
+ if (!span.emphasisMark && span.properName === undefined && !span.bookTitle && !span.sideline)
105
113
  return undefined;
106
114
  const marks = {};
107
115
  if (span.emphasisMark) {
@@ -116,6 +124,13 @@ export function spanMarks(span, vertical) {
116
124
  marks.properName = span.properName;
117
125
  if (span.bookTitle)
118
126
  marks.bookTitle = span.bookTitle.id;
127
+ if (span.sideline) {
128
+ marks.sideline = {
129
+ id: span.sideline.id,
130
+ style: span.sideline.style ?? 'solid',
131
+ position: span.sideline.position ?? (vertical ? 'over' : 'under'),
132
+ };
133
+ }
119
134
  return marks;
120
135
  }
121
136
  /** The part of a style key the marks and added characters make. */
@@ -123,14 +138,32 @@ function marksKey(marks, inserted) {
123
138
  if (!marks && !inserted)
124
139
  return '';
125
140
  const d = marks?.dots;
126
- return `|m:${d ? `${d.style}${d.fill}${d.position}` : ''}:${marks?.properName ?? ''}:${marks?.bookTitle ?? ''}${inserted ? ':ins' : ''}`;
141
+ const l = marks?.sideline;
142
+ return `|m:${d ? `${d.style}${d.fill}${d.position}` : ''}:${marks?.properName ?? ''}:${marks?.bookTitle ?? ''}${l ? `:sl${l.id}${l.style}${l.position}` : ''}${inserted ? ':ins' : ''}`;
143
+ }
144
+ /**
145
+ * The language a span's isolate names (the innermost that names one,
146
+ * `:ltr[…]{lang=en}`), which its segments carry (`VDTLineSegment.lang`)
147
+ * so the renderers declare it (a PDF `Span` with its `/Lang`) and shape it
148
+ * in its own forms (#427): in a Japanese document (the `japan` region), or
149
+ * for an isolate in Japanese. A segment then never holds text of two
150
+ * languages. Other composed text carries none, as before.
151
+ */
152
+ function spanLanguageHere(span) {
153
+ let lang;
154
+ for (let d = span.direction; d && lang === undefined; d = d.outer)
155
+ lang = d.lang;
156
+ if (lang === undefined)
157
+ return undefined;
158
+ return getMeasureRegion() === 'japan' || isJapaneseLanguage(lang) ? lang : undefined;
127
159
  }
128
160
  function styleOf(span, fonts, vertical = false) {
129
161
  const script = spanScriptFields(span, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic);
130
162
  const font = script.scriptFont ?? pickSpanFont(span.bold, span.italic, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic);
131
163
  const marks = spanMarks(span, vertical);
164
+ const lang = spanLanguageHere(span);
132
165
  return {
133
- key: `${span.bold ? 'b' : ''}${span.italic ? 'i' : ''}${span.captionLabel ? 'c' : ''}${span.smallCaps ? 's' : ''}|${script.script ?? ''}|${font}|${script.baselineShift ?? ''}${marksKey(marks, span.inserted)}`,
166
+ key: `${span.bold ? 'b' : ''}${span.italic ? 'i' : ''}${span.captionLabel ? 'c' : ''}${span.smallCaps ? 's' : ''}|${script.script ?? ''}|${font}|${script.baselineShift ?? ''}${marksKey(marks, span.inserted)}${lang !== undefined ? `|lang:${lang}` : ''}`,
134
167
  bold: span.bold,
135
168
  italic: span.italic,
136
169
  ...(span.captionLabel ? { captionLabel: true } : {}),
@@ -139,6 +172,7 @@ function styleOf(span, fonts, vertical = false) {
139
172
  font,
140
173
  ...(marks ? { marks } : {}),
141
174
  ...(span.inserted ? { inserted: true } : {}),
175
+ ...(lang !== undefined ? { lang } : {}),
142
176
  };
143
177
  }
144
178
  /** A grapheme set as a Chinese character here: a CJK grapheme, unless it
@@ -244,6 +278,7 @@ function rubyBaseUnit(span, fonts, letterSpacingPx, vertical) {
244
278
  let position = ruby.position ?? (isZhuyin(ruby.text) ? 'right' : 'over');
245
279
  if (vertical && position === 'right')
246
280
  position = 'over';
281
+ const jis = isJisRuby(ruby, position);
247
282
  return {
248
283
  kind: 'text',
249
284
  text: span.text,
@@ -255,9 +290,198 @@ function rubyBaseUnit(span, fonts, letterSpacingPx, vertical) {
255
290
  lastCjk: isCjkGrapheme(lastG),
256
291
  style,
257
292
  at: 0,
258
- ruby: { span: ruby, position },
293
+ ruby: { span: ruby, position, ...(jis ? { base: width } : {}) },
259
294
  };
260
295
  }
296
+ /** Whether a ruby is set by the Japanese rules (#422, `rubyJis.ts`): one
297
+ * over or under its base, not zhuyin, that has an alignment, an overhang
298
+ * rule, a jukugo word, a `mode=mono` or full-size kana (in Japan every
299
+ * reading has the first two). Others keep the clreq geometry. */
300
+ function isJisRuby(ruby, position) {
301
+ if (position === 'right' || isZhuyin(ruby.text))
302
+ return false;
303
+ return !!(ruby.align || ruby.overhang || ruby.jukugo || ruby.mono || ruby.fullKana);
304
+ }
305
+ /** Hiragana, katakana (small kana, half-width forms and ー too): what a
306
+ * reading may pass onto under `cjk.ruby.overhang: 'kana'`. */
307
+ const KANA_RE = /^[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u31F0-\u31FF\uFF66-\uFF9F]/u;
308
+ /** How far a Japanese reading may pass its base (or its jukugo word)
309
+ * onto the unit `v` beside it (`before`: `v` precedes it), px (JLReq
310
+ * §3.3.8, see `CjkRubyOverhang`); 0 at the paragraph's edge. `prev` is
311
+ * the unit before `v`, whose reading may already run onto it. */
312
+ function rubyOverhangOnto(v, before, prev, mode, rubyEm) {
313
+ if (!v || mode === 'none')
314
+ return 0;
315
+ if (mode === undefined) {
316
+ // clreq: a quarter of the ruby em onto a neighbour without a reading,
317
+ // as the other rubies of the paragraph (`sizeRubies`).
318
+ if (v.kind !== 'text' || v.note)
319
+ return 0;
320
+ if (!v.ruby)
321
+ return rubyEm / 4;
322
+ const theirs = v.ruby.geometry?.rtWidth ?? readingAdvance(v.ruby.span.text, v.ruby.span.fontString ?? v.style.font, v.ruby.position);
323
+ return Math.max(0, Math.min(rubyEm / 4, (v.width - theirs) / 2 - rubyEm / 4));
324
+ }
325
+ if (v.kind === 'atomic' || v.note || v.warichu)
326
+ return 0;
327
+ // A space: its width; never the space between Japanese and Latin text,
328
+ // whose other side is a Western letter.
329
+ if (v.kind === 'space')
330
+ return v.auto ? 0 : Math.min(mode === 'any' ? rubyEm / 2 : rubyEm, v.width);
331
+ if (v.ruby) {
332
+ // Another base: only its reading's free room, and none in kana mode.
333
+ if (mode !== 'any')
334
+ return 0;
335
+ const theirs = v.ruby.chars ? v.ruby.chars.reduce((w, c) => w + c.width, 0) : readingAdvance(v.ruby.span.text, v.ruby.span.fontString ?? v.style.font, v.ruby.position);
336
+ return Math.max(0, Math.min(rubyEm / 2, (v.width - theirs) / 2));
337
+ }
338
+ if (mode === 'any')
339
+ return rubyEm / 2;
340
+ // `kana`: two readings never share a kana from both sides; the later
341
+ // one keeps to its base (JLReq §3.3.8 note).
342
+ if (before && prev?.ruby?.geometry && prev.ruby.jis) {
343
+ const g = prev.ruby.geometry;
344
+ const reach = g.runs.reduce((hi, r) => Math.max(hi, r.dx + flowTextWidth(r.text, r.fontString)), -Infinity);
345
+ if (reach > g.width + 1e-6)
346
+ return 0;
347
+ }
348
+ const box = v.punct;
349
+ const cls = before ? v.last : v.first;
350
+ if (box) {
351
+ // A mark: its blank on the reading's side, never its glyph; at most
352
+ // half a ruby character onto an opening bracket. …… and ―― take a
353
+ // ruby character.
354
+ if (box.cls === 'opening')
355
+ return rubyEm / 2;
356
+ if (box.cls === 'dash' || box.cls === 'ellipsis')
357
+ return rubyEm;
358
+ return Math.min(rubyEm, before ? blankEnd(box) : blankStart(box));
359
+ }
360
+ if (cls === 'opening')
361
+ return rubyEm / 2;
362
+ if (cls === 'ideoSpace' || cls === 'dash' || cls === 'ellipsis')
363
+ return rubyEm;
364
+ // Kana, ー; never kanji, nor a Western run.
365
+ return !v.run && v.graphemes === 1 && KANA_RE.test(v.text) ? rubyEm : 0;
366
+ }
367
+ /** The bases of a Japanese ruby's units (see {@link JisRubyBase}). */
368
+ function jisBases(us) {
369
+ return us.map((u) => ({ width: u.ruby.base, graphemes: u.graphemes, reading: u.ruby.chars }));
370
+ }
371
+ /** Lay out the bases of one Japanese ruby word (or one base). */
372
+ function layoutJisWord(word, bases, allow) {
373
+ return word.jukugo ? layoutJukugo(bases, word.align, word.rubyEm, allow) : [layoutJisRuby(bases[0], word.align, word.rubyEm, allow)];
374
+ }
375
+ /** `u` with the box `box` of its Japanese ruby: its geometry and width. */
376
+ function withJisBox(u, box, word) {
377
+ const ruby = u.ruby;
378
+ const font = ruby.span.fontString ?? withFontSize(u.style.font, emOfFont(u.style.font) / 2);
379
+ const dy = readingBaseline(ruby.span.text, font, ruby.position === 'under' ? 'under' : 'over', emOfFont(u.style.font));
380
+ const geometry = {
381
+ width: box.width,
382
+ inset: box.inset,
383
+ rtWidth: ruby.chars.reduce((w, c) => w + c.width, 0),
384
+ runs: box.pieces.map((p) => ({ text: p.text, dx: p.dx, dy, fontString: font })),
385
+ allowLeft: word.allow.start,
386
+ allowRight: word.allow.end,
387
+ ...(box.tracking > 0 ? { tracking: box.tracking } : {}),
388
+ };
389
+ return { ...u, width: box.width, ruby: { ...ruby, geometry } };
390
+ }
391
+ /**
392
+ * Lay out the Japanese rubies of a paragraph (#422, `rubyJis.ts`): each
393
+ * jukugo word, or each base, with what its neighbours let its reading pass
394
+ * onto, in order (a reading that runs onto a kana keeps the next one off
395
+ * it). Each base also learns how much wider it gets at a line edge
396
+ * (`rubyGrow`), for the breaker. Units are replaced in place.
397
+ */
398
+ function sizeJisRubies(units) {
399
+ const n = units.length;
400
+ for (let k = 0; k < n; k++) {
401
+ const u = units[k];
402
+ if (!u.ruby || u.ruby.base === undefined || u.ruby.geometry)
403
+ continue;
404
+ const span = u.ruby.span;
405
+ let e = k;
406
+ if (span.jukugo) {
407
+ while (e + 1 < n) {
408
+ const v = units[e + 1].ruby;
409
+ if (!v || v.base === undefined || !v.span.jukugo || v.span.id !== span.id || v.position !== u.ruby.position)
410
+ break;
411
+ e++;
412
+ }
413
+ }
414
+ const font = span.fontString ?? withFontSize(u.style.font, emOfFont(u.style.font) / 2);
415
+ const rubyEm = fontEm(font);
416
+ for (let q = k; q <= e; q++) {
417
+ const r = units[q].ruby;
418
+ const painted = r.span.fullKana ? fullSizeKana(r.span.text) : r.span.text;
419
+ r.chars = graphemesOf(painted).map((g) => ({ text: g, width: flowTextWidth(g, font) }));
420
+ }
421
+ const word = {
422
+ jukugo: e > k,
423
+ align: span.align ?? 'center',
424
+ rubyEm,
425
+ allow: {
426
+ start: rubyOverhangOnto(units[k - 1], true, units[k - 2], span.overhang, rubyEm),
427
+ end: rubyOverhangOnto(units[e + 1], false, undefined, span.overhang, rubyEm),
428
+ },
429
+ };
430
+ const members = units.slice(k, e + 1);
431
+ const bases = jisBases(members);
432
+ const boxes = layoutJisWord(word, bases, word.allow);
433
+ for (let q = k; q <= e; q++) {
434
+ units[q] = withJisBox(units[q], boxes[q - k], word);
435
+ units[q].ruby.jis = word;
436
+ }
437
+ // What the line gains when the word (or the base) opens it from that
438
+ // unit on, or closes it with that unit: the reading flush with the
439
+ // edge, the part of a word laid out alone.
440
+ for (let i = 0; i < boxes.length; i++) {
441
+ const head = boxesWidth(boxes.slice(0, i + 1));
442
+ const tail = boxesWidth(boxes.slice(i));
443
+ units[k + i].rubyGrow = {
444
+ start: boxesWidth(layoutJisWord(word, bases.slice(i), { start: 0, end: word.allow.end })) - tail,
445
+ end: boxesWidth(layoutJisWord(word, bases.slice(0, i + 1), { start: word.allow.start, end: 0 })) - head,
446
+ };
447
+ }
448
+ k = e;
449
+ }
450
+ }
451
+ /**
452
+ * A line's Japanese rubies at its edges (#422, JLReq §3.3.8): a word or
453
+ * base that opens or closes the line is laid out again with nothing to
454
+ * pass onto on that side, so a longer reading is set flush with the edge
455
+ * and its base moves in; a jukugo word the line breaks inside is laid
456
+ * out again with the characters the line holds, each keeping its reading.
457
+ * The breaker counted what this adds (`rubyGrow`). Units that change are
458
+ * replaced by copies in `us`.
459
+ */
460
+ function relayJisEdges(us) {
461
+ for (let j = 0; j < us.length; j++) {
462
+ const word = us[j].ruby?.jis;
463
+ if (!word)
464
+ continue;
465
+ let e = j;
466
+ while (e + 1 < us.length && us[e + 1].ruby?.jis === word)
467
+ e++;
468
+ // A word with kanbun marks (#430) keeps its layout: the marks were set
469
+ // against its readings (`sizeKunten`).
470
+ if (us.slice(j, e + 1).some((u) => u.kunten?.vdt)) {
471
+ j = e;
472
+ continue;
473
+ }
474
+ const atStart = j === 0;
475
+ const atEnd = e === us.length - 1;
476
+ if (atStart || atEnd) {
477
+ const allow = { start: atStart ? 0 : word.allow.start, end: atEnd ? 0 : word.allow.end };
478
+ const boxes = layoutJisWord(word, jisBases(us.slice(j, e + 1)), allow);
479
+ for (let q = j; q <= e; q++)
480
+ us[q] = withJisBox(us[q], boxes[q - j], word);
481
+ }
482
+ j = e;
483
+ }
484
+ }
261
485
  /**
262
486
  * Lay out each ruby base's reading (see `cjkAnnotate.ts`): its box grows
263
487
  * to the reading less what the reading may pass it by — a quarter of the
@@ -266,6 +490,9 @@ function rubyBaseUnit(span, fonts, letterSpacingPx, vertical) {
266
490
  * the symbols' em). Run once the units are final.
267
491
  */
268
492
  function sizeRubies(units) {
493
+ // Japanese rubies first (#422): the others read their geometry.
494
+ if (units.some((u) => u.ruby?.base !== undefined))
495
+ sizeJisRubies(units);
269
496
  for (let k = 0; k < units.length; k++) {
270
497
  const u = units[k];
271
498
  if (!u.ruby || u.ruby.geometry)
@@ -395,8 +622,11 @@ function buildUnits(spans, fonts, letterSpacingPx, vertical = false, route = fal
395
622
  // Which units open a span and hold all of it (the candidates for stacked
396
623
  // scripts), by index.
397
624
  const wholeSpan = new Set();
625
+ // Where each span's units start, for the kanbun marks (#430).
626
+ const spanStarts = [];
398
627
  for (let si = 0; si < spans.length; si++) {
399
628
  const span = spans[si];
629
+ spanStarts.push(units.length);
400
630
  // A ruby base: one unit, sized with its reading once its neighbours
401
631
  // are known (`sizeRubies`).
402
632
  if (span.ruby && span.text.length > 0) {
@@ -598,6 +828,8 @@ function buildUnits(spans, fonts, letterSpacingPx, vertical = false, route = fal
598
828
  if (units.length === spanFirst + 1 && units[spanFirst].kind === 'text')
599
829
  wholeSpan.add(spanFirst);
600
830
  }
831
+ if (spans.some((s) => s.kunten))
832
+ attachKunten(spans, spanStarts, units);
601
833
  // A subscript and a superscript that touch are set one over the other
602
834
  // (EF-80): the first advances nothing, the second the pair's advance, and
603
835
  // the two never part.
@@ -619,10 +851,146 @@ function buildUnits(spans, fonts, letterSpacingPx, vertical = false, route = fal
619
851
  b.glueBefore = true;
620
852
  }
621
853
  }
854
+ if (vertical && getMeasureRegion() === 'japan')
855
+ combineMarkPairs(units, letterSpacingPx);
622
856
  if (route)
623
857
  routeSharedMarks(units, letterSpacingPx, vertical);
624
858
  return units;
625
859
  }
860
+ /** Give each kanbun mark (#430) to the last unit its spans made (a space
861
+ * aside): the marks go with the base's last character. `starts[i]` is
862
+ * where span `i`'s units start. */
863
+ function attachKunten(spans, starts, units) {
864
+ const last = new Map();
865
+ spans.forEach((span, i) => {
866
+ if (!span.kunten)
867
+ return;
868
+ const end = i + 1 < starts.length ? starts[i + 1] : units.length;
869
+ for (let k = end - 1; k >= starts[i]; k--) {
870
+ if (units[k].kind === 'space')
871
+ continue;
872
+ last.set(span.kunten, k);
873
+ break;
874
+ }
875
+ });
876
+ for (const [mark, k] of last)
877
+ units[k].kunten = { mark };
878
+ }
879
+ /**
880
+ * Lay out the kanbun marks of each unit that carries some (see
881
+ * `kuntenGeometry`): their runs, and the advance they add after the
882
+ * character, which joins the unit's width. Run once the units are final
883
+ * and the ruby readings sized (a 送り仮名 follows a reading on its side).
884
+ * A character inside a warichu note keeps no marks.
885
+ */
886
+ function sizeKunten(units, letterSpacingPx) {
887
+ for (const u of units) {
888
+ if (!u.kunten || u.kunten.vdt || u.note || u.kind !== 'text')
889
+ continue;
890
+ const mark = u.kunten.mark;
891
+ if (!mark.kaeri && !mark.okuri && !mark.tate)
892
+ continue;
893
+ const em = emOfFont(u.style.font);
894
+ const g = u.ruby?.geometry;
895
+ // Where the character ends: the base inside a ruby box (a Japanese
896
+ // one, #422, spread by `tracking` after each character but its last),
897
+ // else the unit less the tracking after its last character.
898
+ const baseEnd = !g ? u.width - letterSpacingPx
899
+ : u.ruby.base !== undefined ? g.inset + u.ruby.base + (g.tracking ?? 0) * (u.graphemes - 1)
900
+ : u.ruby.position === 'right' ? u.width - g.rtWidth : u.width - g.inset;
901
+ let readingEnd;
902
+ if (g && u.ruby.position !== 'right') {
903
+ for (const r of g.runs)
904
+ readingEnd = Math.max(readingEnd ?? -Infinity, r.dx + flowTextWidth(r.text, r.fontString));
905
+ }
906
+ const over = u.ruby && u.ruby.position !== 'under';
907
+ const { extra, kunten } = kuntenGeometry({
908
+ ...(mark.kaeri ? { kaeri: mark.kaeri } : {}),
909
+ ...(mark.okuri ? { okuri: mark.okuri } : {}),
910
+ ...(mark.tate ? { tate: true } : {}),
911
+ fontString: mark.fontString ?? withFontSize(u.style.font, em / 2),
912
+ ...(mark.color ? { color: mark.color } : {}),
913
+ placement: mark.placement ?? 'inline',
914
+ em,
915
+ baseEnd,
916
+ width: u.width,
917
+ ...(readingEnd !== undefined && over ? { readingOverEnd: readingEnd } : {}),
918
+ ...(readingEnd !== undefined && !over ? { readingUnderEnd: readingEnd } : {}),
919
+ });
920
+ u.kunten = { mark, extra, vdt: kunten };
921
+ u.width += extra;
922
+ // Its Japanese ruby word is not laid out again at a line edge
923
+ // (`relayJisEdges`), so it widens no line there.
924
+ const word = u.ruby?.jis;
925
+ if (word)
926
+ for (const v of units)
927
+ if (v.ruby?.jis === word)
928
+ delete v.rubyGrow;
929
+ }
930
+ }
931
+ /** A unit's characters (a mark pair's possible neighbour). */
932
+ function isPlainTextUnit(u) {
933
+ return u !== undefined && u.kind === 'text' && !u.token && !u.orient && !u.ruby && !u.kunten && !u.note && !u.stacked && !u.place;
934
+ }
935
+ /**
936
+ * Japanese vertical text: each pair of exclamation and question marks
937
+ * (!! !? ?! ?? and !! !? ?! ??, JLReq §3.1.10) becomes one upright cell
938
+ * of one em, as an author's `:tcy[…]` (`Unit.orient`), which breaks like a
939
+ * single ! and keeps a segment of its own (`VDTLineSegment.tcy`). The rule
940
+ * is `isUprightMarkPair`, read over what a segment would hold: a Western
941
+ * run that is the pair alone (`Wow!!` and `(!?)` stay one sideways run),
942
+ * or two full-width marks of one style and link, a mark beside them of the
943
+ * same style and link leaving all three as they are. The painters then
944
+ * read the segment as the measurer did.
945
+ */
946
+ function combineMarkPairs(units, letterSpacingPx) {
947
+ const fullwidth = (g) => String.fromCodePoint(g.codePointAt(0) + 0xFEE0);
948
+ const pairUnit = (u, text, cls) => {
949
+ const out = {
950
+ ...u,
951
+ text,
952
+ width: fontEm(u.style.font) + (letterSpacingPx === 0 ? 0 : letterSpacingPx),
953
+ graphemes: 1,
954
+ first: cls,
955
+ last: cls,
956
+ firstCjk: true,
957
+ lastCjk: true,
958
+ orient: 'tcy',
959
+ };
960
+ delete out.run;
961
+ delete out.cellStart;
962
+ delete out.cellEnd;
963
+ delete out.url;
964
+ return out;
965
+ };
966
+ const sameSegment = (a, b) => isPlainTextUnit(b) && !b.run && b.style.key === a.style.key && b.link === a.link;
967
+ for (let k = 0; k < units.length; k++) {
968
+ const u = units[k];
969
+ if (!isPlainTextUnit(u))
970
+ continue;
971
+ if (u.run) {
972
+ // A Western run that is the pair alone: its own segment.
973
+ if (u.graphemes === 2 && /^[!?]{2}$/.test(u.text) && isUprightMarkPair([...u.text], 0, 'japan')) {
974
+ units[k] = pairUnit(u, u.text, cjkClassOf(fullwidth(u.text[0])));
975
+ }
976
+ continue;
977
+ }
978
+ const v = units[k + 1];
979
+ if (u.graphemes !== 1 || !sameSegment(u, v) || v.graphemes !== 1)
980
+ continue;
981
+ const prev = units[k - 1];
982
+ const next = units[k + 2];
983
+ const context = [
984
+ ...(sameSegment(u, prev) ? [lastGrapheme(prev.text)] : []),
985
+ u.text,
986
+ v.text,
987
+ ...(sameSegment(u, next) ? [graphemesOf(next.text)[0]] : []),
988
+ ];
989
+ if (!isUprightMarkPair(context, sameSegment(u, prev) ? 1 : 0, 'japan'))
990
+ continue;
991
+ units.splice(k, 2, pairUnit(u, u.text + v.text, cjkClassOf(u.text)));
992
+ }
993
+ }
626
994
  /** The marks Latin text shares with Chinese (East Asian Width ambiguous):
627
995
  * quotes, the ellipsis, the em dash and horizontal bar, interpuncts. */
628
996
  const SHARED_MARKS = new Set(['\u201C', '\u201D', '\u2018', '\u2019', '\u2026', '\u22EF', '\u2014', '\u2015', '\u00B7', '\u2027']);
@@ -807,6 +1175,17 @@ function isMarkUnit(u) {
807
1175
  return false;
808
1176
  }
809
1177
  }
1178
+ /** Kanji numerals a 、 or ・ is set solid between (二、三日 "two or three
1179
+ * days", 三・一四 "3.14", JLReq §3.1.3). */
1180
+ const KANJI_DIGITS = new Set('〇一二三四五六七八九十');
1181
+ /** ? and ! (and their doubles) as Japanese sets them: full-width marks
1182
+ * with a full-width space after them inside a paragraph. */
1183
+ const QUESTION_MARKS = new Set(['?', '!', '‼', '⁇', '⁈', '⁉']);
1184
+ /** A single character unit (not a run, a ruby base, a note…) whose text is
1185
+ * in `set`. */
1186
+ function charUnitIn(u, set) {
1187
+ return !!u && u.kind === 'text' && !u.run && u.graphemes === 1 && !u.ruby && !u.kunten && !u.note && !u.orient && !u.style.script && set.has(u.text);
1188
+ }
810
1189
  /**
811
1190
  * The punctuation widths and Han–Latin spaces of a paragraph's units, as
812
1191
  * the composition sets them (see `cjkPunctuation.ts`): each full-width mark
@@ -817,7 +1196,9 @@ function isMarkUnit(u) {
817
1196
  * edges (and the reduction a line makes to take one more character) are
818
1197
  * the breaker's and the line's business. A plain composition changes
819
1198
  * nothing but the mainland interpunct, half an em under every style
820
- * (`punctuationBox`), as its cell is in vertical text.
1199
+ * (`punctuationBox`), as its cell is in vertical text. In Japan, a 、 or
1200
+ * ・ between kanji numerals is set solid (`Unit.solid`), and with
1201
+ * `spaceAfterQuestion` a ? or ! takes its one-em space (`Unit.aki`).
821
1202
  */
822
1203
  function prepareUnits(units, c, letterSpacingPx) {
823
1204
  const plain = isPlainComposition(c);
@@ -826,7 +1207,7 @@ function prepareUnits(units, c, letterSpacingPx) {
826
1207
  return units;
827
1208
  const full = new Map();
828
1209
  for (const u of units) {
829
- if (u.kind !== 'text' || u.run || u.graphemes !== 1 || !u.firstCjk || u.style.script || u.stacked || u.orient || u.ruby || u.note)
1210
+ if (u.kind !== 'text' || u.run || u.graphemes !== 1 || !u.firstCjk || u.style.script || u.stacked || u.orient || u.ruby || u.kunten || u.note)
830
1211
  continue;
831
1212
  if (plain && u.first !== 'interpunct')
832
1213
  continue;
@@ -839,24 +1220,43 @@ function prepareUnits(units, c, letterSpacingPx) {
839
1220
  }
840
1221
  if (plain)
841
1222
  return units;
1223
+ if (c.region === 'japan') {
1224
+ // 二、三日, 三・一四: the mark between two kanji numerals is its glyph
1225
+ // alone (JLReq §3.1.3), and the number is not broken there.
1226
+ for (let k = 1; k + 1 < units.length; k++) {
1227
+ const u = units[k];
1228
+ if (!u.punct || (u.text !== '、' && u.text !== '・'))
1229
+ continue;
1230
+ if (!charUnitIn(units[k - 1], KANJI_DIGITS) || !charUnitIn(units[k + 1], KANJI_DIGITS) || units[k + 1].glueBefore)
1231
+ continue;
1232
+ shrinkPunctuation(u.punct, u.punct.blank);
1233
+ u.width = full.get(u) - boxCut(u.punct);
1234
+ u.solid = true;
1235
+ }
1236
+ }
842
1237
  // Kaiming sets a stop's blank after the closing mark that follows it (。”␣).
843
1238
  const carry = c.punctuationWidth === 'kaiming';
1239
+ const jlreq = isJlreqSpacing(c);
844
1240
  if ((c.compressAdjacent || carry) && full.size > 1) {
845
1241
  for (let k = 1; k < units.length; k++) {
846
1242
  const a = units[k - 1];
847
1243
  const b = units[k];
848
- if (!a.punct || !b.punct || b.zwspBefore)
1244
+ if (!a.punct || !b.punct || b.zwspBefore || a.solid || b.solid)
849
1245
  continue;
850
1246
  if (c.compressAdjacent)
851
- compressPair(a.punct, b.punct);
1247
+ compressPair(a.punct, b.punct, jlreq);
852
1248
  if (carry)
853
1249
  carryStopBlank(a.punct, b.punct);
854
1250
  a.width = full.get(a) - boxCut(a.punct);
855
1251
  b.width = full.get(b) - boxCut(b.punct);
856
1252
  }
857
1253
  }
858
- if ((c.latinSpacing.px ?? c.latinSpacing.em ?? 0) <= 0)
859
- return units;
1254
+ const spaced = (c.latinSpacing.px ?? c.latinSpacing.em ?? 0) > 0 ? latinSpaces(units, c) : units;
1255
+ return c.spaceAfterQuestion ? questionSpaces(spaced) : spaced;
1256
+ }
1257
+ /** `units` with the Han–Latin spaces of `cjk.latinSpacing` (see
1258
+ * {@link prepareUnits}). */
1259
+ function latinSpaces(units, c) {
860
1260
  const out = [];
861
1261
  const han = (u, edge) => !!u && u.kind === 'text' && !u.run && !u.note && u.firstCjk && u[edge] === 'ideograph' && !u.style.script && isLatinSpacingHan(u.text);
862
1262
  // A run whose edge is a number set in one upright cell (vertical text)
@@ -899,6 +1299,62 @@ function prepareUnits(units, c, letterSpacingPx) {
899
1299
  }
900
1300
  return out;
901
1301
  }
1302
+ /**
1303
+ * `units` with the one-em space JLReq §3.1.6 sets after a ? or ! that
1304
+ * ends a sentence inside the paragraph (`cjk.spaceAfterQuestion`,
1305
+ * `Unit.aki`): after the mark and what keeps to it (a note marker), unless
1306
+ * the paragraph ends there or a closing bracket or another ?! follows. A
1307
+ * space the author typed there (U+3000, or a run of word spaces) becomes
1308
+ * the aki, keeping its text. A line may break after it, and it goes at the
1309
+ * line end, as any space does.
1310
+ */
1311
+ function questionSpaces(units) {
1312
+ const out = [];
1313
+ for (let k = 0; k < units.length; k++) {
1314
+ const u = units[k];
1315
+ out.push(u);
1316
+ if (!charUnitIn(u, QUESTION_MARKS))
1317
+ continue;
1318
+ // A note marker or a superscript keeps to the mark: the space follows it.
1319
+ while (k + 1 < units.length && units[k + 1].glueBefore)
1320
+ out.push(units[++k]);
1321
+ const next = units[k + 1];
1322
+ if (!next || charUnitIn(next, QUESTION_MARKS))
1323
+ continue;
1324
+ if (next.kind === 'text' && next.first === 'closing')
1325
+ continue;
1326
+ const aki = {
1327
+ kind: 'space',
1328
+ text: '',
1329
+ width: emOfFont(u.style.font),
1330
+ graphemes: 0,
1331
+ first: 'western',
1332
+ last: 'western',
1333
+ firstCjk: false,
1334
+ lastCjk: false,
1335
+ style: u.style,
1336
+ at: u.at + u.text.length,
1337
+ aki: true,
1338
+ };
1339
+ if (next.kind === 'space' && !next.auto) {
1340
+ // A typed space is replaced: the aki keeps its text.
1341
+ if (units[k + 2] === undefined)
1342
+ continue;
1343
+ out.push({ ...aki, text: next.text, at: next.at });
1344
+ k++;
1345
+ }
1346
+ else if (next.kind === 'text' && next.first === 'ideoSpace' && next.graphemes === 1) {
1347
+ if (units[k + 2] === undefined)
1348
+ continue;
1349
+ out.push({ ...aki, text: next.text, at: next.at });
1350
+ k++;
1351
+ }
1352
+ else {
1353
+ out.push(aki);
1354
+ }
1355
+ }
1356
+ return out;
1357
+ }
902
1358
  const isDigitCode = (c) => (c >= 0x30 && c <= 0x39) || isFullwidthDigit(c);
903
1359
  /** Whether a number keeps to the sign or unit next to it (`50 %`, `50%`,
904
1360
  * `¥599`, `−3 ℃`): a unit ending in a digit before a unit sign, or a
@@ -931,10 +1387,29 @@ function fullwidthGlue(units, k) {
931
1387
  return true;
932
1388
  return fullwidthUnit(a, isFullwidthNumberJoin) && fullwidthUnit(b, isFullwidthDigit) && fullwidthUnit(units[k - 2], isFullwidthDigit);
933
1389
  }
1390
+ /** The grapheme a unit opens (`first`) or ends (`last`) with, which a
1391
+ * Japanese level reads the unit's class from (small kana, ー, hyphens…:
1392
+ * `breakClassOf`); undefined under a Chinese level, and for an inline box
1393
+ * (a chip, a formula, a marker), whose class stands as it is. */
1394
+ function edgeGrapheme(u, edge, level) {
1395
+ if (u.kind !== 'text' || u.text === '' || !isJapaneseLineBreak(level))
1396
+ return undefined;
1397
+ return edge === 'first' ? String.fromCodePoint(u.text.codePointAt(0)) : lastGrapheme(u.text);
1398
+ }
1399
+ /** Whether unit `b` may not open a line at `level` (see
1400
+ * {@link edgeGrapheme}). */
1401
+ function startProhibited(b, level) {
1402
+ return isLineStartProhibited(b.first, level, edgeGrapheme(b, 'first', level));
1403
+ }
1404
+ /** Whether unit `a` may not close a line at `level`. */
1405
+ function endProhibited(a, level) {
1406
+ return isLineEndProhibited(a.last, level, edgeGrapheme(a, 'last', level));
1407
+ }
934
1408
  /** Whether a line may break before each unit (index 0 is never a break).
935
1409
  * A word space is a break unless the unit after it may not open a line,
936
1410
  * the last one before it may not close one, or they are a number and its
937
- * sign; a zero-width space is a break at every level. */
1411
+ * sign; a zero-width space is a break at every level. A kanji number
1412
+ * never breaks at the 、 or ・ set solid inside it (`Unit.solid`). */
938
1413
  function breakOpportunities(units, level) {
939
1414
  const out = new Uint8Array(units.length);
940
1415
  // The last unit before `k` that is not a space.
@@ -955,7 +1430,7 @@ function breakOpportunities(units, level) {
955
1430
  }
956
1431
  if (a.kind === 'space') {
957
1432
  const p = ink >= 0 ? units[ink] : undefined;
958
- if (!p || (!numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level)))
1433
+ if (!p || (!numberGlue(p, b) && !startProhibited(b, level) && !endProhibited(p, level)))
959
1434
  out[k] = 1;
960
1435
  continue;
961
1436
  }
@@ -965,15 +1440,15 @@ function breakOpportunities(units, level) {
965
1440
  while (q > 0 && units[q].noteSpace)
966
1441
  q--;
967
1442
  const p = units[q];
968
- if (!p.noteSpace && !numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level))
1443
+ if (!p.noteSpace && !numberGlue(p, b) && !startProhibited(b, level) && !endProhibited(p, level))
969
1444
  out[k] = 1;
970
1445
  continue;
971
1446
  }
972
- if (b.glueBefore)
1447
+ if (b.glueBefore || a.solid || b.solid)
973
1448
  continue;
974
1449
  if (numberGlue(a, b) || fullwidthGlue(units, k))
975
1450
  continue;
976
- if (cjkBreakAllowed(a.last, a.lastCjk, b.first, b.firstCjk, level))
1451
+ if (cjkBreakAllowed(a.last, a.lastCjk, b.first, b.firstCjk, level, edgeGrapheme(a, 'last', level), edgeGrapheme(b, 'first', level)))
977
1452
  out[k] = 1;
978
1453
  }
979
1454
  return out;
@@ -982,10 +1457,16 @@ function breakOpportunities(units, level) {
982
1457
  * line: between characters, never inside a Western run or a 2-em mark
983
1458
  * (those are single units), never between two Western runs, next to a
984
1459
  * connector or a solidus, after an atomic box, or before a mark that
985
- * keeps to the text before it. Spaces take their own share. */
986
- function gapStretches(a, b) {
1460
+ * keeps to the text before it. Spaces take their own share. With `jlreq`
1461
+ * (Japanese full-width text) also JLReq §3.1.11's inseparable places:
1462
+ * never after an opening bracket or before a closing one (but before the
1463
+ * one and after the other), never next to 、,。.・:;?! or U+3000. */
1464
+ function gapStretches(a, b, jlreq = false) {
987
1465
  if (a.kind !== 'text' || b.kind === 'space')
988
1466
  return false;
1467
+ // Never inside a jukugo word (JLReq §3.3.7).
1468
+ if (a.ruby?.jis && a.ruby.jis === b.ruby?.jis)
1469
+ return false;
989
1470
  if (b.glueBefore || a.stacked)
990
1471
  return false;
991
1472
  if (a.last === 'connector' || a.last === 'solidus' || b.first === 'connector' || b.first === 'solidus')
@@ -994,16 +1475,43 @@ function gapStretches(a, b) {
994
1475
  return false;
995
1476
  if (a.last === b.first && (a.last === 'dash' || a.last === 'ellipsis'))
996
1477
  return false;
1478
+ if (jlreq && (a.last === 'opening' || b.first === 'closing' || jlreqFixed(a.last) || jlreqFixed(b.first)))
1479
+ return false;
997
1480
  return true;
998
1481
  }
1482
+ /** The classes JLReq never spreads next to (§3.1.11): pause and stop
1483
+ * marks, middle dots and the ideographic space. */
1484
+ function jlreqFixed(cls) {
1485
+ return cls === 'pause' || cls === 'stop' || cls === 'interpunct' || cls === 'ideoSpace';
1486
+ }
1487
+ /** Whether the gap after unit `j` of a line takes tracking. A footnote
1488
+ * marker in the line gap (`footnotes.markerPosition: 'side'`) takes no
1489
+ * room in the line: the gap between the characters around it is theirs,
1490
+ * and is set after the marker, which stays against the character it
1491
+ * marks. */
1492
+ function gapAfterStretches(us, j, jlreq = false) {
1493
+ const next = us[j + 1];
1494
+ if (isSideMarker(next))
1495
+ return false;
1496
+ const u = us[j];
1497
+ if (isSideMarker(u))
1498
+ return j > 0 && gapStretches(us[j - 1], next, jlreq);
1499
+ return gapStretches(u, next, jlreq);
1500
+ }
1501
+ function isSideMarker(u) {
1502
+ return u.token?.sideRuns !== undefined;
1503
+ }
999
1504
  function lineFitOf(c) {
1000
1505
  if (isPlainComposition(c))
1001
1506
  return undefined;
1507
+ const jlreq = isJlreqSpacing(c);
1002
1508
  return {
1003
1509
  lead: (u) => (u.punct ? lineEdgeCut(u.punct, c, true, false) : 0),
1004
1510
  tail: (u) => (u.punct ? lineEdgeCut(u.punct, c, false, true) : 0),
1005
1511
  give(u, atStart, atEnd) {
1006
1512
  if (u.kind === 'space') {
1513
+ if (u.aki)
1514
+ return 0;
1007
1515
  const em = emOfFont(u.style.font);
1008
1516
  return Math.max(0, u.width - (u.auto ? em / 8 : em / 4));
1009
1517
  }
@@ -1013,18 +1521,21 @@ function lineFitOf(c) {
1013
1521
  return punctuationShrink(u.punct, c);
1014
1522
  const box = { ...u.punct };
1015
1523
  applyLineEdges(box, c, atStart, atEnd);
1016
- return punctuationShrink(box, c);
1524
+ // JLReq: a mark ending the line gives up the blank after its glyph
1525
+ // (and, a middle dot, nothing before it: §3.8.3 steps 2–3).
1526
+ return punctuationShrink(box, c, jlreq && atEnd);
1017
1527
  },
1018
1528
  edge: (u, start, end) => (u.punct ? applyLineEdges(u.punct, c, start, end) : 0),
1019
1529
  hangs(units, k, unitAt) {
1020
1530
  const u = unitAt(k);
1021
- if (u.kind !== 'text' || u.run || u.graphemes !== 1 || u.note || u.ruby || !mayHang(u.text, u.first, c))
1531
+ if (u.kind !== 'text' || u.run || u.graphemes !== 1 || u.note || u.ruby || u.kunten || !mayHang(u.text, u.first, c))
1022
1532
  return false;
1023
1533
  if (k > 0 && isMarkUnit(unitAt(k - 1)))
1024
1534
  return false;
1025
1535
  return k + 1 >= units.length || !isMarkUnit(units[k + 1]);
1026
1536
  },
1027
1537
  hanging: c.hangingPunctuation,
1538
+ jlreq,
1028
1539
  };
1029
1540
  }
1030
1541
  /** The last unit that must share a line with unit `k` when `k` ends one:
@@ -1158,11 +1669,15 @@ function divideRun(u, room, letterSpacingPx) {
1158
1669
  * unit). A space never overflows: the line ends before it, when the line
1159
1670
  * may break after it. A unit wider
1160
1671
  * than the whole line is divided when it is a Western run (a web address
1161
- * at a joint first), else set on a line of its own.
1672
+ * at a joint first), else set on a line of its own. `pull` names a line
1673
+ * that takes in the rest of the paragraph when the line can give up the
1674
+ * blank for it (push-in, to save a last line of one character).
1162
1675
  */
1163
1676
  function breakUnits(units, breaks, measureOf, reserve, letterSpacingPx, fit, level = getCjkLineBreak(),
1164
1677
  /** End line `line` before unit `at` (a break), whatever else fits. */
1165
- stop) {
1678
+ stop,
1679
+ /** Line `pull` takes every unit left when it can give up the blank. */
1680
+ pull) {
1166
1681
  const out = [];
1167
1682
  const n = units.length;
1168
1683
  let i = 0;
@@ -1176,15 +1691,23 @@ stop) {
1176
1691
  let noteBase = 0;
1177
1692
  const foldOf = (from, to) => {
1178
1693
  const widths = [];
1179
- const startProhibited = [];
1180
- const endProhibited = [];
1694
+ const startNo = [];
1695
+ const endNo = [];
1181
1696
  for (let q = from; q <= to; q++) {
1182
1697
  const uq = unitAt(q);
1183
1698
  widths.push(uq.width);
1184
- startProhibited.push(noteRowStartProhibited(uq, level));
1185
- endProhibited.push(isLineEndProhibited(uq.last, level));
1699
+ startNo.push(noteRowStartProhibited(uq, level));
1700
+ endNo.push(endProhibited(uq, level));
1186
1701
  }
1187
- return foldWidth(widths, splitNote(widths, startProhibited, endProhibited));
1702
+ return foldWidth(widths, splitNote(widths, startNo, endNo));
1703
+ };
1704
+ // A Japanese ruby base at a line edge widens the line (`rubyGrow`): at
1705
+ // its start, and at its end when the line may end after it.
1706
+ const endGrow = (k) => {
1707
+ const g = unitAt(k).rubyGrow;
1708
+ if (!g || g.end <= 0)
1709
+ return 0;
1710
+ return k + 1 >= n || breaks[k + 1] || units[k + 1].kind === 'space' ? g.end : 0;
1188
1711
  };
1189
1712
  for (let li = 0;; li++) {
1190
1713
  while (i < n && unitAt(i).kind === 'space')
@@ -1192,7 +1715,7 @@ stop) {
1192
1715
  if (i >= n)
1193
1716
  break;
1194
1717
  const max = measureOf(li) - reserve;
1195
- let w = 0;
1718
+ let w = Math.max(0, unitAt(i).rubyGrow?.start ?? 0);
1196
1719
  let lastBreak = -1;
1197
1720
  let end = -1;
1198
1721
  let head;
@@ -1226,7 +1749,7 @@ stop) {
1226
1749
  // line may end with was one that folded).
1227
1750
  }
1228
1751
  const lead = fit && k === i ? fit.lead(u) : 0;
1229
- if (!u.note && w + u.width - lead - (fit ? fit.tail(u) : 0) <= max + FIT_EPS) {
1752
+ if (!u.note && w + u.width - lead - (fit ? fit.tail(u) : 0) + endGrow(k) <= max + FIT_EPS) {
1230
1753
  w += u.width - lead;
1231
1754
  if (fit)
1232
1755
  give += fit.give(u, k === i, false);
@@ -1248,13 +1771,13 @@ stop) {
1248
1771
  hang = true;
1249
1772
  break;
1250
1773
  }
1251
- const m = breaks[k] ? -1 : groupEnd(units, breaks, k);
1774
+ const m = pull === li ? n - 1 : breaks[k] ? -1 : groupEnd(units, breaks, k);
1252
1775
  if (m >= k) {
1253
1776
  let width = w;
1254
1777
  let room = give;
1255
1778
  for (let q = k; q <= m; q++) {
1256
1779
  const uq = unitAt(q);
1257
- const t = q === m ? fit.tail(uq) : 0;
1780
+ const t = q === m ? fit.tail(uq) - endGrow(q) : 0;
1258
1781
  width += uq.width - t;
1259
1782
  room += fit.give(uq, false, q === m);
1260
1783
  }
@@ -1342,9 +1865,10 @@ stop) {
1342
1865
  return out;
1343
1866
  }
1344
1867
  /** Whether a note's unit may not open its lower row: a mark that may not
1345
- * start a line, or a word space (it ends the upper row instead). */
1868
+ * start a line (the small kana and the rest of the Japanese classes under
1869
+ * a Japanese level), or a word space (it ends the upper row instead). */
1346
1870
  function noteRowStartProhibited(u, level) {
1347
- return u.noteSpace === true || isLineStartProhibited(u.first, level);
1871
+ return u.noteSpace === true || startProhibited(u, level);
1348
1872
  }
1349
1873
  /** Replace each run of a warichu note's characters on a line by the part
1350
1874
  * folded into its two rows (see `cjkAnnotate.ts`). `us` is changed in
@@ -1372,7 +1896,7 @@ function foldNotes(us, level, em) {
1372
1896
  function noteFragment(part, level, em) {
1373
1897
  const note = part[0].note;
1374
1898
  const widths = part.map((u) => u.width);
1375
- const at = splitNote(widths, part.map((u) => noteRowStartProhibited(u, level)), part.map((u) => isLineEndProhibited(u.last, level)));
1899
+ const at = splitNote(widths, part.map((u) => noteRowStartProhibited(u, level)), part.map((u) => endProhibited(u, level)));
1376
1900
  const noteFont = note.fontString ?? part[0].style.font;
1377
1901
  const rows = noteRowBaselines(em, fontEm(noteFont));
1378
1902
  const runs = [];
@@ -1435,7 +1959,9 @@ function alignEdgeRubies(us) {
1435
1959
  const align = (j, start) => {
1436
1960
  const u = us[j];
1437
1961
  const g = u.ruby?.geometry;
1438
- if (!g || u.ruby.position === 'right' || g.runs.length === 0)
1962
+ // A base with kanbun marks keeps its reading where the marks were set
1963
+ // against it (#430).
1964
+ if (!g || u.ruby.position === 'right' || u.ruby.jis || g.runs.length === 0 || u.kunten)
1439
1965
  return;
1440
1966
  const base = u.width - 2 * g.inset;
1441
1967
  const rt = g.rtWidth;
@@ -1471,7 +1997,8 @@ function clampReadings(segments) {
1471
1997
  let x = 0;
1472
1998
  for (const s of segments) {
1473
1999
  const ruby = s.ruby;
1474
- if (ruby && ruby.position !== 'right' && ruby.runs.length > 0) {
2000
+ // A Japanese reading (`id`) was set inside the line already (#422).
2001
+ if (ruby && ruby.position !== 'right' && ruby.id === undefined && ruby.runs.length > 0) {
1475
2002
  const lo = ruby.runs[0].dx;
1476
2003
  const hi = lo + ruby.rtWidth;
1477
2004
  let shift = 0;
@@ -1508,9 +2035,16 @@ function spread(amount, caps) {
1508
2035
  * character that may not open a line, clreq §6.2.2.3) gives up
1509
2036
  * blank in clreq's order — word spaces to a quarter em, interpuncts,
1510
2037
  * brackets, pause marks, Han–Latin spaces to an eighth of an em, stop marks
1511
- * last — each step shared equally, until it fits. `us` holds the line's
1512
- * units and is changed in place; a unit that changes is replaced by a copy,
1513
- * since the paragraph's units serve every attempt at breaking it.
2038
+ * last — each step shared equally, until it fits. Japanese full-width text
2039
+ * takes JLReq §3.8.3's order instead (`jlreqShrinkStep`): word spaces, the
2040
+ * half em after the line's last mark and then a middle dot's quarter (each
2041
+ * all or nothing: what it gives beyond the need is spread back when the
2042
+ * line is justified), the middle dots' quarters inside the line, the half
2043
+ * ems of brackets and 、, inside it, Han–Latin spaces; never the half em
2044
+ * after a 。 inside the line. A mark that hangs keeps its glyph alone. `us`
2045
+ * holds the line's units and is changed in place; a unit that changes is
2046
+ * replaced by a copy, since the paragraph's units serve every attempt at
2047
+ * breaking it.
1514
2048
  */
1515
2049
  function fitLine(us, units, range, lastK, isLast, measure, fit) {
1516
2050
  const copy = (j) => {
@@ -1527,6 +2061,9 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1527
2061
  hang = copy(us.length - 1);
1528
2062
  us.pop();
1529
2063
  hang.width -= fit.edge(hang, false, true);
2064
+ // ぶら下げ: the hung mark is its glyph, past the measure.
2065
+ if (fit.jlreq && hang.punct)
2066
+ hang.width -= shrinkPunctuation(hang.punct, hang.punct.blank, true);
1530
2067
  while (us.length > 0 && us[us.length - 1].kind === 'space')
1531
2068
  us.pop();
1532
2069
  }
@@ -1549,17 +2086,27 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1549
2086
  if (over <= FIT_EPS)
1550
2087
  return hang;
1551
2088
  // Steps of the reduction order: word spaces (1), interpuncts (2),
1552
- // brackets (3), pause marks (4), Han–Latin spaces (5), stop marks (6).
1553
- const stepOf = (u) => (u.kind === 'space' ? (u.auto ? 5 : 1) : u.punct ? shrinkStep(u.punct) : 0);
2089
+ // brackets (3), pause marks (4), Han–Latin spaces (5), stop marks (6);
2090
+ // in JLReq spacing, word spaces (1), the line end (2, 3), middle dots
2091
+ // (4), brackets and 、, (5), Han–Latin spaces (6).
2092
+ const last = us.length - 1;
2093
+ const atEnd = (j) => fit.jlreq && j === last;
2094
+ const stepOf = (u, j) => {
2095
+ if (u.kind === 'space')
2096
+ return u.aki ? 0 : u.auto ? (fit.jlreq ? 6 : 5) : 1;
2097
+ if (!u.punct)
2098
+ return 0;
2099
+ return fit.jlreq ? jlreqShrinkStep(u.punct, atEnd(j)) : shrinkStep(u.punct);
2100
+ };
1554
2101
  for (let step = 1; step <= 6 && over > FIT_EPS; step++) {
1555
2102
  const members = [];
1556
2103
  const caps = [];
1557
2104
  for (let j = 0; j < us.length; j++) {
1558
2105
  const u = us[j];
1559
- if (stepOf(u) !== step)
2106
+ if (stepOf(u, j) !== step)
1560
2107
  continue;
1561
2108
  // The marks at the edges already gave up their lead and tail.
1562
- const cap = fit.give(u, false, false);
2109
+ const cap = fit.give(u, false, atEnd(j));
1563
2110
  if (cap <= 0)
1564
2111
  continue;
1565
2112
  members.push(j);
@@ -1567,13 +2114,15 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1567
2114
  }
1568
2115
  if (members.length === 0)
1569
2116
  continue;
1570
- const takes = spread(over, caps);
2117
+ // The blank at a JLReq line end goes whole or not at all (§3.1.9).
2118
+ const takes = fit.jlreq && (step === 2 || step === 3) ? caps : spread(over, caps);
1571
2119
  for (let m = 0; m < members.length; m++) {
1572
2120
  const take = takes[m];
1573
2121
  if (take <= 0)
1574
2122
  continue;
1575
- const c = copy(members[m]);
1576
- const given = c.punct ? shrinkPunctuation(c.punct, take) : take;
2123
+ const j = members[m];
2124
+ const c = copy(j);
2125
+ const given = c.punct ? shrinkPunctuation(c.punct, take, atEnd(j)) : take;
1577
2126
  c.width -= given;
1578
2127
  over -= given;
1579
2128
  }
@@ -1609,7 +2158,10 @@ function composeLine(units, range, li, isLast, ctx) {
1609
2158
  // A warichu note's characters fold into their two rows (#195).
1610
2159
  if (us.some((u) => u.note))
1611
2160
  foldNotes(us, ctx.level, ctx.em);
1612
- // A ruby base at either edge aligns to it with its reading (#194).
2161
+ // A ruby base at either edge aligns to it with its reading (#194); a
2162
+ // Japanese one is laid out again there (#422).
2163
+ if (us.some((u) => u.ruby?.jis))
2164
+ relayJisEdges(us);
1613
2165
  alignEdgeRubies(us);
1614
2166
  const hang = ctx.fit ? fitLine(us, units, range, lastK, isLast, measure, ctx.fit) : undefined;
1615
2167
  const justify = ctx.textAlign === 'justify' && !isLast;
@@ -1628,9 +2180,15 @@ function composeLine(units, range, li, isLast, ctx) {
1628
2180
  // Han–Latin spaces (`cjk.latinSpacing`): they flex apart from word
1629
2181
  // spaces, and the renderers leave them as set.
1630
2182
  const autos = [];
2183
+ const jlreq = ctx.fit?.jlreq === true;
1631
2184
  for (let j = 0; j < us.length; j++) {
1632
2185
  const u = us[j];
1633
2186
  content += u.width;
2187
+ // The space after ?! keeps its em (JLReq §3.1.6).
2188
+ if (u.kind === 'space' && u.aki) {
2189
+ stretches.push(false);
2190
+ continue;
2191
+ }
1634
2192
  if (u.kind === 'space' && u.auto)
1635
2193
  autos.push(j);
1636
2194
  else if (u.kind === 'space') {
@@ -1638,7 +2196,7 @@ function composeLine(units, range, li, isLast, ctx) {
1638
2196
  natural += u.width;
1639
2197
  widest = Math.max(widest, u.width);
1640
2198
  }
1641
- const s = j < us.length - 1 && gapStretches(u, us[j + 1]);
2199
+ const s = j < us.length - 1 && gapAfterStretches(us, j, jlreq);
1642
2200
  stretches.push(s);
1643
2201
  if (s)
1644
2202
  gaps++;
@@ -1727,6 +2285,7 @@ function composeLine(units, range, li, isLast, ctx) {
1727
2285
  ...(s.marks ? { cjkMarks: s.marks } : {}),
1728
2286
  ...(s.inserted ? { inserted: true } : {}),
1729
2287
  ...(s.color ? { color: s.color } : {}),
2288
+ ...(s.lang !== undefined ? { lang: s.lang } : {}),
1730
2289
  ...(t !== undefined ? { tracking: t } : {}),
1731
2290
  // Every mark that gave up blank carries its ink offset (0 when only
1732
2291
  // the blank after its glyph went), and so does a shared mark set in
@@ -1738,9 +2297,38 @@ function composeLine(units, range, li, isLast, ctx) {
1738
2297
  ...(u.shift !== undefined ? { baselineShift: u.shift } : {}),
1739
2298
  };
1740
2299
  };
2300
+ /** The segment of a ruby base `width` px wide, `t` its tracking. */
2301
+ const rubySegment = (u, width, t) => {
2302
+ const ruby = u.ruby;
2303
+ const g = ruby.geometry;
2304
+ // The box less the advance kanbun marks add after it (#430).
2305
+ const box = u.width - (u.kunten?.extra ?? 0);
2306
+ // A Japanese ruby (#422) names its annotation (`id`), so renderers
2307
+ // can set one `<ruby>` for a word.
2308
+ const jis = ruby.jis !== undefined;
2309
+ return {
2310
+ ...segmentOf(u, width, t),
2311
+ text: u.text,
2312
+ ...(g.inset > 1e-9 ? { inkOffset: g.inset } : {}),
2313
+ // A base spread 1:2:1 under a longer Japanese reading.
2314
+ ...(g.tracking ? { tracking: g.tracking } : {}),
2315
+ ruby: {
2316
+ text: ruby.span.text,
2317
+ fontString: ruby.span.fontString ?? g.runs[0]?.fontString ?? u.style.font,
2318
+ baseWidth: ruby.base ?? box - g.inset * 2 - (ruby.position === 'right' ? g.rtWidth : 0),
2319
+ rtWidth: g.rtWidth,
2320
+ position: ruby.position,
2321
+ ...(ruby.span.group ? { group: true } : {}),
2322
+ ...(ruby.span.color ? { color: ruby.span.color } : {}),
2323
+ ...(jis ? { id: ruby.span.id } : {}),
2324
+ ...(jis && ruby.span.jukugo ? { jukugo: true } : {}),
2325
+ runs: g.runs,
2326
+ },
2327
+ };
2328
+ };
1741
2329
  for (let j = 0; j < us.length; j++) {
1742
2330
  const u = us[j];
1743
- if (u.kind === 'space' && u.auto) {
2331
+ if (u.kind === 'space' && (u.auto || u.aki)) {
1744
2332
  pieces.push({ seg: { kind: 'space', text: u.text, width: u.width, autospace: true }, parts: [u.text], key: undefined });
1745
2333
  continue;
1746
2334
  }
@@ -1750,6 +2338,9 @@ function composeLine(units, range, li, isLast, ctx) {
1750
2338
  }
1751
2339
  if (u.kind === 'atomic') {
1752
2340
  const seg = tokenSegment(u.token);
2341
+ // A side marker carries the gap after the character it marks.
2342
+ if (seg.sideMarker && tracking > 0 && stretches[j])
2343
+ seg.width += tracking;
1753
2344
  pieces.push({ seg, parts: [seg.text], key: undefined });
1754
2345
  continue;
1755
2346
  }
@@ -1759,6 +2350,21 @@ function composeLine(units, range, li, isLast, ctx) {
1759
2350
  pieces.push({ seg: { kind: 'text', text: u.text, width: u.width + gap, warichu: u.warichu }, parts: [u.text], key: undefined });
1760
2351
  continue;
1761
2352
  }
2353
+ if (u.kunten?.vdt) {
2354
+ // A character with kanbun marks (#430): a segment of its own, the
2355
+ // marks painted from its start; the room they take after it (inline
2356
+ // 返り点, a 竪点, 送り仮名 that run past it) and the gap follow as a
2357
+ // space of their own, so the character is painted at its natural
2358
+ // spacing (a ruby base in it centred under its reading).
2359
+ const extra = u.kunten.extra ?? 0;
2360
+ const own = u.width - extra;
2361
+ const seg = u.ruby?.geometry ? rubySegment(u, own, undefined) : { ...segmentOf(u, own, undefined), text: u.text };
2362
+ seg.kunten = u.kunten.vdt;
2363
+ pieces.push({ seg, parts: [u.text], key: undefined });
2364
+ if (extra + gap > 0)
2365
+ pieces.push({ seg: { kind: 'space', text: '', width: extra + gap, autospace: true }, parts: [''], key: undefined });
2366
+ continue;
2367
+ }
1762
2368
  if (u.ruby?.geometry) {
1763
2369
  // A ruby base: centred in its box, its reading placed from the box's
1764
2370
  // start (#194). One character takes the gap after it as tracking; a
@@ -1766,23 +2372,8 @@ function composeLine(units, range, li, isLast, ctx) {
1766
2372
  // reading, and the gap follows the box as a space of its own (its
1767
2373
  // width final, as a Han–Latin space's is): tracking would be added
1768
2374
  // after each of its characters.
1769
- const g = u.ruby.geometry;
1770
2375
  const gapAfter = gap > 0 && u.graphemes > 1;
1771
- const seg = {
1772
- ...segmentOf(u, gapAfter ? u.width : u.width + gap, gap > 0 && !gapAfter ? gap : undefined),
1773
- text: u.text,
1774
- ...(g.inset > 1e-9 ? { inkOffset: g.inset } : {}),
1775
- ruby: {
1776
- text: u.ruby.span.text,
1777
- fontString: u.ruby.span.fontString ?? g.runs[0]?.fontString ?? u.style.font,
1778
- baseWidth: u.width - g.inset * 2 - (u.ruby.position === 'right' ? g.rtWidth : 0),
1779
- rtWidth: g.rtWidth,
1780
- position: u.ruby.position,
1781
- ...(u.ruby.span.group ? { group: true } : {}),
1782
- ...(u.ruby.span.color ? { color: u.ruby.span.color } : {}),
1783
- runs: g.runs,
1784
- },
1785
- };
2376
+ const seg = rubySegment(u, gapAfter ? u.width : u.width + gap, gap > 0 && !gapAfter ? gap : undefined);
1786
2377
  pieces.push({ seg, parts: [u.text], key: undefined });
1787
2378
  if (gapAfter)
1788
2379
  pieces.push({ seg: { kind: 'space', text: '', width: gap, autospace: true }, parts: [''], key: undefined });
@@ -1848,6 +2439,40 @@ function composeLine(units, range, li, isLast, ctx) {
1848
2439
  cjkComposed: true,
1849
2440
  };
1850
2441
  }
2442
+ /**
2443
+ * The first line's indent of a paragraph that opens with an opening
2444
+ * bracket, as `cjk.paragraphStartBracket` sets it (JLReq §3.1.5), with the
2445
+ * bracket's blank before its glyph given up (it is `units`' own unit,
2446
+ * replaced): pattern ① (`indent`) keeps the indent, ③ (`half`) sets the
2447
+ * bracket's glyph inside the indent so the text after it starts where the
2448
+ * indent ends (the bracket and its half em of blank fill a one-em indent),
2449
+ * 天付き (`flush`) drops the indent. `indentPx` when the composition leaves
2450
+ * the bracket to the line-start rules, the paragraph has no indent or
2451
+ * does not open with a bracket.
2452
+ */
2453
+ function paragraphStartIndent(units, indentPx, c) {
2454
+ const pattern = c.paragraphStartBracket;
2455
+ if (!pattern || indentPx <= 0)
2456
+ return indentPx;
2457
+ const f = units.findIndex((u) => u.kind !== 'space');
2458
+ const u = units[f];
2459
+ if (!u?.punct || u.first !== 'opening')
2460
+ return indentPx;
2461
+ // Its blank before the glyph, whatever `trimLineStart` says.
2462
+ const box = { ...u.punct };
2463
+ const lead = box.blank - box.cutStart;
2464
+ const trimmed = { ...u, punct: box };
2465
+ if (lead > 0 && box.side === 'start') {
2466
+ box.cutStart += lead;
2467
+ trimmed.width -= lead;
2468
+ }
2469
+ units[f] = trimmed;
2470
+ if (pattern === 'flush')
2471
+ return 0;
2472
+ if (pattern === 'half')
2473
+ return Math.max(0, indentPx - trimmed.width);
2474
+ return indentPx;
2475
+ }
1851
2476
  /**
1852
2477
  * Set a Chinese, Japanese or Korean paragraph (see the module comment):
1853
2478
  * `options.cjkLineBreak` (else the document's level) decides where lines
@@ -1885,6 +2510,9 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1885
2510
  // Ruby readings (#194), laid out once their neighbours are known.
1886
2511
  if (units.some((u) => u.ruby))
1887
2512
  sizeRubies(units);
2513
+ // Kanbun marks (#430), beside the readings.
2514
+ if (units.some((u) => u.kunten))
2515
+ sizeKunten(units, letterSpacingPx);
1888
2516
  if (!units.some((u) => u.kind !== 'space'))
1889
2517
  return { lines: [], totalHeight: 0 };
1890
2518
  const fit = lineFitOf(composition);
@@ -1892,7 +2520,8 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1892
2520
  const breaks = breakOpportunities(units, level);
1893
2521
  const indentPx = options?.firstLineIndentPx ?? 0;
1894
2522
  const hanging = options?.hangingIndent ?? false;
1895
- const indentOf = (li) => (indentPx > 0 ? (hanging ? (li === 0 ? 0 : indentPx) : (li === 0 ? indentPx : 0)) : 0);
2523
+ const firstIndent = hanging ? 0 : paragraphStartIndent(units, indentPx, composition);
2524
+ const indentOf = (li) => (indentPx > 0 ? (hanging ? (li === 0 ? 0 : indentPx) : (li === 0 ? firstIndent : 0)) : 0);
1896
2525
  const measureOf = (li) => lineMeasure(maxWidthPx, options?.restWidths, li) - indentOf(li);
1897
2526
  const sizeMatch = FONT_SIZE_RE.exec(normalFont);
1898
2527
  const em = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
@@ -1920,7 +2549,18 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1920
2549
  // gives it a runt penalty): a last line that holds one character, alone
1921
2550
  // or with its closing marks, takes the last character the line above may
1922
2551
  // give up (push-out), and that line is spread to the measure. When it
1923
- // would need more than the tracking cap, the runt stays.
2552
+ // would need more than the tracking cap, the runt stays. Japanese text
2553
+ // (泣き別れ) first tries the other way JLReq prefers (§3.8.2): the line
2554
+ // above takes the character in by giving up blank (push-in), and the
2555
+ // paragraph is a line shorter.
2556
+ if ((options?.runtPenalty ?? 0) > 0 && endsOnOneCharacter(lines) && fit?.jlreq) {
2557
+ const pulled = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, undefined, ranges.length - 2);
2558
+ if (pulled.length === ranges.length - 1) {
2559
+ const set = compose(pulled);
2560
+ if (!set.some((l) => l.cjkLoose) && !endsOnOneCharacter(set))
2561
+ lines = set;
2562
+ }
2563
+ }
1924
2564
  if ((options?.runtPenalty ?? 0) > 0 && endsOnOneCharacter(lines)) {
1925
2565
  const li = ranges.length - 2;
1926
2566
  const above = ranges[li];