postext 1.27.0 → 1.28.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 (544) hide show
  1. package/dist/__tests__/bundle/balloonPlacementPin.test.d.ts +2 -0
  2. package/dist/__tests__/bundle/balloonPlacementPin.test.d.ts.map +1 -0
  3. package/dist/__tests__/bundle/balloonPlacementPin.test.js +71 -0
  4. package/dist/__tests__/bundle/balloonPlacementPin.test.js.map +1 -0
  5. package/dist/__tests__/bundle/cjkRulesPin.test.js +131 -7
  6. package/dist/__tests__/bundle/cjkRulesPin.test.js.map +1 -1
  7. package/dist/__tests__/bundle/designOverflowPin.test.js +3 -3
  8. package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -1
  9. package/dist/__tests__/bundle/flowColumnsPin.test.js +3 -1
  10. package/dist/__tests__/bundle/flowColumnsPin.test.js.map +1 -1
  11. package/dist/__tests__/bundle/gridBalancingPin.test.js +14 -5
  12. package/dist/__tests__/bundle/gridBalancingPin.test.js.map +1 -1
  13. package/dist/__tests__/bundle/gridBreakRulesPin.test.d.ts +2 -0
  14. package/dist/__tests__/bundle/gridBreakRulesPin.test.d.ts.map +1 -0
  15. package/dist/__tests__/bundle/gridBreakRulesPin.test.js +144 -0
  16. package/dist/__tests__/bundle/gridBreakRulesPin.test.js.map +1 -0
  17. package/dist/__tests__/bundle/indexGroupSplitPin.test.d.ts +2 -0
  18. package/dist/__tests__/bundle/indexGroupSplitPin.test.d.ts.map +1 -0
  19. package/dist/__tests__/bundle/indexGroupSplitPin.test.js +55 -0
  20. package/dist/__tests__/bundle/indexGroupSplitPin.test.js.map +1 -0
  21. package/dist/__tests__/bundle/inlineCaptionSidePin.test.d.ts +2 -0
  22. package/dist/__tests__/bundle/inlineCaptionSidePin.test.d.ts.map +1 -0
  23. package/dist/__tests__/bundle/inlineCaptionSidePin.test.js +60 -0
  24. package/dist/__tests__/bundle/inlineCaptionSidePin.test.js.map +1 -0
  25. package/dist/__tests__/bundle/inlineTableSplitPin.test.js +3 -1
  26. package/dist/__tests__/bundle/inlineTableSplitPin.test.js.map +1 -1
  27. package/dist/__tests__/bundle/looseOrderPin.test.d.ts +2 -0
  28. package/dist/__tests__/bundle/looseOrderPin.test.d.ts.map +1 -0
  29. package/dist/__tests__/bundle/looseOrderPin.test.js +76 -0
  30. package/dist/__tests__/bundle/looseOrderPin.test.js.map +1 -0
  31. package/dist/__tests__/bundle/openerHeadFloatsPin.test.js +2 -1
  32. package/dist/__tests__/bundle/openerHeadFloatsPin.test.js.map +1 -1
  33. package/dist/__tests__/bundle/refLabelBreaksPin.test.d.ts +2 -0
  34. package/dist/__tests__/bundle/refLabelBreaksPin.test.d.ts.map +1 -0
  35. package/dist/__tests__/bundle/refLabelBreaksPin.test.js +72 -0
  36. package/dist/__tests__/bundle/refLabelBreaksPin.test.js.map +1 -0
  37. package/dist/__tests__/bundle/scriptContextPin.test.d.ts +2 -0
  38. package/dist/__tests__/bundle/scriptContextPin.test.d.ts.map +1 -0
  39. package/dist/__tests__/bundle/scriptContextPin.test.js +66 -0
  40. package/dist/__tests__/bundle/scriptContextPin.test.js.map +1 -0
  41. package/dist/__tests__/bundle/trailingStepPin.test.d.ts +2 -0
  42. package/dist/__tests__/bundle/trailingStepPin.test.d.ts.map +1 -0
  43. package/dist/__tests__/bundle/trailingStepPin.test.js +59 -0
  44. package/dist/__tests__/bundle/trailingStepPin.test.js.map +1 -0
  45. package/dist/__tests__/bundle/wideFloatStretchPin.test.d.ts +2 -0
  46. package/dist/__tests__/bundle/wideFloatStretchPin.test.d.ts.map +1 -0
  47. package/dist/__tests__/bundle/wideFloatStretchPin.test.js +59 -0
  48. package/dist/__tests__/bundle/wideFloatStretchPin.test.js.map +1 -0
  49. package/dist/__tests__/bundle.test.js +8 -6
  50. package/dist/__tests__/bundle.test.js.map +1 -1
  51. package/dist/__tests__/cjk/annotations.test.js +223 -0
  52. package/dist/__tests__/cjk/annotations.test.js.map +1 -1
  53. package/dist/__tests__/cjk/annotationsRender.test.js +3 -3
  54. package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -1
  55. package/dist/__tests__/cjk/chipLatinSpacing.test.js +13 -2
  56. package/dist/__tests__/cjk/chipLatinSpacing.test.js.map +1 -1
  57. package/dist/__tests__/cjk/designNumberSeams.test.d.ts +2 -0
  58. package/dist/__tests__/cjk/designNumberSeams.test.d.ts.map +1 -0
  59. package/dist/__tests__/cjk/designNumberSeams.test.js +221 -0
  60. package/dist/__tests__/cjk/designNumberSeams.test.js.map +1 -0
  61. package/dist/__tests__/cjk/grid.test.js +1 -1
  62. package/dist/__tests__/cjk/grid.test.js.map +1 -1
  63. package/dist/__tests__/cjk/gridBalancing.test.js +3 -1
  64. package/dist/__tests__/cjk/gridBalancing.test.js.map +1 -1
  65. package/dist/__tests__/cjk/japaneseSpacing.test.js +3 -1
  66. package/dist/__tests__/cjk/japaneseSpacing.test.js.map +1 -1
  67. package/dist/__tests__/cjk/keepAll.test.js +3 -1
  68. package/dist/__tests__/cjk/keepAll.test.js.map +1 -1
  69. package/dist/__tests__/cjk/keepAllLatinQuotation.test.d.ts +2 -0
  70. package/dist/__tests__/cjk/keepAllLatinQuotation.test.d.ts.map +1 -0
  71. package/dist/__tests__/cjk/keepAllLatinQuotation.test.js +166 -0
  72. package/dist/__tests__/cjk/keepAllLatinQuotation.test.js.map +1 -0
  73. package/dist/__tests__/cjk/latinSpacingEdge.test.d.ts +2 -0
  74. package/dist/__tests__/cjk/latinSpacingEdge.test.d.ts.map +1 -0
  75. package/dist/__tests__/cjk/latinSpacingEdge.test.js +179 -0
  76. package/dist/__tests__/cjk/latinSpacingEdge.test.js.map +1 -0
  77. package/dist/__tests__/cjk/runt.test.js +92 -1
  78. package/dist/__tests__/cjk/runt.test.js.map +1 -1
  79. package/dist/__tests__/codeBlocks.test.js +1 -1
  80. package/dist/__tests__/columnBalancing.test.js +216 -0
  81. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  82. package/dist/__tests__/columnRuleBands.test.js +78 -0
  83. package/dist/__tests__/columnRuleBands.test.js.map +1 -1
  84. package/dist/__tests__/comics/readingDirection.test.js +52 -0
  85. package/dist/__tests__/comics/readingDirection.test.js.map +1 -1
  86. package/dist/__tests__/comics/script.test.js +5 -0
  87. package/dist/__tests__/comics/script.test.js.map +1 -1
  88. package/dist/__tests__/contrast.test.d.ts +2 -0
  89. package/dist/__tests__/contrast.test.d.ts.map +1 -0
  90. package/dist/__tests__/contrast.test.js +258 -0
  91. package/dist/__tests__/contrast.test.js.map +1 -0
  92. package/dist/__tests__/defaults/cjk.test.js +4 -4
  93. package/dist/__tests__/defaults/cjk.test.js.map +1 -1
  94. package/dist/__tests__/defaults/gridBreakRules.test.d.ts +2 -0
  95. package/dist/__tests__/defaults/gridBreakRules.test.d.ts.map +1 -0
  96. package/dist/__tests__/defaults/gridBreakRules.test.js +152 -0
  97. package/dist/__tests__/defaults/gridBreakRules.test.js.map +1 -0
  98. package/dist/__tests__/defaults/headingsBalancing.test.js +25 -0
  99. package/dist/__tests__/defaults/headingsBalancing.test.js.map +1 -1
  100. package/dist/__tests__/defaults/stripContextDefaults.test.js +41 -0
  101. package/dist/__tests__/defaults/stripContextDefaults.test.js.map +1 -1
  102. package/dist/__tests__/defaults/stripRoundTrip.test.js +8 -5
  103. package/dist/__tests__/defaults/stripRoundTrip.test.js.map +1 -1
  104. package/dist/__tests__/exports.test.js +30 -0
  105. package/dist/__tests__/exports.test.js.map +1 -1
  106. package/dist/__tests__/fontUsage.test.d.ts +2 -0
  107. package/dist/__tests__/fontUsage.test.d.ts.map +1 -0
  108. package/dist/__tests__/fontUsage.test.js +113 -0
  109. package/dist/__tests__/fontUsage.test.js.map +1 -0
  110. package/dist/__tests__/hardBreaks.test.js +1 -1
  111. package/dist/__tests__/headingInlineMarks.test.js +7 -5
  112. package/dist/__tests__/headingInlineMarks.test.js.map +1 -1
  113. package/dist/__tests__/hyphenationRules.test.d.ts +2 -0
  114. package/dist/__tests__/hyphenationRules.test.d.ts.map +1 -0
  115. package/dist/__tests__/hyphenationRules.test.js +295 -0
  116. package/dist/__tests__/hyphenationRules.test.js.map +1 -0
  117. package/dist/__tests__/indexGroupSplit.test.d.ts +2 -0
  118. package/dist/__tests__/indexGroupSplit.test.d.ts.map +1 -0
  119. package/dist/__tests__/indexGroupSplit.test.js +175 -0
  120. package/dist/__tests__/indexGroupSplit.test.js.map +1 -0
  121. package/dist/__tests__/mathSizeMigration.test.js +7 -5
  122. package/dist/__tests__/mathSizeMigration.test.js.map +1 -1
  123. package/dist/__tests__/multipleColumns.test.js +19 -0
  124. package/dist/__tests__/multipleColumns.test.js.map +1 -1
  125. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
  126. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +3 -3
  127. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js.map +1 -1
  128. package/dist/__tests__/pipeline/closingBandUnderOpener.test.d.ts +2 -0
  129. package/dist/__tests__/pipeline/closingBandUnderOpener.test.d.ts.map +1 -0
  130. package/dist/__tests__/pipeline/closingBandUnderOpener.test.js +90 -0
  131. package/dist/__tests__/pipeline/closingBandUnderOpener.test.js.map +1 -0
  132. package/dist/__tests__/pipeline/colonListRoom.test.js +5 -4
  133. package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
  134. package/dist/__tests__/pipeline/columnHeadSpace.test.d.ts +2 -0
  135. package/dist/__tests__/pipeline/columnHeadSpace.test.d.ts.map +1 -0
  136. package/dist/__tests__/pipeline/columnHeadSpace.test.js +175 -0
  137. package/dist/__tests__/pipeline/columnHeadSpace.test.js.map +1 -0
  138. package/dist/__tests__/pipeline/compoundBreaks.test.js +6 -6
  139. package/dist/__tests__/pipeline/compoundBreaks.test.js.map +1 -1
  140. package/dist/__tests__/pipeline/dashBreaks.test.js +5 -5
  141. package/dist/__tests__/pipeline/dashBreaks.test.js.map +1 -1
  142. package/dist/__tests__/pipeline/flowColumns.test.js +100 -0
  143. package/dist/__tests__/pipeline/flowColumns.test.js.map +1 -1
  144. package/dist/__tests__/pipeline/headingShortBand.test.js +2 -1
  145. package/dist/__tests__/pipeline/headingShortBand.test.js.map +1 -1
  146. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +6 -5
  147. package/dist/__tests__/pipeline/headingSplitOrphans.test.js.map +1 -1
  148. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js +188 -1
  149. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js.map +1 -1
  150. package/dist/__tests__/pipeline/inlineCaptionSide.test.d.ts +2 -0
  151. package/dist/__tests__/pipeline/inlineCaptionSide.test.d.ts.map +1 -0
  152. package/dist/__tests__/pipeline/inlineCaptionSide.test.js +270 -0
  153. package/dist/__tests__/pipeline/inlineCaptionSide.test.js.map +1 -0
  154. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +5 -5
  155. package/dist/__tests__/pipeline/inlineFigureSpace.test.js.map +1 -1
  156. package/dist/__tests__/pipeline/inlineRefRender.test.js +8 -6
  157. package/dist/__tests__/pipeline/inlineRefRender.test.js.map +1 -1
  158. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.d.ts +2 -0
  159. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.d.ts.map +1 -0
  160. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.js +73 -0
  161. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.js.map +1 -0
  162. package/dist/__tests__/pipeline/openerFloats.test.d.ts +2 -0
  163. package/dist/__tests__/pipeline/openerFloats.test.d.ts.map +1 -0
  164. package/dist/__tests__/pipeline/openerFloats.test.js +137 -0
  165. package/dist/__tests__/pipeline/openerFloats.test.js.map +1 -0
  166. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.d.ts +2 -0
  167. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.d.ts.map +1 -0
  168. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.js +162 -0
  169. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.js.map +1 -0
  170. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +68 -8
  171. package/dist/__tests__/pipeline/paragraphContainerTail.test.js.map +1 -1
  172. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
  173. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  174. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.d.ts +2 -0
  175. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.d.ts.map +1 -0
  176. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.js +151 -0
  177. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.js.map +1 -0
  178. package/dist/__tests__/pipeline/raggedOptimal.test.js +6 -6
  179. package/dist/__tests__/pipeline/raggedOptimal.test.js.map +1 -1
  180. package/dist/__tests__/pipeline/refLabelBreaks.test.d.ts +2 -0
  181. package/dist/__tests__/pipeline/refLabelBreaks.test.d.ts.map +1 -0
  182. package/dist/__tests__/pipeline/refLabelBreaks.test.js +324 -0
  183. package/dist/__tests__/pipeline/refLabelBreaks.test.js.map +1 -0
  184. package/dist/__tests__/pipeline/resourceTemplateStyles.test.d.ts +2 -0
  185. package/dist/__tests__/pipeline/resourceTemplateStyles.test.d.ts.map +1 -0
  186. package/dist/__tests__/pipeline/resourceTemplateStyles.test.js +145 -0
  187. package/dist/__tests__/pipeline/resourceTemplateStyles.test.js.map +1 -0
  188. package/dist/__tests__/pipeline/shortListSplit.test.d.ts +2 -0
  189. package/dist/__tests__/pipeline/shortListSplit.test.d.ts.map +1 -0
  190. package/dist/__tests__/pipeline/shortListSplit.test.js +181 -0
  191. package/dist/__tests__/pipeline/shortListSplit.test.js.map +1 -0
  192. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.d.ts +2 -0
  193. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.d.ts.map +1 -0
  194. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.js +133 -0
  195. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.js.map +1 -0
  196. package/dist/__tests__/pipeline/stretchAfterDisplays.test.d.ts +2 -0
  197. package/dist/__tests__/pipeline/stretchAfterDisplays.test.d.ts.map +1 -0
  198. package/dist/__tests__/pipeline/stretchAfterDisplays.test.js +51 -0
  199. package/dist/__tests__/pipeline/stretchAfterDisplays.test.js.map +1 -0
  200. package/dist/__tests__/pipeline/trailingCapFloatHead.test.js +26 -0
  201. package/dist/__tests__/pipeline/trailingCapFloatHead.test.js.map +1 -1
  202. package/dist/__tests__/pipeline/trailingStep.test.d.ts +2 -0
  203. package/dist/__tests__/pipeline/trailingStep.test.d.ts.map +1 -0
  204. package/dist/__tests__/pipeline/trailingStep.test.js +187 -0
  205. package/dist/__tests__/pipeline/trailingStep.test.js.map +1 -0
  206. package/dist/__tests__/pipeline/wideFloatStretch.test.d.ts +2 -0
  207. package/dist/__tests__/pipeline/wideFloatStretch.test.d.ts.map +1 -0
  208. package/dist/__tests__/pipeline/wideFloatStretch.test.js +84 -0
  209. package/dist/__tests__/pipeline/wideFloatStretch.test.js.map +1 -0
  210. package/dist/__tests__/scriptContext.test.d.ts +2 -0
  211. package/dist/__tests__/scriptContext.test.d.ts.map +1 -0
  212. package/dist/__tests__/scriptContext.test.js +919 -0
  213. package/dist/__tests__/scriptContext.test.js.map +1 -0
  214. package/dist/__tests__/softHyphenEdges.test.d.ts +2 -0
  215. package/dist/__tests__/softHyphenEdges.test.d.ts.map +1 -0
  216. package/dist/__tests__/softHyphenEdges.test.js +217 -0
  217. package/dist/__tests__/softHyphenEdges.test.js.map +1 -0
  218. package/dist/__tests__/spaceLimitRatio.test.d.ts +2 -0
  219. package/dist/__tests__/spaceLimitRatio.test.d.ts.map +1 -0
  220. package/dist/__tests__/spaceLimitRatio.test.js +226 -0
  221. package/dist/__tests__/spaceLimitRatio.test.js.map +1 -0
  222. package/dist/__tests__/trackedLigatures.test.js +365 -24
  223. package/dist/__tests__/trackedLigatures.test.js.map +1 -1
  224. package/dist/__tests__/verseLines.test.js +9 -7
  225. package/dist/__tests__/verseLines.test.js.map +1 -1
  226. package/dist/bundle/configVersion.d.ts +201 -6
  227. package/dist/bundle/configVersion.d.ts.map +1 -1
  228. package/dist/bundle/configVersion.js +440 -6
  229. package/dist/bundle/configVersion.js.map +1 -1
  230. package/dist/bundle/index.d.ts +8 -0
  231. package/dist/bundle/index.d.ts.map +1 -1
  232. package/dist/bundle/index.js +8 -0
  233. package/dist/bundle/index.js.map +1 -1
  234. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  235. package/dist/canvas-backend/blockRender.js +3 -2
  236. package/dist/canvas-backend/blockRender.js.map +1 -1
  237. package/dist/canvas-backend/chip.js +1 -1
  238. package/dist/canvas-backend/chip.js.map +1 -1
  239. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  240. package/dist/canvas-backend/headerFooter.js +17 -2
  241. package/dist/canvas-backend/headerFooter.js.map +1 -1
  242. package/dist/canvas-backend/runDirection.d.ts +10 -6
  243. package/dist/canvas-backend/runDirection.d.ts.map +1 -1
  244. package/dist/canvas-backend/runDirection.js +12 -7
  245. package/dist/canvas-backend/runDirection.js.map +1 -1
  246. package/dist/canvas-backend/scriptText.d.ts +11 -0
  247. package/dist/canvas-backend/scriptText.d.ts.map +1 -0
  248. package/dist/canvas-backend/scriptText.js +75 -0
  249. package/dist/canvas-backend/scriptText.js.map +1 -0
  250. package/dist/canvas-backend/segmentText.d.ts.map +1 -1
  251. package/dist/canvas-backend/segmentText.js +9 -1
  252. package/dist/canvas-backend/segmentText.js.map +1 -1
  253. package/dist/canvas-backend/trackedText.d.ts +35 -20
  254. package/dist/canvas-backend/trackedText.d.ts.map +1 -1
  255. package/dist/canvas-backend/trackedText.js +58 -22
  256. package/dist/canvas-backend/trackedText.js.map +1 -1
  257. package/dist/canvas-backend/verticalText.d.ts.map +1 -1
  258. package/dist/canvas-backend/verticalText.js +10 -2
  259. package/dist/canvas-backend/verticalText.js.map +1 -1
  260. package/dist/canvas-backend/wordRuns.d.ts +14 -5
  261. package/dist/canvas-backend/wordRuns.d.ts.map +1 -1
  262. package/dist/canvas-backend/wordRuns.js +20 -7
  263. package/dist/canvas-backend/wordRuns.js.map +1 -1
  264. package/dist/citations/names.d.ts +8 -0
  265. package/dist/citations/names.d.ts.map +1 -0
  266. package/dist/citations/names.js +25 -0
  267. package/dist/citations/names.js.map +1 -0
  268. package/dist/columnClip.d.ts +8 -1
  269. package/dist/columnClip.d.ts.map +1 -1
  270. package/dist/columnClip.js +32 -3
  271. package/dist/columnClip.js.map +1 -1
  272. package/dist/columnRule.d.ts +6 -3
  273. package/dist/columnRule.d.ts.map +1 -1
  274. package/dist/columnRule.js +6 -3
  275. package/dist/columnRule.js.map +1 -1
  276. package/dist/comics/layoutPage.d.ts.map +1 -1
  277. package/dist/comics/layoutPage.js +51 -22
  278. package/dist/comics/layoutPage.js.map +1 -1
  279. package/dist/comics/lettering/__tests__/bystanderFaces.test.d.ts +2 -0
  280. package/dist/comics/lettering/__tests__/bystanderFaces.test.d.ts.map +1 -0
  281. package/dist/comics/lettering/__tests__/bystanderFaces.test.js +204 -0
  282. package/dist/comics/lettering/__tests__/bystanderFaces.test.js.map +1 -0
  283. package/dist/comics/lettering/__tests__/shapeTextDash.test.d.ts +2 -0
  284. package/dist/comics/lettering/__tests__/shapeTextDash.test.d.ts.map +1 -0
  285. package/dist/comics/lettering/__tests__/shapeTextDash.test.js +173 -0
  286. package/dist/comics/lettering/__tests__/shapeTextDash.test.js.map +1 -0
  287. package/dist/comics/lettering/__tests__/strictPlacement.test.d.ts +2 -0
  288. package/dist/comics/lettering/__tests__/strictPlacement.test.d.ts.map +1 -0
  289. package/dist/comics/lettering/__tests__/strictPlacement.test.js +130 -0
  290. package/dist/comics/lettering/__tests__/strictPlacement.test.js.map +1 -0
  291. package/dist/comics/lettering/letter.js +21 -8
  292. package/dist/comics/lettering/letter.js.map +1 -1
  293. package/dist/comics/lettering/placement.d.ts.map +1 -1
  294. package/dist/comics/lettering/placement.js +12 -3
  295. package/dist/comics/lettering/placement.js.map +1 -1
  296. package/dist/comics/lettering/shape-text.d.ts +3 -0
  297. package/dist/comics/lettering/shape-text.d.ts.map +1 -1
  298. package/dist/comics/lettering/shape-text.js +71 -12
  299. package/dist/comics/lettering/shape-text.js.map +1 -1
  300. package/dist/comics/lettering/text.d.ts +5 -0
  301. package/dist/comics/lettering/text.d.ts.map +1 -1
  302. package/dist/comics/lettering/text.js +6 -1
  303. package/dist/comics/lettering/text.js.map +1 -1
  304. package/dist/comics/lettering/types.d.ts +12 -2
  305. package/dist/comics/lettering/types.d.ts.map +1 -1
  306. package/dist/comics/script.js +1 -1
  307. package/dist/comics/script.js.map +1 -1
  308. package/dist/comics/types.d.ts +7 -3
  309. package/dist/comics/types.d.ts.map +1 -1
  310. package/dist/configWarnings.d.ts.map +1 -1
  311. package/dist/configWarnings.js +33 -6
  312. package/dist/configWarnings.js.map +1 -1
  313. package/dist/contrast.d.ts +188 -0
  314. package/dist/contrast.d.ts.map +1 -0
  315. package/dist/contrast.js +583 -0
  316. package/dist/contrast.js.map +1 -0
  317. package/dist/defaults/bodyText.d.ts +31 -3
  318. package/dist/defaults/bodyText.d.ts.map +1 -1
  319. package/dist/defaults/bodyText.js +76 -8
  320. package/dist/defaults/bodyText.js.map +1 -1
  321. package/dist/defaults/citations.d.ts.map +1 -1
  322. package/dist/defaults/citations.js +4 -0
  323. package/dist/defaults/citations.js.map +1 -1
  324. package/dist/defaults/cjk.d.ts +11 -1
  325. package/dist/defaults/cjk.d.ts.map +1 -1
  326. package/dist/defaults/cjk.js +41 -3
  327. package/dist/defaults/cjk.js.map +1 -1
  328. package/dist/defaults/comics.d.ts.map +1 -1
  329. package/dist/defaults/comics.js +2 -0
  330. package/dist/defaults/comics.js.map +1 -1
  331. package/dist/defaults/contrast.d.ts +11 -0
  332. package/dist/defaults/contrast.d.ts.map +1 -0
  333. package/dist/defaults/contrast.js +65 -0
  334. package/dist/defaults/contrast.js.map +1 -0
  335. package/dist/defaults/crossRefs.d.ts +1 -1
  336. package/dist/defaults/crossRefs.d.ts.map +1 -1
  337. package/dist/defaults/crossRefs.js +6 -1
  338. package/dist/defaults/crossRefs.js.map +1 -1
  339. package/dist/defaults/footnotes.d.ts.map +1 -1
  340. package/dist/defaults/footnotes.js +5 -0
  341. package/dist/defaults/footnotes.js.map +1 -1
  342. package/dist/defaults/headings.d.ts +5 -1
  343. package/dist/defaults/headings.d.ts.map +1 -1
  344. package/dist/defaults/headings.js +41 -1
  345. package/dist/defaults/headings.js.map +1 -1
  346. package/dist/defaults/index.d.ts +3 -2
  347. package/dist/defaults/index.d.ts.map +1 -1
  348. package/dist/defaults/index.js +15 -4
  349. package/dist/defaults/index.js.map +1 -1
  350. package/dist/defaults/indexConfig.d.ts +1 -0
  351. package/dist/defaults/indexConfig.d.ts.map +1 -1
  352. package/dist/defaults/indexConfig.js +7 -1
  353. package/dist/defaults/indexConfig.js.map +1 -1
  354. package/dist/defaults/layout.d.ts.map +1 -1
  355. package/dist/defaults/layout.js +13 -1
  356. package/dist/defaults/layout.js.map +1 -1
  357. package/dist/defaults/orderedLists.js +1 -1
  358. package/dist/defaults/orderedLists.js.map +1 -1
  359. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  360. package/dist/defaults/paragraphStyles.js +5 -0
  361. package/dist/defaults/paragraphStyles.js.map +1 -1
  362. package/dist/defaults/shared.d.ts +4 -1
  363. package/dist/defaults/shared.d.ts.map +1 -1
  364. package/dist/defaults/shared.js +5 -0
  365. package/dist/defaults/shared.js.map +1 -1
  366. package/dist/design/cjkText.d.ts +7 -2
  367. package/dist/design/cjkText.d.ts.map +1 -1
  368. package/dist/design/cjkText.js +8 -2
  369. package/dist/design/cjkText.js.map +1 -1
  370. package/dist/design/layout.d.ts.map +1 -1
  371. package/dist/design/layout.js +85 -12
  372. package/dist/design/layout.js.map +1 -1
  373. package/dist/design/richText.d.ts.map +1 -1
  374. package/dist/design/richText.js +7 -3
  375. package/dist/design/richText.js.map +1 -1
  376. package/dist/fonts/usage.d.ts.map +1 -1
  377. package/dist/fonts/usage.js +89 -0
  378. package/dist/fonts/usage.js.map +1 -1
  379. package/dist/html-backend.d.ts.map +1 -1
  380. package/dist/html-backend.js +97 -27
  381. package/dist/html-backend.js.map +1 -1
  382. package/dist/hyphenate.d.ts +58 -1
  383. package/dist/hyphenate.d.ts.map +1 -1
  384. package/dist/hyphenate.js +228 -19
  385. package/dist/hyphenate.js.map +1 -1
  386. package/dist/hyphenationExceptions.d.ts +10 -0
  387. package/dist/hyphenationExceptions.d.ts.map +1 -0
  388. package/dist/hyphenationExceptions.js +29 -0
  389. package/dist/hyphenationExceptions.js.map +1 -0
  390. package/dist/index.d.ts +12 -4
  391. package/dist/index.d.ts.map +1 -1
  392. package/dist/index.js +10 -3
  393. package/dist/index.js.map +1 -1
  394. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
  395. package/dist/knuthPlass/pretextAdapter.js +4 -17
  396. package/dist/knuthPlass/pretextAdapter.js.map +1 -1
  397. package/dist/knuthPlass/richAdapter.d.ts +9 -0
  398. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  399. package/dist/knuthPlass/richAdapter.js +15 -18
  400. package/dist/knuthPlass/richAdapter.js.map +1 -1
  401. package/dist/looseLines.d.ts +15 -4
  402. package/dist/looseLines.d.ts.map +1 -1
  403. package/dist/looseLines.js +17 -4
  404. package/dist/looseLines.js.map +1 -1
  405. package/dist/measure/cache.d.ts.map +1 -1
  406. package/dist/measure/cache.js +14 -4
  407. package/dist/measure/cache.js.map +1 -1
  408. package/dist/measure/canvas.d.ts +18 -0
  409. package/dist/measure/canvas.d.ts.map +1 -1
  410. package/dist/measure/canvas.js +104 -0
  411. package/dist/measure/canvas.js.map +1 -1
  412. package/dist/measure/cjkAnnotate.d.ts +11 -6
  413. package/dist/measure/cjkAnnotate.d.ts.map +1 -1
  414. package/dist/measure/cjkAnnotate.js +27 -11
  415. package/dist/measure/cjkAnnotate.js.map +1 -1
  416. package/dist/measure/cjkCompose.d.ts +6 -2
  417. package/dist/measure/cjkCompose.d.ts.map +1 -1
  418. package/dist/measure/cjkCompose.js +146 -30
  419. package/dist/measure/cjkCompose.js.map +1 -1
  420. package/dist/measure/cjkPunctuation.d.ts +17 -0
  421. package/dist/measure/cjkPunctuation.d.ts.map +1 -1
  422. package/dist/measure/cjkPunctuation.js +15 -1
  423. package/dist/measure/cjkPunctuation.js.map +1 -1
  424. package/dist/measure/font.d.ts +2 -1
  425. package/dist/measure/font.d.ts.map +1 -1
  426. package/dist/measure/font.js +4 -2
  427. package/dist/measure/font.js.map +1 -1
  428. package/dist/measure/hardBreaks.d.ts.map +1 -1
  429. package/dist/measure/hardBreaks.js +6 -2
  430. package/dist/measure/hardBreaks.js.map +1 -1
  431. package/dist/measure/kashida.d.ts.map +1 -1
  432. package/dist/measure/kashida.js +8 -11
  433. package/dist/measure/kashida.js.map +1 -1
  434. package/dist/measure/plain.d.ts +3 -0
  435. package/dist/measure/plain.d.ts.map +1 -1
  436. package/dist/measure/plain.js +70 -5
  437. package/dist/measure/plain.js.map +1 -1
  438. package/dist/measure/rich.d.ts +32 -0
  439. package/dist/measure/rich.d.ts.map +1 -1
  440. package/dist/measure/rich.js +227 -35
  441. package/dist/measure/rich.js.map +1 -1
  442. package/dist/measure/script.d.ts +180 -0
  443. package/dist/measure/script.d.ts.map +1 -0
  444. package/dist/measure/script.js +329 -0
  445. package/dist/measure/script.js.map +1 -0
  446. package/dist/measure/spaceRatios.d.ts +28 -0
  447. package/dist/measure/spaceRatios.d.ts.map +1 -0
  448. package/dist/measure/spaceRatios.js +48 -0
  449. package/dist/measure/spaceRatios.js.map +1 -0
  450. package/dist/measure/spaces.d.ts +10 -0
  451. package/dist/measure/spaces.d.ts.map +1 -1
  452. package/dist/measure/spaces.js +36 -0
  453. package/dist/measure/spaces.js.map +1 -1
  454. package/dist/measure/types.d.ts +9 -0
  455. package/dist/measure/types.d.ts.map +1 -1
  456. package/dist/measure/types.js.map +1 -1
  457. package/dist/numbering.d.ts +5 -0
  458. package/dist/numbering.d.ts.map +1 -1
  459. package/dist/numbering.js +5 -1
  460. package/dist/numbering.js.map +1 -1
  461. package/dist/parse/types.d.ts +13 -0
  462. package/dist/parse/types.d.ts.map +1 -1
  463. package/dist/pipeline/bandCaps.d.ts +35 -1
  464. package/dist/pipeline/bandCaps.d.ts.map +1 -1
  465. package/dist/pipeline/bandCaps.js +95 -3
  466. package/dist/pipeline/bandCaps.js.map +1 -1
  467. package/dist/pipeline/build.d.ts.map +1 -1
  468. package/dist/pipeline/build.js +754 -49
  469. package/dist/pipeline/build.js.map +1 -1
  470. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  471. package/dist/pipeline/buildHelpers.js +5 -1
  472. package/dist/pipeline/buildHelpers.js.map +1 -1
  473. package/dist/pipeline/buildMeasurement.d.ts +9 -0
  474. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  475. package/dist/pipeline/buildMeasurement.js +3 -2
  476. package/dist/pipeline/buildMeasurement.js.map +1 -1
  477. package/dist/pipeline/calloutLayout.d.ts +10 -1
  478. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  479. package/dist/pipeline/calloutLayout.js +45 -9
  480. package/dist/pipeline/calloutLayout.js.map +1 -1
  481. package/dist/pipeline/columnBalancing.d.ts +44 -7
  482. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  483. package/dist/pipeline/columnBalancing.js +167 -40
  484. package/dist/pipeline/columnBalancing.js.map +1 -1
  485. package/dist/pipeline/config.d.ts +2 -2
  486. package/dist/pipeline/config.d.ts.map +1 -1
  487. package/dist/pipeline/config.js +18 -3
  488. package/dist/pipeline/config.js.map +1 -1
  489. package/dist/pipeline/contentWarnings.d.ts +6 -0
  490. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  491. package/dist/pipeline/contentWarnings.js +32 -0
  492. package/dist/pipeline/contentWarnings.js.map +1 -1
  493. package/dist/pipeline/crossRefs.d.ts +12 -1
  494. package/dist/pipeline/crossRefs.d.ts.map +1 -1
  495. package/dist/pipeline/crossRefs.js +22 -14
  496. package/dist/pipeline/crossRefs.js.map +1 -1
  497. package/dist/pipeline/flowColumns.d.ts +2 -1
  498. package/dist/pipeline/flowColumns.d.ts.map +1 -1
  499. package/dist/pipeline/flowColumns.js +12 -2
  500. package/dist/pipeline/flowColumns.js.map +1 -1
  501. package/dist/pipeline/footnotes.d.ts.map +1 -1
  502. package/dist/pipeline/footnotes.js +3 -0
  503. package/dist/pipeline/footnotes.js.map +1 -1
  504. package/dist/pipeline/indexDirective.d.ts.map +1 -1
  505. package/dist/pipeline/indexDirective.js +15 -0
  506. package/dist/pipeline/indexDirective.js.map +1 -1
  507. package/dist/pipeline/lists.d.ts +1 -1
  508. package/dist/pipeline/lists.d.ts.map +1 -1
  509. package/dist/pipeline/lists.js +9 -1
  510. package/dist/pipeline/lists.js.map +1 -1
  511. package/dist/pipeline/measureContentBlock.d.ts +18 -0
  512. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  513. package/dist/pipeline/measureContentBlock.js +13 -3
  514. package/dist/pipeline/measureContentBlock.js.map +1 -1
  515. package/dist/pipeline/paragraphContainers.d.ts +4 -1
  516. package/dist/pipeline/paragraphContainers.d.ts.map +1 -1
  517. package/dist/pipeline/paragraphContainers.js +2 -0
  518. package/dist/pipeline/paragraphContainers.js.map +1 -1
  519. package/dist/pipeline/placement.d.ts +15 -0
  520. package/dist/pipeline/placement.d.ts.map +1 -1
  521. package/dist/pipeline/placement.js +36 -4
  522. package/dist/pipeline/placement.js.map +1 -1
  523. package/dist/pipeline/raggedLines.d.ts +8 -6
  524. package/dist/pipeline/raggedLines.d.ts.map +1 -1
  525. package/dist/pipeline/raggedLines.js +13 -10
  526. package/dist/pipeline/raggedLines.js.map +1 -1
  527. package/dist/pipeline/resourceLayout.d.ts +4 -0
  528. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  529. package/dist/pipeline/resourceLayout.js +22 -7
  530. package/dist/pipeline/resourceLayout.js.map +1 -1
  531. package/dist/pipeline/resourceNumbering.d.ts +6 -3
  532. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  533. package/dist/pipeline/resourceNumbering.js +36 -19
  534. package/dist/pipeline/resourceNumbering.js.map +1 -1
  535. package/dist/pipeline/toc.d.ts +0 -3
  536. package/dist/pipeline/toc.d.ts.map +1 -1
  537. package/dist/pipeline/toc.js +19 -2
  538. package/dist/pipeline/toc.js.map +1 -1
  539. package/dist/types.d.ts +411 -15
  540. package/dist/types.d.ts.map +1 -1
  541. package/dist/vdt.d.ts +90 -7
  542. package/dist/vdt.d.ts.map +1 -1
  543. package/dist/vdt.js.map +1 -1
  544. package/package.json +1 -1
