postext 1.15.1 → 1.16.1

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 (457) 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/chipLatinSpacing.test.d.ts +2 -0
  12. package/dist/__tests__/cjk/chipLatinSpacing.test.d.ts.map +1 -0
  13. package/dist/__tests__/cjk/chipLatinSpacing.test.js +137 -0
  14. package/dist/__tests__/cjk/chipLatinSpacing.test.js.map +1 -0
  15. package/dist/__tests__/cjk/chipTracking.test.d.ts +2 -0
  16. package/dist/__tests__/cjk/chipTracking.test.d.ts.map +1 -0
  17. package/dist/__tests__/cjk/chipTracking.test.js +56 -0
  18. package/dist/__tests__/cjk/chipTracking.test.js.map +1 -0
  19. package/dist/__tests__/cjk/japaneseHtml.test.d.ts +2 -0
  20. package/dist/__tests__/cjk/japaneseHtml.test.d.ts.map +1 -0
  21. package/dist/__tests__/cjk/japaneseHtml.test.js +163 -0
  22. package/dist/__tests__/cjk/japaneseHtml.test.js.map +1 -0
  23. package/dist/__tests__/cjk/japaneseMarks.test.d.ts +2 -0
  24. package/dist/__tests__/cjk/japaneseMarks.test.d.ts.map +1 -0
  25. package/dist/__tests__/cjk/japaneseMarks.test.js +289 -0
  26. package/dist/__tests__/cjk/japaneseMarks.test.js.map +1 -0
  27. package/dist/__tests__/cjk/japaneseMarksRender.test.d.ts +2 -0
  28. package/dist/__tests__/cjk/japaneseMarksRender.test.d.ts.map +1 -0
  29. package/dist/__tests__/cjk/japaneseMarksRender.test.js +161 -0
  30. package/dist/__tests__/cjk/japaneseMarksRender.test.js.map +1 -0
  31. package/dist/__tests__/cjk/japaneseRuby.test.d.ts +2 -0
  32. package/dist/__tests__/cjk/japaneseRuby.test.d.ts.map +1 -0
  33. package/dist/__tests__/cjk/japaneseRuby.test.js +457 -0
  34. package/dist/__tests__/cjk/japaneseRuby.test.js.map +1 -0
  35. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.d.ts +2 -0
  36. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.d.ts.map +1 -0
  37. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js +50 -0
  38. package/dist/__tests__/cjk/japaneseSegmentLanguage.test.js.map +1 -0
  39. package/dist/__tests__/cjk/japaneseSpacing.test.d.ts +2 -0
  40. package/dist/__tests__/cjk/japaneseSpacing.test.d.ts.map +1 -0
  41. package/dist/__tests__/cjk/japaneseSpacing.test.js +417 -0
  42. package/dist/__tests__/cjk/japaneseSpacing.test.js.map +1 -0
  43. package/dist/__tests__/cjk/kanbunKunten.test.d.ts +2 -0
  44. package/dist/__tests__/cjk/kanbunKunten.test.d.ts.map +1 -0
  45. package/dist/__tests__/cjk/kanbunKunten.test.js +230 -0
  46. package/dist/__tests__/cjk/kanbunKunten.test.js.map +1 -0
  47. package/dist/__tests__/cjk/kanbunKuntenRender.test.d.ts +2 -0
  48. package/dist/__tests__/cjk/kanbunKuntenRender.test.d.ts.map +1 -0
  49. package/dist/__tests__/cjk/kanbunKuntenRender.test.js +127 -0
  50. package/dist/__tests__/cjk/kanbunKuntenRender.test.js.map +1 -0
  51. package/dist/__tests__/cjk/keepAll.test.d.ts +2 -0
  52. package/dist/__tests__/cjk/keepAll.test.d.ts.map +1 -0
  53. package/dist/__tests__/cjk/keepAll.test.js +146 -0
  54. package/dist/__tests__/cjk/keepAll.test.js.map +1 -0
  55. package/dist/__tests__/cjk/kinsokuJa.test.d.ts +2 -0
  56. package/dist/__tests__/cjk/kinsokuJa.test.d.ts.map +1 -0
  57. package/dist/__tests__/cjk/kinsokuJa.test.js +388 -0
  58. package/dist/__tests__/cjk/kinsokuJa.test.js.map +1 -0
  59. package/dist/__tests__/cjk/kinsokuJaBuild.test.d.ts +2 -0
  60. package/dist/__tests__/cjk/kinsokuJaBuild.test.d.ts.map +1 -0
  61. package/dist/__tests__/cjk/kinsokuJaBuild.test.js +38 -0
  62. package/dist/__tests__/cjk/kinsokuJaBuild.test.js.map +1 -0
  63. package/dist/__tests__/cjk/resourceAnnotations.test.d.ts +2 -0
  64. package/dist/__tests__/cjk/resourceAnnotations.test.d.ts.map +1 -0
  65. package/dist/__tests__/cjk/resourceAnnotations.test.js +223 -0
  66. package/dist/__tests__/cjk/resourceAnnotations.test.js.map +1 -0
  67. package/dist/__tests__/defaults/cjk.test.js +10 -2
  68. package/dist/__tests__/defaults/cjk.test.js.map +1 -1
  69. package/dist/__tests__/exports.test.js +37 -0
  70. package/dist/__tests__/exports.test.js.map +1 -1
  71. package/dist/__tests__/headingLetterSpacing.test.js +2 -1
  72. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
  73. package/dist/__tests__/indexChinese.test.js +1 -1
  74. package/dist/__tests__/indexJapanese.test.d.ts +2 -0
  75. package/dist/__tests__/indexJapanese.test.d.ts.map +1 -0
  76. package/dist/__tests__/indexJapanese.test.js +242 -0
  77. package/dist/__tests__/indexJapanese.test.js.map +1 -0
  78. package/dist/__tests__/japaneseCaptions.test.d.ts +2 -0
  79. package/dist/__tests__/japaneseCaptions.test.d.ts.map +1 -0
  80. package/dist/__tests__/japaneseCaptions.test.js +79 -0
  81. package/dist/__tests__/japaneseCaptions.test.js.map +1 -0
  82. package/dist/__tests__/japaneseHeadings.test.d.ts +2 -0
  83. package/dist/__tests__/japaneseHeadings.test.d.ts.map +1 -0
  84. package/dist/__tests__/japaneseHeadings.test.js +364 -0
  85. package/dist/__tests__/japaneseHeadings.test.js.map +1 -0
  86. package/dist/__tests__/japaneseLocale.test.d.ts +2 -0
  87. package/dist/__tests__/japaneseLocale.test.d.ts.map +1 -0
  88. package/dist/__tests__/japaneseLocale.test.js +201 -0
  89. package/dist/__tests__/japaneseLocale.test.js.map +1 -0
  90. package/dist/__tests__/japaneseNotes.test.d.ts +2 -0
  91. package/dist/__tests__/japaneseNotes.test.d.ts.map +1 -0
  92. package/dist/__tests__/japaneseNotes.test.js +336 -0
  93. package/dist/__tests__/japaneseNotes.test.js.map +1 -0
  94. package/dist/__tests__/japaneseNumbering.test.d.ts +2 -0
  95. package/dist/__tests__/japaneseNumbering.test.d.ts.map +1 -0
  96. package/dist/__tests__/japaneseNumbering.test.js +292 -0
  97. package/dist/__tests__/japaneseNumbering.test.js.map +1 -0
  98. package/dist/__tests__/parse/listNesting.test.d.ts +2 -0
  99. package/dist/__tests__/parse/listNesting.test.d.ts.map +1 -0
  100. package/dist/__tests__/parse/listNesting.test.js +142 -0
  101. package/dist/__tests__/parse/listNesting.test.js.map +1 -0
  102. package/dist/__tests__/pipeline/citationsJapanese.test.d.ts +2 -0
  103. package/dist/__tests__/pipeline/citationsJapanese.test.d.ts.map +1 -0
  104. package/dist/__tests__/pipeline/citationsJapanese.test.js +110 -0
  105. package/dist/__tests__/pipeline/citationsJapanese.test.js.map +1 -0
  106. package/dist/__tests__/pipeline/listNestingLayout.test.d.ts +2 -0
  107. package/dist/__tests__/pipeline/listNestingLayout.test.d.ts.map +1 -0
  108. package/dist/__tests__/pipeline/listNestingLayout.test.js +90 -0
  109. package/dist/__tests__/pipeline/listNestingLayout.test.js.map +1 -0
  110. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +2 -2
  111. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  112. package/dist/__tests__/pipeline/safeArea.test.d.ts +2 -0
  113. package/dist/__tests__/pipeline/safeArea.test.d.ts.map +1 -0
  114. package/dist/__tests__/pipeline/safeArea.test.js +207 -0
  115. package/dist/__tests__/pipeline/safeArea.test.js.map +1 -0
  116. package/dist/__tests__/vertical/compositionPaint.test.js +6 -5
  117. package/dist/__tests__/vertical/compositionPaint.test.js.map +1 -1
  118. package/dist/__tests__/vertical/japaneseVertical.test.d.ts +2 -0
  119. package/dist/__tests__/vertical/japaneseVertical.test.d.ts.map +1 -0
  120. package/dist/__tests__/vertical/japaneseVertical.test.js +247 -0
  121. package/dist/__tests__/vertical/japaneseVertical.test.js.map +1 -0
  122. package/dist/__tests__/vertical/verticalSpans.d.ts +2 -1
  123. package/dist/__tests__/vertical/verticalSpans.d.ts.map +1 -1
  124. package/dist/__tests__/vertical/verticalSpans.js +3 -2
  125. package/dist/__tests__/vertical/verticalSpans.js.map +1 -1
  126. package/dist/__tests__/video.test.d.ts +2 -0
  127. package/dist/__tests__/video.test.d.ts.map +1 -0
  128. package/dist/__tests__/video.test.js +305 -0
  129. package/dist/__tests__/video.test.js.map +1 -0
  130. package/dist/arabicMarks.d.ts +11 -1
  131. package/dist/arabicMarks.d.ts.map +1 -1
  132. package/dist/arabicMarks.js +72 -5
  133. package/dist/arabicMarks.js.map +1 -1
  134. package/dist/bundle/adapters.d.ts +6 -0
  135. package/dist/bundle/adapters.d.ts.map +1 -1
  136. package/dist/bundle/adapters.js +30 -0
  137. package/dist/bundle/adapters.js.map +1 -1
  138. package/dist/bundle/codec.d.ts.map +1 -1
  139. package/dist/bundle/codec.js +67 -3
  140. package/dist/bundle/codec.js.map +1 -1
  141. package/dist/bundle/index.d.ts +2 -2
  142. package/dist/bundle/index.d.ts.map +1 -1
  143. package/dist/bundle/index.js +2 -2
  144. package/dist/bundle/index.js.map +1 -1
  145. package/dist/bundle/manifest.d.ts +8 -0
  146. package/dist/bundle/manifest.d.ts.map +1 -1
  147. package/dist/bundle/manifest.js +48 -1
  148. package/dist/bundle/manifest.js.map +1 -1
  149. package/dist/bundle/types.d.ts +5 -0
  150. package/dist/bundle/types.d.ts.map +1 -1
  151. package/dist/bundle/zip.js +1 -1
  152. package/dist/bundle/zip.js.map +1 -1
  153. package/dist/canvas-backend/annotations.d.ts +20 -5
  154. package/dist/canvas-backend/annotations.d.ts.map +1 -1
  155. package/dist/canvas-backend/annotations.js +59 -6
  156. package/dist/canvas-backend/annotations.js.map +1 -1
  157. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  158. package/dist/canvas-backend/blockRender.js +29 -3
  159. package/dist/canvas-backend/blockRender.js.map +1 -1
  160. package/dist/canvas-backend/renderResourceBlock.d.ts +6 -1
  161. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  162. package/dist/canvas-backend/renderResourceBlock.js +124 -5
  163. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  164. package/dist/canvas-backend/verticalText.d.ts +9 -4
  165. package/dist/canvas-backend/verticalText.d.ts.map +1 -1
  166. package/dist/canvas-backend/verticalText.js +20 -9
  167. package/dist/canvas-backend/verticalText.js.map +1 -1
  168. package/dist/chineseNumerals.d.ts +3 -2
  169. package/dist/chineseNumerals.d.ts.map +1 -1
  170. package/dist/chineseNumerals.js +11 -8
  171. package/dist/chineseNumerals.js.map +1 -1
  172. package/dist/citations/bibtex.d.ts.map +1 -1
  173. package/dist/citations/bibtex.js +4 -0
  174. package/dist/citations/bibtex.js.map +1 -1
  175. package/dist/citations/data.d.ts.map +1 -1
  176. package/dist/citations/data.js +6 -1
  177. package/dist/citations/data.js.map +1 -1
  178. package/dist/citations/types.d.ts +8 -0
  179. package/dist/citations/types.d.ts.map +1 -1
  180. package/dist/cjkMarks.d.ts +23 -14
  181. package/dist/cjkMarks.d.ts.map +1 -1
  182. package/dist/cjkMarks.js +251 -49
  183. package/dist/cjkMarks.js.map +1 -1
  184. package/dist/configWarnings.js +10 -4
  185. package/dist/configWarnings.js.map +1 -1
  186. package/dist/defaults/captionStyle.d.ts +19 -4
  187. package/dist/defaults/captionStyle.d.ts.map +1 -1
  188. package/dist/defaults/captionStyle.js +27 -8
  189. package/dist/defaults/captionStyle.js.map +1 -1
  190. package/dist/defaults/cjk.d.ts +64 -14
  191. package/dist/defaults/cjk.d.ts.map +1 -1
  192. package/dist/defaults/cjk.js +209 -29
  193. package/dist/defaults/cjk.js.map +1 -1
  194. package/dist/defaults/footnotes.d.ts +27 -4
  195. package/dist/defaults/footnotes.d.ts.map +1 -1
  196. package/dist/defaults/footnotes.js +95 -36
  197. package/dist/defaults/footnotes.js.map +1 -1
  198. package/dist/defaults/headings.d.ts +5 -0
  199. package/dist/defaults/headings.d.ts.map +1 -1
  200. package/dist/defaults/headings.js +46 -1
  201. package/dist/defaults/headings.js.map +1 -1
  202. package/dist/defaults/index.d.ts +4 -3
  203. package/dist/defaults/index.d.ts.map +1 -1
  204. package/dist/defaults/index.js +14 -5
  205. package/dist/defaults/index.js.map +1 -1
  206. package/dist/defaults/indexConfig.d.ts.map +1 -1
  207. package/dist/defaults/indexConfig.js +7 -4
  208. package/dist/defaults/indexConfig.js.map +1 -1
  209. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  210. package/dist/defaults/paragraphStyles.js +8 -0
  211. package/dist/defaults/paragraphStyles.js.map +1 -1
  212. package/dist/defaults/resourceTypes.d.ts +16 -3
  213. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  214. package/dist/defaults/resourceTypes.js +56 -8
  215. package/dist/defaults/resourceTypes.js.map +1 -1
  216. package/dist/defaults/shared.d.ts.map +1 -1
  217. package/dist/defaults/shared.js +21 -0
  218. package/dist/defaults/shared.js.map +1 -1
  219. package/dist/defaults/tableStyle.d.ts.map +1 -1
  220. package/dist/defaults/tableStyle.js +4 -1
  221. package/dist/defaults/tableStyle.js.map +1 -1
  222. package/dist/defaults/videoStyle.d.ts +9 -0
  223. package/dist/defaults/videoStyle.d.ts.map +1 -0
  224. package/dist/defaults/videoStyle.js +130 -0
  225. package/dist/defaults/videoStyle.js.map +1 -0
  226. package/dist/design/placeholders.d.ts.map +1 -1
  227. package/dist/design/placeholders.js +5 -3
  228. package/dist/design/placeholders.js.map +1 -1
  229. package/dist/html-backend.d.ts +13 -0
  230. package/dist/html-backend.d.ts.map +1 -1
  231. package/dist/html-backend.js +233 -26
  232. package/dist/html-backend.js.map +1 -1
  233. package/dist/htmlAnnotations.d.ts +47 -9
  234. package/dist/htmlAnnotations.d.ts.map +1 -1
  235. package/dist/htmlAnnotations.js +89 -17
  236. package/dist/htmlAnnotations.js.map +1 -1
  237. package/dist/index.d.ts +13 -7
  238. package/dist/index.d.ts.map +1 -1
  239. package/dist/index.js +8 -4
  240. package/dist/index.js.map +1 -1
  241. package/dist/japaneseNumerals.d.ts +44 -0
  242. package/dist/japaneseNumerals.d.ts.map +1 -0
  243. package/dist/japaneseNumerals.js +100 -0
  244. package/dist/japaneseNumerals.js.map +1 -0
  245. package/dist/knuthPlass/richAdapter.d.ts +5 -1
  246. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  247. package/dist/knuthPlass/richAdapter.js +2 -0
  248. package/dist/knuthPlass/richAdapter.js.map +1 -1
  249. package/dist/locale.d.ts +19 -12
  250. package/dist/locale.d.ts.map +1 -1
  251. package/dist/locale.js +24 -12
  252. package/dist/locale.js.map +1 -1
  253. package/dist/measure/cache.d.ts.map +1 -1
  254. package/dist/measure/cache.js +20 -9
  255. package/dist/measure/cache.js.map +1 -1
  256. package/dist/measure/canvas.d.ts +21 -0
  257. package/dist/measure/canvas.d.ts.map +1 -1
  258. package/dist/measure/canvas.js +82 -0
  259. package/dist/measure/canvas.js.map +1 -1
  260. package/dist/measure/cjk.d.ts +3 -1
  261. package/dist/measure/cjk.d.ts.map +1 -1
  262. package/dist/measure/cjk.js +4 -2
  263. package/dist/measure/cjk.js.map +1 -1
  264. package/dist/measure/cjkAnnotate.d.ts +62 -3
  265. package/dist/measure/cjkAnnotate.d.ts.map +1 -1
  266. package/dist/measure/cjkAnnotate.js +99 -2
  267. package/dist/measure/cjkAnnotate.js.map +1 -1
  268. package/dist/measure/cjkClasses.d.ts +93 -13
  269. package/dist/measure/cjkClasses.d.ts.map +1 -1
  270. package/dist/measure/cjkClasses.js +204 -14
  271. package/dist/measure/cjkClasses.js.map +1 -1
  272. package/dist/measure/cjkCompose.d.ts +10 -4
  273. package/dist/measure/cjkCompose.d.ts.map +1 -1
  274. package/dist/measure/cjkCompose.js +834 -85
  275. package/dist/measure/cjkCompose.js.map +1 -1
  276. package/dist/measure/cjkPunctuation.d.ts +73 -14
  277. package/dist/measure/cjkPunctuation.d.ts.map +1 -1
  278. package/dist/measure/cjkPunctuation.js +97 -21
  279. package/dist/measure/cjkPunctuation.js.map +1 -1
  280. package/dist/measure/plain.js +2 -2
  281. package/dist/measure/plain.js.map +1 -1
  282. package/dist/measure/rich.d.ts +12 -2
  283. package/dist/measure/rich.d.ts.map +1 -1
  284. package/dist/measure/rich.js +46 -8
  285. package/dist/measure/rich.js.map +1 -1
  286. package/dist/measure/rubyJis.d.ts +96 -0
  287. package/dist/measure/rubyJis.d.ts.map +1 -0
  288. package/dist/measure/rubyJis.js +245 -0
  289. package/dist/measure/rubyJis.js.map +1 -0
  290. package/dist/measure/types.d.ts +6 -1
  291. package/dist/measure/types.d.ts.map +1 -1
  292. package/dist/measure/types.js.map +1 -1
  293. package/dist/measure/vertical.d.ts +13 -5
  294. package/dist/measure/vertical.d.ts.map +1 -1
  295. package/dist/measure/vertical.js +16 -6
  296. package/dist/measure/vertical.js.map +1 -1
  297. package/dist/numberWords.d.ts +4 -3
  298. package/dist/numberWords.d.ts.map +1 -1
  299. package/dist/numberWords.js +15 -5
  300. package/dist/numberWords.js.map +1 -1
  301. package/dist/numbering.d.ts +24 -12
  302. package/dist/numbering.d.ts.map +1 -1
  303. package/dist/numbering.js +57 -11
  304. package/dist/numbering.js.map +1 -1
  305. package/dist/parse/annotations.d.ts +21 -18
  306. package/dist/parse/annotations.d.ts.map +1 -1
  307. package/dist/parse/annotations.js +140 -51
  308. package/dist/parse/annotations.js.map +1 -1
  309. package/dist/parse/blockParser.d.ts.map +1 -1
  310. package/dist/parse/blockParser.js +12 -1
  311. package/dist/parse/blockParser.js.map +1 -1
  312. package/dist/parse/index.d.ts +1 -1
  313. package/dist/parse/index.d.ts.map +1 -1
  314. package/dist/parse/index.js.map +1 -1
  315. package/dist/parse/indexMarks.d.ts.map +1 -1
  316. package/dist/parse/indexMarks.js +42 -0
  317. package/dist/parse/indexMarks.js.map +1 -1
  318. package/dist/parse/inlineFormatting.d.ts +4 -3
  319. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  320. package/dist/parse/inlineFormatting.js +9 -8
  321. package/dist/parse/inlineFormatting.js.map +1 -1
  322. package/dist/parse/listNesting.d.ts +47 -0
  323. package/dist/parse/listNesting.d.ts.map +1 -0
  324. package/dist/parse/listNesting.js +65 -0
  325. package/dist/parse/listNesting.js.map +1 -0
  326. package/dist/parse/types.d.ts +93 -5
  327. package/dist/parse/types.d.ts.map +1 -1
  328. package/dist/pipeline/annotations.d.ts +25 -15
  329. package/dist/pipeline/annotations.d.ts.map +1 -1
  330. package/dist/pipeline/annotations.js +102 -16
  331. package/dist/pipeline/annotations.js.map +1 -1
  332. package/dist/pipeline/build.d.ts.map +1 -1
  333. package/dist/pipeline/build.js +284 -31
  334. package/dist/pipeline/build.js.map +1 -1
  335. package/dist/pipeline/buildBlockKind.d.ts +7 -0
  336. package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
  337. package/dist/pipeline/buildBlockKind.js +25 -2
  338. package/dist/pipeline/buildBlockKind.js.map +1 -1
  339. package/dist/pipeline/buildHelpers.d.ts +2 -0
  340. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  341. package/dist/pipeline/buildHelpers.js +17 -0
  342. package/dist/pipeline/buildHelpers.js.map +1 -1
  343. package/dist/pipeline/buildMeasurement.d.ts +3 -0
  344. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  345. package/dist/pipeline/buildMeasurement.js +1 -0
  346. package/dist/pipeline/buildMeasurement.js.map +1 -1
  347. package/dist/pipeline/calloutLayout.d.ts +2 -0
  348. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  349. package/dist/pipeline/calloutLayout.js +5 -5
  350. package/dist/pipeline/calloutLayout.js.map +1 -1
  351. package/dist/pipeline/citations.d.ts +1 -1
  352. package/dist/pipeline/citations.d.ts.map +1 -1
  353. package/dist/pipeline/citations.js +15 -7
  354. package/dist/pipeline/citations.js.map +1 -1
  355. package/dist/pipeline/columnBalancing.d.ts +22 -0
  356. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  357. package/dist/pipeline/columnBalancing.js +80 -3
  358. package/dist/pipeline/columnBalancing.js.map +1 -1
  359. package/dist/pipeline/config.d.ts.map +1 -1
  360. package/dist/pipeline/config.js +4 -3
  361. package/dist/pipeline/config.js.map +1 -1
  362. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  363. package/dist/pipeline/contentWarnings.js +19 -0
  364. package/dist/pipeline/contentWarnings.js.map +1 -1
  365. package/dist/pipeline/continuation.d.ts.map +1 -1
  366. package/dist/pipeline/continuation.js +3 -3
  367. package/dist/pipeline/continuation.js.map +1 -1
  368. package/dist/pipeline/crossRefs.d.ts.map +1 -1
  369. package/dist/pipeline/crossRefs.js +3 -0
  370. package/dist/pipeline/crossRefs.js.map +1 -1
  371. package/dist/pipeline/footnotes.d.ts +20 -12
  372. package/dist/pipeline/footnotes.d.ts.map +1 -1
  373. package/dist/pipeline/footnotes.js +62 -16
  374. package/dist/pipeline/footnotes.js.map +1 -1
  375. package/dist/pipeline/headingStyles.js +1 -1
  376. package/dist/pipeline/headingStyles.js.map +1 -1
  377. package/dist/pipeline/indexDirective.d.ts.map +1 -1
  378. package/dist/pipeline/indexDirective.js +89 -12
  379. package/dist/pipeline/indexDirective.js.map +1 -1
  380. package/dist/pipeline/indexGroups.d.ts +5 -3
  381. package/dist/pipeline/indexGroups.d.ts.map +1 -1
  382. package/dist/pipeline/indexGroups.js +9 -4
  383. package/dist/pipeline/indexGroups.js.map +1 -1
  384. package/dist/pipeline/indexJapanese.d.ts +75 -0
  385. package/dist/pipeline/indexJapanese.d.ts.map +1 -0
  386. package/dist/pipeline/indexJapanese.js +264 -0
  387. package/dist/pipeline/indexJapanese.js.map +1 -0
  388. package/dist/pipeline/lineSpan.d.ts +36 -0
  389. package/dist/pipeline/lineSpan.d.ts.map +1 -0
  390. package/dist/pipeline/lineSpan.js +36 -0
  391. package/dist/pipeline/lineSpan.js.map +1 -0
  392. package/dist/pipeline/lists.js +2 -2
  393. package/dist/pipeline/lists.js.map +1 -1
  394. package/dist/pipeline/measureContentBlock.d.ts +4 -0
  395. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  396. package/dist/pipeline/measureContentBlock.js +57 -5
  397. package/dist/pipeline/measureContentBlock.js.map +1 -1
  398. package/dist/pipeline/outline.d.ts.map +1 -1
  399. package/dist/pipeline/outline.js +3 -1
  400. package/dist/pipeline/outline.js.map +1 -1
  401. package/dist/pipeline/paragraphContainers.d.ts +25 -2
  402. package/dist/pipeline/paragraphContainers.d.ts.map +1 -1
  403. package/dist/pipeline/paragraphContainers.js +82 -2
  404. package/dist/pipeline/paragraphContainers.js.map +1 -1
  405. package/dist/pipeline/parts.d.ts +2 -2
  406. package/dist/pipeline/parts.js +3 -3
  407. package/dist/pipeline/parts.js.map +1 -1
  408. package/dist/pipeline/placement.d.ts.map +1 -1
  409. package/dist/pipeline/placement.js +2 -2
  410. package/dist/pipeline/placement.js.map +1 -1
  411. package/dist/pipeline/resourceLayout.d.ts +6 -0
  412. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  413. package/dist/pipeline/resourceLayout.js +93 -17
  414. package/dist/pipeline/resourceLayout.js.map +1 -1
  415. package/dist/pipeline/resourceNumbering.d.ts +5 -2
  416. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  417. package/dist/pipeline/resourceNumbering.js +12 -7
  418. package/dist/pipeline/resourceNumbering.js.map +1 -1
  419. package/dist/pipeline/safeArea.d.ts +29 -0
  420. package/dist/pipeline/safeArea.d.ts.map +1 -0
  421. package/dist/pipeline/safeArea.js +89 -0
  422. package/dist/pipeline/safeArea.js.map +1 -0
  423. package/dist/pipeline/styles.d.ts +16 -1
  424. package/dist/pipeline/styles.d.ts.map +1 -1
  425. package/dist/pipeline/styles.js +8 -1
  426. package/dist/pipeline/styles.js.map +1 -1
  427. package/dist/pipeline/verse.d.ts +3 -0
  428. package/dist/pipeline/verse.d.ts.map +1 -1
  429. package/dist/pipeline/verse.js +1 -1
  430. package/dist/pipeline/verse.js.map +1 -1
  431. package/dist/pipeline/verticalMetrics.d.ts.map +1 -1
  432. package/dist/pipeline/verticalMetrics.js +4 -0
  433. package/dist/pipeline/verticalMetrics.js.map +1 -1
  434. package/dist/pipeline/videoOverlay.d.ts +24 -0
  435. package/dist/pipeline/videoOverlay.d.ts.map +1 -0
  436. package/dist/pipeline/videoOverlay.js +134 -0
  437. package/dist/pipeline/videoOverlay.js.map +1 -0
  438. package/dist/types.d.ts +594 -50
  439. package/dist/types.d.ts.map +1 -1
  440. package/dist/uprightArabic.js +1 -1
  441. package/dist/uprightArabic.js.map +1 -1
  442. package/dist/vdt.d.ts +221 -17
  443. package/dist/vdt.d.ts.map +1 -1
  444. package/dist/vdt.js.map +1 -1
  445. package/dist/video/qr.d.ts +21 -0
  446. package/dist/video/qr.d.ts.map +1 -0
  447. package/dist/video/qr.js +326 -0
  448. package/dist/video/qr.js.map +1 -0
  449. package/dist/video/url.d.ts +62 -0
  450. package/dist/video/url.d.ts.map +1 -0
  451. package/dist/video/url.js +206 -0
  452. package/dist/video/url.js.map +1 -0
  453. package/dist/writingMode.d.ts +74 -20
  454. package/dist/writingMode.d.ts.map +1 -1
  455. package/dist/writingMode.js +146 -27
  456. package/dist/writingMode.js.map +1 -1
  457. 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,8 +290,197 @@ 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 } : {}) },
