postext 1.8.6 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (565) hide show
  1. package/dist/__bench__/cjkChapter.bench.test.d.ts +2 -0
  2. package/dist/__bench__/cjkChapter.bench.test.d.ts.map +1 -0
  3. package/dist/__bench__/cjkChapter.bench.test.js +71 -0
  4. package/dist/__bench__/cjkChapter.bench.test.js.map +1 -0
  5. package/dist/__tests__/bundle.test.js +16 -0
  6. package/dist/__tests__/bundle.test.js.map +1 -1
  7. package/dist/__tests__/canvasDirection.test.d.ts +2 -0
  8. package/dist/__tests__/canvasDirection.test.d.ts.map +1 -0
  9. package/dist/__tests__/canvasDirection.test.js +45 -0
  10. package/dist/__tests__/canvasDirection.test.js.map +1 -0
  11. package/dist/__tests__/chineseLocale.test.d.ts +2 -0
  12. package/dist/__tests__/chineseLocale.test.d.ts.map +1 -0
  13. package/dist/__tests__/chineseLocale.test.js +253 -0
  14. package/dist/__tests__/chineseLocale.test.js.map +1 -0
  15. package/dist/__tests__/chineseNumbering.test.d.ts +2 -0
  16. package/dist/__tests__/chineseNumbering.test.d.ts.map +1 -0
  17. package/dist/__tests__/chineseNumbering.test.js +376 -0
  18. package/dist/__tests__/chineseNumbering.test.js.map +1 -0
  19. package/dist/__tests__/cjk/annotations.test.d.ts +2 -0
  20. package/dist/__tests__/cjk/annotations.test.d.ts.map +1 -0
  21. package/dist/__tests__/cjk/annotations.test.js +483 -0
  22. package/dist/__tests__/cjk/annotations.test.js.map +1 -0
  23. package/dist/__tests__/cjk/annotationsRender.test.d.ts +2 -0
  24. package/dist/__tests__/cjk/annotationsRender.test.d.ts.map +1 -0
  25. package/dist/__tests__/cjk/annotationsRender.test.js +263 -0
  26. package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -0
  27. package/dist/__tests__/cjk/composer.test.d.ts +2 -0
  28. package/dist/__tests__/cjk/composer.test.d.ts.map +1 -0
  29. package/dist/__tests__/cjk/composer.test.js +237 -0
  30. package/dist/__tests__/cjk/composer.test.js.map +1 -0
  31. package/dist/__tests__/cjk/composerEdges.test.d.ts +2 -0
  32. package/dist/__tests__/cjk/composerEdges.test.d.ts.map +1 -0
  33. package/dist/__tests__/cjk/composerEdges.test.js +352 -0
  34. package/dist/__tests__/cjk/composerEdges.test.js.map +1 -0
  35. package/dist/__tests__/cjk/dashRule.test.d.ts +2 -0
  36. package/dist/__tests__/cjk/dashRule.test.d.ts.map +1 -0
  37. package/dist/__tests__/cjk/dashRule.test.js +261 -0
  38. package/dist/__tests__/cjk/dashRule.test.js.map +1 -0
  39. package/dist/__tests__/cjk/fuzz.test.d.ts +2 -0
  40. package/dist/__tests__/cjk/fuzz.test.d.ts.map +1 -0
  41. package/dist/__tests__/cjk/fuzz.test.js +102 -0
  42. package/dist/__tests__/cjk/fuzz.test.js.map +1 -0
  43. package/dist/__tests__/cjk/grid.test.d.ts +2 -0
  44. package/dist/__tests__/cjk/grid.test.d.ts.map +1 -0
  45. package/dist/__tests__/cjk/grid.test.js +261 -0
  46. package/dist/__tests__/cjk/grid.test.js.map +1 -0
  47. package/dist/__tests__/cjk/justifyRender.test.d.ts +2 -0
  48. package/dist/__tests__/cjk/justifyRender.test.d.ts.map +1 -0
  49. package/dist/__tests__/cjk/justifyRender.test.js +139 -0
  50. package/dist/__tests__/cjk/justifyRender.test.js.map +1 -0
  51. package/dist/__tests__/cjk/latinFastPath.test.d.ts +2 -0
  52. package/dist/__tests__/cjk/latinFastPath.test.d.ts.map +1 -0
  53. package/dist/__tests__/cjk/latinFastPath.test.js +208 -0
  54. package/dist/__tests__/cjk/latinFastPath.test.js.map +1 -0
  55. package/dist/__tests__/cjk/markCutsLatin.test.d.ts +2 -0
  56. package/dist/__tests__/cjk/markCutsLatin.test.d.ts.map +1 -0
  57. package/dist/__tests__/cjk/markCutsLatin.test.js +69 -0
  58. package/dist/__tests__/cjk/markCutsLatin.test.js.map +1 -0
  59. package/dist/__tests__/cjk/markRuns.test.d.ts +2 -0
  60. package/dist/__tests__/cjk/markRuns.test.d.ts.map +1 -0
  61. package/dist/__tests__/cjk/markRuns.test.js +347 -0
  62. package/dist/__tests__/cjk/markRuns.test.js.map +1 -0
  63. package/dist/__tests__/cjk/punctuation.test.d.ts +2 -0
  64. package/dist/__tests__/cjk/punctuation.test.d.ts.map +1 -0
  65. package/dist/__tests__/cjk/punctuation.test.js +440 -0
  66. package/dist/__tests__/cjk/punctuation.test.js.map +1 -0
  67. package/dist/__tests__/cjk/punctuationRender.test.d.ts +2 -0
  68. package/dist/__tests__/cjk/punctuationRender.test.d.ts.map +1 -0
  69. package/dist/__tests__/cjk/punctuationRender.test.js +135 -0
  70. package/dist/__tests__/cjk/punctuationRender.test.js.map +1 -0
  71. package/dist/__tests__/cjk/rubyDescenders.test.d.ts +2 -0
  72. package/dist/__tests__/cjk/rubyDescenders.test.d.ts.map +1 -0
  73. package/dist/__tests__/cjk/rubyDescenders.test.js +77 -0
  74. package/dist/__tests__/cjk/rubyDescenders.test.js.map +1 -0
  75. package/dist/__tests__/cjk/runt.test.d.ts +2 -0
  76. package/dist/__tests__/cjk/runt.test.d.ts.map +1 -0
  77. package/dist/__tests__/cjk/runt.test.js +103 -0
  78. package/dist/__tests__/cjk/runt.test.js.map +1 -0
  79. package/dist/__tests__/cjk/sharedMarks.test.d.ts +2 -0
  80. package/dist/__tests__/cjk/sharedMarks.test.d.ts.map +1 -0
  81. package/dist/__tests__/cjk/sharedMarks.test.js +347 -0
  82. package/dist/__tests__/cjk/sharedMarks.test.js.map +1 -0
  83. package/dist/__tests__/cjk/uprightDots.test.d.ts +2 -0
  84. package/dist/__tests__/cjk/uprightDots.test.d.ts.map +1 -0
  85. package/dist/__tests__/cjk/uprightDots.test.js +47 -0
  86. package/dist/__tests__/cjk/uprightDots.test.js.map +1 -0
  87. package/dist/__tests__/cjk/wordJoiner.test.d.ts +2 -0
  88. package/dist/__tests__/cjk/wordJoiner.test.d.ts.map +1 -0
  89. package/dist/__tests__/cjk/wordJoiner.test.js +47 -0
  90. package/dist/__tests__/cjk/wordJoiner.test.js.map +1 -0
  91. package/dist/__tests__/cjk/zhuyinTones.test.d.ts +2 -0
  92. package/dist/__tests__/cjk/zhuyinTones.test.d.ts.map +1 -0
  93. package/dist/__tests__/cjk/zhuyinTones.test.js +72 -0
  94. package/dist/__tests__/cjk/zhuyinTones.test.js.map +1 -0
  95. package/dist/__tests__/contentLocale.test.d.ts +2 -0
  96. package/dist/__tests__/contentLocale.test.d.ts.map +1 -0
  97. package/dist/__tests__/contentLocale.test.js +167 -0
  98. package/dist/__tests__/contentLocale.test.js.map +1 -0
  99. package/dist/__tests__/defaults/cjk.test.d.ts +2 -0
  100. package/dist/__tests__/defaults/cjk.test.d.ts.map +1 -0
  101. package/dist/__tests__/defaults/cjk.test.js +70 -0
  102. package/dist/__tests__/defaults/cjk.test.js.map +1 -0
  103. package/dist/__tests__/design/designTextRender.test.js +31 -0
  104. package/dist/__tests__/design/designTextRender.test.js.map +1 -1
  105. package/dist/__tests__/design/imageAltText.test.d.ts +2 -0
  106. package/dist/__tests__/design/imageAltText.test.d.ts.map +1 -0
  107. package/dist/__tests__/design/imageAltText.test.js +126 -0
  108. package/dist/__tests__/design/imageAltText.test.js.map +1 -0
  109. package/dist/__tests__/exports.test.js +61 -0
  110. package/dist/__tests__/exports.test.js.map +1 -1
  111. package/dist/__tests__/headingLetterSpacing.test.js +1 -1
  112. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
  113. package/dist/__tests__/indexChinese.test.d.ts +2 -0
  114. package/dist/__tests__/indexChinese.test.d.ts.map +1 -0
  115. package/dist/__tests__/indexChinese.test.js +190 -0
  116. package/dist/__tests__/indexChinese.test.js.map +1 -0
  117. package/dist/__tests__/parse/annotations.test.d.ts +2 -0
  118. package/dist/__tests__/parse/annotations.test.d.ts.map +1 -0
  119. package/dist/__tests__/parse/annotations.test.js +175 -0
  120. package/dist/__tests__/parse/annotations.test.js.map +1 -0
  121. package/dist/__tests__/parse/attrQuoting.test.js +10 -8
  122. package/dist/__tests__/parse/attrQuoting.test.js.map +1 -1
  123. package/dist/__tests__/parse/chineseText.test.d.ts +2 -0
  124. package/dist/__tests__/parse/chineseText.test.d.ts.map +1 -0
  125. package/dist/__tests__/parse/chineseText.test.js +352 -0
  126. package/dist/__tests__/parse/chineseText.test.js.map +1 -0
  127. package/dist/__tests__/parts.test.js +25 -0
  128. package/dist/__tests__/parts.test.js.map +1 -1
  129. package/dist/__tests__/pipeline/colonListRoom.test.js +20 -0
  130. package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
  131. package/dist/__tests__/presets/hongloumeng.test.d.ts +2 -0
  132. package/dist/__tests__/presets/hongloumeng.test.d.ts.map +1 -0
  133. package/dist/__tests__/presets/hongloumeng.test.js +484 -0
  134. package/dist/__tests__/presets/hongloumeng.test.js.map +1 -0
  135. package/dist/__tests__/vertical/canvasPaint.test.d.ts +2 -0
  136. package/dist/__tests__/vertical/canvasPaint.test.d.ts.map +1 -0
  137. package/dist/__tests__/vertical/canvasPaint.test.js +229 -0
  138. package/dist/__tests__/vertical/canvasPaint.test.js.map +1 -0
  139. package/dist/__tests__/vertical/compositionPaint.test.d.ts +2 -0
  140. package/dist/__tests__/vertical/compositionPaint.test.d.ts.map +1 -0
  141. package/dist/__tests__/vertical/compositionPaint.test.js +538 -0
  142. package/dist/__tests__/vertical/compositionPaint.test.js.map +1 -0
  143. package/dist/__tests__/vertical/geometry.test.d.ts +2 -0
  144. package/dist/__tests__/vertical/geometry.test.d.ts.map +1 -0
  145. package/dist/__tests__/vertical/geometry.test.js +276 -0
  146. package/dist/__tests__/vertical/geometry.test.js.map +1 -0
  147. package/dist/__tests__/vertical/htmlSmoke.test.d.ts +2 -0
  148. package/dist/__tests__/vertical/htmlSmoke.test.d.ts.map +1 -0
  149. package/dist/__tests__/vertical/htmlSmoke.test.js +25 -0
  150. package/dist/__tests__/vertical/htmlSmoke.test.js.map +1 -0
  151. package/dist/__tests__/vertical/htmlTurnedCells.test.d.ts +2 -0
  152. package/dist/__tests__/vertical/htmlTurnedCells.test.d.ts.map +1 -0
  153. package/dist/__tests__/vertical/htmlTurnedCells.test.js +99 -0
  154. package/dist/__tests__/vertical/htmlTurnedCells.test.js.map +1 -0
  155. package/dist/__tests__/vertical/htmlVertical.test.d.ts +2 -0
  156. package/dist/__tests__/vertical/htmlVertical.test.d.ts.map +1 -0
  157. package/dist/__tests__/vertical/htmlVertical.test.js +145 -0
  158. package/dist/__tests__/vertical/htmlVertical.test.js.map +1 -0
  159. package/dist/__tests__/vertical/lineAxis.test.d.ts +2 -0
  160. package/dist/__tests__/vertical/lineAxis.test.d.ts.map +1 -0
  161. package/dist/__tests__/vertical/lineAxis.test.js +128 -0
  162. package/dist/__tests__/vertical/lineAxis.test.js.map +1 -0
  163. package/dist/__tests__/vertical/measurePaint.test.d.ts +2 -0
  164. package/dist/__tests__/vertical/measurePaint.test.d.ts.map +1 -0
  165. package/dist/__tests__/vertical/measurePaint.test.js +259 -0
  166. package/dist/__tests__/vertical/measurePaint.test.js.map +1 -0
  167. package/dist/__tests__/vertical/orientation.test.d.ts +2 -0
  168. package/dist/__tests__/vertical/orientation.test.d.ts.map +1 -0
  169. package/dist/__tests__/vertical/orientation.test.js +153 -0
  170. package/dist/__tests__/vertical/orientation.test.js.map +1 -0
  171. package/dist/__tests__/vertical/stub.d.ts +8 -0
  172. package/dist/__tests__/vertical/stub.d.ts.map +1 -0
  173. package/dist/__tests__/vertical/stub.js +37 -0
  174. package/dist/__tests__/vertical/stub.js.map +1 -0
  175. package/dist/__tests__/vertical/tateChuYoko.test.d.ts +2 -0
  176. package/dist/__tests__/vertical/tateChuYoko.test.d.ts.map +1 -0
  177. package/dist/__tests__/vertical/tateChuYoko.test.js +270 -0
  178. package/dist/__tests__/vertical/tateChuYoko.test.js.map +1 -0
  179. package/dist/__tests__/vertical/uprightResources.test.d.ts +2 -0
  180. package/dist/__tests__/vertical/uprightResources.test.d.ts.map +1 -0
  181. package/dist/__tests__/vertical/uprightResources.test.js +182 -0
  182. package/dist/__tests__/vertical/uprightResources.test.js.map +1 -0
  183. package/dist/__tests__/vertical/verticalDesignText.test.d.ts +2 -0
  184. package/dist/__tests__/vertical/verticalDesignText.test.d.ts.map +1 -0
  185. package/dist/__tests__/vertical/verticalDesignText.test.js +190 -0
  186. package/dist/__tests__/vertical/verticalDesignText.test.js.map +1 -0
  187. package/dist/__tests__/vertical/verticalSpans.d.ts +25 -0
  188. package/dist/__tests__/vertical/verticalSpans.d.ts.map +1 -0
  189. package/dist/__tests__/vertical/verticalSpans.js +39 -0
  190. package/dist/__tests__/vertical/verticalSpans.js.map +1 -0
  191. package/dist/__tests__/vertical/verticalTwins.test.d.ts +2 -0
  192. package/dist/__tests__/vertical/verticalTwins.test.d.ts.map +1 -0
  193. package/dist/__tests__/vertical/verticalTwins.test.js +71 -0
  194. package/dist/__tests__/vertical/verticalTwins.test.js.map +1 -0
  195. package/dist/bundle/adapters.d.ts +14 -5
  196. package/dist/bundle/adapters.d.ts.map +1 -1
  197. package/dist/bundle/adapters.js +4 -3
  198. package/dist/bundle/adapters.js.map +1 -1
  199. package/dist/bundle/codec.d.ts.map +1 -1
  200. package/dist/bundle/codec.js +27 -4
  201. package/dist/bundle/codec.js.map +1 -1
  202. package/dist/bundle/index.d.ts +1 -1
  203. package/dist/bundle/index.d.ts.map +1 -1
  204. package/dist/bundle/manifest.d.ts +6 -4
  205. package/dist/bundle/manifest.d.ts.map +1 -1
  206. package/dist/bundle/manifest.js +31 -32
  207. package/dist/bundle/manifest.js.map +1 -1
  208. package/dist/bundle/types.d.ts +7 -2
  209. package/dist/bundle/types.d.ts.map +1 -1
  210. package/dist/canvas-backend/annotations.d.ts +31 -0
  211. package/dist/canvas-backend/annotations.d.ts.map +1 -0
  212. package/dist/canvas-backend/annotations.js +120 -0
  213. package/dist/canvas-backend/annotations.js.map +1 -0
  214. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  215. package/dist/canvas-backend/blockRender.js +195 -15
  216. package/dist/canvas-backend/blockRender.js.map +1 -1
  217. package/dist/canvas-backend/decorations.d.ts +5 -0
  218. package/dist/canvas-backend/decorations.d.ts.map +1 -1
  219. package/dist/canvas-backend/decorations.js +27 -0
  220. package/dist/canvas-backend/decorations.js.map +1 -1
  221. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  222. package/dist/canvas-backend/headerFooter.js +32 -16
  223. package/dist/canvas-backend/headerFooter.js.map +1 -1
  224. package/dist/canvas-backend/index.d.ts +2 -0
  225. package/dist/canvas-backend/index.d.ts.map +1 -1
  226. package/dist/canvas-backend/index.js +62 -2
  227. package/dist/canvas-backend/index.js.map +1 -1
  228. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  229. package/dist/canvas-backend/renderResourceBlock.js +42 -5
  230. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  231. package/dist/canvas-backend/segmentText.d.ts +23 -0
  232. package/dist/canvas-backend/segmentText.d.ts.map +1 -0
  233. package/dist/canvas-backend/segmentText.js +41 -0
  234. package/dist/canvas-backend/segmentText.js.map +1 -0
  235. package/dist/canvas-backend/verticalText.d.ts +113 -0
  236. package/dist/canvas-backend/verticalText.d.ts.map +1 -0
  237. package/dist/canvas-backend/verticalText.js +437 -0
  238. package/dist/canvas-backend/verticalText.js.map +1 -0
  239. package/dist/chineseNumerals.d.ts +39 -0
  240. package/dist/chineseNumerals.d.ts.map +1 -0
  241. package/dist/chineseNumerals.js +192 -0
  242. package/dist/chineseNumerals.js.map +1 -0
  243. package/dist/cjkMarks.d.ts +61 -0
  244. package/dist/cjkMarks.d.ts.map +1 -0
  245. package/dist/cjkMarks.js +441 -0
  246. package/dist/cjkMarks.js.map +1 -0
  247. package/dist/columnClip.d.ts +13 -4
  248. package/dist/columnClip.d.ts.map +1 -1
  249. package/dist/columnClip.js +33 -5
  250. package/dist/columnClip.js.map +1 -1
  251. package/dist/configWarnings.d.ts.map +1 -1
  252. package/dist/configWarnings.js +18 -3
  253. package/dist/configWarnings.js.map +1 -1
  254. package/dist/defaults/bodyText.d.ts +4 -1
  255. package/dist/defaults/bodyText.d.ts.map +1 -1
  256. package/dist/defaults/bodyText.js +27 -5
  257. package/dist/defaults/bodyText.js.map +1 -1
  258. package/dist/defaults/calloutStyles.js +1 -1
  259. package/dist/defaults/calloutStyles.js.map +1 -1
  260. package/dist/defaults/captionStyle.d.ts.map +1 -1
  261. package/dist/defaults/captionStyle.js +14 -0
  262. package/dist/defaults/captionStyle.js.map +1 -1
  263. package/dist/defaults/cjk.d.ts +33 -0
  264. package/dist/defaults/cjk.d.ts.map +1 -0
  265. package/dist/defaults/cjk.js +234 -0
  266. package/dist/defaults/cjk.js.map +1 -0
  267. package/dist/defaults/headerFooter.d.ts.map +1 -1
  268. package/dist/defaults/headerFooter.js +2 -0
  269. package/dist/defaults/headerFooter.js.map +1 -1
  270. package/dist/defaults/headingStyles.d.ts.map +1 -1
  271. package/dist/defaults/headingStyles.js +4 -0
  272. package/dist/defaults/headingStyles.js.map +1 -1
  273. package/dist/defaults/headings.d.ts.map +1 -1
  274. package/dist/defaults/headings.js +13 -6
  275. package/dist/defaults/headings.js.map +1 -1
  276. package/dist/defaults/index.d.ts +1 -0
  277. package/dist/defaults/index.d.ts.map +1 -1
  278. package/dist/defaults/index.js +10 -1
  279. package/dist/defaults/index.js.map +1 -1
  280. package/dist/defaults/indexConfig.d.ts +2 -1
  281. package/dist/defaults/indexConfig.d.ts.map +1 -1
  282. package/dist/defaults/indexConfig.js +5 -0
  283. package/dist/defaults/indexConfig.js.map +1 -1
  284. package/dist/defaults/layout.d.ts.map +1 -1
  285. package/dist/defaults/layout.js +7 -0
  286. package/dist/defaults/layout.js.map +1 -1
  287. package/dist/defaults/orderedLists.d.ts +5 -2
  288. package/dist/defaults/orderedLists.d.ts.map +1 -1
  289. package/dist/defaults/orderedLists.js +20 -5
  290. package/dist/defaults/orderedLists.js.map +1 -1
  291. package/dist/defaults/page.d.ts +8 -2
  292. package/dist/defaults/page.d.ts.map +1 -1
  293. package/dist/defaults/page.js +24 -7
  294. package/dist/defaults/page.js.map +1 -1
  295. package/dist/defaults/resourceTypes.d.ts +3 -3
  296. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  297. package/dist/defaults/resourceTypes.js +23 -9
  298. package/dist/defaults/resourceTypes.js.map +1 -1
  299. package/dist/defaults/tableStyle.d.ts.map +1 -1
  300. package/dist/defaults/tableStyle.js +8 -5
  301. package/dist/defaults/tableStyle.js.map +1 -1
  302. package/dist/design/layout.d.ts +33 -0
  303. package/dist/design/layout.d.ts.map +1 -1
  304. package/dist/design/layout.js +124 -18
  305. package/dist/design/layout.js.map +1 -1
  306. package/dist/design/placeholders.d.ts.map +1 -1
  307. package/dist/design/placeholders.js +5 -1
  308. package/dist/design/placeholders.js.map +1 -1
  309. package/dist/design/richText.d.ts +24 -4
  310. package/dist/design/richText.d.ts.map +1 -1
  311. package/dist/design/richText.js +77 -16
  312. package/dist/design/richText.js.map +1 -1
  313. package/dist/html-backend.d.ts.map +1 -1
  314. package/dist/html-backend.js +635 -25
  315. package/dist/html-backend.js.map +1 -1
  316. package/dist/htmlAnnotations.d.ts +29 -0
  317. package/dist/htmlAnnotations.d.ts.map +1 -0
  318. package/dist/htmlAnnotations.js +86 -0
  319. package/dist/htmlAnnotations.js.map +1 -0
  320. package/dist/hyphenate.js +2 -1
  321. package/dist/hyphenate.js.map +1 -1
  322. package/dist/index.d.ts +28 -14
  323. package/dist/index.d.ts.map +1 -1
  324. package/dist/index.js +16 -8
  325. package/dist/index.js.map +1 -1
  326. package/dist/knuthPlass/pretextAdapter.d.ts +4 -1
  327. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
  328. package/dist/knuthPlass/pretextAdapter.js +9 -5
  329. package/dist/knuthPlass/pretextAdapter.js.map +1 -1
  330. package/dist/knuthPlass/richAdapter.d.ts +15 -2
  331. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  332. package/dist/knuthPlass/richAdapter.js +16 -6
  333. package/dist/knuthPlass/richAdapter.js.map +1 -1
  334. package/dist/knuthPlass/tracking.d.ts +1 -1
  335. package/dist/knuthPlass/tracking.d.ts.map +1 -1
  336. package/dist/knuthPlass/tracking.js +4 -3
  337. package/dist/knuthPlass/tracking.js.map +1 -1
  338. package/dist/knuthPlass/types.d.ts +3 -0
  339. package/dist/knuthPlass/types.d.ts.map +1 -1
  340. package/dist/lineInk.d.ts +13 -0
  341. package/dist/lineInk.d.ts.map +1 -1
  342. package/dist/lineInk.js +28 -1
  343. package/dist/lineInk.js.map +1 -1
  344. package/dist/locale.d.ts +91 -3
  345. package/dist/locale.d.ts.map +1 -1
  346. package/dist/locale.js +245 -1
  347. package/dist/locale.js.map +1 -1
  348. package/dist/looseLines.d.ts +11 -1
  349. package/dist/looseLines.d.ts.map +1 -1
  350. package/dist/looseLines.js +26 -1
  351. package/dist/looseLines.js.map +1 -1
  352. package/dist/measure/cache.d.ts.map +1 -1
  353. package/dist/measure/cache.js +51 -3
  354. package/dist/measure/cache.js.map +1 -1
  355. package/dist/measure/canvas.d.ts +25 -0
  356. package/dist/measure/canvas.d.ts.map +1 -1
  357. package/dist/measure/canvas.js +90 -1
  358. package/dist/measure/canvas.js.map +1 -1
  359. package/dist/measure/cjk.d.ts +8 -13
  360. package/dist/measure/cjk.d.ts.map +1 -1
  361. package/dist/measure/cjk.js +29 -53
  362. package/dist/measure/cjk.js.map +1 -1
  363. package/dist/measure/cjkAnnotate.d.ts +96 -0
  364. package/dist/measure/cjkAnnotate.d.ts.map +1 -0
  365. package/dist/measure/cjkAnnotate.js +237 -0
  366. package/dist/measure/cjkAnnotate.js.map +1 -0
  367. package/dist/measure/cjkClasses.d.ts +133 -0
  368. package/dist/measure/cjkClasses.d.ts.map +1 -0
  369. package/dist/measure/cjkClasses.js +273 -0
  370. package/dist/measure/cjkClasses.js.map +1 -0
  371. package/dist/measure/cjkCompose.d.ts +86 -0
  372. package/dist/measure/cjkCompose.d.ts.map +1 -0
  373. package/dist/measure/cjkCompose.js +2022 -0
  374. package/dist/measure/cjkCompose.js.map +1 -0
  375. package/dist/measure/cjkPunctuation.d.ts +248 -0
  376. package/dist/measure/cjkPunctuation.d.ts.map +1 -0
  377. package/dist/measure/cjkPunctuation.js +492 -0
  378. package/dist/measure/cjkPunctuation.js.map +1 -0
  379. package/dist/measure/graphemes.d.ts +13 -0
  380. package/dist/measure/graphemes.d.ts.map +1 -0
  381. package/dist/measure/graphemes.js +38 -0
  382. package/dist/measure/graphemes.js.map +1 -0
  383. package/dist/measure/index.d.ts +3 -0
  384. package/dist/measure/index.d.ts.map +1 -1
  385. package/dist/measure/index.js +2 -0
  386. package/dist/measure/index.js.map +1 -1
  387. package/dist/measure/links.d.ts +6 -2
  388. package/dist/measure/links.d.ts.map +1 -1
  389. package/dist/measure/links.js +15 -5
  390. package/dist/measure/links.js.map +1 -1
  391. package/dist/measure/markCuts.d.ts +37 -0
  392. package/dist/measure/markCuts.d.ts.map +1 -0
  393. package/dist/measure/markCuts.js +77 -0
  394. package/dist/measure/markCuts.js.map +1 -0
  395. package/dist/measure/plain.d.ts.map +1 -1
  396. package/dist/measure/plain.js +33 -11
  397. package/dist/measure/plain.js.map +1 -1
  398. package/dist/measure/rich.d.ts +88 -1
  399. package/dist/measure/rich.d.ts.map +1 -1
  400. package/dist/measure/rich.js +233 -111
  401. package/dist/measure/rich.js.map +1 -1
  402. package/dist/measure/rubyLift.d.ts +11 -0
  403. package/dist/measure/rubyLift.d.ts.map +1 -0
  404. package/dist/measure/rubyLift.js +57 -0
  405. package/dist/measure/rubyLift.js.map +1 -0
  406. package/dist/measure/spaces.d.ts +12 -2
  407. package/dist/measure/spaces.d.ts.map +1 -1
  408. package/dist/measure/spaces.js +21 -7
  409. package/dist/measure/spaces.js.map +1 -1
  410. package/dist/measure/types.d.ts +20 -1
  411. package/dist/measure/types.d.ts.map +1 -1
  412. package/dist/measure/types.js.map +1 -1
  413. package/dist/measure/vertical.d.ts +104 -0
  414. package/dist/measure/vertical.d.ts.map +1 -0
  415. package/dist/measure/vertical.js +184 -0
  416. package/dist/measure/vertical.js.map +1 -0
  417. package/dist/numberWords.d.ts +3 -10
  418. package/dist/numberWords.d.ts.map +1 -1
  419. package/dist/numberWords.js +21 -9
  420. package/dist/numberWords.js.map +1 -1
  421. package/dist/numbering.d.ts +23 -5
  422. package/dist/numbering.d.ts.map +1 -1
  423. package/dist/numbering.js +99 -14
  424. package/dist/numbering.js.map +1 -1
  425. package/dist/parse/annotations.d.ts +101 -0
  426. package/dist/parse/annotations.d.ts.map +1 -0
  427. package/dist/parse/annotations.js +556 -0
  428. package/dist/parse/annotations.js.map +1 -0
  429. package/dist/parse/attrs.d.ts +48 -1
  430. package/dist/parse/attrs.d.ts.map +1 -1
  431. package/dist/parse/attrs.js +121 -8
  432. package/dist/parse/attrs.js.map +1 -1
  433. package/dist/parse/blockParser.d.ts.map +1 -1
  434. package/dist/parse/blockParser.js +50 -24
  435. package/dist/parse/blockParser.js.map +1 -1
  436. package/dist/parse/index.d.ts +3 -1
  437. package/dist/parse/index.d.ts.map +1 -1
  438. package/dist/parse/index.js +1 -0
  439. package/dist/parse/index.js.map +1 -1
  440. package/dist/parse/injectSpans.d.ts +3 -1
  441. package/dist/parse/injectSpans.d.ts.map +1 -1
  442. package/dist/parse/injectSpans.js +29 -8
  443. package/dist/parse/injectSpans.js.map +1 -1
  444. package/dist/parse/inlineFormatting.d.ts +38 -6
  445. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  446. package/dist/parse/inlineFormatting.js +139 -26
  447. package/dist/parse/inlineFormatting.js.map +1 -1
  448. package/dist/parse/inlineSnippet.d.ts +2 -1
  449. package/dist/parse/inlineSnippet.d.ts.map +1 -1
  450. package/dist/parse/inlineSnippet.js +6 -2
  451. package/dist/parse/inlineSnippet.js.map +1 -1
  452. package/dist/parse/orientationMarks.d.ts +49 -0
  453. package/dist/parse/orientationMarks.d.ts.map +1 -0
  454. package/dist/parse/orientationMarks.js +138 -0
  455. package/dist/parse/orientationMarks.js.map +1 -0
  456. package/dist/parse/softBreaks.d.ts +56 -0
  457. package/dist/parse/softBreaks.d.ts.map +1 -0
  458. package/dist/parse/softBreaks.js +271 -0
  459. package/dist/parse/softBreaks.js.map +1 -0
  460. package/dist/parse/sourceMapping.d.ts.map +1 -1
  461. package/dist/parse/sourceMapping.js +41 -0
  462. package/dist/parse/sourceMapping.js.map +1 -1
  463. package/dist/parse/types.d.ts +84 -0
  464. package/dist/parse/types.d.ts.map +1 -1
  465. package/dist/pipeline/annotations.d.ts +56 -0
  466. package/dist/pipeline/annotations.d.ts.map +1 -0
  467. package/dist/pipeline/annotations.js +181 -0
  468. package/dist/pipeline/annotations.js.map +1 -0
  469. package/dist/pipeline/build.d.ts.map +1 -1
  470. package/dist/pipeline/build.js +158 -47
  471. package/dist/pipeline/build.js.map +1 -1
  472. package/dist/pipeline/buildBlockKind.d.ts +3 -0
  473. package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
  474. package/dist/pipeline/buildBlockKind.js +5 -3
  475. package/dist/pipeline/buildBlockKind.js.map +1 -1
  476. package/dist/pipeline/buildHelpers.d.ts +36 -6
  477. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  478. package/dist/pipeline/buildHelpers.js +55 -8
  479. package/dist/pipeline/buildHelpers.js.map +1 -1
  480. package/dist/pipeline/buildMeasurement.d.ts +4 -0
  481. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  482. package/dist/pipeline/buildMeasurement.js +3 -1
  483. package/dist/pipeline/buildMeasurement.js.map +1 -1
  484. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  485. package/dist/pipeline/calloutLayout.js +25 -11
  486. package/dist/pipeline/calloutLayout.js.map +1 -1
  487. package/dist/pipeline/cjkGrid.d.ts +133 -0
  488. package/dist/pipeline/cjkGrid.d.ts.map +1 -0
  489. package/dist/pipeline/cjkGrid.js +191 -0
  490. package/dist/pipeline/cjkGrid.js.map +1 -0
  491. package/dist/pipeline/config.d.ts +13 -9
  492. package/dist/pipeline/config.d.ts.map +1 -1
  493. package/dist/pipeline/config.js +43 -18
  494. package/dist/pipeline/config.js.map +1 -1
  495. package/dist/pipeline/contentWarnings.d.ts +10 -0
  496. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  497. package/dist/pipeline/contentWarnings.js +174 -2
  498. package/dist/pipeline/contentWarnings.js.map +1 -1
  499. package/dist/pipeline/floatPlacement.d.ts +9 -3
  500. package/dist/pipeline/floatPlacement.d.ts.map +1 -1
  501. package/dist/pipeline/floatPlacement.js +17 -6
  502. package/dist/pipeline/floatPlacement.js.map +1 -1
  503. package/dist/pipeline/headerFooter.d.ts +5 -2
  504. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  505. package/dist/pipeline/headerFooter.js +123 -28
  506. package/dist/pipeline/headerFooter.js.map +1 -1
  507. package/dist/pipeline/headingDesignCuts.d.ts.map +1 -1
  508. package/dist/pipeline/headingDesignCuts.js +3 -1
  509. package/dist/pipeline/headingDesignCuts.js.map +1 -1
  510. package/dist/pipeline/headingStyles.d.ts +9 -3
  511. package/dist/pipeline/headingStyles.d.ts.map +1 -1
  512. package/dist/pipeline/headingStyles.js +23 -5
  513. package/dist/pipeline/headingStyles.js.map +1 -1
  514. package/dist/pipeline/indexDirective.d.ts.map +1 -1
  515. package/dist/pipeline/indexDirective.js +64 -20
  516. package/dist/pipeline/indexDirective.js.map +1 -1
  517. package/dist/pipeline/indexGroups.d.ts +55 -0
  518. package/dist/pipeline/indexGroups.d.ts.map +1 -0
  519. package/dist/pipeline/indexGroups.js +151 -0
  520. package/dist/pipeline/indexGroups.js.map +1 -0
  521. package/dist/pipeline/lists.d.ts +17 -0
  522. package/dist/pipeline/lists.d.ts.map +1 -1
  523. package/dist/pipeline/lists.js +43 -51
  524. package/dist/pipeline/lists.js.map +1 -1
  525. package/dist/pipeline/measureContentBlock.d.ts +3 -0
  526. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  527. package/dist/pipeline/measureContentBlock.js +31 -4
  528. package/dist/pipeline/measureContentBlock.js.map +1 -1
  529. package/dist/pipeline/outline.d.ts.map +1 -1
  530. package/dist/pipeline/outline.js +13 -8
  531. package/dist/pipeline/outline.js.map +1 -1
  532. package/dist/pipeline/parts.d.ts +6 -3
  533. package/dist/pipeline/parts.d.ts.map +1 -1
  534. package/dist/pipeline/parts.js +28 -5
  535. package/dist/pipeline/parts.js.map +1 -1
  536. package/dist/pipeline/placeholders.d.ts +11 -0
  537. package/dist/pipeline/placeholders.d.ts.map +1 -1
  538. package/dist/pipeline/placeholders.js +48 -18
  539. package/dist/pipeline/placeholders.js.map +1 -1
  540. package/dist/pipeline/placement.d.ts +10 -6
  541. package/dist/pipeline/placement.d.ts.map +1 -1
  542. package/dist/pipeline/placement.js +33 -16
  543. package/dist/pipeline/placement.js.map +1 -1
  544. package/dist/pipeline/resourceLayout.d.ts +18 -1
  545. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  546. package/dist/pipeline/resourceLayout.js +109 -21
  547. package/dist/pipeline/resourceLayout.js.map +1 -1
  548. package/dist/pipeline/toc.d.ts.map +1 -1
  549. package/dist/pipeline/toc.js +6 -5
  550. package/dist/pipeline/toc.js.map +1 -1
  551. package/dist/pipeline/verticalMetrics.d.ts +14 -0
  552. package/dist/pipeline/verticalMetrics.d.ts.map +1 -0
  553. package/dist/pipeline/verticalMetrics.js +154 -0
  554. package/dist/pipeline/verticalMetrics.js.map +1 -0
  555. package/dist/types.d.ts +344 -13
  556. package/dist/types.d.ts.map +1 -1
  557. package/dist/vdt.d.ts +396 -6
  558. package/dist/vdt.d.ts.map +1 -1
  559. package/dist/vdt.js +49 -0
  560. package/dist/vdt.js.map +1 -1
  561. package/dist/writingMode.d.ts +156 -0
  562. package/dist/writingMode.d.ts.map +1 -0
  563. package/dist/writingMode.js +291 -0
  564. package/dist/writingMode.js.map +1 -0
  565. package/package.json +1 -1