@@ -0,0 +1,919 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { renderPageToCanvas } from '../canvas-backend';
3
+ import { beginMirroredFlow } from '../canvas-backend/mirrorFrame';
4
+ import { fillScriptWord } from '../canvas-backend/scriptText';
5
+ import { renderToHtml } from '../html-backend';
6
+ import { buildDocument } from '../pipeline';
7
+ import { clearMeasurementCache } from '../measure';
8
+ import { measureBlock } from '../measure/plain';
9
+ import { measureRichBlock } from '../measure/rich';
10
+ import { cutDoubledMarks, faceReadsScript, hanMarksOfLocale, isDoubledMark, isLetterless, pieceScriptContext, resolveScriptContext, scriptAt, scriptMark, scriptTextWidth, setMeasureScriptContext, withScriptMark, wordPieceScript } from '../measure/script';
11
+ import { measureTextWidth } from '../measure/canvas';
12
+ import { spanIsolates } from '../bidi';
13
+ import { parseInlineFormatting } from '../parse/inlineFormatting';
14
+ // #725: a word with no letter (`46.`, `1367)`) has no script of its own. A
15
+ // face may draw its punctuation by script (Amiri: a Latin full stop and
16
+ // brackets under `latn`, Arabic ones otherwise, of other widths), and each
17
+ // shaper handed such a word alone picks one by a rule of its own. Chrome
18
+ // 131's canvas took the font's default forms, Chrome 154's the Latin ones,
19
+ // so a document broke its lines otherwise from one to the other. The
20
+ // layout now gives the word the script of the text around it and every
21
+ // stage shapes it in that script.
22
+ const NBSP = '\u00A0';
23
+ const ELLIPSIS = '\u2026';
24
+ const EM_DASH = '\u2014';
25
+ const HAN_MARK = '\u4E00';
26
+ const BEH = '\u0628';
27
+ const VERSE_SIGN = '\u060E';
28
+ function ownScript(ch) {
29
+ if (/\p{Script=Han}/u.test(ch))
30
+ return 'Hani';
31
+ if (/\p{Script=Latin}/u.test(ch))
32
+ return 'Latn';
33
+ if (/\p{Script=Arabic}/u.test(ch))
34
+ return 'Arab';
35
+ if (/\p{Script=Greek}/u.test(ch))
36
+ return 'Grek';
37
+ if (/\p{Script=Cyrillic}/u.test(ch))
38
+ return 'Cyrl';
39
+ if (/\p{Script=Hebrew}/u.test(ch))
40
+ return 'Hebr';
41
+ return undefined;
42
+ }
43
+ /** Advances in a face like Amiri, in ems: the Latin forms of the full
44
+ * stop, the brackets and the guillemets are another width than the
45
+ * default ones. A face named `Plain` draws them one way. */
46
+ function advance(ch, script, sensitive, hebrewUnkerned = false, song = false) {
47
+ const latin = !sensitive || script === 'Latn';
48
+ if (ch === ' ' || ch === NBSP)
49
+ return 0.25;
50
+ // A face like Noto Serif SC (`Song`): its digits are proportional and
51
+ // its ellipsis three dots on the baseline under a Western script, half
52
+ // an em and a full em of centred dots otherwise (measured: 0.55 against
53
+ // 0.47 em, 0.9 against 1 em).
54
+ if (song) {
55
+ const western = script === 'Latn' || script === 'Grek' || script === 'Cyrl';
56
+ if (isDigit(ch))
57
+ return western ? 0.55 : 0.47;
58
+ if (ch === ELLIPSIS)
59
+ return western ? 0.9 : 1;
60
+ if (ch === EM_DASH || ownScript(ch) === 'Hani')
61
+ return 1;
62
+ if (ch === '.' || ch === '/' || ch === ':' || ch === '-')
63
+ return 0.3;
64
+ return 0.5;
65
+ }
66
+ // A kerned Latin face: a shaper applies no kerning in a Hebrew run of a
67
+ // face with no Hebrew positioning, so its marks come out wider there.
68
+ if (hebrewUnkerned && script === 'Hebr' && ch === ',')
69
+ return 0.23;
70
+ if (ch === '.')
71
+ return latin ? 0.2 : 0.3;
72
+ if (ch === '(' || ch === ')' || ch === '[' || ch === ']')
73
+ return latin ? 0.25 : 0.45;
74
+ if (ch === '\u00AB' || ch === '\u00BB')
75
+ return latin ? 0.47 : 0.41;
76
+ if (ch === ',')
77
+ return 0.2;
78
+ return 0.5;
79
+ }
80
+ const isRtlLetter = (ch) => /[\u0590-\u08FF]/.test(ch) && /\p{L}/u.test(ch);
81
+ const isLtrLetter = (ch) => /\p{L}/u.test(ch) && !isRtlLetter(ch);
82
+ const isDigit = (ch) => /[0-9]/.test(ch);
83
+ /** The browser whose canvas is measured and painted on: 131 reads the
84
+ * script of a word from all of it and sets a word with no letter in the
85
+ * font's default forms; 154 reads each bidi run alone and sets one with
86
+ * no letter in Latin forms. Both shape the words of a text one by one. */
87
+ let chrome = 154;
88
+ function shapedWidth(text, font, direction) {
89
+ const size = parseFloat(/(\d*\.?\d+)px/.exec(font)?.[1] ?? '10');
90
+ const sensitive = !font.includes('Plain');
91
+ let width = 0;
92
+ for (const word of text.split(/( )/)) {
93
+ if (word === '')
94
+ continue;
95
+ const chars = [...word];
96
+ // The runs a version itemises on its own.
97
+ const units = [];
98
+ if (chrome === 131)
99
+ units.push(chars);
100
+ else {
101
+ const rtl = direction === 'rtl';
102
+ const side = (ch) => (rtl ? (isLtrLetter(ch) || isDigit(ch) ? 'l' : 'r') : isRtlLetter(ch) ? 'r' : 'l');
103
+ for (const ch of chars) {
104
+ const last = units[units.length - 1];
105
+ if (last && side(last[0]) === side(ch))
106
+ last.push(ch);
107
+ else
108
+ units.push([ch]);
109
+ }
110
+ }
111
+ for (const unit of units) {
112
+ const first = unit.map(ownScript).find((s) => s !== undefined);
113
+ let script = first ?? (chrome === 131 ? 'DFLT' : 'Latn');
114
+ for (const ch of unit) {
115
+ script = ownScript(ch) ?? script;
116
+ // A current Chrome gives a run with no letter Latin forms, but
117
+ // leaves an ellipsis in it as the face draws it by default.
118
+ const drawn = first === undefined && chrome === 154 && ch === ELLIPSIS ? 'DFLT' : script;
119
+ width += advance(ch, drawn, sensitive, font.includes('Kerned'), font.includes('Song')) * size;
120
+ }
121
+ }
122
+ }
123
+ return width;
124
+ }
125
+ class BrowserCtx {
126
+ font = '10px Body';
127
+ direction = 'ltr';
128
+ letterSpacing = '0px';
129
+ measureText(s) {
130
+ return { width: shapedWidth(s, this.font, this.direction) };
131
+ }
132
+ }
133
+ globalThis.OffscreenCanvas = class {
134
+ getContext() {
135
+ return new BrowserCtx();
136
+ }
137
+ };
138
+ function on(version) {
139
+ chrome = version;
140
+ clearMeasurementCache();
141
+ }
142
+ beforeEach(() => {
143
+ on(154);
144
+ setMeasureScriptContext({ enabled: true, script: 'Latn' });
145
+ });
146
+ const AMIRI = '10px Amiri';
147
+ const PLAIN = '10px Plain';
148
+ describe('a word with no letter (#725)', () => {
149
+ it('is one of digits, punctuation or symbols alone', () => {
150
+ for (const word of ['46.', '1367)', '\u00BB.', '(', '2016).', '3.14', '\u2014', '12\u00A0000'])
151
+ expect(isLetterless(word)).toBe(true);
152
+ for (const word of ['a.', '(d.', '\u0661', BEH, 'n\u00BA', ''])
153
+ expect(isLetterless(word)).toBe(false);
154
+ });
155
+ it('alone, it is measured otherwise by each browser in a face that draws punctuation by script', () => {
156
+ on(131);
157
+ expect(measureTextWidth('46.', AMIRI)).toBeCloseTo(13, 6);
158
+ expect(measureTextWidth('46.', PLAIN)).toBeCloseTo(12, 6);
159
+ on(154);
160
+ expect(measureTextWidth('46.', AMIRI)).toBeCloseTo(12, 6);
161
+ });
162
+ it('beside its mark it is measured the same by both, in each script and direction', () => {
163
+ for (const version of [131, 154]) {
164
+ on(version);
165
+ expect(faceReadsScript(AMIRI)).toBe(true);
166
+ expect(faceReadsScript(PLAIN)).toBe(false);
167
+ expect(scriptTextWidth('46.', AMIRI, 'Latn', false)).toBeCloseTo(12, 6);
168
+ expect(scriptTextWidth('1367)', AMIRI, 'Latn', false)).toBeCloseTo(22.5, 6);
169
+ // Arabic forms, painted left to right (a number in an Arabic sentence)…
170
+ expect(scriptTextWidth('3.14', AMIRI, 'Arab', false)).toBeCloseTo(18, 6);
171
+ // …and right to left (its brackets).
172
+ expect(scriptTextWidth('(', AMIRI, 'Arab', true)).toBeCloseTo(4.5, 6);
173
+ expect(scriptTextWidth(').', AMIRI, 'Arab', true)).toBeCloseTo(7.5, 6);
174
+ // A script the face has no forms for takes its default ones.
175
+ expect(scriptTextWidth('46.', AMIRI, 'Grek', false)).toBeCloseTo(13, 6);
176
+ expect(scriptTextWidth('46.', AMIRI, 'Hebr', false)).toBeCloseTo(13, 6);
177
+ expect(scriptTextWidth(')', AMIRI, 'Hebr', true)).toBeCloseTo(4.5, 6);
178
+ }
179
+ });
180
+ it('a letter after a left-to-right word would not do on a current browser: the mark is a sign of the script with no direction', () => {
181
+ expect(scriptMark('Latn', false)).toEqual({ lead: '', trail: `${NBSP}n` });
182
+ expect(scriptMark('Arab', false)).toEqual({ lead: '', trail: `${NBSP}${VERSE_SIGN}` });
183
+ expect(scriptMark('Arab', true)).toEqual({ lead: `${BEH}${NBSP}`, trail: '' });
184
+ expect(scriptMark('Hebr', true)).toEqual({ lead: `\u05D0${NBSP}`, trail: '' });
185
+ expect(withScriptMark('46.', 'Latn', false)).toBe(`46.${NBSP}n`);
186
+ // An Arabic letter opens a right-to-left run, which Chrome 154 shapes
187
+ // apart from the number before it.
188
+ on(154);
189
+ const ctx = new BrowserCtx();
190
+ ctx.font = AMIRI;
191
+ expect(ctx.measureText(`3.14${BEH}`).width - ctx.measureText(BEH).width).toBeCloseTo(17, 6);
192
+ expect(ctx.measureText(`3.14${NBSP}${VERSE_SIGN}`).width - ctx.measureText(`${NBSP}${VERSE_SIGN}`).width).toBeCloseTo(18, 6);
193
+ });
194
+ });
195
+ describe('the script of a word with no letter (#725)', () => {
196
+ const at = (text, word, odd = false, isolates = [], ...fallback) => {
197
+ const start = text.indexOf(word);
198
+ return resolveScriptContext(text, start, start + word.length, odd, isolates, fallback.length > 0 ? fallback[0] : 'Latn');
199
+ };
200
+ it('the recipes: a Latin note set in an Arabic face', () => {
201
+ expect(at('in bayts 12, 16 and 46. The', '46.')).toBe('Latn');
202
+ expect(at('a mark between \u00AB \u00BB. The', '\u00BB.')).toBe('Latn');
203
+ expect(at('a mark between \u00AB \u00BB. The', '\u00AB')).toBe('Latn');
204
+ expect(at('al-Maqrizi (d. 1367) wrote', '1367)')).toBe('Latn');
205
+ expect(at('W3C, Layout Requirements (W3C, 2016).', '2016).')).toBe('Latn');
206
+ });
207
+ it('an Arabic sentence: the number and its brackets in Arabic forms', () => {
208
+ const text = '\u062A\u0648\u0641\u064A (1367) \u0647\u0646\u0627';
209
+ expect(at(text, '1367')).toBe('Arab');
210
+ expect(at(text, '(', true)).toBe('Arab');
211
+ expect(at(text, ')', true)).toBe('Arab');
212
+ });
213
+ it('a word that opens its text takes the script of the letter after it', () => {
214
+ expect(at('1367) was the year', '1367)')).toBe('Latn');
215
+ expect(at(`(1) ${BEH}${BEH}`, '(1)')).toBe('Arab');
216
+ });
217
+ it('a word in an isolate reads the isolate, not the text around it', () => {
218
+ // An English title set as a left-to-right isolate in an Arabic sentence.
219
+ const text = `${BEH}${BEH} Moby Dick (1851) ${BEH}${BEH}`;
220
+ const iso = [{ start: 3, end: 19, direction: 'ltr' }];
221
+ expect(text.slice(3, 19)).toBe('Moby Dick (1851)');
222
+ expect(at(text, '(1851)', false, iso)).toBe('Latn');
223
+ // The same number with nothing but itself in its isolate: the
224
+ // document's language decides.
225
+ const alone = `${BEH}${BEH} (1851) ${BEH}${BEH}`;
226
+ expect(at(alone, '(1851)', false, [{ start: 3, end: 9, direction: 'ltr' }], 'Arab')).toBe('Arab');
227
+ expect(at(alone, '(1851)', false, [{ start: 3, end: 9, direction: 'ltr' }], 'Latn')).toBe('Latn');
228
+ // And the isolates of real spans.
229
+ const spans = parseInlineFormatting(`${BEH}${BEH} :ltr[Moby Dick (1851)] ${BEH}`);
230
+ const joined = spans.map((s) => s.text).join('');
231
+ const ranges = spanIsolates(spans);
232
+ expect(ranges).toHaveLength(1);
233
+ expect(resolveScriptContext(joined, joined.indexOf('(1851)'), joined.indexOf('(1851)') + 6, false, ranges, 'Arab')).toBe('Latn');
234
+ });
235
+ it('a word that ends a Latin phrase in a right-to-left paragraph: its number Latin, its stop Arabic', () => {
236
+ // The stop takes the paragraph's direction (level 1): the layout cuts
237
+ // the word there, and each piece is asked on its own.
238
+ const text = `${BEH}${BEH} see 46.`;
239
+ expect(at(text, '46')).toBe('Latn');
240
+ expect(resolveScriptContext(text, text.length - 1, text.length, true, [], 'Arab')).toBe('Arab');
241
+ });
242
+ it('a word painted right to left takes a right-to-left script only', () => {
243
+ expect(at('abc ( def', '(', true, [], 'Latn')).toBeUndefined();
244
+ expect(at('abc ( def', '(', true, [], 'Arab')).toBe('Arab');
245
+ expect(at(`abc ( \u05D0\u05D1`, '(', true)).toBe('Hebr');
246
+ });
247
+ it('a paragraph with no letter takes the script of the document\'s language, or none', () => {
248
+ expect(at('1367)', '1367)', false, [], 'Latn')).toBe('Latn');
249
+ expect(at('1367)', '1367)', false, [], 'Arab')).toBe('Arab');
250
+ expect(at('1367)', '1367)', false, [], undefined)).toBeUndefined();
251
+ });
252
+ it('beside a script the engine does not act on, the word is left alone', () => {
253
+ expect(at('\u7EA2\u697C 1367) \u68A6', '1367)')).toBeUndefined();
254
+ expect(at('\u0E01\u0E02 46.', '46.')).toBeUndefined();
255
+ });
256
+ });
257
+ const pt = (value) => ({ value, unit: 'pt' });
258
+ // 72 dpi: 1 pt = 1 px.
259
+ const config = (family, extra = {}) => ({
260
+ page: { width: pt(190), height: pt(600), dpi: 72, margins: { top: pt(10), bottom: pt(10), left: pt(10), right: pt(10) } },
261
+ layout: { layoutType: 'single' },
262
+ header: { elements: [] },
263
+ footer: { elements: [] },
264
+ ...extra,
265
+ bodyText: { fontFamily: family, fontSize: pt(10), lineHeight: pt(14), firstLineIndent: pt(0), textAlign: 'left', hyphenation: { enabled: false }, ...extra.bodyText },
266
+ });
267
+ const NOTE = 'In bayts 12, 16 and 46. The mark stands between \u00AB \u00BB. He died (d. 1367) and the work (W3C, 2016). So it ends.';
268
+ const paragraph = (doc) => doc.blocks.find((b) => b.type === 'paragraph');
269
+ const segmentsOf = (doc) => paragraph(doc).lines.flatMap((l) => l.segments ?? []);
270
+ const layout = (doc) => JSON.parse(JSON.stringify(paragraph(doc).lines.map((l) => ({ text: l.text, width: l.bbox.width, segments: l.segments }))));
271
+ const build = (markdown, cfg) => buildDocument({ markdown }, cfg);
272
+ describe('a Latin note set in a face that draws punctuation by script (#725)', () => {
273
+ it('breaks its lines the same on Chrome 131 and on a current Chrome', () => {
274
+ on(131);
275
+ const old = build(NOTE, config('Amiri'));
276
+ on(154);
277
+ const current = build(NOTE, config('Amiri'));
278
+ expect(layout(current)).toEqual(layout(old));
279
+ expect(paragraph(current).lines.length).toBeGreaterThan(2);
280
+ // Each word with no letter in its Latin forms, at their width.
281
+ const words = Object.fromEntries(segmentsOf(current).filter((s) => s.scriptContext).map((s) => [s.text, [s.scriptContext, +s.width.toFixed(4)]]));
282
+ expect(words).toEqual({
283
+ '12,': ['Latn', 12],
284
+ '16': ['Latn', 10],
285
+ '46.': ['Latn', 12],
286
+ '\u00AB': ['Latn', 4.7],
287
+ '\u00BB.': ['Latn', 6.7],
288
+ '1367)': ['Latn', 22.5],
289
+ '2016).': ['Latn', 24.5],
290
+ });
291
+ // No other segment says anything.
292
+ expect(segmentsOf(current).filter((s) => !s.scriptContext && s.kind === 'text').every((s) => /\p{L}/u.test(s.text))).toBe(true);
293
+ });
294
+ it('with the rule off, each browser measures them its own way and the lines differ', () => {
295
+ const off = config('Amiri', { bodyText: { scriptContext: false } });
296
+ on(131);
297
+ const old = build(NOTE, off);
298
+ on(154);
299
+ const current = build(NOTE, off);
300
+ expect(segmentsOf(old).some((s) => s.scriptContext)).toBe(false);
301
+ expect(segmentsOf(current).some((s) => s.scriptContext)).toBe(false);
302
+ const width = (doc, text) => segmentsOf(doc).find((s) => s.text === text).width;
303
+ expect(width(old, '46.')).toBeCloseTo(13, 6);
304
+ expect(width(current, '46.')).toBeCloseTo(12, 6);
305
+ expect(width(old, '1367)')).toBeCloseTo(24.5, 6);
306
+ expect(width(current, '1367)')).toBeCloseTo(22.5, 6);
307
+ expect(layout(current)).not.toEqual(layout(old));
308
+ // On a current Chrome the rule gives what that browser gave alone.
309
+ const bare = (value) => JSON.parse(JSON.stringify(value, (key, v) => (key === 'scriptContext' ? undefined : v)));
310
+ expect(bare(layout(build(NOTE, config('Amiri'))))).toEqual(layout(current));
311
+ });
312
+ it('a face that draws them one way under every script is laid out as before, with no field', () => {
313
+ for (const version of [131, 154]) {
314
+ on(version);
315
+ const doc = build(NOTE, config('Plain'));
316
+ expect(segmentsOf(doc).some((s) => 'scriptContext' in s)).toBe(false);
317
+ expect(layout(doc)).toEqual(layout(build(NOTE, config('Plain', { bodyText: { scriptContext: false } }))));
318
+ }
319
+ });
320
+ it('a kerned Latin face, wider in a Hebrew run alone, is not taken for one that draws marks by script', () => {
321
+ const KERNED = '10px Plain Kerned';
322
+ const ctx = new BrowserCtx();
323
+ ctx.font = KERNED;
324
+ // Beside the Hebrew mark the comma is 0.3 px wider (the marks are as
325
+ // wide as each other here)…
326
+ expect(ctx.measureText(`12,${NBSP}\uFB29`).width - ctx.measureText(`12,${NBSP}n`).width).toBeCloseTo(0.3, 6);
327
+ // …and the face is still left alone.
328
+ expect(faceReadsScript(KERNED)).toBe(false);
329
+ const doc = build(NOTE, config('Plain Kerned'));
330
+ expect(segmentsOf(doc).some((s) => 'scriptContext' in s)).toBe(false);
331
+ });
332
+ it('a paragraph with no formatting goes to the word-by-word breaker only when it holds such a word in such a face', () => {
333
+ const opts = { textAlign: 'left' };
334
+ // Pretext's lines carry no segments in this path; the breaker's do.
335
+ const withWord = measureBlock('It ends in 46. Then.', AMIRI, 400, 14, opts);
336
+ expect(withWord.lines[0].segments?.find((s) => s.text === '46.')?.scriptContext).toBe('Latn');
337
+ const none = measureBlock('It ends in forty. Then.', AMIRI, 400, 14, opts);
338
+ expect((none.lines[0].segments ?? []).some((s) => s.scriptContext)).toBe(false);
339
+ const plain = measureBlock('It ends in 46. Then.', PLAIN, 400, 14, opts);
340
+ expect((plain.lines[0].segments ?? []).some((s) => s.scriptContext)).toBe(false);
341
+ setMeasureScriptContext({ enabled: false });
342
+ const off = measureBlock('It ends in 46. Then.', AMIRI, 400, 14, opts);
343
+ expect((off.lines[0].segments ?? []).some((s) => s.scriptContext)).toBe(false);
344
+ });
345
+ });
346
+ const AR = '\u062A\u0648\u0641\u064A';
347
+ const RTL = (extra = {}) => config('Amiri', { direction: 'rtl', locale: 'ar', ...extra });
348
+ describe('an Arabic sentence (#725)', () => {
349
+ it('(1367) keeps its Arabic brackets: three pieces, each in the Arabic script', () => {
350
+ for (const version of [131, 154]) {
351
+ on(version);
352
+ const doc = build(`${AR} (1367) ${AR}`, RTL());
353
+ const pieces = segmentsOf(doc).filter((s) => s.scriptContext);
354
+ expect(pieces.map((s) => [s.text, s.scriptContext, s.rtl ?? false, +s.width.toFixed(4)])).toEqual([
355
+ ['(', 'Arab', true, 4.5],
356
+ ['1367', 'Arab', false, 20],
357
+ [')', 'Arab', true, 4.5],
358
+ ]);
359
+ }
360
+ });
361
+ it('a Latin phrase that ends the paragraph: its number Latin, its stop Arabic, the same on both browsers', () => {
362
+ on(131);
363
+ const old = build(`${AR} see 46.`, RTL());
364
+ on(154);
365
+ const current = build(`${AR} see 46.`, RTL());
366
+ expect(layout(current)).toEqual(layout(old));
367
+ expect(segmentsOf(current).filter((s) => s.scriptContext).map((s) => [s.text, s.scriptContext, s.rtl ?? false, +s.width.toFixed(4)])).toEqual([
368
+ ['46', 'Latn', false, 10],
369
+ ['.', 'Arab', true, 3],
370
+ ]);
371
+ });
372
+ it('a paragraph with no letter takes the script of the document\'s language', () => {
373
+ const arabic = build('1367).', RTL());
374
+ expect(segmentsOf(arabic).filter((s) => s.kind === 'text').every((s) => s.scriptContext === 'Arab')).toBe(true);
375
+ const english = build('1367).', config('Amiri'));
376
+ expect(segmentsOf(english).map((s) => [s.text, s.scriptContext])).toEqual([['1367).', 'Latn']]);
377
+ });
378
+ });
379
+ function paintingCtx() {
380
+ const seen = [];
381
+ const measure = new BrowserCtx();
382
+ const state = { font: '10px Body', textAlign: 'left', direction: 'ltr', textBaseline: 'alphabetic', letterSpacing: '0px', fillStyle: '' };
383
+ // The transform as far as a mirror goes: x' = a·x + e.
384
+ let a = 1;
385
+ let e = 0;
386
+ let clip;
387
+ let pending;
388
+ const stack = [];
389
+ const api = {
390
+ save() { stack.push({ a, e, clip, state: { ...state } }); },
391
+ restore() {
392
+ const top = stack.pop();
393
+ if (!top)
394
+ return;
395
+ ({ a, e, clip } = top);
396
+ Object.assign(state, top.state);
397
+ },
398
+ transform(ta, _b, _c, _d, te) { e += a * te; a *= ta; },
399
+ translate(x) { e += a * x; },
400
+ scale(x) { a *= x; },
401
+ beginPath() { pending = undefined; },
402
+ rect(x, _y, w) { pending = [x, x + w]; },
403
+ clip() {
404
+ if (!pending)
405
+ return;
406
+ const [x0, x1] = [a * pending[0] + e, a * pending[1] + e];
407
+ clip = [Math.min(x0, x1), Math.max(x0, x1)];
408
+ },
409
+ measureText(s) {
410
+ measure.font = state.font;
411
+ measure.direction = state.direction;
412
+ return measure.measureText(s);
413
+ },
414
+ fillText(text, x, y) {
415
+ seen.push({ text, x: a * x + e, y, direction: state.direction, align: state.textAlign, spacing: parseFloat(state.letterSpacing) || 0, ...(clip ? { clip } : {}), flip: a });
416
+ },
417
+ };
418
+ const ctx = new Proxy(state, {
419
+ get(target, key) {
420
+ if (typeof key === 'string' && key in api)
421
+ return api[key];
422
+ if (typeof key === 'string' && key in target)
423
+ return target[key];
424
+ return () => undefined;
425
+ },
426
+ set(target, key, value) {
427
+ if (typeof key === 'string' && key in api)
428
+ api[key] = value;
429
+ else
430
+ target[key] = value;
431
+ return true;
432
+ },
433
+ has(target, key) { return typeof key === 'string' && (key in api || key in target); },
434
+ deleteProperty() { return true; },
435
+ getOwnPropertyDescriptor() { return undefined; },
436
+ });
437
+ const canvas = { width: 0, height: 0, getContext: () => ctx };
438
+ return { canvas, ctx, seen };
439
+ }
440
+ /** The mirror a right-to-left page paints its flow through. */
441
+ const mirrorOf = (doc) => doc.pages[0].flow?.mirror;
442
+ function painted(doc) {
443
+ const { canvas, seen } = paintingCtx();
444
+ renderPageToCanvas(doc.pages[0], doc, canvas);
445
+ return seen;
446
+ }
447
+ describe('a word with no letter on canvas (#725)', () => {
448
+ it('is painted beside its mark, the mark clipped away at the word\'s advance', () => {
449
+ const { ctx, seen } = paintingCtx();
450
+ ctx.font = AMIRI;
451
+ fillScriptWord(ctx, '46.', 'Latn', false, 40, 50, (text, x, y) => ctx.fillText(text, x, y));
452
+ expect(seen).toHaveLength(1);
453
+ const [p] = seen;
454
+ expect(p.text).toBe(`46.${NBSP}n`);
455
+ expect([p.x, p.y, p.direction]).toEqual([40, 50, 'ltr']);
456
+ // The word is 12 px wide; the clip ends half a space past it, before
457
+ // the mark's letter (a space further on).
458
+ expect(p.clip[1]).toBeCloseTo(40 + 12 + 1.25, 6);
459
+ expect(p.clip[0]).toBeLessThan(0);
460
+ });
461
+ it('a right-to-left word has its mark before it, which stands on its right', () => {
462
+ const { ctx, seen } = paintingCtx();
463
+ ctx.font = AMIRI;
464
+ ctx.direction = 'rtl';
465
+ fillScriptWord(ctx, ').', 'Arab', true, 40, 50, (text, x, y) => ctx.fillText(text, x, y));
466
+ const [p] = seen;
467
+ expect(p.text).toBe(`${BEH}${NBSP}).`);
468
+ expect([p.x, p.direction, p.align]).toEqual([40, 'rtl', 'left']);
469
+ expect(p.clip[1]).toBeCloseTo(40 + 7.5 + 1.25, 6);
470
+ });
471
+ it('inside a mirrored frame the word is turned about its own box, not the box of the word and its mark', () => {
472
+ const { ctx, seen } = paintingCtx();
473
+ ctx.font = AMIRI;
474
+ const end = beginMirroredFlow(ctx, 200);
475
+ // A word with a letter beside it, turned by the frame as always.
476
+ ctx.fillText('and', 20, 50);
477
+ fillScriptWord(ctx, '46.', 'Latn', false, 40, 50, (text, x, y) => ctx.fillText(text, x, y));
478
+ end();
479
+ const [and, word] = seen;
480
+ // "and" is 15 px wide: its box [20, 35] of the flow is [165, 180] on the sheet.
481
+ expect(and.x).toBeCloseTo(165, 6);
482
+ // "46." is 12 px wide: its box [40, 52] is [148, 160] on the sheet, and
483
+ // the text starts there, unmirrored.
484
+ expect(word.text).toBe(`46.${NBSP}n`);
485
+ expect(word.x).toBeCloseTo(148, 6);
486
+ expect(word.flip).toBe(1);
487
+ expect(word.clip[1]).toBeCloseTo(160 + 1.25, 6);
488
+ // Turned about the box of the two it would start a mark's width off.
489
+ expect(word.x).not.toBeCloseTo(200 - 40 - (12 + 7.5), 3);
490
+ });
491
+ it('a page paints each such word in its script, on both browsers', () => {
492
+ for (const version of [131, 154]) {
493
+ on(version);
494
+ const doc = build(NOTE, config('Amiri'));
495
+ const texts = painted(doc).map((p) => p.text);
496
+ for (const word of ['12,', '16', '46.', '\u00AB', '\u00BB.', '1367)', '2016).']) {
497
+ expect(texts).toContain(`${word}${NBSP}n`);
498
+ expect(texts).not.toContain(word);
499
+ }
500
+ // Every marked text is clipped half a space past its word.
501
+ const widths = { '12,': 12, '16': 10, '46.': 12, '\u00AB': 4.7, '\u00BB.': 6.7, '1367)': 22.5, '2016).': 24.5 };
502
+ for (const p of painted(doc).filter((q) => q.text.endsWith(`${NBSP}n`))) {
503
+ expect(p.clip[1] - p.x).toBeCloseTo(widths[p.text.slice(0, -2)] + 1.25, 6);
504
+ }
505
+ }
506
+ });
507
+ it('a right-to-left page: the pieces of (1367) where the layout put them, each in the Arabic script', () => {
508
+ const doc = build(`${AR} (1367) ${AR}`, RTL());
509
+ expect(mirrorOf(doc)).toBeDefined();
510
+ const seen = painted(doc);
511
+ const open = seen.find((p) => p.text === `${BEH}${NBSP}(`);
512
+ const close = seen.find((p) => p.text === `${BEH}${NBSP})`);
513
+ const number = seen.find((p) => p.text === `1367${NBSP}${VERSE_SIGN}`);
514
+ expect([open.direction, close.direction, number.direction]).toEqual(['rtl', 'rtl', 'ltr']);
515
+ expect(seen.every((p) => p.flip === 1)).toBe(true);
516
+ // On the sheet the sentence reads from the right: the opening bracket
517
+ // right of the number, the closing one left of it, each 4.5 px wide
518
+ // and the number 20.
519
+ expect(open.x).toBeCloseTo(number.x + 20, 6);
520
+ expect(close.x).toBeCloseTo(number.x - 4.5, 6);
521
+ // And the Arabic words on either side, clear of them by a space.
522
+ const words = seen.filter((p) => p.text === AR).sort((p, q) => p.x - q.x);
523
+ expect(words).toHaveLength(2);
524
+ expect(words[1].x).toBeCloseTo(open.x + 4.5 + 2.5, 6);
525
+ expect(close.x).toBeCloseTo(words[0].x + 20 + 2.5, 6);
526
+ });
527
+ it('a left-to-right block of a right-to-left page, and a right-to-left one of a left-to-right page', () => {
528
+ // The same Latin note: on a left-to-right page, and as a `dir=ltr`
529
+ // block of a mirrored one. Each word stands the same distance from
530
+ // the start of its line, the line running from the sheet's left.
531
+ const plain = build('It ends in 46. Then.', config('Amiri'));
532
+ const mirrored = build(':::paragraphs{dir=ltr}\nIt ends in 46. Then.\n:::', RTL());
533
+ expect(mirrorOf(mirrored)).toBeDefined();
534
+ // The mirrored page paints its line from the other end: read both
535
+ // from the sheet's left.
536
+ const a = painted(plain).sort((p, q) => p.x - q.x);
537
+ const b = painted(mirrored).sort((p, q) => p.x - q.x);
538
+ expect(b.map((p) => p.text)).toEqual(a.map((p) => p.text));
539
+ expect(b.map((p) => p.text)).toContain(`46.${NBSP}n`);
540
+ const fromStart = (list) => list.map((p) => +(p.x - list[0].x).toFixed(6));
541
+ expect(fromStart(b)).toEqual(fromStart(a));
542
+ expect(b.every((p) => p.flip === 1)).toBe(true);
543
+ // And an Arabic block of a left-to-right page: no mirror, the same pieces.
544
+ const quoted = build(`:::paragraphs{dir=rtl}\n${AR} (1367) ${AR}\n:::`, config('Amiri'));
545
+ expect(mirrorOf(quoted)).toBeUndefined();
546
+ const c = painted(quoted);
547
+ const open = c.find((p) => p.text === `${BEH}${NBSP}(`);
548
+ const number = c.find((p) => p.text === `1367${NBSP}${VERSE_SIGN}`);
549
+ const close = c.find((p) => p.text === `${BEH}${NBSP})`);
550
+ expect(open.x).toBeCloseTo(number.x + 20, 6);
551
+ expect(close.x).toBeCloseTo(number.x - 4.5, 6);
552
+ });
553
+ });
554
+ describe('a word with no letter in HTML (#725)', () => {
555
+ const MARK = (mark) => `<span aria-hidden="true" style="font-size:0;-webkit-user-select:none;user-select:none;">${mark}</span>`;
556
+ /** The text a reader selects and copies: the marks are no part of it. */
557
+ const copied = (html) => html.replace(/<span aria-hidden="true" style="font-size:0;[^>]*>[^<]*<\/span>/g, '').replace(/<[^>]+>/g, '');
558
+ it('holds the mark of its script in a box of no size, after it', () => {
559
+ const html = renderToHtml(build('It ends in 46. Then.', config('Amiri')));
560
+ expect(html).toContain(`>46.${MARK(`${NBSP}n`)}</span>`);
561
+ expect(copied(html)).toContain('It ends in 46. Then.');
562
+ expect(copied(html)).not.toContain(`${NBSP}n`);
563
+ });
564
+ it('a right-to-left word has it before it; the number between the brackets after it', () => {
565
+ const html = renderToHtml(build(`${AR} (1367) ${AR}`, RTL()));
566
+ expect(html).toContain(`>${MARK(`${BEH}${NBSP}`)}(</span>`);
567
+ expect(html).toContain(`>1367${MARK(`${NBSP}${VERSE_SIGN}`)}</span>`);
568
+ expect(html).toContain(`>${MARK(`${BEH}${NBSP}`)})</span>`);
569
+ expect(copied(html)).toContain(`${AR} (1367) ${AR}`);
570
+ });
571
+ it('a face that draws them one way, and a document with the rule off, carry no mark', () => {
572
+ expect(renderToHtml(build(NOTE, config('Plain')))).not.toContain('font-size:0');
573
+ expect(renderToHtml(build(NOTE, config('Amiri', { bodyText: { scriptContext: false } })))).not.toContain('font-size:0');
574
+ });
575
+ });
576
+ describe('a word with no letter on a tracked line (#725)', () => {
577
+ // Level-2 headings tracked half a point after each letter.
578
+ const tracked = (cfg) => ({ ...cfg, page: { ...cfg.page, width: pt(400) }, headings: { fontFamily: 'Amiri', levels: [{ level: 2, letterSpacing: pt(0.5), fontSize: pt(10) }] } });
579
+ const heading = (doc) => doc.blocks.find((b) => b.type === 'heading');
580
+ const headingSegments = (doc) => heading(doc).lines.flatMap((l) => l.segments ?? []);
581
+ it('in a Latin context it is tracked as any word, the same on both browsers', () => {
582
+ for (const version of [131, 154]) {
583
+ on(version);
584
+ const doc = build('## It ends in 46.', tracked(config('Amiri')));
585
+ const seg = headingSegments(doc).find((s) => s.text === '46.');
586
+ expect(seg.scriptContext).toBe('Latn');
587
+ // 12 px in its Latin forms and half a point after each character.
588
+ expect(seg.width).toBeCloseTo(12 + 3 * 0.5, 6);
589
+ const word = painted(doc).find((p) => p.text === `46.${NBSP}n`);
590
+ expect(word.spacing).toBe(0.5);
591
+ }
592
+ });
593
+ it('in the Arabic script it takes no tracking, as the Arabic words around it take none', () => {
594
+ for (const version of [131, 154]) {
595
+ on(version);
596
+ const doc = build(`## ${AR} (1367) ${AR}`, tracked(RTL()));
597
+ expect(heading(doc).letterSpacing ?? heading(doc).lines[0].letterSpacing).toBeCloseTo(0.5, 6);
598
+ const pieces = headingSegments(doc).filter((s) => s.scriptContext);
599
+ // Measured with no tracking: a current browser spaces no run of a
600
+ // cursive script, where Chrome 131 did.
601
+ expect(pieces.map((s) => [s.text, s.scriptContext, +s.width.toFixed(4)])).toEqual([['(', 'Arab', 4.5], ['1367', 'Arab', 20], [')', 'Arab', 4.5]]);
602
+ const seen = painted(doc);
603
+ for (const text of [`${BEH}${NBSP}(`, `1367${NBSP}${VERSE_SIGN}`, `${BEH}${NBSP})`]) {
604
+ expect(seen.find((p) => p.text === text).spacing, text).toBe(0);
605
+ }
606
+ // And the HTML says so on each of them.
607
+ const html = renderToHtml(doc);
608
+ expect(html.match(/letter-spacing:0;[^"]*"><span aria-hidden="true" style="font-size:0;/g) ?? []).toHaveLength(2);
609
+ expect(html).toMatch(/letter-spacing:0;[^"]*">1367<span aria-hidden="true"/);
610
+ }
611
+ });
612
+ });
613
+ describe('a right-to-left word with a mark at its edge (#725)', () => {
614
+ // The closing bracket of `(1367\u0645)` goes with the letter before it:
615
+ // one right-to-left word. Measured in a left-to-right context the bracket
616
+ // falls outside the word's run, and a current browser shapes it alone.
617
+ const MEEM = '\u0645';
618
+ it('is measured as it is painted, right to left: the same width on both browsers', () => {
619
+ const widths = (version, cfg) => {
620
+ on(version);
621
+ return segmentsOf(build(`${AR} (1367${MEEM}) ${AR}`, cfg)).filter((s) => s.text === `${MEEM})`).map((s) => +s.width.toFixed(4));
622
+ };
623
+ // The letter and the Arabic bracket: 5 + 4.5 px.
624
+ expect(widths(131, RTL())).toEqual([9.5]);
625
+ expect(widths(154, RTL())).toEqual([9.5]);
626
+ // With the rule off a current browser measures the bracket in its
627
+ // Latin form, 2 px narrower than the canvas paints it.
628
+ const off = RTL({ bodyText: { scriptContext: false } });
629
+ expect(widths(131, off)).toEqual([9.5]);
630
+ expect(widths(154, off)).toEqual([7.5]);
631
+ });
632
+ it('carries no field: it is painted as any right-to-left word', () => {
633
+ const seg = segmentsOf(build(`${AR} (1367${MEEM}) ${AR}`, RTL())).find((s) => s.text === `${MEEM})`);
634
+ expect(seg.rtl).toBe(true);
635
+ expect('scriptContext' in seg).toBe(false);
636
+ expect('scriptRtl' in seg).toBe(false);
637
+ });
638
+ });
639
+ describe('the leader of a contents entry (#725)', () => {
640
+ const MD = '# Contents {style="front"}\n\n:::toc\n\n# First\n\nText.\n\n# Second\n\nText.';
641
+ const book = (cfg) => ({ ...cfg, page: { ...cfg.page, width: pt(300) }, headingStyles: [{ id: 'front', numbered: false, toc: false }], headings: { fontFamily: 'Amiri' }, toc: { pageNumber: { fontFamily: 'Amiri' } } });
642
+ const leaders = (doc) => doc.blocks.filter((b) => b.tocEntry !== undefined).flatMap((b) => b.lines.flatMap((l) => l.segments ?? [])).filter((s) => s.leader === 'text');
643
+ it('is set in the script of the document\'s language: the same dots on both browsers', () => {
644
+ const dots = (version, cfg) => {
645
+ on(version);
646
+ return leaders(build(MD, book(cfg))).map((s) => [s.scriptContext, s.text.length, +s.width.toFixed(3)]);
647
+ };
648
+ const latin = dots(131, config('Amiri'));
649
+ expect(latin.length).toBeGreaterThan(0);
650
+ expect(latin.every(([script]) => script === 'Latn')).toBe(true);
651
+ expect(dots(154, config('Amiri'))).toEqual(latin);
652
+ // An Arabic book: the Arabic stop, half as wide again, so fewer dots.
653
+ const arabic = dots(131, RTL());
654
+ expect(arabic.every(([script]) => script === 'Arab')).toBe(true);
655
+ expect(dots(154, RTL())).toEqual(arabic);
656
+ expect(arabic[0][1]).toBeLessThan(latin[0][1]);
657
+ // With the rule off each browser counts its own.
658
+ const off = RTL({ bodyText: { scriptContext: false } });
659
+ expect(dots(131, off)[0][1]).not.toBe(dots(154, off)[0][1]);
660
+ expect(dots(131, off).every(([script]) => script === undefined)).toBe(true);
661
+ });
662
+ it('a face that draws its stop one way keeps its leader as it was', () => {
663
+ const doc = build(MD, { ...book(config('Plain')), headings: { fontFamily: 'Plain' }, toc: { pageNumber: { fontFamily: 'Plain' } } });
664
+ expect(leaders(doc).length).toBeGreaterThan(0);
665
+ expect(leaders(doc).some((s) => 'scriptContext' in s)).toBe(false);
666
+ });
667
+ });
668
+ // A word that has a letter is one script run, and each piece it is painted
669
+ // in belongs to it. The joints of a DOI or a web address, the hyphen of a
670
+ // compound and a cut in a word wider than its line leave pieces with no
671
+ // letter (`10.1006/`, `.2001`, `19`), each a text of its own to a
672
+ // renderer. Their places are measured inside the word, in its script;
673
+ // painted alone they took the browser's pick, and in a face that draws
674
+ // digits by script (Noto Serif SC: 0.47 em by default, 0.55 em under
675
+ // `latn`) the difference was air after the next piece.
676
+ const SONG = '10px Song';
677
+ const DOI = 'DOI: [10.1006/ijhc.2001.0458](https://doi.org/10.1006/ijhc.2001.0458) end.';
678
+ const chinese = (extra = {}) => config('Song', { locale: 'zh-Hans', ...extra });
679
+ const pieces = (doc) => segmentsOf(doc).filter((seg) => seg.kind === 'text').map((seg) => [seg.text, seg.scriptContext, +seg.width.toFixed(4)]);
680
+ describe('the pieces of a word that has a letter (#725)', () => {
681
+ it('a word of letters and marks has one script for its pieces with no letter: its first letter\'s', () => {
682
+ expect(wordPieceScript('10.1006/ijhc.2001.0458')).toBe('Latn');
683
+ expect(wordPieceScript('COVID-19')).toBe('Latn');
684
+ expect(wordPieceScript(`37${NBSP}\u00B0C`)).toBe('Latn');
685
+ expect(wordPieceScript('\u03B1-2')).toBe('Grek');
686
+ // Letters only, no letter at all, or a script the engine does not act on.
687
+ expect(wordPieceScript('ijhc')).toBeUndefined();
688
+ expect(wordPieceScript('10.1006/')).toBeUndefined();
689
+ expect(wordPieceScript('\u0E01-2')).toBeUndefined();
690
+ });
691
+ it('a piece with no letter takes the script of the letter before it in the word, else the word\'s', () => {
692
+ const word = '10.1006/ijhc.2001.0458';
693
+ expect(pieceScriptContext(word, 0, 8, 'Latn')).toBe('Latn');
694
+ expect(pieceScriptContext(word, 12, 17, 'Latn')).toBe('Latn');
695
+ // A piece with a letter is shaped by its own.
696
+ expect(pieceScriptContext(word, 8, 12, 'Latn')).toBeUndefined();
697
+ expect(pieceScriptContext(word, 0, 12, 'Latn')).toBeUndefined();
698
+ // Two scripts in one word: the nearer letter before the piece.
699
+ expect(pieceScriptContext('ab-\u03B1\u03B2-19', 6, 8, 'Latn')).toBe('Grek');
700
+ expect(scriptAt('ab-\u03B1\u03B2-19', 3, 'Latn')).toBe('Latn');
701
+ // A word with no script for its pieces says nothing of them.
702
+ expect(pieceScriptContext(word, 0, 8, undefined)).toBeUndefined();
703
+ });
704
+ it('a DOI: every piece in its Latin forms, each box the width of its own ink, on both browsers', () => {
705
+ for (const version of [131, 154]) {
706
+ on(version);
707
+ expect(faceReadsScript(SONG)).toBe(true);
708
+ expect(pieces(build(DOI, chinese()))).toEqual([
709
+ ['DOI:', undefined, 18],
710
+ // Six digits at 5.5 px, the stop and the stroke at 3.
711
+ ['10.1006/', 'Latn', 39],
712
+ ['ijhc', undefined, 20],
713
+ ['.2001', 'Latn', 25],
714
+ ['.0458', 'Latn', 25],
715
+ ['end.', undefined, 18],
716
+ ]);
717
+ }
718
+ });
719
+ it('with the rule off the lines are what postext 1.27 measured, the first piece alone', () => {
720
+ const off = chinese({ bodyText: { scriptContext: false } });
721
+ on(131);
722
+ // `10.1006/` in the default forms (six digits at 4.7 px), and the
723
+ // 4.8 px it lacks left after `ijhc`.
724
+ expect(pieces(build(DOI, off))).toEqual([
725
+ ['DOI:', undefined, 18],
726
+ ['10.1006/', undefined, 34.2],
727
+ ['ijhc', undefined, 24.8],
728
+ ['.2001', undefined, 25],
729
+ ['.0458', undefined, 25],
730
+ ['end.', undefined, 18],
731
+ ]);
732
+ on(154);
733
+ expect(pieces(build(DOI, off)).map(([text, script, width]) => [text, script, width])).toEqual([
734
+ ['DOI:', undefined, 18],
735
+ ['10.1006/', undefined, 39],
736
+ ['ijhc', undefined, 20],
737
+ ['.2001', undefined, 25],
738
+ ['.0458', undefined, 25],
739
+ ['end.', undefined, 18],
740
+ ]);
741
+ });
742
+ it('an address with no letter at all takes the script of the sentence, its pieces too', () => {
743
+ for (const version of [131, 154]) {
744
+ on(version);
745
+ const doc = build('See [10.1006/2001.0458](https://doi.org/10.1006/2001.0458) there.', chinese());
746
+ expect(pieces(doc).filter(([, script]) => script)).toEqual([['10.1006/', 'Latn', 39], ['2001', 'Latn', 22], ['.0458', 'Latn', 25]]);
747
+ }
748
+ });
749
+ it('the canvas and the HTML paint each such piece beside the mark of its script', () => {
750
+ for (const version of [131, 154]) {
751
+ on(version);
752
+ const doc = build(DOI, chinese());
753
+ const seen = painted(doc);
754
+ const texts = seen.map((p) => p.text);
755
+ expect(texts).toEqual(['DOI:', `10.1006/${NBSP}n`, 'ijhc', `.2001${NBSP}n`, `.0458${NBSP}n`, 'end.']);
756
+ // Each piece starts where the one before it ends: no air in the word.
757
+ const at = (text) => seen.find((p) => p.text === text).x;
758
+ expect(at('ijhc') - at(`10.1006/${NBSP}n`)).toBeCloseTo(39, 6);
759
+ expect(at(`.2001${NBSP}n`) - at('ijhc')).toBeCloseTo(20, 6);
760
+ expect(at(`.0458${NBSP}n`) - at(`.2001${NBSP}n`)).toBeCloseTo(25, 6);
761
+ const html = renderToHtml(doc);
762
+ const mark = `<span aria-hidden="true" style="font-size:0;-webkit-user-select:none;user-select:none;">${NBSP}n</span>`;
763
+ for (const piece of ['10.1006/', '.2001', '.0458'])
764
+ expect(html).toContain(`>${piece}${mark}</`);
765
+ expect(html).not.toContain(`>ijhc${mark}`);
766
+ }
767
+ });
768
+ it('both breakers: a compound broken after its hyphen, the number on the next line in the word\'s script', () => {
769
+ const spans = [{ text: 'COVID-19 cc', bold: false, italic: false }];
770
+ for (const optimal of [true, false]) {
771
+ for (const version of [131, 154]) {
772
+ on(version);
773
+ // The compound is 39 px wide in a measure of 35: it parts at its hyphen.
774
+ const block = measureRichBlock(spans, SONG, SONG, SONG, SONG, 35, 14, { textAlign: 'left', optimal, hyphenate: false });
775
+ expect(block.lines.map((l) => l.text), `${optimal} ${version}`).toEqual(['COVID-', '19 cc']);
776
+ const number = block.lines[1].segments[0];
777
+ expect([number.text, number.scriptContext, +number.width.toFixed(4)]).toEqual(['19', 'Latn', 11]);
778
+ const head = block.lines[0].segments.find((seg) => seg.text === 'COVID-');
779
+ expect(head.scriptContext).toBeUndefined();
780
+ expect(head.width).toBeCloseTo(28, 6);
781
+ }
782
+ }
783
+ });
784
+ it('a word cut where its line ends: what is left of it with no letter keeps the word\'s script', () => {
785
+ for (const version of [131, 154]) {
786
+ on(version);
787
+ // Wider than any line: cut between two characters, a hyphen added.
788
+ const block = measureRichBlock([{ text: 'ab12345678901234567890', bold: false, italic: false }], SONG, SONG, SONG, SONG, 60, 14, { textAlign: 'left', optimal: false, hyphenate: false });
789
+ expect(block.lines.length).toBeGreaterThan(1);
790
+ const rest = block.lines.slice(1).flatMap((l) => l.segments ?? []).filter((seg) => seg.kind === 'text');
791
+ expect(rest.length).toBeGreaterThan(0);
792
+ for (const seg of rest) {
793
+ expect(seg.scriptContext, seg.text).toBe('Latn');
794
+ // Its digits at 5.5 px, and the hyphen a cut adds at 3.
795
+ const digits = seg.text.replace(/-/g, '').length;
796
+ expect(seg.width).toBeCloseTo(digits * 5.5 + (seg.text.length - digits) * 3, 6);
797
+ }
798
+ }
799
+ });
800
+ it('a paragraph with no formatting goes to the word-by-word breaker when a word of it may part into such a piece', () => {
801
+ const opts = { textAlign: 'left' };
802
+ const lines = (text, width) => measureBlock(text, SONG, width, 14, opts).lines;
803
+ const marked = (text, width) => lines(text, width).flatMap((l) => l.segments ?? []).filter((seg) => seg.scriptContext).map((seg) => [seg.text, seg.scriptContext, +seg.width.toFixed(4)]);
804
+ for (const version of [131, 154]) {
805
+ on(version);
806
+ // Pretext ends a line after any hyphen and measures `19` alone, each
807
+ // browser its way; the breaker parts a compound between two letters
808
+ // only (`breaksAfterHardHyphen`), so the number stays with its word.
809
+ expect(lines('see COVID-19 there', 50).map((l) => l.text.trim())).toEqual(['see', 'COVID-19', 'there']);
810
+ // A word wider than its line is cut where the line ends: what is
811
+ // left of it, seven digits and a stop, in the word's Latin forms.
812
+ expect(marked('DOI:10.1006/ijhc.2001.0458 end', 90)).toEqual([['001.0458', 'Latn', 41.5]]);
813
+ }
814
+ // With the rule off the paragraph stays with pretext, as in postext
815
+ // 1.27: the number alone on the next line, 9.4 px wide on Chrome 131
816
+ // and 11 px on a current one.
817
+ setMeasureScriptContext({ enabled: false });
818
+ on(131);
819
+ const old = lines('see COVID-19 there', 50);
820
+ expect(old.map((l) => l.text.trim())).toEqual(['see COVID-', '19 there']);
821
+ expect(old[1].segments[0].width).toBeCloseTo(9.4, 6);
822
+ on(154);
823
+ expect(lines('see COVID-19 there', 50)[1].segments[0].width).toBeCloseTo(11, 6);
824
+ // A paragraph with no such word stays with pretext under the rule too.
825
+ setMeasureScriptContext({ enabled: true, script: 'Latn' });
826
+ expect(lines('see after-dinner talk there', 60).map((l) => l.text.trim())).toEqual(measureRichBlock([{ text: 'see after-dinner talk there', bold: false, italic: false }], SONG, SONG, SONG, SONG, 60, 14, opts).lines.map((l) => l.text.trim()));
827
+ expect(lines('see after-dinner talk there', 60).flatMap((l) => l.segments ?? []).some((seg) => seg.scriptContext)).toBe(false);
828
+ });
829
+ it('a face that draws digits one way, and a word of letters, are left as they were', () => {
830
+ const plain = build(DOI, config('Plain', { locale: 'zh-Hans' }));
831
+ expect(segmentsOf(plain).some((seg) => 'scriptContext' in seg)).toBe(false);
832
+ expect(layout(plain)).toEqual(layout(build(DOI, config('Plain', { locale: 'zh-Hans', bodyText: { scriptContext: false } }))));
833
+ const words = measureRichBlock([{ text: 'after-dinner talk', bold: false, italic: false }], SONG, SONG, SONG, SONG, 40, 14, { textAlign: 'left', optimal: true, hyphenate: false });
834
+ expect(words.lines.flatMap((l) => l.segments ?? []).some((seg) => seg.scriptContext)).toBe(false);
835
+ });
836
+ });
837
+ // `……` and `——` are the Chinese ellipsis and dash (and the Japanese
838
+ // ones): two marks that make one. Cited in an English sentence of a
839
+ // Chinese book ("Cuts inside a paragraph are marked …….") the nearest
840
+ // letter is Latin, and Noto Serif SC drew the pair in its Latin form:
841
+ // three dots on the baseline, twice, run into the full stop.
842
+ describe('a doubled ellipsis or dash in a Chinese or Japanese document (#725)', () => {
843
+ const COLOPHON = `Dashes are set as ${EM_DASH}${EM_DASH} here. Cuts inside a paragraph are marked ${ELLIPSIS}${ELLIPSIS}. Set in Song.`;
844
+ it('is told from one mark, and cut out of a word with no letter', () => {
845
+ for (const mark of [`${ELLIPSIS}${ELLIPSIS}`, `${EM_DASH}${EM_DASH}`, '\u2015\u2015', `${ELLIPSIS}${ELLIPSIS}${ELLIPSIS}`])
846
+ expect(isDoubledMark(mark)).toBe(true);
847
+ for (const mark of [ELLIPSIS, EM_DASH, `${ELLIPSIS}${EM_DASH}`, '...', `${ELLIPSIS}${ELLIPSIS}.`])
848
+ expect(isDoubledMark(mark)).toBe(false);
849
+ expect(cutDoubledMarks(`${ELLIPSIS}${ELLIPSIS}.`)).toEqual([`${ELLIPSIS}${ELLIPSIS}`, '.']);
850
+ expect(cutDoubledMarks(`\u201C${EM_DASH}${EM_DASH}\u201D,`)).toEqual(['\u201C', `${EM_DASH}${EM_DASH}`, '\u201D,']);
851
+ // The pair alone needs no cut; one mark, and a word with a letter, are left whole.
852
+ expect(cutDoubledMarks(`${ELLIPSIS}${ELLIPSIS}`)).toBeUndefined();
853
+ expect(cutDoubledMarks(`${ELLIPSIS}.`)).toBeUndefined();
854
+ expect(cutDoubledMarks(`wait${ELLIPSIS}${ELLIPSIS}and`)).toBeUndefined();
855
+ });
856
+ it('the document\'s language says so: Chinese and Japanese', () => {
857
+ for (const tag of ['zh', 'zh-Hans', 'zh-Hant-TW', 'ja', 'ja-JP'])
858
+ expect(hanMarksOfLocale(tag)).toBe(true);
859
+ for (const tag of ['en', 'ko', 'ar', '', undefined])
860
+ expect(hanMarksOfLocale(tag)).toBe(false);
861
+ });
862
+ it('beside the Han mark both browsers measure the pair in its Han forms', () => {
863
+ expect(scriptMark('Hani', false)).toEqual({ lead: '', trail: `${NBSP}${HAN_MARK}` });
864
+ for (const version of [131, 154]) {
865
+ on(version);
866
+ expect(scriptTextWidth(`${ELLIPSIS}${ELLIPSIS}`, SONG, 'Hani', false)).toBeCloseTo(20, 6);
867
+ expect(scriptTextWidth(`${ELLIPSIS}${ELLIPSIS}`, SONG, 'Latn', false)).toBeCloseTo(18, 6);
868
+ }
869
+ });
870
+ it('cited in a Latin sentence it keeps its Han forms, and the stop after it the sentence\'s', () => {
871
+ for (const version of [131, 154]) {
872
+ on(version);
873
+ const doc = build(COLOPHON, chinese({ page: { width: pt(520), height: pt(600), dpi: 72, margins: { top: pt(10), bottom: pt(10), left: pt(10), right: pt(10) } } }));
874
+ expect(pieces(doc).filter(([, script]) => script)).toEqual([
875
+ [`${EM_DASH}${EM_DASH}`, 'Hani', 20],
876
+ // Two full ems of centred dots, not 18 px of dots on the baseline.
877
+ [`${ELLIPSIS}${ELLIPSIS}`, 'Hani', 20],
878
+ ['.', 'Latn', 3],
879
+ ]);
880
+ // The pair and its stop are one word: nothing between them.
881
+ const segs = segmentsOf(doc);
882
+ const at = segs.findIndex((seg) => seg.text === `${ELLIPSIS}${ELLIPSIS}`);
883
+ expect(segs[at + 1].text).toBe('.');
884
+ // Canvas and HTML paint it beside a Han character.
885
+ expect(painted(doc).map((p) => p.text)).toContain(`${ELLIPSIS}${ELLIPSIS}${NBSP}${HAN_MARK}`);
886
+ expect(renderToHtml(doc)).toContain(`>${ELLIPSIS}${ELLIPSIS}<span aria-hidden="true" style="font-size:0;-webkit-user-select:none;user-select:none;">${NBSP}${HAN_MARK}</span></`);
887
+ }
888
+ });
889
+ it('the pair and its stop never part at the end of a line', () => {
890
+ on(131);
891
+ const spans = [{ text: `are marked ${ELLIPSIS}${ELLIPSIS}. Set`, bold: false, italic: false }];
892
+ setMeasureScriptContext({ enabled: true, hanMarks: true });
893
+ for (const optimal of [true, false]) {
894
+ // `are marked ……` fits (70 px), its stop (3 px more) does not.
895
+ const block = measureRichBlock(spans, SONG, SONG, SONG, SONG, 72, 14, { textAlign: 'left', optimal, hyphenate: false });
896
+ expect(block.lines.map((l) => l.text), String(optimal)).toEqual(['are marked', `${ELLIPSIS}${ELLIPSIS}. Set`]);
897
+ }
898
+ });
899
+ it('in a document of another language the pair takes the script of the sentence, as any word with no letter', () => {
900
+ on(131);
901
+ const doc = build(COLOPHON, config('Song', { locale: 'en', page: { width: pt(520), height: pt(600), dpi: 72, margins: { top: pt(10), bottom: pt(10), left: pt(10), right: pt(10) } } }));
902
+ expect(pieces(doc).filter(([, script]) => script)).toEqual([
903
+ [`${EM_DASH}${EM_DASH}`, 'Latn', 20],
904
+ [`${ELLIPSIS}${ELLIPSIS}.`, 'Latn', 21],
905
+ ]);
906
+ });
907
+ it('with the rule off, and in a face that draws it one way, nothing is cut', () => {
908
+ on(131);
909
+ const wide = { page: { width: pt(520), height: pt(600), dpi: 72, margins: { top: pt(10), bottom: pt(10), left: pt(10), right: pt(10) } } };
910
+ const off = build(COLOPHON, chinese({ ...wide, bodyText: { scriptContext: false } }));
911
+ expect(segmentsOf(off).some((seg) => seg.scriptContext)).toBe(false);
912
+ // What postext 1.27 set: the word whole, in the default forms.
913
+ expect(pieces(off).find(([text]) => text.startsWith(ELLIPSIS))).toEqual([`${ELLIPSIS}${ELLIPSIS}.`, undefined, 23]);
914
+ const plain = build(COLOPHON, config('Plain', { locale: 'zh-Hans', ...wide }));
915
+ expect(segmentsOf(plain).some((seg) => 'scriptContext' in seg)).toBe(false);
916
+ expect(segmentsOf(plain).some((seg) => seg.text === `${ELLIPSIS}${ELLIPSIS}.`)).toBe(true);
917
+ });
918
+ });
919
+ //# sourceMappingURL=scriptContext.test.js.map