294
+ };
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 } : {}),
259
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
+ }
260
484
  }
261
485
  /**
262
486
  * Lay out each ruby base's reading (see `cjkAnnotate.ts`): its box grows
@@ -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,31 +1220,89 @@ 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
+ /** The character a chip's words open (`first`) or close (`last`) with, in
1258
+ * their logical order; undefined for a unit that is not a chip, or a chip
1259
+ * in a superscript or a subscript. */
1260
+ function chipEdgeGrapheme(u, edge) {
1261
+ const runs = u.kind === 'atomic' && !u.style.script ? u.token?.chip?.runs : undefined;
1262
+ if (!runs)
1263
+ return undefined;
1264
+ const text = runs.map((r) => r.text).join('');
1265
+ if (text === '')
1266
+ return undefined;
1267
+ return edge === 'first' ? String.fromCodePoint(text.codePointAt(0)) : lastGrapheme(text);
1268
+ }
1269
+ /** `units` with the Han–Latin spaces of `cjk.latinSpacing` (see
1270
+ * {@link prepareUnits}).
1271
+ *
1272
+ * A chip (`:chip[…]`) is a box of text: on each side it takes the space
1273
+ * its own words would (#462). `:chip[unicodedata]` between kana is Latin
1274
+ * on both sides and gets the quarter em JLReq §3.2.2 puts between
1275
+ * Japanese and Western text, as `unicodedata` set plain would;
1276
+ * `:chip[漢字]` is Han on both sides, so a Latin word touching it gets
1277
+ * the space and a kana does not. Its line-break class stays that of an
1278
+ * inline box. The rest follows from the rule for plain text: the space
1279
+ * goes at a line end and is dropped at a line start like any space, and
1280
+ * a bracket, a mark or a sign next to the chip is neither Han nor Latin,
1281
+ * so 「:chip[x]」 takes none. Down a vertical line the chip is set
1282
+ * sideways, as a Latin run is, and the same rule holds. */
1283
+ function latinSpaces(units, c) {
860
1284
  const out = [];
861
- const han = (u, edge) => !!u && u.kind === 'text' && !u.run && !u.note && u.firstCjk && u[edge] === 'ideograph' && !u.style.script && isLatinSpacingHan(u.text);
1285
+ const han = (u, edge) => {
1286
+ if (!u)
1287
+ return false;
1288
+ const chipEdge = chipEdgeGrapheme(u, edge);
1289
+ if (chipEdge !== undefined)
1290
+ return isLatinSpacingHan(chipEdge);
1291
+ return u.kind === 'text' && !u.run && !u.note && u.firstCjk && u[edge] === 'ideograph' && !u.style.script && isLatinSpacingHan(u.text);
1292
+ };
862
1293
  // A run whose edge is a number set in one upright cell (vertical text)
863
1294
  // takes no Han–Latin space on that side; neither does a unit the author
864
- // set upright or in one cell.
865
- const latin = (u, edge) => !!u && u.kind === 'text' && !!u.run && !u.note && !u.style.script && !(edge === 'first' ? u.cellStart : u.cellEnd)
866
- && isLatinSpacingLatin(edge === 'first' ? String.fromCodePoint(u.text.codePointAt(0)) : lastGrapheme(u.text));
1295
+ // set upright or in one cell. A chip's edge is Latin when its character
1296
+ // is a letter or a digit that is not CJK (a full-width A is not).
1297
+ const latin = (u, edge) => {
1298
+ if (!u)
1299
+ return false;
1300
+ const chipEdge = chipEdgeGrapheme(u, edge);
1301
+ if (chipEdge !== undefined)
1302
+ return !isCjkGrapheme(chipEdge) && !isLatinSpacingHan(chipEdge) && isLatinSpacingLatin(chipEdge);
1303
+ return u.kind === 'text' && !!u.run && !u.note && !u.style.script && !(edge === 'first' ? u.cellStart : u.cellEnd)
1304
+ && isLatinSpacingLatin(edge === 'first' ? String.fromCodePoint(u.text.codePointAt(0)) : lastGrapheme(u.text));
1305
+ };
867
1306
  const spaceOf = (h) => latinSpacingPx(c, emOfFont(h.style.font));
868
1307
  for (let k = 0; k < units.length; k++) {
869
1308
  const u = units[k];
@@ -899,6 +1338,62 @@ function prepareUnits(units, c, letterSpacingPx) {
899
1338
  }
900
1339
  return out;
901
1340
  }
1341
+ /**
1342
+ * `units` with the one-em space JLReq §3.1.6 sets after a ? or ! that
1343
+ * ends a sentence inside the paragraph (`cjk.spaceAfterQuestion`,
1344
+ * `Unit.aki`): after the mark and what keeps to it (a note marker), unless
1345
+ * the paragraph ends there or a closing bracket or another ?! follows. A
1346
+ * space the author typed there (U+3000, or a run of word spaces) becomes
1347
+ * the aki, keeping its text. A line may break after it, and it goes at the
1348
+ * line end, as any space does.
1349
+ */
1350
+ function questionSpaces(units) {
1351
+ const out = [];
1352
+ for (let k = 0; k < units.length; k++) {
1353
+ const u = units[k];
1354
+ out.push(u);
1355
+ if (!charUnitIn(u, QUESTION_MARKS))
1356
+ continue;
1357
+ // A note marker or a superscript keeps to the mark: the space follows it.
1358
+ while (k + 1 < units.length && units[k + 1].glueBefore)
1359
+ out.push(units[++k]);
1360
+ const next = units[k + 1];
1361
+ if (!next || charUnitIn(next, QUESTION_MARKS))
1362
+ continue;
1363
+ if (next.kind === 'text' && next.first === 'closing')
1364
+ continue;
1365
+ const aki = {
1366
+ kind: 'space',
1367
+ text: '',
1368
+ width: emOfFont(u.style.font),
1369
+ graphemes: 0,
1370
+ first: 'western',
1371
+ last: 'western',
1372
+ firstCjk: false,
1373
+ lastCjk: false,
1374
+ style: u.style,
1375
+ at: u.at + u.text.length,
1376
+ aki: true,
1377
+ };
1378
+ if (next.kind === 'space' && !next.auto) {
1379
+ // A typed space is replaced: the aki keeps its text.
1380
+ if (units[k + 2] === undefined)
1381
+ continue;
1382
+ out.push({ ...aki, text: next.text, at: next.at });
1383
+ k++;
1384
+ }
1385
+ else if (next.kind === 'text' && next.first === 'ideoSpace' && next.graphemes === 1) {
1386
+ if (units[k + 2] === undefined)
1387
+ continue;
1388
+ out.push({ ...aki, text: next.text, at: next.at });
1389
+ k++;
1390
+ }
1391
+ else {
1392
+ out.push(aki);
1393
+ }
1394
+ }
1395
+ return out;
1396
+ }
902
1397
  const isDigitCode = (c) => (c >= 0x30 && c <= 0x39) || isFullwidthDigit(c);
903
1398
  /** Whether a number keeps to the sign or unit next to it (`50 %`, `50%`,
904
1399
  * `¥599`, `−3 ℃`): a unit ending in a digit before a unit sign, or a
@@ -931,10 +1426,29 @@ function fullwidthGlue(units, k) {
931
1426
  return true;
932
1427
  return fullwidthUnit(a, isFullwidthNumberJoin) && fullwidthUnit(b, isFullwidthDigit) && fullwidthUnit(units[k - 2], isFullwidthDigit);
933
1428
  }
1429
+ /** The grapheme a unit opens (`first`) or ends (`last`) with, which a
1430
+ * Japanese level reads the unit's class from (small kana, ー, hyphens…:
1431
+ * `breakClassOf`); undefined under a Chinese level, and for an inline box
1432
+ * (a chip, a formula, a marker), whose class stands as it is. */
1433
+ function edgeGrapheme(u, edge, level) {
1434
+ if (u.kind !== 'text' || u.text === '' || !isJapaneseLineBreak(level))
1435
+ return undefined;
1436
+ return edge === 'first' ? String.fromCodePoint(u.text.codePointAt(0)) : lastGrapheme(u.text);
1437
+ }
1438
+ /** Whether unit `b` may not open a line at `level` (see
1439
+ * {@link edgeGrapheme}). */
1440
+ function startProhibited(b, level) {
1441
+ return isLineStartProhibited(b.first, level, edgeGrapheme(b, 'first', level));
1442
+ }
1443
+ /** Whether unit `a` may not close a line at `level`. */
1444
+ function endProhibited(a, level) {
1445
+ return isLineEndProhibited(a.last, level, edgeGrapheme(a, 'last', level));
1446
+ }
934
1447
  /** Whether a line may break before each unit (index 0 is never a break).
935
1448
  * A word space is a break unless the unit after it may not open a line,
936
1449
  * 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. */
1450
+ * sign; a zero-width space is a break at every level. A kanji number
1451
+ * never breaks at the 、 or ・ set solid inside it (`Unit.solid`). */
938
1452
  function breakOpportunities(units, level) {
939
1453
  const out = new Uint8Array(units.length);
940
1454
  // The last unit before `k` that is not a space.
@@ -955,7 +1469,7 @@ function breakOpportunities(units, level) {
955
1469
  }
956
1470
  if (a.kind === 'space') {
957
1471
  const p = ink >= 0 ? units[ink] : undefined;
958
- if (!p || (!numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level)))
1472
+ if (!p || (!numberGlue(p, b) && !startProhibited(b, level) && !endProhibited(p, level)))
959
1473
  out[k] = 1;
960
1474
  continue;
961
1475
  }
@@ -965,27 +1479,72 @@ function breakOpportunities(units, level) {
965
1479
  while (q > 0 && units[q].noteSpace)
966
1480
  q--;
967
1481
  const p = units[q];
968
- if (!p.noteSpace && !numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level))
1482
+ if (!p.noteSpace && !numberGlue(p, b) && !startProhibited(b, level) && !endProhibited(p, level))
969
1483
  out[k] = 1;
970
1484
  continue;
971
1485
  }