@@ -0,0 +1,2022 @@
1
+ /**
2
+ * The CJK line composer: Chinese (and Japanese, Korean) paragraphs, on both
3
+ * the plain and the formatted path, broken between characters under the
4
+ * clreq prohibitions of the document's `cjk.lineBreak` and, when justified,
5
+ * spread between characters.
6
+ *
7
+ * Linear in the paragraph: every unit (one grapheme, a Western run a line
8
+ * never breaks inside, a 2-em dash or ellipsis, an atomic box) is measured
9
+ * once — the width cache holds single characters, not prefixes — and the
10
+ * breaker walks the units once with a running width. A line that would
11
+ * start with a prohibited mark gives up characters to the next line
12
+ * (push-out), back to the last break the rules allow.
13
+ *
14
+ * Justification (clreq §6.2.2.4): a justified line that is not its
15
+ * paragraph's last is stretched to the measure — word spaces first, up to
16
+ * ½ em each, then every gap between characters equally, never inside a
17
+ * Western run (a word, a number with its signs), a 2-em dash or ellipsis,
18
+ * and never next to a connector or a solidus. The spread is carried as
19
+ * segment `tracking` (px after each grapheme, in the segment's width); a
20
+ * Western run or a dash pair that a gap follows gives its last grapheme a
21
+ * segment of its own. A line that needs more than the cap (½ em, or
22
+ * `bodyText.maxJustifyTracking` when set) is set with the cap and flagged
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.
25
+ *
26
+ * Segments: a Western run keeps one of its own, and single characters
27
+ * share one only when they have one style, link and spacing and advance
28
+ * alike, so a caret spread evenly over a segment lands on its characters
29
+ * and a Markdown link covers only its own.
30
+ */
31
+ import { createBoundingBox } from '../vdt';
32
+ import { lineMeasure } from './types';
33
+ import { measureInkBox, measureInkExtent, measureTextWidth, normalSpaceWidthFor } from './canvas';
34
+ import { atomicSpanToken, 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';
36
+ import { hasCJK } from './cjk';
37
+ import { graphemeCount, graphemesOf, lastGrapheme } from './graphemes';
38
+ import { isBreakingSpace } from './spaces';
39
+ 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';
44
+ /** Fit tolerance of the breaker (px): running sums of many widths may
45
+ * land a hair past a measure they fill exactly. */
46
+ const FIT_EPS = 1e-6;
47
+ const FONT_SIZE_RE = /(\d*\.?\d+)px/;
48
+ /** Letters of the scripts set without spaces (Han, kana, bopomofo, hangul),
49
+ * as {@link isCjkParagraph} counts them. */
50
+ const CJK_LETTER_RE = /[\u3005-\u3007\u3040-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3100-\u312F\u31A0-\u31BF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF\uFF66-\uFF9F\u{20000}-\u{3FFFF}]/u;
51
+ /** Combining marks and variation selectors: they belong to the character
52
+ * before them. */
53
+ const MARK_RE = /\p{M}/u;
54
+ /**
55
+ * Whether a paragraph is set by the CJK composer: it holds more CJK letters
56
+ * than word spaces. A word space is a run of spaces between two characters
57
+ * that are not CJK. A space that touches a CJK character or mark is the
58
+ * one web text types at each boundary (`2026 年 9 月 28 日`, `安装 Node.js
59
+ * 和 Git`), which the composer replaces with the Han–Latin space.
60
+ * A Latin paragraph that quotes a Chinese title or name ("the novel 紅樓夢
61
+ * was …") has more word spaces, stays with Knuth–Plass and breaks next to
62
+ * the characters it quotes; a Chinese paragraph with Latin words in it
63
+ * ("用 iPhone 拍照") is composed, with its typed spaces or without them.
64
+ */
65
+ export function isCjkParagraph(text) {
66
+ let letters = 0;
67
+ let spaces = 0;
68
+ let inSpace = false;
69
+ // Whether the character before the run of spaces is CJK.
70
+ let spaceAfterCjk = false;
71
+ let lastCjk = false;
72
+ for (const ch of text) {
73
+ if (isBreakingSpace(ch)) {
74
+ if (!inSpace)
75
+ spaceAfterCjk = lastCjk;
76
+ inSpace = true;
77
+ continue;
78
+ }
79
+ if (MARK_RE.test(ch))
80
+ continue;
81
+ const cjk = hasCJK(ch);
82
+ if (inSpace && !spaceAfterCjk && !cjk)
83
+ spaces++;
84
+ inSpace = false;
85
+ lastCjk = cjk;
86
+ if (CJK_LETTER_RE.test(ch))
87
+ letters++;
88
+ }
89
+ if (inSpace && !spaceAfterCjk)
90
+ spaces++;
91
+ return letters > spaces;
92
+ }
93
+ /** Whether the CJK composer sets a paragraph of this text: it holds CJK
94
+ * text (not only unit squares or marks Latin shares with Chinese) and more
95
+ * CJK letters than word spaces (see {@link isCjkParagraph}). Both
96
+ * measuring paths ask this, so they agree. */
97
+ export function composesAsCjk(text) {
98
+ return hasCJK(text) && isCjkParagraph(text);
99
+ }
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. */
103
+ export function spanMarks(span, vertical) {
104
+ if (!span.emphasisMark && span.properName === undefined && !span.bookTitle)
105
+ return undefined;
106
+ const marks = {};
107
+ if (span.emphasisMark) {
108
+ const style = span.emphasisMark.style ?? 'dot';
109
+ marks.dots = {
110
+ style,
111
+ fill: span.emphasisMark.fill ?? (style === 'circle' ? 'open' : 'filled'),
112
+ position: span.emphasisMark.position ?? (vertical ? 'over' : 'under'),
113
+ };
114
+ }
115
+ if (span.properName !== undefined)
116
+ marks.properName = span.properName;
117
+ if (span.bookTitle)
118
+ marks.bookTitle = span.bookTitle.id;
119
+ return marks;
120
+ }
121
+ /** The part of a style key the marks and added characters make. */
122
+ function marksKey(marks, inserted) {
123
+ if (!marks && !inserted)
124
+ return '';
125
+ const d = marks?.dots;
126
+ return `|m:${d ? `${d.style}${d.fill}${d.position}` : ''}:${marks?.properName ?? ''}:${marks?.bookTitle ?? ''}${inserted ? ':ins' : ''}`;
127
+ }
128
+ function styleOf(span, fonts, vertical = false) {
129
+ const script = spanScriptFields(span, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic);
130
+ const font = script.scriptFont ?? pickSpanFont(span.bold, span.italic, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic);
131
+ const marks = spanMarks(span, vertical);
132
+ return {
133
+ key: `${span.bold ? 'b' : ''}${span.italic ? 'i' : ''}${span.captionLabel ? 'c' : ''}${span.smallCaps ? 's' : ''}|${script.script ?? ''}|${font}|${script.baselineShift ?? ''}${marksKey(marks, span.inserted)}`,
134
+ bold: span.bold,
135
+ italic: span.italic,
136
+ ...(span.captionLabel ? { captionLabel: true } : {}),
137
+ ...script,
138
+ ...(span.smallCaps ? { smallCaps: true } : {}),
139
+ font,
140
+ ...(marks ? { marks } : {}),
141
+ ...(span.inserted ? { inserted: true } : {}),
142
+ };
143
+ }
144
+ /** A grapheme set as a Chinese character here: a CJK grapheme, unless it
145
+ * is the apostrophe or the interpunct of a Latin word ("don’t", "l·l"). */
146
+ function isCjkHere(g, prev, next) {
147
+ return isCjkGrapheme(g) && !isWordInnerMark(g, prev, next);
148
+ }
149
+ /** Marks that pair up into one 2-em unit (破折号, 省略号). */
150
+ function pairable(g) {
151
+ return g === '\u2014' || g === '\u2015' || g === '\u2026' || g === '\u22EF'; // — ― … ⋯
152
+ }
153
+ /** Whether a vertical Western run opens and ends with a short number set
154
+ * in one upright cell (`cjk.uprightDigits`, see `verticalRuns`);
155
+ * undefined when neither. */
156
+ function numberCellEdges(run) {
157
+ const digits = getMeasureUprightDigits();
158
+ if (digits === 0 || !run.some((g) => g >= '0' && g <= '9' && g.length === 1))
159
+ return undefined;
160
+ const runs = verticalRuns(run, getMeasureRegion(), digits);
161
+ const start = runs[0]?.glyph.orient === 'tcy';
162
+ const end = runs[runs.length - 1]?.glyph.orient === 'tcy';
163
+ return start || end ? { start, end } : undefined;
164
+ }
165
+ /**
166
+ * The units of a span the author set apart in vertical text (see
167
+ * `Unit.orient`): `:tcy[…]` one upright cell of one em, `:upright[…]` one
168
+ * cell per character (a line never breaks between them), `:sideways[…]`
169
+ * one Western run at its horizontal width. The cells break and spread like
170
+ * Chinese characters and take no Han–Latin space; the block's tracking
171
+ * follows each cell once.
172
+ */
173
+ function orientedUnits(span, style, letterSpacingPx, zwsp, units) {
174
+ const track = (n) => (letterSpacingPx === 0 ? 0 : letterSpacingPx * n);
175
+ const em = fontEm(style.font);
176
+ const base = { style, at: 0, ...(zwsp ? { zwspBefore: true } : {}) };
177
+ if (span.orientation === 'sideways' && !span.combineUpright) {
178
+ const graphemes = graphemesOf(span.text);
179
+ units.push({
180
+ ...base,
181
+ kind: 'text',
182
+ text: span.text,
183
+ width: measureTextWidth(span.text, style.font) + track(graphemes.length),
184
+ graphemes: graphemes.length,
185
+ first: 'western',
186
+ last: 'western',
187
+ firstCjk: false,
188
+ lastCjk: false,
189
+ run: true,
190
+ orient: 'sideways',
191
+ });
192
+ return;
193
+ }
194
+ if (span.combineUpright) {
195
+ units.push({
196
+ ...base,
197
+ kind: 'text',
198
+ text: span.text,
199
+ width: em + track(1),
200
+ graphemes: 1,
201
+ first: 'ideograph',
202
+ last: 'ideograph',
203
+ firstCjk: true,
204
+ lastCjk: true,
205
+ orient: 'tcy',
206
+ });
207
+ return;
208
+ }
209
+ let offset = 0;
210
+ graphemesOf(span.text).forEach((g, i) => {
211
+ units.push({
212
+ ...base,
213
+ ...(i > 0 ? { glueBefore: true, zwspBefore: undefined } : {}),
214
+ kind: 'text',
215
+ text: g,
216
+ width: em + track(1),
217
+ graphemes: 1,
218
+ first: 'ideograph',
219
+ last: 'ideograph',
220
+ firstCjk: true,
221
+ lastCjk: true,
222
+ at: offset,
223
+ orient: 'upright',
224
+ });
225
+ offset += g.length;
226
+ });
227
+ }
228
+ /** The unit of a ruby base (#194): its characters as one box, measured as
229
+ * the composer measures them (cells down a vertical line), the reading
230
+ * laid out later (`sizeRubies`). Where the reading goes: `pos`, else
231
+ * zhuyin right of each character and anything else over; in vertical text
232
+ * `right` is the over side. */
233
+ function rubyBaseUnit(span, fonts, letterSpacingPx, vertical) {
234
+ const ruby = span.ruby;
235
+ const style = styleOf(span, fonts, vertical);
236
+ const graphemes = graphemesOf(span.text);
237
+ let width = 0;
238
+ for (const g of graphemes) {
239
+ width += isCjkGrapheme(g) ? cellAdvance(g, style.font, vertical, cjkClassOf(g)) : textWidth(g, style.font, style.smallCaps);
240
+ }
241
+ width += letterSpacingPx * graphemes.length;
242
+ const firstG = graphemes[0] ?? '';
243
+ const lastG = graphemes[graphemes.length - 1] ?? '';
244
+ let position = ruby.position ?? (isZhuyin(ruby.text) ? 'right' : 'over');
245
+ if (vertical && position === 'right')
246
+ position = 'over';
247
+ return {
248
+ kind: 'text',
249
+ text: span.text,
250
+ width,
251
+ graphemes: graphemes.length,
252
+ first: cjkClassOf(firstG),
253
+ last: cjkClassOf(lastG),
254
+ firstCjk: isCjkGrapheme(firstG),
255
+ lastCjk: isCjkGrapheme(lastG),
256
+ style,
257
+ at: 0,
258
+ ruby: { span: ruby, position },
259
+ };
260
+ }
261
+ /**
262
+ * Lay out each ruby base's reading (see `cjkAnnotate.ts`): its box grows
263
+ * to the reading less what the reading may pass it by — a quarter of the
264
+ * ruby em onto a neighbour without ruby, and it keeps as much from a
265
+ * neighbour's reading on the same side (two zhuyin readings: a quarter of
266
+ * the symbols' em). Run once the units are final.
267
+ */
268
+ function sizeRubies(units) {
269
+ for (let k = 0; k < units.length; k++) {
270
+ const u = units[k];
271
+ if (!u.ruby || u.ruby.geometry)
272
+ continue;
273
+ const fontOf = (v) => v.ruby.span.fontString ?? withFontSize(v.style.font, emOfFont(v.style.font) / 2);
274
+ const font = fontOf(u);
275
+ const q = fontEm(font) / 4;
276
+ const zhuyin = isZhuyin(u.ruby.span.text);
277
+ // How far the reading may pass its box towards a neighbour: a quarter
278
+ // of the ruby em onto one without a reading; next to a reading on the
279
+ // same side, what keeps the two a quarter em apart (the neighbour's
280
+ // box is at least its base). Zhuyin is set at 60 % of the ruby size,
281
+ // so two zhuyin readings keep a quarter of that em apart: three
282
+ // symbols beside each of two characters stay in their cells (clreq
283
+ // §5.5.3 centres each column on its own character).
284
+ const allow = (v) => {
285
+ if (!v || v.kind !== 'text' || v.note)
286
+ return 0;
287
+ if (!v.ruby)
288
+ return q;
289
+ if (v.ruby.position !== u.ruby.position || v.ruby.position === 'right')
290
+ return q;
291
+ const theirs = v.ruby.geometry?.rtWidth ?? readingAdvance(v.ruby.span.text, fontOf(v), v.ruby.position);
292
+ const gap = zhuyin && isZhuyin(v.ruby.span.text) ? (fontEm(font) * ZHUYIN_SIZE_RATIO) / 4 : q;
293
+ return Math.min(q, (v.width - theirs) / 2 - gap);
294
+ };
295
+ const geometry = rubyGeometry({
296
+ reading: u.ruby.span.text,
297
+ fontString: font,
298
+ position: u.ruby.position,
299
+ baseWidth: u.width,
300
+ em: emOfFont(u.style.font),
301
+ allowLeft: allow(units[k - 1]),
302
+ allowRight: allow(units[k + 1]),
303
+ });
304
+ u.ruby.geometry = geometry;
305
+ u.width = geometry.width;
306
+ }
307
+ }
308
+ /** The units of a paragraph's spans, the characters of each warichu note
309
+ * (#195) measured at the note's size and flagged with it, between the
310
+ * note's brackets (added text at the body size, in the note's colour). */
311
+ function buildAllUnits(spans, fonts, letterSpacingPx, vertical, route) {
312
+ if (!spans.some((s) => s.warichu))
313
+ return buildUnits(spans, fonts, letterSpacingPx, vertical, route);
314
+ const out = [];
315
+ let i = 0;
316
+ while (i < spans.length) {
317
+ const note = spans[i].warichu;
318
+ let j = i + 1;
319
+ while (j < spans.length && spans[j].warichu === note)
320
+ j++;
321
+ const group = spans.slice(i, j);
322
+ if (!note)
323
+ out.push(...buildUnits(group, fonts, letterSpacingPx, vertical, route));
324
+ else
325
+ out.push(...noteUnits(group, note, fonts, vertical));
326
+ i = j;
327
+ }
328
+ return out;
329
+ }
330
+ /** The units of one warichu note: its characters at the note's size (a
331
+ * word space inside it is a character of the note), its brackets. */
332
+ function noteUnits(spans, note, fonts, vertical) {
333
+ const size = note.fontString ? fontEm(note.fontString) : emOfFont(fonts.normal) / 2;
334
+ const noteFonts = {
335
+ normal: withFontSize(fonts.normal, size),
336
+ bold: withFontSize(fonts.bold, size),
337
+ italic: withFontSize(fonts.italic, size),
338
+ boldItalic: withFontSize(fonts.boldItalic, size),
339
+ };
340
+ const inner = buildUnits(spans, noteFonts, 0, vertical, false);
341
+ for (const u of inner) {
342
+ u.note = note;
343
+ if (u.kind === 'space') {
344
+ u.kind = 'text';
345
+ u.firstCjk = u.lastCjk = false;
346
+ u.noteSpace = true;
347
+ }
348
+ }
349
+ const bracket = (text) => {
350
+ const g = graphemesOf(text);
351
+ const cls = cjkClassOf(g[0] ?? '');
352
+ const style = {
353
+ key: `ins|${note.color ?? ''}|${fonts.normal}`,
354
+ bold: false,
355
+ italic: false,
356
+ font: fonts.normal,
357
+ inserted: true,
358
+ ...(note.color ? { color: note.color } : {}),
359
+ };
360
+ let width = 0;
361
+ for (const c of g)
362
+ width += cellAdvance(c, fonts.normal, vertical, cjkClassOf(c));
363
+ return {
364
+ kind: 'text',
365
+ text,
366
+ width,
367
+ graphemes: g.length,
368
+ first: cls,
369
+ last: cjkClassOf(g[g.length - 1] ?? ''),
370
+ firstCjk: isCjkGrapheme(g[0] ?? ''),
371
+ lastCjk: isCjkGrapheme(g[g.length - 1] ?? ''),
372
+ style,
373
+ at: 0,
374
+ };
375
+ };
376
+ return [...(note.open ? [bracket(note.open)] : []), ...inner, ...(note.close ? [bracket(note.close)] : [])];
377
+ }
378
+ /**
379
+ * The units of a paragraph's spans, in order. `letterSpacingPx` (the
380
+ * block's tracking) is measured into every width, per grapheme. In
381
+ * `vertical` text a CJK character advances by its cell (`cellAdvance`).
382
+ */
383
+ function buildUnits(spans, fonts, letterSpacingPx, vertical = false, route = false) {
384
+ const units = [];
385
+ const track = (n) => (letterSpacingPx === 0 ? 0 : letterSpacingPx * n);
386
+ let zwsp = false;
387
+ // A word joiner (U+2060) before the next unit: no break there, in one
388
+ // span or across two (`**①**⁠文`). It takes no room and is left out.
389
+ let wj = false;
390
+ const glue = () => {
391
+ const g = wj;
392
+ wj = false;
393
+ return g ? { glueBefore: true } : {};
394
+ };
395
+ // Which units open a span and hold all of it (the candidates for stacked
396
+ // scripts), by index.
397
+ const wholeSpan = new Set();
398
+ for (let si = 0; si < spans.length; si++) {
399
+ const span = spans[si];
400
+ // A ruby base: one unit, sized with its reading once its neighbours
401
+ // are known (`sizeRubies`).
402
+ if (span.ruby && span.text.length > 0) {
403
+ units.push({ ...rubyBaseUnit(span, fonts, letterSpacingPx, vertical), ...(zwsp ? { zwspBefore: true } : {}), ...glue() });
404
+ zwsp = false;
405
+ continue;
406
+ }
407
+ if (vertical && (span.combineUpright || span.orientation) && span.text.length > 0 && !setsObject(span)) {
408
+ const first = units.length;
409
+ orientedUnits(span, styleOf(span, fonts, vertical), letterSpacingPx, zwsp, units);
410
+ if (units.length > first)
411
+ Object.assign(units[first], glue());
412
+ zwsp = false;
413
+ continue;
414
+ }
415
+ const atomic = atomicSpanToken(span, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic, letterSpacingPx);
416
+ if (atomic) {
417
+ const style = styleOf(span, fonts, vertical);
418
+ const graphemes = graphemesOf(atomic.text);
419
+ const object = atomic.chip !== undefined || atomic.mathRender !== undefined || atomic.swatch !== undefined;
420
+ const firstG = graphemes[0] ?? '';
421
+ const lastG = graphemes[graphemes.length - 1] ?? '';
422
+ units.push({
423
+ kind: 'atomic',
424
+ text: atomic.text,
425
+ width: atomic.width,
426
+ graphemes: graphemes.length,
427
+ first: object ? 'ideograph' : cjkClassOf(firstG),
428
+ last: object ? 'ideograph' : cjkClassOf(lastG),
429
+ firstCjk: object || isCjkGrapheme(firstG),
430
+ lastCjk: object || isCjkGrapheme(lastG),
431
+ style,
432
+ at: 0,
433
+ token: atomic,
434
+ ...(atomic.refResourceId !== undefined || atomic.footnoteId !== undefined ? { glueBefore: true } : {}),
435
+ ...(zwsp ? { zwspBefore: true } : {}),
436
+ ...glue(),
437
+ });
438
+ zwsp = false;
439
+ continue;
440
+ }
441
+ if (span.text.length === 0)
442
+ continue;
443
+ const style = styleOf(span, fonts, vertical);
444
+ const graphemes = graphemesOf(span.text);
445
+ const spanFirst = units.length;
446
+ let run = [];
447
+ let runAt = 0;
448
+ let runLink;
449
+ let space = '';
450
+ let spaceAt = 0;
451
+ // A lone mark that may pair with the next one (—, …), by unit index.
452
+ let pairOpen = -1;
453
+ let at = 0;
454
+ // The link a character at `offset` of the span is part of: its span and
455
+ // range, so two links to one target stay apart.
456
+ const links = span.links;
457
+ const linkAt = (offset) => {
458
+ if (!links || links.length === 0)
459
+ return undefined;
460
+ for (let li = 0; li < links.length; li++) {
461
+ const l = links[li];
462
+ if (offset >= l.start && offset < l.end)
463
+ return `${si}:${li}`;
464
+ }
465
+ return undefined;
466
+ };
467
+ const push = (u) => {
468
+ units.push({
469
+ ...u,
470
+ style,
471
+ ...(zwsp ? { zwspBefore: true } : {}),
472
+ ...(style.script && units.length === spanFirst ? { glueBefore: true } : {}),
473
+ ...glue(),
474
+ });
475
+ zwsp = false;
476
+ };
477
+ const flushRun = () => {
478
+ if (run.length === 0)
479
+ return;
480
+ const text = run.join('');
481
+ const firstG = run[0];
482
+ const lastG = run[run.length - 1];
483
+ // Vertical text: a short number at either end stands in one upright
484
+ // cell (`cjk.uprightDigits`), and the run is Chinese on that side;
485
+ // tracking follows the cell once.
486
+ const cells = vertical ? numberCellEdges(run) : undefined;
487
+ push({
488
+ kind: 'text',
489
+ text,
490
+ width: textWidth(text, style.font, style.smallCaps) + track(vertical && letterSpacingPx !== 0 ? verticalTrackCount(text) : run.length),
491
+ graphemes: run.length,
492
+ first: cells?.start ? 'ideograph' : cjkClassOf(firstG),
493
+ last: cells?.end ? 'ideograph' : cjkClassOf(lastG),
494
+ firstCjk: cells?.start === true,
495
+ lastCjk: cells?.end === true,
496
+ at: runAt,
497
+ run: true,
498
+ ...(cells?.start ? { cellStart: true } : {}),
499
+ ...(cells?.end ? { cellEnd: true } : {}),
500
+ ...(URL_LIKE_RE.test(text) ? { url: true } : {}),
501
+ ...(runLink !== undefined ? { link: runLink } : {}),
502
+ });
503
+ run = [];
504
+ pairOpen = -1;
505
+ };
506
+ const flushSpace = () => {
507
+ if (space === '')
508
+ return;
509
+ units.push({
510
+ kind: 'space',
511
+ text: space,
512
+ width: measureTextWidth(space, style.font) + track(graphemeCount(space)),
513
+ graphemes: graphemeCount(space),
514
+ first: 'western',
515
+ last: 'western',
516
+ firstCjk: false,
517
+ lastCjk: false,
518
+ style,
519
+ at: spaceAt,
520
+ });
521
+ space = '';
522
+ pairOpen = -1;
523
+ };
524
+ for (let i = 0; i < graphemes.length; i++) {
525
+ const g = graphemes[i];
526
+ const gAt = at;
527
+ at += g.length;
528
+ if (isBreakingSpace(g[0])) {
529
+ flushRun();
530
+ wj = false;
531
+ if (space === '')
532
+ spaceAt = gAt;
533
+ space += g;
534
+ continue;
535
+ }
536
+ flushSpace();
537
+ if (g === '\u200B') {
538
+ flushRun();
539
+ zwsp = true;
540
+ pairOpen = -1;
541
+ continue;
542
+ }
543
+ if (g === '\u2060') {
544
+ flushRun();
545
+ wj = true;
546
+ pairOpen = -1;
547
+ continue;
548
+ }
549
+ // A soft hyphen: the composer breaks Western words only when they are
550
+ // wider than the line, so it is left out, and the word stays whole.
551
+ if (g === '\u00AD')
552
+ continue;
553
+ const link = linkAt(gAt);
554
+ if (!isCjkHere(g, graphemes[i - 1], graphemes[i + 1])) {
555
+ // A link that starts or ends inside a run cuts it: the two parts
556
+ // still never part (neither is CJK) and take no space between them.
557
+ if (run.length > 0 && link !== runLink)
558
+ flushRun();
559
+ if (run.length === 0) {
560
+ runAt = gAt;
561
+ runLink = link;
562
+ }
563
+ run.push(g);
564
+ continue;
565
+ }
566
+ flushRun();
567
+ // —— and …… are one unit of two ems; a third mark opens another.
568
+ if (pairOpen >= 0 && units[pairOpen].text === g && !zwsp && units[pairOpen].link === link) {
569
+ const u = units[pairOpen];
570
+ u.text += g;
571
+ u.width += cellAdvance(g, style.font, vertical, u.first) + track(1);
572
+ u.graphemes = 2;
573
+ u.first = u.last = g === '\u2026' || g === '\u22EF' ? 'ellipsis' : 'dash';
574
+ pairOpen = -1;
575
+ continue;
576
+ }
577
+ let cls = cjkClassOf(g);
578
+ // A single em dash (or horizontal bar) joins two words as a connector
579
+ // (clreq §6.1.1): never at a line start.
580
+ if (g === '\u2014' || g === '\u2015')
581
+ cls = 'connector';
582
+ push({
583
+ kind: 'text',
584
+ text: g,
585
+ width: (style.smallCaps && !vertical ? textWidth(g, style.font, true) : cellAdvance(g, style.font, vertical, cls)) + track(1),
586
+ graphemes: 1,
587
+ first: cls,
588
+ last: cls,
589
+ firstCjk: true,
590
+ lastCjk: true,
591
+ at: gAt,
592
+ ...(link !== undefined ? { link } : {}),
593
+ });
594
+ pairOpen = pairable(g) ? units.length - 1 : -1;
595
+ }
596
+ flushRun();
597
+ flushSpace();
598
+ if (units.length === spanFirst + 1 && units[spanFirst].kind === 'text')
599
+ wholeSpan.add(spanFirst);
600
+ }
601
+ // A subscript and a superscript that touch are set one over the other
602
+ // (EF-80): the first advances nothing, the second the pair's advance, and
603
+ // the two never part.
604
+ if (units.some((u) => u.style.script)) {
605
+ const scripts = units.map((u, i) => (wholeSpan.has(i) && u.style.script && !u.style.smallCaps ? u.style.script : undefined));
606
+ for (const i of stackedScriptPairs(scripts)) {
607
+ const a = units[i];
608
+ const b = units[i + 1];
609
+ const sub = a.style.script === 'sub' ? a : b;
610
+ sub.style = {
611
+ ...sub.style,
612
+ key: `${sub.style.key}|stacked`,
613
+ baselineShift: scriptMetrics(pickSpanFont(sub.style.bold, sub.style.italic, fonts.normal, fonts.bold, fonts.italic, fonts.boldItalic), 'sub', true).baselineShift,
614
+ };
615
+ b.width = Math.max(a.width, b.width);
616
+ a.width = 0;
617
+ a.stacked = 'first';
618
+ b.stacked = 'second';
619
+ b.glueBefore = true;
620
+ }
621
+ }
622
+ if (route)
623
+ routeSharedMarks(units, letterSpacingPx, vertical);
624
+ return units;
625
+ }
626
+ /** The marks Latin text shares with Chinese (East Asian Width ambiguous):
627
+ * quotes, the ellipsis, the em dash and horizontal bar, interpuncts. */
628
+ const SHARED_MARKS = new Set(['\u201C', '\u201D', '\u2018', '\u2019', '\u2026', '\u22EF', '\u2014', '\u2015', '\u00B7', '\u2027']);
629
+ /** Whether a unit is one shared mark, or a pair of them (—— ……). */
630
+ function isSharedMarkUnit(u) {
631
+ if (u.kind !== 'text' || u.run || u.stacked || u.style.script || u.style.smallCaps)
632
+ return false;
633
+ if (u.graphemes === 1)
634
+ return SHARED_MARKS.has(u.text);
635
+ return u.graphemes === 2 && SHARED_MARKS.has(u.text[0]) && u.text[0] === u.text[1];
636
+ }
637
+ /** The script a unit sets for the shared marks next to it: Chinese (a CJK
638
+ * character or mark), Western (a Latin run or a word space), or none for
639
+ * one that is transparent to them (another shared mark, an inline box). */
640
+ function unitScript(v) {
641
+ if (v.kind === 'space')
642
+ return 'western';
643
+ if (v.kind === 'atomic' || isSharedMarkUnit(v))
644
+ return undefined;
645
+ return !v.run && v.firstCjk ? 'cjk' : 'western';
646
+ }
647
+ /**
648
+ * The marks Latin text shares with Chinese (“ ” ‘ ’ … — ·, East Asian
649
+ * Width ambiguous) take the box of a Chinese mark when they stand in
650
+ * Chinese text (clreq §3.1; research note on context routing): a text on
651
+ * either side is Chinese, or none is Western. A font whose glyphs for them
652
+ * are proportional (LXGW WenKai sets “ ” at 0.35 em, Noto Serif SC the em
653
+ * dash at 0.89 em and · at a third) would otherwise crowd them against the
654
+ * characters they belong to. Each grapheme advances one em (with the
655
+ * block's tracking) whatever its font's advance, and its glyph sits where
656
+ * a Chinese font puts it: an opening quote at the end of its box, a
657
+ * closing one at its start, an interpunct, an ellipsis and a single dash
658
+ * centred; an ellipsis pair (……) is set as the font sets the two
659
+ * together and centred in its two ems, so its six dots keep one spacing,
660
+ * and a 破折号 (——) is stretched into one rule over its two ems
661
+ * (`dashRule`). The composition then adjusts the box as
662
+ * it does any mark's (`cjk.punctuationWidth`). A glyph one em wide already changes
663
+ * nothing. Next to Western text on both sides (`He said “yes”`) they keep
664
+ * their own advance, and so do they in Japanese and Korean text (the
665
+ * composer calls this for Chinese text only, `routesSharedMarks`).
666
+ *
667
+ * Down a vertical line (`vertical`) every mark stands in a cell of its own
668
+ * already; only the 破折号 is set here: the font's vertical form of a dash
669
+ * leaves blank at both ends of its cell (Noto CJK's, 0.07 em), so the pair
670
+ * would print as two strokes. Its dashes are stretched into one rule as in
671
+ * horizontal text, and the renderers paint them turned with the page's
672
+ * frame (sideways), the stretch running down the column
673
+ * (`VDTLineSegment.inkScale`).
674
+ */
675
+ function routeSharedMarks(units, letterSpacingPx, vertical = false) {
676
+ if (!units.some(isSharedMarkUnit))
677
+ return;
678
+ // The script of the nearest unit on each side that is not transparent
679
+ // (`unitScript`), past runs of shared marks and inline boxes: two
680
+ // linear passes, so a long run of marks costs no more than its length.
681
+ const n = units.length;
682
+ const before = new Array(n);
683
+ const after = new Array(n);
684
+ let seen;
685
+ for (let k = 0; k < n; k++) {
686
+ before[k] = seen;
687
+ seen = unitScript(units[k]) ?? seen;
688
+ }
689
+ seen = undefined;
690
+ for (let k = n - 1; k >= 0; k--) {
691
+ after[k] = seen;
692
+ seen = unitScript(units[k]) ?? seen;
693
+ }
694
+ for (let k = 0; k < n; k++) {
695
+ const u = units[k];
696
+ if (!isSharedMarkUnit(u))
697
+ continue;
698
+ const dash = u.graphemes === 2 && (u.text[0] === '\u2014' || u.text[0] === '\u2015');
699
+ // Vertical text: a 破折号 only, and not one whose orientation the
700
+ // author set (`:upright[…]`, `:sideways[…]`).
701
+ if (vertical && (!dash || u.orient))
702
+ continue;
703
+ const b = before[k];
704
+ const a = after[k];
705
+ if (b !== 'cjk' && a !== 'cjk' && (b !== undefined || a !== undefined))
706
+ continue;
707
+ const em = emOfFont(u.style.font);
708
+ const font = u.style.font;
709
+ let place;
710
+ const rule = dash ? dashRule(u.text[0], font, em, em + letterSpacingPx, vertical) : undefined;
711
+ if (rule) {
712
+ place = rule.place;
713
+ if (rule.scale !== undefined)
714
+ u.scale = rule.scale;
715
+ if (rule.shift !== undefined)
716
+ u.shift = rule.shift;
717
+ if (vertical) {
718
+ // The cells keep their length down the line.
719
+ u.place = place;
720
+ continue;
721
+ }
722
+ }
723
+ else if (vertical) {
724
+ // No ink metrics: each dash in its vertical form, as it was.
725
+ continue;
726
+ }
727
+ else if (u.graphemes === 2) {
728
+ // ……, or —— when the measurer gives no ink metrics: the pair as the
729
+ // font sets it (a face may kern the dashes into one line), centred
730
+ // in its two ems.
731
+ const g = u.text[0];
732
+ const adv = measureTextWidth(g, font);
733
+ const pair = measureTextWidth(u.text, font);
734
+ if (Math.abs(adv - em) < 1e-6 && Math.abs(pair - 2 * em) < 1e-6)
735
+ continue;
736
+ const start = em - pair / 2;
737
+ place = [start, start + pair - adv - em];
738
+ }
739
+ else {
740
+ const adv = measureTextWidth(u.text, font);
741
+ if (Math.abs(adv - em) < 1e-6)
742
+ continue;
743
+ const slack = em - adv;
744
+ const cls = cjkClassOf(u.text);
745
+ place = [cls === 'opening' ? slack : cls === 'closing' ? 0 : slack / 2];
746
+ }
747
+ u.width = u.graphemes * (em + letterSpacingPx);
748
+ u.place = place;
749
+ }
750
+ }
751
+ /** How far past the join each dash of a 破折号 runs, em: the two strokes
752
+ * overlap, so no seam shows between them at any resolution. */
753
+ const DASH_JOIN_EM = 0.02;
754
+ /**
755
+ * A 破折号 (——) in Chinese text is one unbroken rule two ems long, on the
756
+ * characters' centre line (GB/T 15834). Faces whose em dash is a
757
+ * proportional Latin stroke (Noto Serif SC: 0.89 em advance, ink from 0.04
758
+ * to 0.85 em, at the height of a Latin dash) print the pair as a short
759
+ * rule with blank on both sides, and low. Each dash is stretched over its
760
+ * cell (`cell`, px: an em and the block's tracking): the rule starts and
761
+ * ends the face's own side bearing inside the pair's two cells (at most a
762
+ * tenth of an em) and each stroke runs `DASH_JOIN_EM` past the join. Its
763
+ * glyphs move to the centre of the ideographic em box
764
+ * (`measureCentralBaseline`). Undefined when the measurer gives no ink
765
+ * metrics, or when the dash already fills its em (a face whose two dashes
766
+ * join by themselves). Down a vertical line (`vertical`) the scale is set
767
+ * even when it is 1: it tells the renderers to paint the dash turned.
768
+ */
769
+ function dashRule(g, font, em, cell, vertical = false) {
770
+ const ink = measureInkExtent(g, font);
771
+ // No ink metrics, or a dash already drawn edge to edge across its em.
772
+ if (!ink || (ink.start <= 0.01 * em && ink.end >= cell - 0.01 * em))
773
+ return undefined;
774
+ const side = Math.min(Math.max(ink.start, 0), 0.1 * em);
775
+ const join = DASH_JOIN_EM * em;
776
+ const scale = (cell - side + join) / (ink.end - ink.start);
777
+ const place = [side - ink.start * scale, -join - ink.start * scale];
778
+ const box = measureInkBox(g, font);
779
+ const shift = box ? (box.ascent - box.descent) / 2 - measureCentralBaseline(fontFamilyOf(font)) * em : 0;
780
+ return {
781
+ place,
782
+ ...(vertical || Math.abs(scale - 1) > 1e-3 ? { scale } : {}),
783
+ ...(Math.abs(shift) > 0.01 * em ? { shift } : {}),
784
+ };
785
+ }
786
+ /** The em (px) of a font shorthand: its size. */
787
+ function emOfFont(font) {
788
+ const m = FONT_SIZE_RE.exec(font);
789
+ return m ? parseFloat(m[1]) : 16;
790
+ }
791
+ /** Whether a unit is a single CJK mark (a bracket, a pause, stop or
792
+ * interpunct mark, a dash or an ellipsis): two of them in a row never
793
+ * hang. */
794
+ function isMarkUnit(u) {
795
+ if (!u || u.kind !== 'text' || u.run || !u.firstCjk)
796
+ return false;
797
+ switch (u.first) {
798
+ case 'opening':
799
+ case 'closing':
800
+ case 'pause':
801
+ case 'stop':
802
+ case 'interpunct':
803
+ case 'dash':
804
+ case 'ellipsis':
805
+ return true;
806
+ default:
807
+ return false;
808
+ }
809
+ }
810
+ /**
811
+ * The punctuation widths and Han–Latin spaces of a paragraph's units, as
812
+ * the composition sets them (see `cjkPunctuation.ts`): each full-width mark
813
+ * takes the width its style gives it (`Unit.punct`), two marks that meet
814
+ * give up the blank between them (`compressAdjacent`), and a space unit
815
+ * (`Unit.auto`) goes between each Han character and a Latin letter or digit
816
+ * it touches — the space the author typed there turns into one. The line
817
+ * edges (and the reduction a line makes to take one more character) are
818
+ * the breaker's and the line's business. A plain composition changes
819
+ * nothing but the mainland interpunct, half an em under every style
820
+ * (`punctuationBox`), as its cell is in vertical text.
821
+ */
822
+ function prepareUnits(units, c, letterSpacingPx) {
823
+ const plain = isPlainComposition(c);
824
+ // Down the line the mainland interpunct's cell is half an em already.
825
+ if (plain && (c.region !== 'mainland' || c.vertical))
826
+ return units;
827
+ const full = new Map();
828
+ 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)
830
+ continue;
831
+ if (plain && u.first !== 'interpunct')
832
+ continue;
833
+ const box = punctuationBox(u.text, u.first, u.width - letterSpacingPx, emOfFont(u.style.font), c);
834
+ if (!box)
835
+ continue;
836
+ full.set(u, u.width);
837
+ u.punct = box;
838
+ u.width -= boxCut(box);
839
+ }
840
+ if (plain)
841
+ return units;
842
+ // Kaiming sets a stop's blank after the closing mark that follows it (。”␣).
843
+ const carry = c.punctuationWidth === 'kaiming';
844
+ if ((c.compressAdjacent || carry) && full.size > 1) {
845
+ for (let k = 1; k < units.length; k++) {
846
+ const a = units[k - 1];
847
+ const b = units[k];
848
+ if (!a.punct || !b.punct || b.zwspBefore)
849
+ continue;
850
+ if (c.compressAdjacent)
851
+ compressPair(a.punct, b.punct);
852
+ if (carry)
853
+ carryStopBlank(a.punct, b.punct);
854
+ a.width = full.get(a) - boxCut(a.punct);
855
+ b.width = full.get(b) - boxCut(b.punct);
856
+ }
857
+ }
858
+ if ((c.latinSpacing.px ?? c.latinSpacing.em ?? 0) <= 0)
859
+ return units;
860
+ 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);
862
+ // A run whose edge is a number set in one upright cell (vertical text)
863
+ // 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));
867
+ const spaceOf = (h) => latinSpacingPx(c, emOfFont(h.style.font));
868
+ for (let k = 0; k < units.length; k++) {
869
+ const u = units[k];
870
+ const prev = out[out.length - 1];
871
+ if (u.kind === 'space' && /^ +$/.test(u.text)) {
872
+ // A space typed between Han and Latin is replaced by the Han–Latin
873
+ // space (CSS `text-autospace: replace`).
874
+ const next = units[k + 1];
875
+ const h = han(prev, 'last') && latin(next, 'first') && !next.glueBefore ? prev
876
+ : latin(prev, 'last') && han(next, 'first') && !next.glueBefore ? next : undefined;
877
+ if (h) {
878
+ out.push({ ...u, width: spaceOf(h), auto: true });
879
+ continue;
880
+ }
881
+ }
882
+ else if (prev && !u.glueBefore && ((han(prev, 'last') && latin(u, 'first')) || (latin(prev, 'last') && han(u, 'first')))) {
883
+ const h = han(prev, 'last') ? prev : u;
884
+ out.push({
885
+ kind: 'space',
886
+ text: '',
887
+ width: spaceOf(h),
888
+ graphemes: 0,
889
+ first: 'western',
890
+ last: 'western',
891
+ firstCjk: false,
892
+ lastCjk: false,
893
+ style: h.style,
894
+ at: u.at,
895
+ auto: true,
896
+ });
897
+ }
898
+ out.push(u);
899
+ }
900
+ return out;
901
+ }
902
+ const isDigitCode = (c) => (c >= 0x30 && c <= 0x39) || isFullwidthDigit(c);
903
+ /** Whether a number keeps to the sign or unit next to it (`50 %`, `50%`,
904
+ * `¥599`, `−3 ℃`): a unit ending in a digit before a unit sign, or a
905
+ * currency or plus-minus sign before a digit. At every level. */
906
+ function numberGlue(a, b) {
907
+ if (a.kind !== 'text' || b.kind !== 'text')
908
+ return false;
909
+ if (b.first === 'postfix' && isDigitCode(a.text.charCodeAt(a.text.length - 1)))
910
+ return true;
911
+ return a.last === 'prefix' && isDigitCode(b.text.charCodeAt(0));
912
+ }
913
+ /** A fullwidth character unit (one grapheme, not a Western run) whose code
914
+ * point passes `test`. */
915
+ function fullwidthUnit(u, test) {
916
+ return u !== undefined && u.kind === 'text' && !u.run && u.graphemes === 1 && test(u.text.charCodeAt(0));
917
+ }
918
+ /** Marks that join fullwidth digits into one number: a decimal point, a
919
+ * thousands separator, the colon of a time, the solidus of a fraction
920
+ * (3.14, 12:30, 1/2). */
921
+ const isFullwidthNumberJoin = (cp) => cp === 0xFF0E || cp === 0xFF0C || cp === 0xFF1A || cp === 0xFF0F;
922
+ /** Whether units `k - 1` and `k` belong to one fullwidth number or word
923
+ * (123, ABC, 3.14): each character is a unit of its own, spread
924
+ * like Han when the line is justified, but the line never breaks inside. */
925
+ function fullwidthGlue(units, k) {
926
+ const a = units[k - 1];
927
+ const b = units[k];
928
+ if (fullwidthUnit(a, isFullwidthAlnum) && fullwidthUnit(b, isFullwidthAlnum))
929
+ return true;
930
+ if (fullwidthUnit(a, isFullwidthDigit) && fullwidthUnit(b, isFullwidthNumberJoin) && fullwidthUnit(units[k + 1], isFullwidthDigit))
931
+ return true;
932
+ return fullwidthUnit(a, isFullwidthNumberJoin) && fullwidthUnit(b, isFullwidthDigit) && fullwidthUnit(units[k - 2], isFullwidthDigit);
933
+ }
934
+ /** Whether a line may break before each unit (index 0 is never a break).
935
+ * A word space is a break unless the unit after it may not open a line,
936
+ * 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. */
938
+ function breakOpportunities(units, level) {
939
+ const out = new Uint8Array(units.length);
940
+ // The last unit before `k` that is not a space.
941
+ let ink = -1;
942
+ for (let k = 1; k < units.length; k++) {
943
+ const a = units[k - 1];
944
+ const b = units[k];
945
+ if (a.kind !== 'space')
946
+ ink = k - 1;
947
+ if (b.kind === 'space')
948
+ continue;
949
+ // A line never opens with a warichu note's word space.
950
+ if (b.noteSpace)
951
+ continue;
952
+ if (b.zwspBefore) {
953
+ out[k] = 1;
954
+ continue;
955
+ }
956
+ if (a.kind === 'space') {
957
+ const p = ink >= 0 ? units[ink] : undefined;
958
+ if (!p || (!numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level)))
959
+ out[k] = 1;
960
+ continue;
961
+ }
962
+ if (a.noteSpace) {
963
+ // A word space inside a warichu note: the note's words part there.
964
+ let q = k - 1;
965
+ while (q > 0 && units[q].noteSpace)
966
+ q--;
967
+ const p = units[q];
968
+ if (!p.noteSpace && !numberGlue(p, b) && !isLineStartProhibited(b.first, level) && !isLineEndProhibited(p.last, level))
969
+ out[k] = 1;
970
+ continue;
971
+ }
972
+ if (b.glueBefore)
973
+ continue;
974
+ if (numberGlue(a, b) || fullwidthGlue(units, k))
975
+ continue;
976
+ if (cjkBreakAllowed(a.last, a.lastCjk, b.first, b.firstCjk, level))
977
+ out[k] = 1;
978
+ }
979
+ return out;
980
+ }
981
+ /** Whether justification may add space between two units that touch on a
982
+ * line: between characters, never inside a Western run or a 2-em mark
983
+ * (those are single units), never between two Western runs, next to a
984
+ * 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) {
987
+ if (a.kind !== 'text' || b.kind === 'space')
988
+ return false;
989
+ if (b.glueBefore || a.stacked)
990
+ return false;
991
+ if (a.last === 'connector' || a.last === 'solidus' || b.first === 'connector' || b.first === 'solidus')
992
+ return false;
993
+ if (!a.lastCjk && !b.firstCjk)
994
+ return false;
995
+ if (a.last === b.first && (a.last === 'dash' || a.last === 'ellipsis'))
996
+ return false;
997
+ return true;
998
+ }
999
+ function lineFitOf(c) {
1000
+ if (isPlainComposition(c))
1001
+ return undefined;
1002
+ return {
1003
+ lead: (u) => (u.punct ? lineEdgeCut(u.punct, c, true, false) : 0),
1004
+ tail: (u) => (u.punct ? lineEdgeCut(u.punct, c, false, true) : 0),
1005
+ give(u, atStart, atEnd) {
1006
+ if (u.kind === 'space') {
1007
+ const em = emOfFont(u.style.font);
1008
+ return Math.max(0, u.width - (u.auto ? em / 8 : em / 4));
1009
+ }
1010
+ if (!u.punct)
1011
+ return 0;
1012
+ if (!atStart && !atEnd)
1013
+ return punctuationShrink(u.punct, c);
1014
+ const box = { ...u.punct };
1015
+ applyLineEdges(box, c, atStart, atEnd);
1016
+ return punctuationShrink(box, c);
1017
+ },
1018
+ edge: (u, start, end) => (u.punct ? applyLineEdges(u.punct, c, start, end) : 0),
1019
+ hangs(units, k, unitAt) {
1020
+ const u = unitAt(k);
1021
+ if (u.kind !== 'text' || u.run || u.graphemes !== 1 || u.note || u.ruby || !mayHang(u.text, u.first, c))
1022
+ return false;
1023
+ if (k > 0 && isMarkUnit(unitAt(k - 1)))
1024
+ return false;
1025
+ return k + 1 >= units.length || !isMarkUnit(units[k + 1]);
1026
+ },
1027
+ hanging: c.hangingPunctuation,
1028
+ };
1029
+ }
1030
+ /** The last unit that must share a line with unit `k` when `k` ends one:
1031
+ * the units after it up to the next break (a closing quote after a
1032
+ * full stop). -1 when that is more than a few units away or crosses a
1033
+ * space the line may not break after. */
1034
+ function groupEnd(units, breaks, k) {
1035
+ const n = units.length;
1036
+ for (let q = k; q < k + 6; q++) {
1037
+ if (q + 1 >= n)
1038
+ return q;
1039
+ if (units[q + 1].kind === 'space') {
1040
+ let r = q + 1;
1041
+ while (r < n && units[r].kind === 'space')
1042
+ r++;
1043
+ return r >= n || breaks[r] ? q : -1;
1044
+ }
1045
+ if (breaks[q + 1])
1046
+ return q;
1047
+ }
1048
+ return -1;
1049
+ }
1050
+ /** The advance of the first `idx` UTF-16 units of a Western run, the
1051
+ * block's tracking included. */
1052
+ function prefixWidth(u, idx, letterSpacingPx) {
1053
+ const head = u.text.slice(0, idx);
1054
+ return textWidth(head, u.style.font, u.style.smallCaps) + (letterSpacingPx === 0 ? 0 : letterSpacingPx * graphemeCount(head));
1055
+ }
1056
+ /** Characters of a run kept past the first that does not fit when it is
1057
+ * cut: enough for the dictionary and the joints of a web address to read
1058
+ * the text around every cut that fits as they read the whole run. */
1059
+ const CUT_CONTEXT = 32;
1060
+ /** The shortest prefix of a Western run (in UTF-16 units) wider than
1061
+ * `room`, or its length when all of it fits: found by doubling, then
1062
+ * halving, so a run many lines long costs what one line of it does. */
1063
+ function overflowAt(u, room, letterSpacingPx) {
1064
+ const len = u.text.length;
1065
+ let fit = 0;
1066
+ let over = len;
1067
+ for (let step = 1; fit + step < len; step *= 2) {
1068
+ if (prefixWidth(u, fit + step, letterSpacingPx) > room + FIT_EPS) {
1069
+ over = fit + step;
1070
+ break;
1071
+ }
1072
+ fit += step;
1073
+ }
1074
+ if (over === len && u.width <= room + FIT_EPS)
1075
+ return len;
1076
+ while (over - fit > 1) {
1077
+ const mid = (fit + over) >> 1;
1078
+ if (prefixWidth(u, mid, letterSpacingPx) > room + FIT_EPS)
1079
+ over = mid;
1080
+ else
1081
+ fit = mid;
1082
+ }
1083
+ return over;
1084
+ }
1085
+ /** The rest of a run after its first `from` UTF-16 units (`cutWidth` px). */
1086
+ function runTail(u, from, cutWidth) {
1087
+ const tail = u.text.slice(from);
1088
+ return {
1089
+ ...u,
1090
+ text: tail,
1091
+ width: u.width - cutWidth,
1092
+ graphemes: u.graphemes - graphemeCount(u.text.slice(0, from)),
1093
+ first: cjkClassOf(tail),
1094
+ at: u.at + from,
1095
+ glueBefore: false,
1096
+ zwspBefore: false,
1097
+ };
1098
+ }
1099
+ /** Cut a web address at its last joint (after a slash, before a dot…)
1100
+ * whose head fits `room`: nothing is added at the break. Only the joints
1101
+ * before the first character that does not fit are tried. */
1102
+ function cutAtJoint(u, room, letterSpacingPx) {
1103
+ const over = overflowAt(u, room, letterSpacingPx);
1104
+ const joints = urlBreakIndices(u.text.slice(0, Math.min(u.text.length, over + CUT_CONTEXT)));
1105
+ for (let j = joints.length - 1; j >= 0; j--) {
1106
+ const idx = joints[j];
1107
+ if (idx >= over)
1108
+ continue;
1109
+ const head = u.text.slice(0, idx);
1110
+ const w = prefixWidth(u, idx, letterSpacingPx);
1111
+ if (w > room + FIT_EPS)
1112
+ continue;
1113
+ return {
1114
+ head: { ...u, text: head, width: w, graphemes: graphemeCount(head), last: cjkClassOf(lastGrapheme(head)), url: false },
1115
+ tail: runTail(u, idx, w),
1116
+ };
1117
+ }
1118
+ return null;
1119
+ }
1120
+ /** Divide a Western run wider than the whole line (see `emergencySplit`):
1121
+ * at a dictionary syllable with a hyphen, else at the last character that
1122
+ * fits. Only the run up to a little past the first character that does not
1123
+ * fit is handed to the divider, so each line of a long run costs what the
1124
+ * line holds. */
1125
+ function divideRun(u, room, letterSpacingPx) {
1126
+ const end = Math.min(u.text.length, overflowAt(u, room, letterSpacingPx) + CUT_CONTEXT);
1127
+ const text = end === u.text.length ? u.text : u.text.slice(0, end);
1128
+ const width = end === u.text.length ? u.width : prefixWidth(u, end, letterSpacingPx);
1129
+ const token = {
1130
+ text,
1131
+ bold: u.style.bold,
1132
+ italic: u.style.italic,
1133
+ kind: 'text',
1134
+ width,
1135
+ ...(u.style.smallCaps ? { smallCaps: true } : {}),
1136
+ };
1137
+ const split = emergencySplit(token, u.style.font, letterSpacingPx, room);
1138
+ if (!split)
1139
+ return null;
1140
+ const headText = split.head.text;
1141
+ const hyphen = headText.length > 0 && headText.endsWith('-') && !u.text.startsWith(headText);
1142
+ // Where the rest starts (past a no-break space the divider parted at)
1143
+ // and the advance of what comes before it.
1144
+ const from = text.length - split.tail.text.length;
1145
+ return {
1146
+ head: { ...u, text: headText, width: split.head.width, graphemes: graphemeCount(headText), last: cjkClassOf(lastGrapheme(headText)), url: false },
1147
+ tail: runTail(u, from, width - split.tail.width),
1148
+ hyphen,
1149
+ hard: split.hard === true,
1150
+ };
1151
+ }
1152
+ /**
1153
+ * Break the units into lines, first fit with push-out: a line takes units
1154
+ * while they fit its measure less `reserve`; the unit that does not fit
1155
+ * opens the next line when a break before it is allowed, else (when the
1156
+ * composition cannot push it in or hang it) the line ends
1157
+ * at the last allowed break (the characters after it go down with the
1158
+ * unit). A space never overflows: the line ends before it, when the line
1159
+ * may break after it. A unit wider
1160
+ * 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.
1162
+ */
1163
+ function breakUnits(units, breaks, measureOf, reserve, letterSpacingPx, fit, level = getCjkLineBreak(),
1164
+ /** End line `line` before unit `at` (a break), whatever else fits. */
1165
+ stop) {
1166
+ const out = [];
1167
+ const n = units.length;
1168
+ let i = 0;
1169
+ let carried;
1170
+ let carriedAt = -1;
1171
+ const unitAt = (k) => (k === carriedAt && carried ? carried : units[k]);
1172
+ // A warichu note's characters on the line take the advance of their two
1173
+ // rows (`foldWidth`), not their sum: where the note's part on this line
1174
+ // starts, and the line's width before it.
1175
+ let noteFrom = -1;
1176
+ let noteBase = 0;
1177
+ const foldOf = (from, to) => {
1178
+ const widths = [];
1179
+ const startProhibited = [];
1180
+ const endProhibited = [];
1181
+ for (let q = from; q <= to; q++) {
1182
+ const uq = unitAt(q);
1183
+ widths.push(uq.width);
1184
+ startProhibited.push(noteRowStartProhibited(uq, level));
1185
+ endProhibited.push(isLineEndProhibited(uq.last, level));
1186
+ }
1187
+ return foldWidth(widths, splitNote(widths, startProhibited, endProhibited));
1188
+ };
1189
+ for (let li = 0;; li++) {
1190
+ while (i < n && unitAt(i).kind === 'space')
1191
+ i++;
1192
+ if (i >= n)
1193
+ break;
1194
+ const max = measureOf(li) - reserve;
1195
+ let w = 0;
1196
+ let lastBreak = -1;
1197
+ let end = -1;
1198
+ let head;
1199
+ let tail;
1200
+ let hyphenated = false;
1201
+ let hardHyphen = false;
1202
+ let hang = false;
1203
+ // What the line could give up to take one more character (push-in).
1204
+ let give = 0;
1205
+ for (let k = i; k < n; k++) {
1206
+ if (stop && stop.line === li && k >= stop.at) {
1207
+ end = k;
1208
+ break;
1209
+ }
1210
+ const u = unitAt(k);
1211
+ if (k > i && breaks[k])
1212
+ lastBreak = k;
1213
+ if (u.note) {
1214
+ if (k === i || unitAt(k - 1).note !== u.note) {
1215
+ noteFrom = k;
1216
+ noteBase = w;
1217
+ }
1218
+ const folded = noteBase + foldOf(noteFrom, k);
1219
+ if (folded <= max + FIT_EPS) {
1220
+ w = folded;
1221
+ continue;
1222
+ }
1223
+ // The part no longer folds into the line: the line ends here or at
1224
+ // the last break before, never taking the character at its own
1225
+ // advance (the fold of a part is not monotonic, so every part the
1226
+ // line may end with was one that folded).
1227
+ }
1228
+ 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) {
1230
+ w += u.width - lead;
1231
+ if (fit)
1232
+ give += fit.give(u, k === i, false);
1233
+ continue;
1234
+ }
1235
+ if (fit && k > i && u.kind !== 'space' && !u.note) {
1236
+ // A pause or stop mark that does not fit hangs past the measure:
1237
+ // at once under 'force', after compressing the line failed under
1238
+ // 'allow'. Else, when the unit may not open the next line (no
1239
+ // break before it), the line gives up blank to take it and what
1240
+ // must stay with it (push-in, clreq §6.2.2.3), before it would
1241
+ // push characters down. A unit that may open a line goes down and
1242
+ // the line is spread: compressing marks to take one more
1243
+ // character there would narrow Kaiming's stop marks inside the
1244
+ // line.
1245
+ const canHang = fit.hanging !== 'none' && groupEnd(units, breaks, k) === k && fit.hangs(units, k, unitAt);
1246
+ if (canHang && fit.hanging === 'force') {
1247
+ end = k + 1;
1248
+ hang = true;
1249
+ break;
1250
+ }
1251
+ const m = breaks[k] ? -1 : groupEnd(units, breaks, k);
1252
+ if (m >= k) {
1253
+ let width = w;
1254
+ let room = give;
1255
+ for (let q = k; q <= m; q++) {
1256
+ const uq = unitAt(q);
1257
+ const t = q === m ? fit.tail(uq) : 0;
1258
+ width += uq.width - t;
1259
+ room += fit.give(uq, false, q === m);
1260
+ }
1261
+ if (width - room <= max + FIT_EPS) {
1262
+ end = m + 1;
1263
+ break;
1264
+ }
1265
+ }
1266
+ if (canHang) {
1267
+ end = k + 1;
1268
+ hang = true;
1269
+ break;
1270
+ }
1271
+ }
1272
+ if (u.kind === 'space') {
1273
+ // The line ends before the space when it may break after it; else
1274
+ // it gives up characters to the next line, as for any other unit.
1275
+ let m = k + 1;
1276
+ while (m < n && units[m].kind === 'space')
1277
+ m++;
1278
+ if (m >= n || breaks[m]) {
1279
+ end = k;
1280
+ break;
1281
+ }
1282
+ }
1283
+ if (u.url) {
1284
+ const cut = cutAtJoint(u, max - w, letterSpacingPx);
1285
+ if (cut) {
1286
+ end = k;
1287
+ head = cut.head;
1288
+ tail = cut.tail;
1289
+ hyphenated = true;
1290
+ hardHyphen = false;
1291
+ break;
1292
+ }
1293
+ }
1294
+ if (k > i && breaks[k]) {
1295
+ end = k;
1296
+ break;
1297
+ }
1298
+ if (lastBreak > i) {
1299
+ end = lastBreak;
1300
+ break;
1301
+ }
1302
+ if (k === i) {
1303
+ if (u.run) {
1304
+ const split = divideRun(u, max, letterSpacingPx);
1305
+ if (split) {
1306
+ end = k;
1307
+ head = split.head;
1308
+ tail = split.tail;
1309
+ hyphenated = true;
1310
+ hardHyphen = split.hard;
1311
+ break;
1312
+ }
1313
+ }
1314
+ end = k + 1;
1315
+ break;
1316
+ }
1317
+ // Nothing on the line may end it: break before the unit anyway.
1318
+ end = k;
1319
+ break;
1320
+ }
1321
+ if (end < 0)
1322
+ end = n;
1323
+ out.push({
1324
+ start: i,
1325
+ end,
1326
+ ...(carriedAt === i && carried ? { first: carried } : {}),
1327
+ ...(head ? { head } : {}),
1328
+ hyphenated,
1329
+ ...(hardHyphen ? { hardHyphen: true } : {}),
1330
+ ...(hang ? { hang: true } : {}),
1331
+ });
1332
+ if (tail) {
1333
+ carried = tail;
1334
+ carriedAt = end;
1335
+ }
1336
+ else if (carriedAt < end) {
1337
+ carried = undefined;
1338
+ carriedAt = -1;
1339
+ }
1340
+ i = end;
1341
+ }
1342
+ return out;
1343
+ }
1344
+ /** 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). */
1346
+ function noteRowStartProhibited(u, level) {
1347
+ return u.noteSpace === true || isLineStartProhibited(u.first, level);
1348
+ }
1349
+ /** Replace each run of a warichu note's characters on a line by the part
1350
+ * folded into its two rows (see `cjkAnnotate.ts`). `us` is changed in
1351
+ * place. */
1352
+ function foldNotes(us, level, em) {
1353
+ const out = [];
1354
+ for (let j = 0; j < us.length;) {
1355
+ const u = us[j];
1356
+ if (!u.note) {
1357
+ out.push(u);
1358
+ j++;
1359
+ continue;
1360
+ }
1361
+ let e = j + 1;
1362
+ while (e < us.length && us[e].note === u.note)
1363
+ e++;
1364
+ out.push(noteFragment(us.slice(j, e), level, em));
1365
+ j = e;
1366
+ }
1367
+ us.splice(0, us.length, ...out);
1368
+ }
1369
+ /** A line's part of a warichu note: its characters split into an upper and
1370
+ * a lower row, each painted in runs of one font; the part's advance is the
1371
+ * wider row's. */
1372
+ function noteFragment(part, level, em) {
1373
+ const note = part[0].note;
1374
+ 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)));
1376
+ const noteFont = note.fontString ?? part[0].style.font;
1377
+ const rows = noteRowBaselines(em, fontEm(noteFont));
1378
+ const runs = [];
1379
+ const row = (units, dy) => {
1380
+ let dx = 0;
1381
+ let text = '';
1382
+ let run;
1383
+ for (const u of units) {
1384
+ if (run && run.fontString === u.style.font)
1385
+ run.text += u.text;
1386
+ else {
1387
+ run = { text: u.text, dx, dy, fontString: u.style.font };
1388
+ runs.push(run);
1389
+ }
1390
+ dx += u.width;
1391
+ text += u.text;
1392
+ }
1393
+ return { text, width: dx };
1394
+ };
1395
+ const upper = row(part.slice(0, at), rows.upper);
1396
+ const lower = row(part.slice(at), rows.lower);
1397
+ const first = part[0];
1398
+ const last = part[part.length - 1];
1399
+ let graphemes = 0;
1400
+ for (const u of part)
1401
+ graphemes += u.graphemes;
1402
+ return {
1403
+ kind: 'text',
1404
+ text: upper.text + lower.text,
1405
+ width: Math.max(upper.width, lower.width),
1406
+ graphemes,
1407
+ first: first.first,
1408
+ last: last.last,
1409
+ firstCjk: first.firstCjk,
1410
+ lastCjk: last.lastCjk,
1411
+ style: first.style,
1412
+ at: first.at,
1413
+ warichu: {
1414
+ upper: upper.text,
1415
+ lower: lower.text,
1416
+ fontString: noteFont,
1417
+ upperDy: rows.upper,
1418
+ lowerDy: rows.lower,
1419
+ ...(note.color ? { color: note.color } : {}),
1420
+ runs,
1421
+ },
1422
+ };
1423
+ }
1424
+ /**
1425
+ * A ruby base that opens or closes a line (clreq §5.5.4): base and reading
1426
+ * align to that edge. The base gives up its inset on that side, and the
1427
+ * box keeps only what the reading needs past its other side (less what it
1428
+ * may pass the box by there), never more than it was, so the line the
1429
+ * breaker set never grows. A base alone on its line stays centred.
1430
+ * Units that change are replaced by copies in `us`.
1431
+ */
1432
+ function alignEdgeRubies(us) {
1433
+ if (us.length < 2)
1434
+ return;
1435
+ const align = (j, start) => {
1436
+ const u = us[j];
1437
+ const g = u.ruby?.geometry;
1438
+ if (!g || u.ruby.position === 'right' || g.runs.length === 0)
1439
+ return;
1440
+ const base = u.width - 2 * g.inset;
1441
+ const rt = g.rtWidth;
1442
+ const wide = rt > base;
1443
+ // How far the reading reaches from the edge: its advance, or to the
1444
+ // far side of the base it is centred on.
1445
+ const reach = wide ? rt : (base + rt) / 2;
1446
+ const width = Math.min(u.width, Math.max(base, reach - (start ? g.allowRight : g.allowLeft)));
1447
+ const inset = start ? 0 : width - base;
1448
+ const readingAt = wide ? (start ? 0 : width - rt) : inset + (base - rt) / 2;
1449
+ const shift = readingAt - g.runs[0].dx;
1450
+ if (Math.abs(width - u.width) < 1e-9 && Math.abs(inset - g.inset) < 1e-9 && Math.abs(shift) < 1e-9)
1451
+ return;
1452
+ us[j] = {
1453
+ ...u,
1454
+ width,
1455
+ ruby: { ...u.ruby, geometry: { ...g, width, inset, runs: g.runs.map((r) => ({ ...r, dx: r.dx + shift })) } },
1456
+ };
1457
+ };
1458
+ align(0, true);
1459
+ align(us.length - 1, false);
1460
+ }
1461
+ /** Keep the readings of a line's ruby bases inside the line: a reading
1462
+ * that would pass the line's start or end is moved back to it (clreq
1463
+ * §5.5.4: at a line edge base and ruby align to the edge). */
1464
+ function clampReadings(segments) {
1465
+ if (!segments.some((s) => s.ruby))
1466
+ return;
1467
+ let lineWidth = 0;
1468
+ for (const s of segments)
1469
+ if (!s.hangs)
1470
+ lineWidth += s.width;
1471
+ let x = 0;
1472
+ for (const s of segments) {
1473
+ const ruby = s.ruby;
1474
+ if (ruby && ruby.position !== 'right' && ruby.runs.length > 0) {
1475
+ const lo = ruby.runs[0].dx;
1476
+ const hi = lo + ruby.rtWidth;
1477
+ let shift = 0;
1478
+ if (x + lo < 0)
1479
+ shift = -(x + lo);
1480
+ else if (x + hi > lineWidth + 1e-6)
1481
+ shift = lineWidth - (x + hi);
1482
+ if (shift !== 0)
1483
+ ruby.runs = ruby.runs.map((r) => ({ ...r, dx: r.dx + shift }));
1484
+ }
1485
+ x += s.width;
1486
+ }
1487
+ }
1488
+ /** Share `amount` among `caps` equally, none past its cap: what each
1489
+ * takes. */
1490
+ function spread(amount, caps) {
1491
+ const takes = caps.map(() => 0);
1492
+ const order = caps.map((_, i) => i).sort((a, b) => caps[a] - caps[b]);
1493
+ let rest = amount;
1494
+ for (let o = 0; o < order.length && rest > 1e-12; o++) {
1495
+ const i = order[o];
1496
+ const take = Math.min(caps[i], rest / (order.length - o));
1497
+ takes[i] = take;
1498
+ rest -= take;
1499
+ }
1500
+ return takes;
1501
+ }
1502
+ /**
1503
+ * A line's units as its edges and its measure set them (see
1504
+ * `cjkPunctuation.ts`): the first mark gives up its lead and the last its
1505
+ * tail, each taking back the blank it gave to a mark on the other side of
1506
+ * the break (the two no longer meet); a mark that hangs is taken out and
1507
+ * returned apart; and a line wider than its measure (it took one more
1508
+ * character that may not open a line, clreq §6.2.2.3) gives up
1509
+ * blank in clreq's order — word spaces to a quarter em, interpuncts,
1510
+ * 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.
1514
+ */
1515
+ function fitLine(us, units, range, lastK, isLast, measure, fit) {
1516
+ const copy = (j) => {
1517
+ const u = us[j];
1518
+ const c = { ...u, ...(u.punct ? { punct: { ...u.punct } } : {}) };
1519
+ us[j] = c;
1520
+ return c;
1521
+ };
1522
+ let hang;
1523
+ if (us.length > 1) {
1524
+ const unitAt = (k) => (k === range.start && range.first ? range.first : units[k]);
1525
+ const forced = fit.hanging === 'force' && !isLast && !range.head && fit.hangs(units, lastK, unitAt);
1526
+ if (range.hang || forced) {
1527
+ hang = copy(us.length - 1);
1528
+ us.pop();
1529
+ hang.width -= fit.edge(hang, false, true);
1530
+ while (us.length > 0 && us[us.length - 1].kind === 'space')
1531
+ us.pop();
1532
+ }
1533
+ }
1534
+ if (us.length === 0)
1535
+ return hang;
1536
+ // The marks at the edges: what they gave to a mark across the break
1537
+ // comes back, then the edge trims.
1538
+ if (fit.lead(us[0]) !== 0) {
1539
+ const c = copy(0);
1540
+ c.width -= fit.edge(c, true, false);
1541
+ }
1542
+ if (fit.tail(us[us.length - 1]) !== 0) {
1543
+ const c = copy(us.length - 1);
1544
+ c.width -= fit.edge(c, false, true);
1545
+ }
1546
+ let over = -measure;
1547
+ for (const u of us)
1548
+ over += u.width;
1549
+ if (over <= FIT_EPS)
1550
+ return hang;
1551
+ // 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);
1554
+ for (let step = 1; step <= 6 && over > FIT_EPS; step++) {
1555
+ const members = [];
1556
+ const caps = [];
1557
+ for (let j = 0; j < us.length; j++) {
1558
+ const u = us[j];
1559
+ if (stepOf(u) !== step)
1560
+ continue;
1561
+ // The marks at the edges already gave up their lead and tail.
1562
+ const cap = fit.give(u, false, false);
1563
+ if (cap <= 0)
1564
+ continue;
1565
+ members.push(j);
1566
+ caps.push(cap);
1567
+ }
1568
+ if (members.length === 0)
1569
+ continue;
1570
+ const takes = spread(over, caps);
1571
+ for (let m = 0; m < members.length; m++) {
1572
+ const take = takes[m];
1573
+ if (take <= 0)
1574
+ continue;
1575
+ const c = copy(members[m]);
1576
+ const given = c.punct ? shrinkPunctuation(c.punct, take) : take;
1577
+ c.width -= given;
1578
+ over -= given;
1579
+ }
1580
+ }
1581
+ return hang;
1582
+ }
1583
+ /** Where a unit's glyph (its `grapheme`th) is painted from its segment's
1584
+ * start, px (`VDTLineSegment.inkOffset`): a shared mark's place in its
1585
+ * box, less the blank its box gave up before it. Undefined for a unit
1586
+ * painted at its own advances. */
1587
+ function inkOffsetOf(u, grapheme = 0) {
1588
+ const cutStart = u.punct?.cutStart ?? 0;
1589
+ if (u.place)
1590
+ return u.place[grapheme] - cutStart;
1591
+ if (u.punct && boxCut(u.punct) > 0)
1592
+ return cutStart > 0 ? -cutStart : 0;
1593
+ return undefined;
1594
+ }
1595
+ /** The segments, text and flags of one line (see the module comment for
1596
+ * the spreading). */
1597
+ function composeLine(units, range, li, isLast, ctx) {
1598
+ const us = [];
1599
+ for (let k = range.start; k < range.end; k++)
1600
+ us.push(k === range.start && range.first ? range.first : units[k]);
1601
+ if (range.head)
1602
+ us.push(range.head);
1603
+ let lastK = range.end - 1;
1604
+ while (us.length > 0 && us[us.length - 1].kind === 'space') {
1605
+ us.pop();
1606
+ lastK--;
1607
+ }
1608
+ const measure = ctx.measureOf(li);
1609
+ // A warichu note's characters fold into their two rows (#195).
1610
+ if (us.some((u) => u.note))
1611
+ foldNotes(us, ctx.level, ctx.em);
1612
+ // A ruby base at either edge aligns to it with its reading (#194).
1613
+ alignEdgeRubies(us);
1614
+ const hang = ctx.fit ? fitLine(us, units, range, lastK, isLast, measure, ctx.fit) : undefined;
1615
+ const justify = ctx.textAlign === 'justify' && !isLast;
1616
+ let spaceWidth;
1617
+ let tracking = 0;
1618
+ let loose = false;
1619
+ let spaceRatio;
1620
+ let ragged = false;
1621
+ const stretches = [];
1622
+ if (justify) {
1623
+ let content = 0;
1624
+ let spaces = 0;
1625
+ let natural = 0;
1626
+ let widest = 0;
1627
+ let gaps = 0;
1628
+ // Han–Latin spaces (`cjk.latinSpacing`): they flex apart from word
1629
+ // spaces, and the renderers leave them as set.
1630
+ const autos = [];
1631
+ for (let j = 0; j < us.length; j++) {
1632
+ const u = us[j];
1633
+ content += u.width;
1634
+ if (u.kind === 'space' && u.auto)
1635
+ autos.push(j);
1636
+ else if (u.kind === 'space') {
1637
+ spaces++;
1638
+ natural += u.width;
1639
+ widest = Math.max(widest, u.width);
1640
+ }
1641
+ const s = j < us.length - 1 && gapStretches(u, us[j + 1]);
1642
+ stretches.push(s);
1643
+ if (s)
1644
+ gaps++;
1645
+ }
1646
+ let slack = measure - content;
1647
+ if (slack > FIT_EPS) {
1648
+ if (gaps > 0 || autos.length > 0) {
1649
+ if (spaces > 0) {
1650
+ // Word spaces first, up to half an em each, all set alike.
1651
+ spaceWidth = Math.max(widest, Math.min((natural + slack) / spaces, Math.max(ctx.em / 2, widest)));
1652
+ slack -= spaceWidth * spaces - natural;
1653
+ if (ctx.normalSpace > 0)
1654
+ spaceRatio = spaceWidth / ctx.normalSpace;
1655
+ }
1656
+ if (autos.length > 0 && slack > FIT_EPS) {
1657
+ // Then the Han–Latin spaces, up to half an em each (clreq
1658
+ // §6.2.2.4).
1659
+ const caps = autos.map((j) => Math.max(0, emOfFont(us[j].style.font) / 2 - us[j].width));
1660
+ const takes = spread(slack, caps);
1661
+ autos.forEach((j, m) => {
1662
+ if (takes[m] <= 0)
1663
+ return;
1664
+ us[j] = { ...us[j], width: us[j].width + takes[m] };
1665
+ slack -= takes[m];
1666
+ });
1667
+ }
1668
+ if (slack > FIT_EPS) {
1669
+ // Then every gap between characters, the Han–Latin spaces too.
1670
+ tracking = slack / (gaps + autos.length);
1671
+ if (tracking > ctx.cap + 1e-9) {
1672
+ tracking = ctx.cap;
1673
+ loose = true;
1674
+ }
1675
+ for (const j of autos)
1676
+ us[j] = { ...us[j], width: us[j].width + tracking };
1677
+ }
1678
+ }
1679
+ else if (spaces > 0) {
1680
+ // No gap between characters: the spaces take it all, as in a Latin
1681
+ // line (the renderers stretch them).
1682
+ if (ctx.normalSpace > 0)
1683
+ spaceRatio = (natural + slack) / spaces / ctx.normalSpace;
1684
+ }
1685
+ else if (us.some((u) => u.kind === 'text' && (u.firstCjk || u.lastCjk))) {
1686
+ // A CJK character with nothing to spread against.
1687
+ loose = true;
1688
+ }
1689
+ else {
1690
+ // Western text only (the head of a web address, a divided word): set
1691
+ // ragged, as a Latin line of one word is, and not reported.
1692
+ ragged = true;
1693
+ }
1694
+ }
1695
+ }
1696
+ const pieces = [];
1697
+ const addText = (u, text, width, t) => {
1698
+ // Single characters (not Western runs, which keep a segment each) of
1699
+ // one style, link and spacing that advance alike share a segment.
1700
+ // A mark that gave up blank is a segment of its own: its glyph may be
1701
+ // painted before its box (`inkOffset`), and the caret spreads a
1702
+ // segment's width evenly over its characters.
1703
+ const cut = u.punct ? boxCut(u.punct) : 0;
1704
+ // Upright letters (`:upright[…]`) share a segment with each other only.
1705
+ const key = u.stacked || u.run || u.graphemes !== 1 || cut > 0 || u.place || u.orient === 'tcy' ? undefined : `${u.style.key}|${u.link ?? ''}|${t ?? ''}${u.orient ? `|${u.orient}` : ''}`;
1706
+ const last = pieces[pieces.length - 1];
1707
+ if (key !== undefined && last && last.key === key && last.cell !== undefined && Math.abs(last.cell - width) < 1e-3) {
1708
+ last.parts.push(text);
1709
+ last.seg.width += width;
1710
+ return;
1711
+ }
1712
+ pieces.push({ seg: segmentOf(u, width, t), parts: [text], key, ...(key !== undefined ? { cell: width } : {}) });
1713
+ };
1714
+ const segmentOf = (u, width, t, grapheme = 0) => {
1715
+ const s = u.style;
1716
+ const ink = inkOffsetOf(u, grapheme);
1717
+ return {
1718
+ kind: 'text',
1719
+ text: '',
1720
+ width,
1721
+ bold: s.bold || undefined,
1722
+ italic: s.italic || undefined,
1723
+ ...(s.captionLabel ? { captionLabel: true } : {}),
1724
+ ...(s.script ? { script: s.script, fontString: s.scriptFont, baselineShift: s.baselineShift } : {}),
1725
+ ...(u.stacked === 'first' ? { stacked: true } : {}),
1726
+ ...(s.smallCaps ? { smallCaps: true } : {}),
1727
+ ...(s.marks ? { cjkMarks: s.marks } : {}),
1728
+ ...(s.inserted ? { inserted: true } : {}),
1729
+ ...(s.color ? { color: s.color } : {}),
1730
+ ...(t !== undefined ? { tracking: t } : {}),
1731
+ // Every mark that gave up blank carries its ink offset (0 when only
1732
+ // the blank after its glyph went), and so does a shared mark set in
1733
+ // a Chinese box, so no renderer paints its line in one run at the
1734
+ // glyphs' own advances.
1735
+ ...(ink !== undefined ? { inkOffset: ink } : {}),
1736
+ ...(u.orient === 'tcy' ? { tcy: true } : u.orient ? { orientation: u.orient } : {}),
1737
+ ...(u.scale !== undefined ? { inkScale: u.scale } : {}),
1738
+ ...(u.shift !== undefined ? { baselineShift: u.shift } : {}),
1739
+ };
1740
+ };
1741
+ for (let j = 0; j < us.length; j++) {
1742
+ const u = us[j];
1743
+ if (u.kind === 'space' && u.auto) {
1744
+ pieces.push({ seg: { kind: 'space', text: u.text, width: u.width, autospace: true }, parts: [u.text], key: undefined });
1745
+ continue;
1746
+ }
1747
+ if (u.kind === 'space') {
1748
+ pieces.push({ seg: { kind: 'space', text: u.text, width: spaceWidth ?? u.width }, parts: [u.text], key: undefined });
1749
+ continue;
1750
+ }
1751
+ if (u.kind === 'atomic') {
1752
+ const seg = tokenSegment(u.token);
1753
+ pieces.push({ seg, parts: [seg.text], key: undefined });
1754
+ continue;
1755
+ }
1756
+ const gap = tracking > 0 && stretches[j] ? tracking : 0;
1757
+ if (u.warichu) {
1758
+ // A warichu note's part: the rows are painted, not the text.
1759
+ pieces.push({ seg: { kind: 'text', text: u.text, width: u.width + gap, warichu: u.warichu }, parts: [u.text], key: undefined });
1760
+ continue;
1761
+ }
1762
+ if (u.ruby?.geometry) {
1763
+ // A ruby base: centred in its box, its reading placed from the box's
1764
+ // start (#194). One character takes the gap after it as tracking; a
1765
+ // base of several keeps its natural spacing, centred under its
1766
+ // reading, and the gap follows the box as a space of its own (its
1767
+ // width final, as a Han–Latin space's is): tracking would be added
1768
+ // after each of its characters.
1769
+ const g = u.ruby.geometry;
1770
+ 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
+ };
1786
+ pieces.push({ seg, parts: [u.text], key: undefined });
1787
+ if (gapAfter)
1788
+ pieces.push({ seg: { kind: 'space', text: '', width: gap, autospace: true }, parts: [''], key: undefined });
1789
+ continue;
1790
+ }
1791
+ if (u.place && u.graphemes === 2) {
1792
+ // A pair (—— ……) set in Chinese boxes: one segment per grapheme,
1793
+ // each glyph placed in its own em.
1794
+ const cell = u.width / 2;
1795
+ pieces.push({ seg: { ...segmentOf(u, cell, undefined, 0), text: u.text[0] }, parts: [u.text[0]], key: undefined });
1796
+ pieces.push({ seg: { ...segmentOf(u, cell + gap, gap > 0 ? gap : undefined, 1), text: u.text[1] }, parts: [u.text[1]], key: undefined });
1797
+ }
1798
+ else if (gap === 0) {
1799
+ addText(u, u.text, u.width, undefined);
1800
+ }
1801
+ else if (ctx.vertical && u.run) {
1802
+ // In vertical text a run is painted whole, its cells and sideways
1803
+ // letters as it was measured: the gap after it is advance only, and
1804
+ // cutting its last letter off would lose the neighbour an apostrophe
1805
+ // or an interpunct inside it is read with (`verticalRuns`).
1806
+ addText(u, u.text, u.width + gap, undefined);
1807
+ }
1808
+ else if (u.graphemes <= 1) {
1809
+ addText(u, u.text, u.width + gap, gap);
1810
+ }
1811
+ else {
1812
+ // A run the gap follows: its last grapheme carries the gap, the rest
1813
+ // keeps its natural spacing.
1814
+ const lastG = lastGrapheme(u.text);
1815
+ const head = u.text.slice(0, u.text.length - lastG.length);
1816
+ const headWidth = textWidth(head, u.style.font, u.style.smallCaps) + (ctx.letterSpacingPx === 0 ? 0 : ctx.letterSpacingPx * (u.graphemes - 1));
1817
+ addText(u, head, headWidth, undefined);
1818
+ addText(u, lastG, u.width - headWidth + gap, gap);
1819
+ }
1820
+ }
1821
+ const segments = [];
1822
+ for (const p of pieces) {
1823
+ if (p.parts.length > 1 || p.seg.text === '')
1824
+ p.seg.text = p.parts.join('');
1825
+ segments.push(p.seg);
1826
+ }
1827
+ const trimmed = trimChipLineEdges(segments);
1828
+ clampReadings(trimmed);
1829
+ let width = 0;
1830
+ for (const s of trimmed)
1831
+ width += s.width;
1832
+ if (hang) {
1833
+ // The hung mark follows the line, outside its measure and its width.
1834
+ trimmed.push({ ...segmentOf(hang, hang.width, undefined), text: hang.text, hangs: true });
1835
+ }
1836
+ const text = trimmed.map((s) => s.text).join('');
1837
+ const y = li * ctx.lineHeightPx;
1838
+ return {
1839
+ text,
1840
+ bbox: createBoundingBox(ctx.indentOf(li), y, width, ctx.lineHeightPx),
1841
+ baseline: y + ctx.baselineOffset,
1842
+ hyphenated: range.hyphenated,
1843
+ ...(range.hyphenated && range.hardHyphen ? { hardHyphen: true } : {}),
1844
+ segments: trimmed,
1845
+ isLastLine: isLast,
1846
+ ...(spaceRatio !== undefined && !loose ? { justifiedSpaceRatio: spaceRatio } : {}),
1847
+ ...(loose ? { ragged: true, cjkLoose: true } : ragged ? { ragged: true } : {}),
1848
+ cjkComposed: true,
1849
+ };
1850
+ }
1851
+ /**
1852
+ * Set a Chinese, Japanese or Korean paragraph (see the module comment):
1853
+ * `options.cjkLineBreak` (else the document's level) decides where lines
1854
+ * may break; `textAlign: 'justify'` spreads every line but the last to the
1855
+ * measure. First-line and hanging indents, the block's tracking
1856
+ * (`letterSpacingPx`), other measures for later lines (`restWidths`) and a
1857
+ * positive `looseness` (one line more, for column balancing: the lines are
1858
+ * broken a little short of the measure and spread to it) are honoured;
1859
+ * Knuth–Plass options are not used.
1860
+ */
1861
+ export function composeCjkParagraph(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, options) {
1862
+ // A writing mode asked for this paragraph alone: the Latin runs' widths
1863
+ // (`textWidth`) read it too.
1864
+ if (options?.writingMode !== undefined && options.writingMode !== getMeasureWritingMode()) {
1865
+ const opts = options;
1866
+ return withMeasureWritingMode(opts.writingMode, () => composeCjkParagraph(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, opts));
1867
+ }
1868
+ const fonts = { normal: normalFont, bold: boldFont, italic: italicFont, boldItalic: boldItalicFont };
1869
+ const letterSpacingPx = options?.letterSpacingPx ?? 0;
1870
+ const vertical = getMeasureWritingMode() === 'vertical-rl';
1871
+ // The composition works along the line in either writing mode; vertical
1872
+ // text keeps :;?! at one em (`CjkComposition.vertical`).
1873
+ const composition = compositionFor(options?.cjkComposition ?? getCjkComposition(), vertical);
1874
+ // The marks Latin text shares with Chinese take Chinese boxes in Chinese
1875
+ // text only (`routesSharedMarks`).
1876
+ const route = routesSharedMarks(composition, spans.map((s) => s.text).join(''));
1877
+ const units = prepareUnits(buildAllUnits(spans, fonts, letterSpacingPx, vertical, route), composition, letterSpacingPx);
1878
+ // Ruby readings (#194), laid out once their neighbours are known.
1879
+ if (units.some((u) => u.ruby))
1880
+ sizeRubies(units);
1881
+ if (!units.some((u) => u.kind !== 'space'))
1882
+ return { lines: [], totalHeight: 0 };
1883
+ const fit = lineFitOf(composition);
1884
+ const level = options?.cjkLineBreak ?? getCjkLineBreak();
1885
+ const breaks = breakOpportunities(units, level);
1886
+ const indentPx = options?.firstLineIndentPx ?? 0;
1887
+ const hanging = options?.hangingIndent ?? false;
1888
+ const indentOf = (li) => (indentPx > 0 ? (hanging ? (li === 0 ? 0 : indentPx) : (li === 0 ? indentPx : 0)) : 0);
1889
+ const measureOf = (li) => lineMeasure(maxWidthPx, options?.restWidths, li) - indentOf(li);
1890
+ const sizeMatch = FONT_SIZE_RE.exec(normalFont);
1891
+ const em = sizeMatch ? parseFloat(sizeMatch[1]) : 16;
1892
+ const trackingCap = options?.justifyTrackingPx && options.justifyTrackingPx > 0 ? Math.min(em / 2, options.justifyTrackingPx) : em / 2;
1893
+ const textAlign = options?.textAlign ?? 'left';
1894
+ const ctx = {
1895
+ fonts,
1896
+ textAlign,
1897
+ letterSpacingPx,
1898
+ em,
1899
+ cap: trackingCap,
1900
+ normalSpace: textAlign === 'justify' ? normalSpaceWidthFor(normalFont) + letterSpacingPx : 0,
1901
+ lineHeightPx,
1902
+ baselineOffset: lineBaselineOffset(lineHeightPx, normalFont),
1903
+ indentOf,
1904
+ measureOf,
1905
+ ...(vertical ? { vertical: true } : {}),
1906
+ ...(fit ? { fit } : {}),
1907
+ level,
1908
+ };
1909
+ const compose = (ranges) => ranges.map((r, li) => composeLine(units, r, li, li === ranges.length - 1, ctx));
1910
+ const ranges = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level);
1911
+ let lines = compose(ranges);
1912
+ // 孤字 (clreq §7.2), where the paragraph avoids runts (`bodyText.avoidRunts`
1913
+ // gives it a runt penalty): a last line that holds one character, alone
1914
+ // or with its closing marks, takes the last character the line above may
1915
+ // give up (push-out), and that line is spread to the measure. When it
1916
+ // would need more than the tracking cap, the runt stays.
1917
+ if ((options?.runtPenalty ?? 0) > 0 && endsOnOneCharacter(lines)) {
1918
+ const li = ranges.length - 2;
1919
+ const above = ranges[li];
1920
+ let at = above.end - 1;
1921
+ while (at > above.start && !breaks[at])
1922
+ at--;
1923
+ if (!above.head && !above.hyphenated && !ranges[li + 1].first && at > above.start) {
1924
+ const pushed = breakUnits(units, breaks, measureOf, 0, letterSpacingPx, fit, level, { line: li, at });
1925
+ if (pushed.length === ranges.length) {
1926
+ const set = compose(pushed);
1927
+ if (!set[li].cjkLoose && !endsOnOneCharacter(set))
1928
+ lines = set;
1929
+ }
1930
+ }
1931
+ }
1932
+ // Column balancing asks for a paragraph one line longer: break each line
1933
+ // a little short of its measure, in eighths of an em, until the paragraph
1934
+ // gains the lines without a line past the tracking cap. The first step
1935
+ // that gains them usually carries a single character down; a setting
1936
+ // whose last line is one character, alone or with its marks (孤字), is
1937
+ // passed over for the next step's, which carries more.
1938
+ const looseness = options?.looseness ?? 0;
1939
+ if (looseness > 0) {
1940
+ const target = lines.length + looseness;
1941
+ for (let step = 1; step <= 16; step++) {
1942
+ const ranges = breakUnits(units, breaks, measureOf, (step * em) / 8, letterSpacingPx, fit, level);
1943
+ if (ranges.length < target)
1944
+ continue;
1945
+ if (ranges.length > target)
1946
+ break;
1947
+ const loose = compose(ranges);
1948
+ if (!loose.some((l) => l.cjkLoose) && !endsOnOneCharacter(loose)) {
1949
+ lines = loose;
1950
+ break;
1951
+ }
1952
+ }
1953
+ }
1954
+ if (lines.some((l) => l.segments?.some((s) => s.smallCaps))) {
1955
+ expandSmallCaps(lines, normalFont, boldFont, italicFont, boldItalicFont, letterSpacingPx);
1956
+ }
1957
+ return { lines, totalHeight: lines.length * lineHeightPx };
1958
+ }
1959
+ /** Whether a paragraph of two lines or more ends on a line that holds one
1960
+ * CJK character, alone or followed by the marks that may not open a line
1961
+ * (`图`, `版。`, `说。”`): the 孤字 Chinese typesetting avoids. */
1962
+ function endsOnOneCharacter(lines) {
1963
+ if (lines.length < 2)
1964
+ return false;
1965
+ const gs = graphemesOf(lines[lines.length - 1].text.trim());
1966
+ let n = gs.length;
1967
+ while (n > 1 && isLineStartProhibited(cjkClassOf(gs[n - 1]), 'basic'))
1968
+ n--;
1969
+ return n === 1 && isCjkGrapheme(gs[0]) && cjkClassOf(gs[0]) === 'ideograph';
1970
+ }
1971
+ /**
1972
+ * The breaks of one word (no breaking space inside) that holds CJK
1973
+ * characters, in a paragraph set by the word-by-word breaker (a Latin
1974
+ * paragraph quoting CJK): next to its CJK characters, under the document's
1975
+ * line-break rules. Each unit is measured once and the widths before a
1976
+ * break come from their running sums, so a long run of ideographs costs
1977
+ * linear time.
1978
+ */
1979
+ export function cjkWordBreaks(word, font, smallCaps, letterSpacingPx, level = getCjkLineBreak()) {
1980
+ const span = { text: word, bold: false, italic: false, ...(smallCaps ? { smallCaps: true } : {}) };
1981
+ const units = buildUnits([span], { normal: font, bold: font, italic: font, boldItalic: font }, letterSpacingPx, getMeasureWritingMode() === 'vertical-rl');
1982
+ const opportunities = breakOpportunities(units, level);
1983
+ const starts = [];
1984
+ const sums = [0];
1985
+ const breaks = [];
1986
+ let total = 0;
1987
+ for (let k = 0; k < units.length; k++) {
1988
+ const u = units[k];
1989
+ starts.push(u.at);
1990
+ if (k > 0 && opportunities[k])
1991
+ breaks.push(u.at);
1992
+ total += u.width;
1993
+ sums.push(total);
1994
+ }
1995
+ const widthBefore = (idx) => {
1996
+ // The last unit that starts at or before `idx`.
1997
+ let lo = 0;
1998
+ let hi = starts.length - 1;
1999
+ let found = -1;
2000
+ while (lo <= hi) {
2001
+ const mid = (lo + hi) >> 1;
2002
+ if (starts[mid] <= idx) {
2003
+ found = mid;
2004
+ lo = mid + 1;
2005
+ }
2006
+ else
2007
+ hi = mid - 1;
2008
+ }
2009
+ if (found < 0)
2010
+ return 0;
2011
+ const u = units[found];
2012
+ const inside = idx - u.at;
2013
+ if (inside <= 0)
2014
+ return sums[found];
2015
+ if (inside >= u.text.length)
2016
+ return sums[found + 1];
2017
+ const part = u.text.slice(0, inside);
2018
+ return sums[found] + textWidth(part, font, smallCaps) + (letterSpacingPx === 0 ? 0 : letterSpacingPx * graphemeCount(part));
2019
+ };
2020
+ return { breaks, width: total, widthBefore };
2021
+ }
2022
+ //# sourceMappingURL=cjkCompose.js.map