972
- if (b.glueBefore)
1486
+ if (b.glueBefore || a.solid || b.solid)
973
1487
  continue;
974
1488
  if (numberGlue(a, b) || fullwidthGlue(units, k))
975
1489
  continue;
976
- if (cjkBreakAllowed(a.last, a.lastCjk, b.first, b.firstCjk, level))
1490
+ if (cjkBreakAllowed(a.last, a.lastCjk, b.first, b.firstCjk, level, edgeGrapheme(a, 'last', level), edgeGrapheme(b, 'first', level)))
977
1491
  out[k] = 1;
978
1492
  }
979
1493
  return out;
980
1494
  }
1495
+ /** Whether a unit opens (`first`) or ends (`last`) with a letter, which
1496
+ * `keep-all` never parts from a letter next to it: a CJK character (kanji,
1497
+ * kana, hangul, an iteration mark, ー), a Western run or a sign glued to
1498
+ * a number, an inline box. Punctuation, dashes, ellipses, connectors and
1499
+ * the ideographic space are not letters: lines still break next to them
1500
+ * where the level allows (after 、。」, before 「). */
1501
+ function isLetterEdge(u, edge) {
1502
+ if (u.kind === 'space')
1503
+ return false;
1504
+ const cls = edge === 'first' ? u.first : u.last;
1505
+ return cls === 'ideograph' || cls === 'iteration' || cls === 'western' || cls === 'prefix' || cls === 'postfix';
1506
+ }
1507
+ /** The breaks of `cjk.wordBreak: 'keep-all'` (CSS `word-break: keep-all`)
1508
+ * out of the paragraph's `breaks`: those after a space the author typed,
1509
+ * a warichu note's word space or before a zero-width space stay, and so
1510
+ * do those next to punctuation; none is left between two letters
1511
+ * ({@link isLetterEdge}), so a phrase of kana written between spaces
1512
+ * (分かち書き) is never divided while it fits a line. The Han–Latin space
1513
+ * the composition inserts (`Unit.auto`, no text) is no space between
1514
+ * phrases: `Tシャツ` stays whole. */
1515
+ function keepAllBreaks(units, breaks) {
1516
+ const out = breaks.slice();
1517
+ for (let k = 1; k < units.length; k++) {
1518
+ if (!out[k])
1519
+ continue;
1520
+ let a = units[k - 1];
1521
+ const b = units[k];
1522
+ if (a.noteSpace || b.zwspBefore)
1523
+ continue;
1524
+ if (a.kind === 'space') {
1525
+ if (!(a.auto && a.text === '') || k < 2)
1526
+ continue;
1527
+ a = units[k - 2];
1528
+ }
1529
+ if (isLetterEdge(a, 'last') && isLetterEdge(b, 'first'))
1530
+ out[k] = 0;
1531
+ }
1532
+ return out;
1533
+ }
981
1534
  /** Whether justification may add space between two units that touch on a
982
1535
  * line: between characters, never inside a Western run or a 2-em mark
983
1536
  * (those are single units), never between two Western runs, next to a
984
1537
  * 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) {
1538
+ * keeps to the text before it. Spaces take their own share. With `jlreq`
1539
+ * (Japanese full-width text) also JLReq §3.1.11's inseparable places:
1540
+ * never after an opening bracket or before a closing one (but before the
1541
+ * one and after the other), never next to 、,。.・:;?! or U+3000. */
1542
+ function gapStretches(a, b, jlreq = false) {
987
1543
  if (a.kind !== 'text' || b.kind === 'space')
988
1544
  return false;
1545
+ // Never inside a jukugo word (JLReq §3.3.7).
1546
+ if (a.ruby?.jis && a.ruby.jis === b.ruby?.jis)
1547
+ return false;
989
1548
  if (b.glueBefore || a.stacked)
990
1549
  return false;
991
1550
  if (a.last === 'connector' || a.last === 'solidus' || b.first === 'connector' || b.first === 'solidus')
@@ -994,16 +1553,43 @@ function gapStretches(a, b) {
994
1553
  return false;
995
1554
  if (a.last === b.first && (a.last === 'dash' || a.last === 'ellipsis'))
996
1555
  return false;
1556
+ if (jlreq && (a.last === 'opening' || b.first === 'closing' || jlreqFixed(a.last) || jlreqFixed(b.first)))
1557
+ return false;
997
1558
  return true;
998
1559
  }
1560
+ /** The classes JLReq never spreads next to (§3.1.11): pause and stop
1561
+ * marks, middle dots and the ideographic space. */
1562
+ function jlreqFixed(cls) {
1563
+ return cls === 'pause' || cls === 'stop' || cls === 'interpunct' || cls === 'ideoSpace';
1564
+ }
1565
+ /** Whether the gap after unit `j` of a line takes tracking. A footnote
1566
+ * marker in the line gap (`footnotes.markerPosition: 'side'`) takes no
1567
+ * room in the line: the gap between the characters around it is theirs,
1568
+ * and is set after the marker, which stays against the character it
1569
+ * marks. */
1570
+ function gapAfterStretches(us, j, jlreq = false) {
1571
+ const next = us[j + 1];
1572
+ if (isSideMarker(next))
1573
+ return false;
1574
+ const u = us[j];
1575
+ if (isSideMarker(u))
1576
+ return j > 0 && gapStretches(us[j - 1], next, jlreq);
1577
+ return gapStretches(u, next, jlreq);
1578
+ }
1579
+ function isSideMarker(u) {
1580
+ return u.token?.sideRuns !== undefined;
1581
+ }
999
1582
  function lineFitOf(c) {
1000
1583
  if (isPlainComposition(c))
1001
1584
  return undefined;
1585
+ const jlreq = isJlreqSpacing(c);
1002
1586
  return {
1003
1587
  lead: (u) => (u.punct ? lineEdgeCut(u.punct, c, true, false) : 0),
1004
1588
  tail: (u) => (u.punct ? lineEdgeCut(u.punct, c, false, true) : 0),
1005
1589
  give(u, atStart, atEnd) {
1006
1590
  if (u.kind === 'space') {
1591
+ if (u.aki)
1592
+ return 0;
1007
1593
  const em = emOfFont(u.style.font);
1008
1594
  return Math.max(0, u.width - (u.auto ? em / 8 : em / 4));
1009
1595
  }
@@ -1013,18 +1599,21 @@ function lineFitOf(c) {
1013
1599
  return punctuationShrink(u.punct, c);
1014
1600
  const box = { ...u.punct };
1015
1601
  applyLineEdges(box, c, atStart, atEnd);
1016
- return punctuationShrink(box, c);
1602
+ // JLReq: a mark ending the line gives up the blank after its glyph
1603
+ // (and, a middle dot, nothing before it: §3.8.3 steps 2–3).
1604
+ return punctuationShrink(box, c, jlreq && atEnd);
1017
1605
  },
1018
1606
  edge: (u, start, end) => (u.punct ? applyLineEdges(u.punct, c, start, end) : 0),
1019
1607
  hangs(units, k, unitAt) {
1020
1608
  const u = unitAt(k);
1021
- if (u.kind !== 'text' || u.run || u.graphemes !== 1 || u.note || u.ruby || !mayHang(u.text, u.first, c))
1609
+ if (u.kind !== 'text' || u.run || u.graphemes !== 1 || u.note || u.ruby || u.kunten || !mayHang(u.text, u.first, c))
1022
1610
  return false;
1023
1611
  if (k > 0 && isMarkUnit(unitAt(k - 1)))
1024
1612
  return false;
1025
1613
  return k + 1 >= units.length || !isMarkUnit(units[k + 1]);
1026
1614
  },
1027
1615
  hanging: c.hangingPunctuation,
1616
+ jlreq,
1028
1617
  };
1029
1618
  }
1030
1619
  /** The last unit that must share a line with unit `k` when `k` ends one:
@@ -1158,11 +1747,19 @@ function divideRun(u, room, letterSpacingPx) {
1158
1747
  * unit). A space never overflows: the line ends before it, when the line
1159
1748
  * may break after it. A unit wider
1160
1749
  * 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.
1750
+ * at a joint first), else set on a line of its own. `pull` names a line
1751
+ * that takes in the rest of the paragraph when the line can give up the
1752
+ * blank for it (push-in, to save a last line of one character).
1162
1753
  */
1163
1754
  function breakUnits(units, breaks, measureOf, reserve, letterSpacingPx, fit, level = getCjkLineBreak(),
1164
1755
  /** End line `line` before unit `at` (a break), whatever else fits. */
1165
- stop) {
1756
+ stop,
1757
+ /** Line `pull` takes every unit left when it can give up the blank. */
1758
+ pull,
1759
+ /** Under `keep-all` (`breaks` holds only the breaks between phrases): the
1760
+ * breaks the level allows between any two characters, where a phrase
1761
+ * longer than the line is broken. */
1762
+ emergency) {
1166
1763
  const out = [];
1167
1764
  const n = units.length;
1168
1765
  let i = 0;
@@ -1176,15 +1773,23 @@ stop) {
1176
1773
  let noteBase = 0;
1177
1774
  const foldOf = (from, to) => {
1178
1775
  const widths = [];
1179
- const startProhibited = [];
1180
- const endProhibited = [];
1776
+ const startNo = [];
1777
+ const endNo = [];
1181
1778
  for (let q = from; q <= to; q++) {
1182
1779
  const uq = unitAt(q);
1183
1780
  widths.push(uq.width);
1184
- startProhibited.push(noteRowStartProhibited(uq, level));
1185
- endProhibited.push(isLineEndProhibited(uq.last, level));
1781
+ startNo.push(noteRowStartProhibited(uq, level));
1782
+ endNo.push(endProhibited(uq, level));
1186
1783
  }
1187
- return foldWidth(widths, splitNote(widths, startProhibited, endProhibited));
1784
+ return foldWidth(widths, splitNote(widths, startNo, endNo));
1785
+ };
1786
+ // A Japanese ruby base at a line edge widens the line (`rubyGrow`): at
1787
+ // its start, and at its end when the line may end after it.
1788
+ const endGrow = (k) => {
1789
+ const g = unitAt(k).rubyGrow;
1790
+ if (!g || g.end <= 0)
1791
+ return 0;
1792
+ return k + 1 >= n || breaks[k + 1] || units[k + 1].kind === 'space' ? g.end : 0;
1188
1793
  };
1189
1794
  for (let li = 0;; li++) {
1190
1795
  while (i < n && unitAt(i).kind === 'space')
@@ -1192,7 +1797,7 @@ stop) {
1192
1797
  if (i >= n)
1193
1798
  break;
1194
1799
  const max = measureOf(li) - reserve;
1195
- let w = 0;
1800
+ let w = Math.max(0, unitAt(i).rubyGrow?.start ?? 0);
1196
1801
  let lastBreak = -1;
1197
1802
  let end = -1;
1198
1803
  let head;
@@ -1226,7 +1831,7 @@ stop) {
1226
1831
  // line may end with was one that folded).
1227
1832
  }
1228
1833
  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) {
1834
+ if (!u.note && w + u.width - lead - (fit ? fit.tail(u) : 0) + endGrow(k) <= max + FIT_EPS) {
1230
1835
  w += u.width - lead;
1231
1836
  if (fit)
1232
1837
  give += fit.give(u, k === i, false);
@@ -1248,13 +1853,13 @@ stop) {
1248
1853
  hang = true;
1249
1854
  break;
1250
1855
  }
1251
- const m = breaks[k] ? -1 : groupEnd(units, breaks, k);
1856
+ const m = pull === li ? n - 1 : breaks[k] ? -1 : groupEnd(units, breaks, k);
1252
1857
  if (m >= k) {
1253
1858
  let width = w;
1254
1859
  let room = give;
1255
1860
  for (let q = k; q <= m; q++) {
1256
1861
  const uq = unitAt(q);
1257
- const t = q === m ? fit.tail(uq) : 0;
1862
+ const t = q === m ? fit.tail(uq) - endGrow(q) : 0;
1258
1863
  width += uq.width - t;
1259
1864
  room += fit.give(uq, false, q === m);
1260
1865
  }
@@ -1314,6 +1919,17 @@ stop) {
1314
1919
  end = k + 1;
1315
1920
  break;
1316
1921
  }
1922
+ if (emergency) {
1923
+ // A phrase longer than the line (keep-all): it is broken inside,
1924
+ // at the last place the level allows.
1925
+ let q = k;
1926
+ while (q > i && !emergency[q])
1927
+ q--;
1928
+ if (q > i) {
1929
+ end = q;
1930
+ break;
1931
+ }
1932
+ }
1317
1933
  // Nothing on the line may end it: break before the unit anyway.
1318
1934
  end = k;
1319
1935
  break;
@@ -1342,9 +1958,10 @@ stop) {
1342
1958
  return out;
1343
1959
  }
1344
1960
  /** 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). */
1961
+ * start a line (the small kana and the rest of the Japanese classes under
1962
+ * a Japanese level), or a word space (it ends the upper row instead). */
1346
1963
  function noteRowStartProhibited(u, level) {
1347
- return u.noteSpace === true || isLineStartProhibited(u.first, level);
1964
+ return u.noteSpace === true || startProhibited(u, level);
1348
1965
  }
1349
1966
  /** Replace each run of a warichu note's characters on a line by the part
1350
1967
  * folded into its two rows (see `cjkAnnotate.ts`). `us` is changed in
@@ -1372,7 +1989,7 @@ function foldNotes(us, level, em) {
1372
1989
  function noteFragment(part, level, em) {
1373
1990
  const note = part[0].note;
1374
1991
  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)));
1992
+ const at = splitNote(widths, part.map((u) => noteRowStartProhibited(u, level)), part.map((u) => endProhibited(u, level)));
1376
1993
  const noteFont = note.fontString ?? part[0].style.font;
1377
1994
  const rows = noteRowBaselines(em, fontEm(noteFont));
1378
1995
  const runs = [];
@@ -1435,7 +2052,9 @@ function alignEdgeRubies(us) {
1435
2052
  const align = (j, start) => {
1436
2053
  const u = us[j];
1437
2054
  const g = u.ruby?.geometry;
1438
- if (!g || u.ruby.position === 'right' || g.runs.length === 0)
2055
+ // A base with kanbun marks keeps its reading where the marks were set
2056
+ // against it (#430).
2057
+ if (!g || u.ruby.position === 'right' || u.ruby.jis || g.runs.length === 0 || u.kunten)
1439
2058
  return;
1440
2059
  const base = u.width - 2 * g.inset;
1441
2060
  const rt = g.rtWidth;
@@ -1471,7 +2090,8 @@ function clampReadings(segments) {
1471
2090
  let x = 0;
1472
2091
  for (const s of segments) {
1473
2092
  const ruby = s.ruby;
1474
- if (ruby && ruby.position !== 'right' && ruby.runs.length > 0) {
2093
+ // A Japanese reading (`id`) was set inside the line already (#422).
2094
+ if (ruby && ruby.position !== 'right' && ruby.id === undefined && ruby.runs.length > 0) {
1475
2095
  const lo = ruby.runs[0].dx;
1476
2096
  const hi = lo + ruby.rtWidth;
1477
2097
  let shift = 0;
@@ -1508,9 +2128,16 @@ function spread(amount, caps) {
1508
2128
  * character that may not open a line, clreq §6.2.2.3) gives up
1509
2129
  * blank in clreq's order — word spaces to a quarter em, interpuncts,
1510
2130
  * 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.
2131
+ * last — each step shared equally, until it fits. Japanese full-width text
2132
+ * takes JLReq §3.8.3's order instead (`jlreqShrinkStep`): word spaces, the
2133
+ * half em after the line's last mark and then a middle dot's quarter (each
2134
+ * all or nothing: what it gives beyond the need is spread back when the
2135
+ * line is justified), the middle dots' quarters inside the line, the half
2136
+ * ems of brackets and 、, inside it, Han–Latin spaces; never the half em
2137
+ * after a 。 inside the line. A mark that hangs keeps its glyph alone. `us`
2138
+ * holds the line's units and is changed in place; a unit that changes is
2139
+ * replaced by a copy, since the paragraph's units serve every attempt at
2140
+ * breaking it.
1514
2141
  */
1515
2142
  function fitLine(us, units, range, lastK, isLast, measure, fit) {
1516
2143
  const copy = (j) => {
@@ -1527,6 +2154,9 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1527
2154
  hang = copy(us.length - 1);
1528
2155
  us.pop();
1529
2156
  hang.width -= fit.edge(hang, false, true);
2157
+ // ぶら下げ: the hung mark is its glyph, past the measure.
2158
+ if (fit.jlreq && hang.punct)
2159
+ hang.width -= shrinkPunctuation(hang.punct, hang.punct.blank, true);
1530
2160
  while (us.length > 0 && us[us.length - 1].kind === 'space')
1531
2161
  us.pop();
1532
2162
  }
@@ -1549,17 +2179,27 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1549
2179
  if (over <= FIT_EPS)
1550
2180
  return hang;
1551
2181
  // 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);
2182
+ // brackets (3), pause marks (4), Han–Latin spaces (5), stop marks (6);
2183
+ // in JLReq spacing, word spaces (1), the line end (2, 3), middle dots
2184
+ // (4), brackets and 、, (5), Han–Latin spaces (6).
2185
+ const last = us.length - 1;
2186
+ const atEnd = (j) => fit.jlreq && j === last;
2187
+ const stepOf = (u, j) => {
2188
+ if (u.kind === 'space')
2189
+ return u.aki ? 0 : u.auto ? (fit.jlreq ? 6 : 5) : 1;
2190
+ if (!u.punct)
2191
+ return 0;
2192
+ return fit.jlreq ? jlreqShrinkStep(u.punct, atEnd(j)) : shrinkStep(u.punct);
2193
+ };
1554
2194
  for (let step = 1; step <= 6 && over > FIT_EPS; step++) {
1555
2195
  const members = [];
1556
2196
  const caps = [];
1557
2197
  for (let j = 0; j < us.length; j++) {
1558
2198
  const u = us[j];
1559
- if (stepOf(u) !== step)
2199
+ if (stepOf(u, j) !== step)
1560
2200
  continue;
1561
2201
  // The marks at the edges already gave up their lead and tail.
1562
- const cap = fit.give(u, false, false);
2202
+ const cap = fit.give(u, false, atEnd(j));
1563
2203
  if (cap <= 0)
1564
2204
  continue;
1565
2205
  members.push(j);
@@ -1567,13 +2207,15 @@ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1567
2207
  }
1568
2208
  if (members.length === 0)
1569
2209
  continue;
1570
- const takes = spread(over, caps);
2210
+ // The blank at a JLReq line end goes whole or not at all (§3.1.9).
2211
+ const takes = fit.jlreq && (step === 2 || step === 3) ? caps : spread(over, caps);
1571
2212
  for (let m = 0; m < members.length; m++) {
1572
2213
  const take = takes[m];
1573
2214
  if (take <= 0)
1574
2215
  continue;
1575
- const c = copy(members[m]);
1576
- const given = c.punct ? shrinkPunctuation(c.punct, take) : take;
2216
+ const j = members[m];
2217
+ const c = copy(j);
2218
+ const given = c.punct ? shrinkPunctuation(c.punct, take, atEnd(j)) : take;
1577
2219
  c.width -= given;
1578
2220
  over -= given;
1579
2221
  }
@@ -1609,7 +2251,10 @@ function composeLine(units, range, li, isLast, ctx) {
1609
2251
  // A warichu note's characters fold into their two rows (#195).
1610
2252
  if (us.some((u) => u.note))
1611
2253
  foldNotes(us, ctx.level, ctx.em);
1612
- // A ruby base at either edge aligns to it with its reading (#194).
2254
+ // A ruby base at either edge aligns to it with its reading (#194); a
2255
+ // Japanese one is laid out again there (#422).
2256
+ if (us.some((u) => u.ruby?.jis))
2257
+ relayJisEdges(us);
1613
2258
  alignEdgeRubies(us);
1614
2259
  const hang = ctx.fit ? fitLine(us, units, range, lastK, isLast, measure, ctx.fit) : undefined;
1615
2260
  const justify = ctx.textAlign === 'justify' && !isLast;
@@ -1628,9 +2273,15 @@ function composeLine(units, range, li, isLast, ctx) {
1628
2273
  // Han–Latin spaces (`cjk.latinSpacing`): they flex apart from word
1629
2274
  // spaces, and the renderers leave them as set.
1630
2275
  const autos = [];
2276
+ const jlreq = ctx.fit?.jlreq === true;
1631
2277
  for (let j = 0; j < us.length; j++) {
1632
2278
  const u = us[j];
1633
2279
  content += u.width;
2280
+ // The space after ?! keeps its em (JLReq §3.1.6).
2281
+ if (u.kind === 'space' && u.aki) {
2282
+ stretches.push(false);
2283
+ continue;
2284
+ }
1634
2285
  if (u.kind === 'space' && u.auto)
1635
2286
  autos.push(j);
1636
2287
  else if (u.kind === 'space') {
@@ -1638,7 +2289,7 @@ function composeLine(units, range, li, isLast, ctx) {
1638
2289
  natural += u.width;
1639
2290
  widest = Math.max(widest, u.width);
1640
2291
  }
1641
- const s = j < us.length - 1 && gapStretches(u, us[j + 1]);
2292
+ const s = j < us.length - 1 && gapAfterStretches(us, j, jlreq);
1642
2293
  stretches.push(s);
1643
2294
  if (s)
1644
2295
  gaps++;
@@ -1676,9 +2327,20 @@ function composeLine(units, range, li, isLast, ctx) {
1676
2327
  us[j] = { ...us[j], width: us[j].width + tracking };
1677
2328
  }
1678
2329
  }
2330
+ else if (spaces > 0 && us.some((u) => u.kind === 'text' && (u.firstCjk || u.lastCjk))) {
2331
+ // CJK characters with no gap between them that may spread (phrases
2332
+ // glued by word joiners, marks JLReq never spreads next to), and
2333
+ // word spaces (#463): the spaces take it all and the line ends on
2334
+ // the measure. Their final width is the segments', as every gap of
2335
+ // a composed line is; a Latin word space's stretch limit
2336
+ // (`justifiedSpaceRatio`, which sets a looser line ragged) is not
2337
+ // the rule of a CJK line, so the line reports none.
2338
+ spaceWidth = (natural + slack) / spaces;
2339
+ }
1679
2340
  else if (spaces > 0) {
1680
- // No gap between characters: the spaces take it all, as in a Latin
1681
- // line (the renderers stretch them).
2341
+ // Latin words only (an English sentence in a CJK paragraph): the
2342
+ // spaces take it all, as in a Latin line (the renderers stretch
2343
+ // them, and a line past the ratio is set ragged).
1682
2344
  if (ctx.normalSpace > 0)
1683
2345
  spaceRatio = (natural + slack) / spaces / ctx.normalSpace;
1684
2346
  }
@@ -1727,6 +2389,7 @@ function composeLine(units, range, li, isLast, ctx) {
1727
2389
  ...(s.marks ? { cjkMarks: s.marks } : {}),
1728
2390
  ...(s.inserted ? { inserted: true } : {}),
1729
2391
  ...(s.color ? { color: s.color } : {}),
2392
+ ...(s.lang !== undefined ? { lang: s.lang } : {}),
1730
2393
  ...(t !== undefined ? { tracking: t } : {}),
1731
2394
  // Every mark that gave up blank carries its ink offset (0 when only
1732
2395
  // the blank after its glyph went), and so does a shared mark set in
@@ -1738,9 +2401,38 @@ function composeLine(units, range, li, isLast, ctx) {
1738
2401
  ...(u.shift !== undefined ? { baselineShift: u.shift } : {}),
1739
2402
  };
1740
2403
  };
2404
+ /** The segment of a ruby base `width` px wide, `t` its tracking. */
2405
+ const rubySegment = (u, width, t) => {
2406
+ const ruby = u.ruby;
2407
+ const g = ruby.geometry;
2408
+ // The box less the advance kanbun marks add after it (#430).
2409
+ const box = u.width - (u.kunten?.extra ?? 0);
2410
+ // A Japanese ruby (#422) names its annotation (`id`), so renderers
2411
+ // can set one `<ruby>` for a word.
2412
+ const jis = ruby.jis !== undefined;
2413
+ return {
2414
+ ...segmentOf(u, width, t),
2415
+ text: u.text,
2416
+ ...(g.inset > 1e-9 ? { inkOffset: g.inset } : {}),
2417
+ // A base spread 1:2:1 under a longer Japanese reading.
2418
+ ...(g.tracking ? { tracking: g.tracking } : {}),
2419
+ ruby: {
2420
+ text: ruby.span.text,
2421
+ fontString: ruby.span.fontString ?? g.runs[0]?.fontString ?? u.style.font,
2422
+ baseWidth: ruby.base ?? box - g.inset * 2 - (ruby.position === 'right' ? g.rtWidth : 0),
2423
+ rtWidth: g.rtWidth,
2424
+ position: ruby.position,
2425
+ ...(ruby.span.group ? { group: true } : {}),
2426
+ ...(ruby.span.color ? { color: ruby.span.color } : {}),
2427
+ ...(jis ? { id: ruby.span.id } : {}),
2428
+ ...(jis && ruby.span.jukugo ? { jukugo: true } : {}),
2429
+ runs: g.runs,
2430
+ },
2431
+ };
2432
+ };
1741
2433
  for (let j = 0; j < us.length; j++) {
1742
2434
  const u = us[j];
1743
- if (u.kind === 'space' && u.auto) {
2435
+ if (u.kind === 'space' && (u.auto || u.aki)) {
1744
2436
  pieces.push({ seg: { kind: 'space', text: u.text, width: u.width, autospace: true }, parts: [u.text], key: undefined });
1745
2437
  continue;
1746
2438
  }
@@ -1750,6 +2442,9 @@ function composeLine(units, range, li, isLast, ctx) {
1750
2442
  }
1751
2443
  if (u.kind === 'atomic') {
1752
2444
  const seg = tokenSegment(u.token);
2445
+ // A side marker carries the gap after the character it marks.
2446
+ if (seg.sideMarker && tracking > 0 && stretches[j])
2447
+ seg.width += tracking;
1753
2448
  pieces.push({ seg, parts: [seg.text], key: undefined });
1754
2449
  continue;
1755
2450
  }
@@ -1759,6 +2454,21 @@ function composeLine(units, range, li, isLast, ctx) {
1759
2454
  pieces.push({ seg: { kind: 'text', text: u.text, width: u.width + gap, warichu: u.warichu }, parts: [u.text], key: undefined });
1760
2455
  continue;
1761
2456
  }
2457
+ if (u.kunten?.vdt) {
2458
+ // A character with kanbun marks (#430): a segment of its own, the
2459
+ // marks painted from its start; the room they take after it (inline
2460
+ // 返り点, a 竪点, 送り仮名 that run past it) and the gap follow as a
2461
+ // space of their own, so the character is painted at its natural
2462
+ // spacing (a ruby base in it centred under its reading).
2463
+ const extra = u.kunten.extra ?? 0;
2464
+ const own = u.width - extra;
2465
+ const seg = u.ruby?.geometry ? rubySegment(u, own, undefined) : { ...segmentOf(u, own, undefined), text: u.text };
2466
+ seg.kunten = u.kunten.vdt;
2467
+ pieces.push({ seg, parts: [u.text], key: undefined });
2468
+ if (extra + gap > 0)
2469
+ pieces.push({ seg: { kind: 'space', text: '', width: extra + gap, autospace: true }, parts: [''], key: undefined });
2470
+ continue;
2471
+ }
1762
2472
  if (u.ruby?.geometry) {
1763
2473
  // A ruby base: centred in its box, its reading placed from the box's
1764
2474
  // start (#194). One character takes the gap after it as tracking; a
@@ -1766,23 +2476,8 @@ function composeLine(units, range, li, isLast, ctx) {
1766
2476
  // reading, and the gap follows the box as a space of its own (its
1767
2477
  // width final, as a Han–Latin space's is): tracking would be added
1768
2478
  // after each of its characters.
1769
- const g = u.ruby.geometry;
1770
2479
  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
- };
2480
+ const seg = rubySegment(u, gapAfter ? u.width : u.width + gap, gap > 0 && !gapAfter ? gap : undefined);
1786
2481
  pieces.push({ seg, parts: [u.text], key: undefined });
1787
2482
  if (gapAfter)
1788
2483
  pieces.push({ seg: { kind: 'space', text: '', width: gap, autospace: true }, parts: [''], key: undefined });
@@ -1848,6 +2543,40 @@ function composeLine(units, range, li, isLast, ctx) {
1848
2543
  cjkComposed: true,
1849
2544
  };
1850
2545
  }
2546
+ /**
2547
+ * The first line's indent of a paragraph that opens with an opening
2548
+ * bracket, as `cjk.paragraphStartBracket` sets it (JLReq §3.1.5), with the
2549
+ * bracket's blank before its glyph given up (it is `units`' own unit,
2550
+ * replaced): pattern ① (`indent`) keeps the indent, ③ (`half`) sets the
2551
+ * bracket's glyph inside the indent so the text after it starts where the
2552
+ * indent ends (the bracket and its half em of blank fill a one-em indent),
2553
+ * 天付き (`flush`) drops the indent. `indentPx` when the composition leaves
2554
+ * the bracket to the line-start rules, the paragraph has no indent or
2555
+ * does not open with a bracket.
2556
+ */
2557
+ function paragraphStartIndent(units, indentPx, c) {
2558
+ const pattern = c.paragraphStartBracket;
2559
+ if (!pattern || indentPx <= 0)
2560
+ return indentPx;
2561
+ const f = units.findIndex((u) => u.kind !== 'space');
2562
+ const u = units[f];
2563
+ if (!u?.punct || u.first !== 'opening')
2564
+ return indentPx;
2565
+ // Its blank before the glyph, whatever `trimLineStart` says.
2566
+ const box = { ...u.punct };
2567
+ const lead = box.blank - box.cutStart;
2568
+ const trimmed = { ...u, punct: box };
2569
+ if (lead > 0 && box.side === 'start') {
2570
+ box.cutStart += lead;
2571
+ trimmed.width -= lead;
2572
+ }
2573
+ units[f] = trimmed;
2574
+ if (pattern === 'flush')
2575
+ return 0;
2576
+ if (pattern === 'half')
2577
+ return Math.max(0, indentPx - trimmed.width);
2578
+ return indentPx;
2579
+ }
1851
2580
  /**
1852
2581
  * Set a Chinese, Japanese or Korean paragraph (see the module comment):
1853
2582
  * `options.cjkLineBreak` (else the document's level) decides where lines
@@ -1885,14 +2614,23 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1885
2614
  // Ruby readings (#194), laid out once their neighbours are known.
1886
2615
  if (units.some((u) => u.ruby))
1887
2616
  sizeRubies(units);
2617
+ // Kanbun marks (#430), beside the readings.
2618
+ if (units.some((u) => u.kunten))
2619
+ sizeKunten(units, letterSpacingPx);
1888
2620
  if (!units.some((u) => u.kind !== 'space'))
1889
2621
  return { lines: [], totalHeight: 0 };
1890
2622
  const fit = lineFitOf(composition);
1891
2623
  const level = options?.cjkLineBreak ?? getCjkLineBreak();
1892
- const breaks = breakOpportunities(units, level);
2624
+ // `keep-all` (the paragraph's own, else the document's): lines break
2625
+ // between phrases, and inside one only when it is longer than the line.
2626
+ const keepAll = options?.cjkWordBreak !== undefined ? options.cjkWordBreak === 'keep-all' : composition.keepAll === true;
2627
+ const levelBreaks = breakOpportunities(units, level);
2628
+ const breaks = keepAll ? keepAllBreaks(units, levelBreaks) : levelBreaks;
2629
+ const emergency = keepAll ? levelBreaks : undefined;
1893
2630
  const indentPx = options?.firstLineIndentPx ?? 0;
1894
2631
  const hanging = options?.hangingIndent ?? false;
1895
- const indentOf = (li) => (indentPx > 0 ? (hanging ? (li === 0 ? 0 : indentPx) : (li === 0 ? indentPx : 0)) : 0);
2632
+ const firstIndent = hanging ? 0 : paragraphStartIndent(units, indentPx, composition);
2633
+ const indentOf = (li) => (indentPx > 0 ? (hanging ? (li === 0 ? 0 : indentPx) : (li === 0 ? firstIndent : 0)) : 0);
1896
2634
  const measureOf = (li) => lineMeasure(maxWidthPx, options?.restWidths, li) - indentOf(li);
1897
2635
  const sizeMatch = FONT_SIZE_RE.exec(normalFont);
1898
2636
  const em = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
@@ -1914,13 +2652,24 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1914
2652
  level,
1915
2653
  };
1916
2654
  const compose = (ranges) => ranges.map((r, li) => composeLine(units, r, li, li === ranges.length - 1, ctx));
1917
- const ranges = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level);
2655
+ const ranges = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, undefined, undefined, emergency);
1918
2656
  let lines = compose(ranges);
1919
2657
  // 孤字 (clreq §7.2), where the paragraph avoids runts (`bodyText.avoidRunts`
1920
2658
  // gives it a runt penalty): a last line that holds one character, alone
1921
2659
  // or with its closing marks, takes the last character the line above may
1922
2660
  // 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.
2661
+ // would need more than the tracking cap, the runt stays. Japanese text
2662
+ // (泣き別れ) first tries the other way JLReq prefers (§3.8.2): the line
2663
+ // above takes the character in by giving up blank (push-in), and the
2664
+ // paragraph is a line shorter.
2665
+ if ((options?.runtPenalty ?? 0) > 0 && endsOnOneCharacter(lines) && fit?.jlreq) {
2666
+ const pulled = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, undefined, ranges.length - 2, emergency);
2667
+ if (pulled.length === ranges.length - 1) {
2668
+ const set = compose(pulled);
2669
+ if (!set.some((l) => l.cjkLoose) && !endsOnOneCharacter(set))
2670
+ lines = set;
2671
+ }
2672
+ }
1924
2673
  if ((options?.runtPenalty ?? 0) > 0 && endsOnOneCharacter(lines)) {
1925
2674
  const li = ranges.length - 2;
1926
2675
  const above = ranges[li];
@@ -1928,7 +2677,7 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1928
2677
  while (at > above.start && !breaks[at])
1929
2678
  at--;
1930
2679
  if (!above.head && !above.hyphenated && !ranges[li + 1].first && at > above.start) {
1931
- const pushed = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, { line: li, at });
2680
+ const pushed = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, { line: li, at }, undefined, emergency);
1932
2681
  if (pushed.length === ranges.length) {
1933
2682
  const set = compose(pushed);
1934
2683
  if (!set[li].cjkLoose && !endsOnOneCharacter(set))
@@ -1946,7 +2695,7 @@ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, bol
1946
2695
  if (looseness > 0) {
1947
2696
  const target = lines.length + looseness;
1948
2697
  for (let step = 1; step <= 16; step++) {
1949
- const ranges = breakUnits(units, breaks, measureOf, (step * em) / 8, letterSpacingPx, fit, level);
2698
+ const ranges = breakUnits(units, breaks, measureOf, (step * em) / 8, letterSpacingPx, fit, level, undefined, undefined, emergency);
1950
2699
  if (ranges.length < target)
1951
2700
  continue;
1952
2701
  if (ranges.length > target)