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.
- package/dist/__bench__/cjkChapter.bench.test.d.ts +2 -0
- package/dist/__bench__/cjkChapter.bench.test.d.ts.map +1 -0
- package/dist/__bench__/cjkChapter.bench.test.js +71 -0
- package/dist/__bench__/cjkChapter.bench.test.js.map +1 -0
- package/dist/__tests__/bundle.test.js +16 -0
- package/dist/__tests__/bundle.test.js.map +1 -1
- package/dist/__tests__/canvasDirection.test.d.ts +2 -0
- package/dist/__tests__/canvasDirection.test.d.ts.map +1 -0
- package/dist/__tests__/canvasDirection.test.js +45 -0
- package/dist/__tests__/canvasDirection.test.js.map +1 -0
- package/dist/__tests__/chineseLocale.test.d.ts +2 -0
- package/dist/__tests__/chineseLocale.test.d.ts.map +1 -0
- package/dist/__tests__/chineseLocale.test.js +253 -0
- package/dist/__tests__/chineseLocale.test.js.map +1 -0
- package/dist/__tests__/chineseNumbering.test.d.ts +2 -0
- package/dist/__tests__/chineseNumbering.test.d.ts.map +1 -0
- package/dist/__tests__/chineseNumbering.test.js +376 -0
- package/dist/__tests__/chineseNumbering.test.js.map +1 -0
- package/dist/__tests__/cjk/annotations.test.d.ts +2 -0
- package/dist/__tests__/cjk/annotations.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/annotations.test.js +483 -0
- package/dist/__tests__/cjk/annotations.test.js.map +1 -0
- package/dist/__tests__/cjk/annotationsRender.test.d.ts +2 -0
- package/dist/__tests__/cjk/annotationsRender.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/annotationsRender.test.js +263 -0
- package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -0
- package/dist/__tests__/cjk/composer.test.d.ts +2 -0
- package/dist/__tests__/cjk/composer.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/composer.test.js +237 -0
- package/dist/__tests__/cjk/composer.test.js.map +1 -0
- package/dist/__tests__/cjk/composerEdges.test.d.ts +2 -0
- package/dist/__tests__/cjk/composerEdges.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/composerEdges.test.js +352 -0
- package/dist/__tests__/cjk/composerEdges.test.js.map +1 -0
- package/dist/__tests__/cjk/dashRule.test.d.ts +2 -0
- package/dist/__tests__/cjk/dashRule.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/dashRule.test.js +261 -0
- package/dist/__tests__/cjk/dashRule.test.js.map +1 -0
- package/dist/__tests__/cjk/fuzz.test.d.ts +2 -0
- package/dist/__tests__/cjk/fuzz.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/fuzz.test.js +102 -0
- package/dist/__tests__/cjk/fuzz.test.js.map +1 -0
- package/dist/__tests__/cjk/grid.test.d.ts +2 -0
- package/dist/__tests__/cjk/grid.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/grid.test.js +261 -0
- package/dist/__tests__/cjk/grid.test.js.map +1 -0
- package/dist/__tests__/cjk/justifyRender.test.d.ts +2 -0
- package/dist/__tests__/cjk/justifyRender.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/justifyRender.test.js +139 -0
- package/dist/__tests__/cjk/justifyRender.test.js.map +1 -0
- package/dist/__tests__/cjk/latinFastPath.test.d.ts +2 -0
- package/dist/__tests__/cjk/latinFastPath.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/latinFastPath.test.js +208 -0
- package/dist/__tests__/cjk/latinFastPath.test.js.map +1 -0
- package/dist/__tests__/cjk/markCutsLatin.test.d.ts +2 -0
- package/dist/__tests__/cjk/markCutsLatin.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/markCutsLatin.test.js +69 -0
- package/dist/__tests__/cjk/markCutsLatin.test.js.map +1 -0
- package/dist/__tests__/cjk/markRuns.test.d.ts +2 -0
- package/dist/__tests__/cjk/markRuns.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/markRuns.test.js +347 -0
- package/dist/__tests__/cjk/markRuns.test.js.map +1 -0
- package/dist/__tests__/cjk/punctuation.test.d.ts +2 -0
- package/dist/__tests__/cjk/punctuation.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/punctuation.test.js +440 -0
- package/dist/__tests__/cjk/punctuation.test.js.map +1 -0
- package/dist/__tests__/cjk/punctuationRender.test.d.ts +2 -0
- package/dist/__tests__/cjk/punctuationRender.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/punctuationRender.test.js +135 -0
- package/dist/__tests__/cjk/punctuationRender.test.js.map +1 -0
- package/dist/__tests__/cjk/rubyDescenders.test.d.ts +2 -0
- package/dist/__tests__/cjk/rubyDescenders.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/rubyDescenders.test.js +77 -0
- package/dist/__tests__/cjk/rubyDescenders.test.js.map +1 -0
- package/dist/__tests__/cjk/runt.test.d.ts +2 -0
- package/dist/__tests__/cjk/runt.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/runt.test.js +103 -0
- package/dist/__tests__/cjk/runt.test.js.map +1 -0
- package/dist/__tests__/cjk/sharedMarks.test.d.ts +2 -0
- package/dist/__tests__/cjk/sharedMarks.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/sharedMarks.test.js +347 -0
- package/dist/__tests__/cjk/sharedMarks.test.js.map +1 -0
- package/dist/__tests__/cjk/uprightDots.test.d.ts +2 -0
- package/dist/__tests__/cjk/uprightDots.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/uprightDots.test.js +47 -0
- package/dist/__tests__/cjk/uprightDots.test.js.map +1 -0
- package/dist/__tests__/cjk/wordJoiner.test.d.ts +2 -0
- package/dist/__tests__/cjk/wordJoiner.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/wordJoiner.test.js +47 -0
- package/dist/__tests__/cjk/wordJoiner.test.js.map +1 -0
- package/dist/__tests__/cjk/zhuyinTones.test.d.ts +2 -0
- package/dist/__tests__/cjk/zhuyinTones.test.d.ts.map +1 -0
- package/dist/__tests__/cjk/zhuyinTones.test.js +72 -0
- package/dist/__tests__/cjk/zhuyinTones.test.js.map +1 -0
- package/dist/__tests__/contentLocale.test.d.ts +2 -0
- package/dist/__tests__/contentLocale.test.d.ts.map +1 -0
- package/dist/__tests__/contentLocale.test.js +167 -0
- package/dist/__tests__/contentLocale.test.js.map +1 -0
- package/dist/__tests__/defaults/cjk.test.d.ts +2 -0
- package/dist/__tests__/defaults/cjk.test.d.ts.map +1 -0
- package/dist/__tests__/defaults/cjk.test.js +70 -0
- package/dist/__tests__/defaults/cjk.test.js.map +1 -0
- package/dist/__tests__/design/designTextRender.test.js +31 -0
- package/dist/__tests__/design/designTextRender.test.js.map +1 -1
- package/dist/__tests__/design/imageAltText.test.d.ts +2 -0
- package/dist/__tests__/design/imageAltText.test.d.ts.map +1 -0
- package/dist/__tests__/design/imageAltText.test.js +126 -0
- package/dist/__tests__/design/imageAltText.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +61 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/headingLetterSpacing.test.js +1 -1
- package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
- package/dist/__tests__/indexChinese.test.d.ts +2 -0
- package/dist/__tests__/indexChinese.test.d.ts.map +1 -0
- package/dist/__tests__/indexChinese.test.js +190 -0
- package/dist/__tests__/indexChinese.test.js.map +1 -0
- package/dist/__tests__/parse/annotations.test.d.ts +2 -0
- package/dist/__tests__/parse/annotations.test.d.ts.map +1 -0
- package/dist/__tests__/parse/annotations.test.js +175 -0
- package/dist/__tests__/parse/annotations.test.js.map +1 -0
- package/dist/__tests__/parse/attrQuoting.test.js +10 -8
- package/dist/__tests__/parse/attrQuoting.test.js.map +1 -1
- package/dist/__tests__/parse/chineseText.test.d.ts +2 -0
- package/dist/__tests__/parse/chineseText.test.d.ts.map +1 -0
- package/dist/__tests__/parse/chineseText.test.js +352 -0
- package/dist/__tests__/parse/chineseText.test.js.map +1 -0
- package/dist/__tests__/parts.test.js +25 -0
- package/dist/__tests__/parts.test.js.map +1 -1
- package/dist/__tests__/pipeline/colonListRoom.test.js +20 -0
- package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
- package/dist/__tests__/presets/hongloumeng.test.d.ts +2 -0
- package/dist/__tests__/presets/hongloumeng.test.d.ts.map +1 -0
- package/dist/__tests__/presets/hongloumeng.test.js +484 -0
- package/dist/__tests__/presets/hongloumeng.test.js.map +1 -0
- package/dist/__tests__/vertical/canvasPaint.test.d.ts +2 -0
- package/dist/__tests__/vertical/canvasPaint.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/canvasPaint.test.js +229 -0
- package/dist/__tests__/vertical/canvasPaint.test.js.map +1 -0
- package/dist/__tests__/vertical/compositionPaint.test.d.ts +2 -0
- package/dist/__tests__/vertical/compositionPaint.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/compositionPaint.test.js +538 -0
- package/dist/__tests__/vertical/compositionPaint.test.js.map +1 -0
- package/dist/__tests__/vertical/geometry.test.d.ts +2 -0
- package/dist/__tests__/vertical/geometry.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/geometry.test.js +276 -0
- package/dist/__tests__/vertical/geometry.test.js.map +1 -0
- package/dist/__tests__/vertical/htmlSmoke.test.d.ts +2 -0
- package/dist/__tests__/vertical/htmlSmoke.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/htmlSmoke.test.js +25 -0
- package/dist/__tests__/vertical/htmlSmoke.test.js.map +1 -0
- package/dist/__tests__/vertical/htmlTurnedCells.test.d.ts +2 -0
- package/dist/__tests__/vertical/htmlTurnedCells.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/htmlTurnedCells.test.js +99 -0
- package/dist/__tests__/vertical/htmlTurnedCells.test.js.map +1 -0
- package/dist/__tests__/vertical/htmlVertical.test.d.ts +2 -0
- package/dist/__tests__/vertical/htmlVertical.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/htmlVertical.test.js +145 -0
- package/dist/__tests__/vertical/htmlVertical.test.js.map +1 -0
- package/dist/__tests__/vertical/lineAxis.test.d.ts +2 -0
- package/dist/__tests__/vertical/lineAxis.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/lineAxis.test.js +128 -0
- package/dist/__tests__/vertical/lineAxis.test.js.map +1 -0
- package/dist/__tests__/vertical/measurePaint.test.d.ts +2 -0
- package/dist/__tests__/vertical/measurePaint.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/measurePaint.test.js +259 -0
- package/dist/__tests__/vertical/measurePaint.test.js.map +1 -0
- package/dist/__tests__/vertical/orientation.test.d.ts +2 -0
- package/dist/__tests__/vertical/orientation.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/orientation.test.js +153 -0
- package/dist/__tests__/vertical/orientation.test.js.map +1 -0
- package/dist/__tests__/vertical/stub.d.ts +8 -0
- package/dist/__tests__/vertical/stub.d.ts.map +1 -0
- package/dist/__tests__/vertical/stub.js +37 -0
- package/dist/__tests__/vertical/stub.js.map +1 -0
- package/dist/__tests__/vertical/tateChuYoko.test.d.ts +2 -0
- package/dist/__tests__/vertical/tateChuYoko.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/tateChuYoko.test.js +270 -0
- package/dist/__tests__/vertical/tateChuYoko.test.js.map +1 -0
- package/dist/__tests__/vertical/uprightResources.test.d.ts +2 -0
- package/dist/__tests__/vertical/uprightResources.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/uprightResources.test.js +182 -0
- package/dist/__tests__/vertical/uprightResources.test.js.map +1 -0
- package/dist/__tests__/vertical/verticalDesignText.test.d.ts +2 -0
- package/dist/__tests__/vertical/verticalDesignText.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/verticalDesignText.test.js +190 -0
- package/dist/__tests__/vertical/verticalDesignText.test.js.map +1 -0
- package/dist/__tests__/vertical/verticalSpans.d.ts +25 -0
- package/dist/__tests__/vertical/verticalSpans.d.ts.map +1 -0
- package/dist/__tests__/vertical/verticalSpans.js +39 -0
- package/dist/__tests__/vertical/verticalSpans.js.map +1 -0
- package/dist/__tests__/vertical/verticalTwins.test.d.ts +2 -0
- package/dist/__tests__/vertical/verticalTwins.test.d.ts.map +1 -0
- package/dist/__tests__/vertical/verticalTwins.test.js +71 -0
- package/dist/__tests__/vertical/verticalTwins.test.js.map +1 -0
- package/dist/bundle/adapters.d.ts +14 -5
- package/dist/bundle/adapters.d.ts.map +1 -1
- package/dist/bundle/adapters.js +4 -3
- package/dist/bundle/adapters.js.map +1 -1
- package/dist/bundle/codec.d.ts.map +1 -1
- package/dist/bundle/codec.js +27 -4
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/manifest.d.ts +6 -4
- package/dist/bundle/manifest.d.ts.map +1 -1
- package/dist/bundle/manifest.js +31 -32
- package/dist/bundle/manifest.js.map +1 -1
- package/dist/bundle/types.d.ts +7 -2
- package/dist/bundle/types.d.ts.map +1 -1
- package/dist/canvas-backend/annotations.d.ts +31 -0
- package/dist/canvas-backend/annotations.d.ts.map +1 -0
- package/dist/canvas-backend/annotations.js +120 -0
- package/dist/canvas-backend/annotations.js.map +1 -0
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +195 -15
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/decorations.d.ts +5 -0
- package/dist/canvas-backend/decorations.d.ts.map +1 -1
- package/dist/canvas-backend/decorations.js +27 -0
- package/dist/canvas-backend/decorations.js.map +1 -1
- package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
- package/dist/canvas-backend/headerFooter.js +32 -16
- package/dist/canvas-backend/headerFooter.js.map +1 -1
- package/dist/canvas-backend/index.d.ts +2 -0
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +62 -2
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +42 -5
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/canvas-backend/segmentText.d.ts +23 -0
- package/dist/canvas-backend/segmentText.d.ts.map +1 -0
- package/dist/canvas-backend/segmentText.js +41 -0
- package/dist/canvas-backend/segmentText.js.map +1 -0
- package/dist/canvas-backend/verticalText.d.ts +113 -0
- package/dist/canvas-backend/verticalText.d.ts.map +1 -0
- package/dist/canvas-backend/verticalText.js +437 -0
- package/dist/canvas-backend/verticalText.js.map +1 -0
- package/dist/chineseNumerals.d.ts +39 -0
- package/dist/chineseNumerals.d.ts.map +1 -0
- package/dist/chineseNumerals.js +192 -0
- package/dist/chineseNumerals.js.map +1 -0
- package/dist/cjkMarks.d.ts +61 -0
- package/dist/cjkMarks.d.ts.map +1 -0
- package/dist/cjkMarks.js +441 -0
- package/dist/cjkMarks.js.map +1 -0
- package/dist/columnClip.d.ts +13 -4
- package/dist/columnClip.d.ts.map +1 -1
- package/dist/columnClip.js +33 -5
- package/dist/columnClip.js.map +1 -1
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +18 -3
- package/dist/configWarnings.js.map +1 -1
- package/dist/defaults/bodyText.d.ts +4 -1
- package/dist/defaults/bodyText.d.ts.map +1 -1
- package/dist/defaults/bodyText.js +27 -5
- package/dist/defaults/bodyText.js.map +1 -1
- package/dist/defaults/calloutStyles.js +1 -1
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/captionStyle.d.ts.map +1 -1
- package/dist/defaults/captionStyle.js +14 -0
- package/dist/defaults/captionStyle.js.map +1 -1
- package/dist/defaults/cjk.d.ts +33 -0
- package/dist/defaults/cjk.d.ts.map +1 -0
- package/dist/defaults/cjk.js +234 -0
- package/dist/defaults/cjk.js.map +1 -0
- package/dist/defaults/headerFooter.d.ts.map +1 -1
- package/dist/defaults/headerFooter.js +2 -0
- package/dist/defaults/headerFooter.js.map +1 -1
- package/dist/defaults/headingStyles.d.ts.map +1 -1
- package/dist/defaults/headingStyles.js +4 -0
- package/dist/defaults/headingStyles.js.map +1 -1
- package/dist/defaults/headings.d.ts.map +1 -1
- package/dist/defaults/headings.js +13 -6
- package/dist/defaults/headings.js.map +1 -1
- package/dist/defaults/index.d.ts +1 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +10 -1
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/indexConfig.d.ts +2 -1
- package/dist/defaults/indexConfig.d.ts.map +1 -1
- package/dist/defaults/indexConfig.js +5 -0
- package/dist/defaults/indexConfig.js.map +1 -1
- package/dist/defaults/layout.d.ts.map +1 -1
- package/dist/defaults/layout.js +7 -0
- package/dist/defaults/layout.js.map +1 -1
- package/dist/defaults/orderedLists.d.ts +5 -2
- package/dist/defaults/orderedLists.d.ts.map +1 -1
- package/dist/defaults/orderedLists.js +20 -5
- package/dist/defaults/orderedLists.js.map +1 -1
- package/dist/defaults/page.d.ts +8 -2
- package/dist/defaults/page.d.ts.map +1 -1
- package/dist/defaults/page.js +24 -7
- package/dist/defaults/page.js.map +1 -1
- package/dist/defaults/resourceTypes.d.ts +3 -3
- package/dist/defaults/resourceTypes.d.ts.map +1 -1
- package/dist/defaults/resourceTypes.js +23 -9
- package/dist/defaults/resourceTypes.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +8 -5
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/design/layout.d.ts +33 -0
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +124 -18
- package/dist/design/layout.js.map +1 -1
- package/dist/design/placeholders.d.ts.map +1 -1
- package/dist/design/placeholders.js +5 -1
- package/dist/design/placeholders.js.map +1 -1
- package/dist/design/richText.d.ts +24 -4
- package/dist/design/richText.d.ts.map +1 -1
- package/dist/design/richText.js +77 -16
- package/dist/design/richText.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +635 -25
- package/dist/html-backend.js.map +1 -1
- package/dist/htmlAnnotations.d.ts +29 -0
- package/dist/htmlAnnotations.d.ts.map +1 -0
- package/dist/htmlAnnotations.js +86 -0
- package/dist/htmlAnnotations.js.map +1 -0
- package/dist/hyphenate.js +2 -1
- package/dist/hyphenate.js.map +1 -1
- package/dist/index.d.ts +28 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -8
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/pretextAdapter.d.ts +4 -1
- package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/pretextAdapter.js +9 -5
- package/dist/knuthPlass/pretextAdapter.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +15 -2
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +16 -6
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/knuthPlass/tracking.d.ts +1 -1
- package/dist/knuthPlass/tracking.d.ts.map +1 -1
- package/dist/knuthPlass/tracking.js +4 -3
- package/dist/knuthPlass/tracking.js.map +1 -1
- package/dist/knuthPlass/types.d.ts +3 -0
- package/dist/knuthPlass/types.d.ts.map +1 -1
- package/dist/lineInk.d.ts +13 -0
- package/dist/lineInk.d.ts.map +1 -1
- package/dist/lineInk.js +28 -1
- package/dist/lineInk.js.map +1 -1
- package/dist/locale.d.ts +91 -3
- package/dist/locale.d.ts.map +1 -1
- package/dist/locale.js +245 -1
- package/dist/locale.js.map +1 -1
- package/dist/looseLines.d.ts +11 -1
- package/dist/looseLines.d.ts.map +1 -1
- package/dist/looseLines.js +26 -1
- package/dist/looseLines.js.map +1 -1
- package/dist/measure/cache.d.ts.map +1 -1
- package/dist/measure/cache.js +51 -3
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/canvas.d.ts +25 -0
- package/dist/measure/canvas.d.ts.map +1 -1
- package/dist/measure/canvas.js +90 -1
- package/dist/measure/canvas.js.map +1 -1
- package/dist/measure/cjk.d.ts +8 -13
- package/dist/measure/cjk.d.ts.map +1 -1
- package/dist/measure/cjk.js +29 -53
- package/dist/measure/cjk.js.map +1 -1
- package/dist/measure/cjkAnnotate.d.ts +96 -0
- package/dist/measure/cjkAnnotate.d.ts.map +1 -0
- package/dist/measure/cjkAnnotate.js +237 -0
- package/dist/measure/cjkAnnotate.js.map +1 -0
- package/dist/measure/cjkClasses.d.ts +133 -0
- package/dist/measure/cjkClasses.d.ts.map +1 -0
- package/dist/measure/cjkClasses.js +273 -0
- package/dist/measure/cjkClasses.js.map +1 -0
- package/dist/measure/cjkCompose.d.ts +86 -0
- package/dist/measure/cjkCompose.d.ts.map +1 -0
- package/dist/measure/cjkCompose.js +2022 -0
- package/dist/measure/cjkCompose.js.map +1 -0
- package/dist/measure/cjkPunctuation.d.ts +248 -0
- package/dist/measure/cjkPunctuation.d.ts.map +1 -0
- package/dist/measure/cjkPunctuation.js +492 -0
- package/dist/measure/cjkPunctuation.js.map +1 -0
- package/dist/measure/graphemes.d.ts +13 -0
- package/dist/measure/graphemes.d.ts.map +1 -0
- package/dist/measure/graphemes.js +38 -0
- package/dist/measure/graphemes.js.map +1 -0
- package/dist/measure/index.d.ts +3 -0
- package/dist/measure/index.d.ts.map +1 -1
- package/dist/measure/index.js +2 -0
- package/dist/measure/index.js.map +1 -1
- package/dist/measure/links.d.ts +6 -2
- package/dist/measure/links.d.ts.map +1 -1
- package/dist/measure/links.js +15 -5
- package/dist/measure/links.js.map +1 -1
- package/dist/measure/markCuts.d.ts +37 -0
- package/dist/measure/markCuts.d.ts.map +1 -0
- package/dist/measure/markCuts.js +77 -0
- package/dist/measure/markCuts.js.map +1 -0
- package/dist/measure/plain.d.ts.map +1 -1
- package/dist/measure/plain.js +33 -11
- package/dist/measure/plain.js.map +1 -1
- package/dist/measure/rich.d.ts +88 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +233 -111
- package/dist/measure/rich.js.map +1 -1
- package/dist/measure/rubyLift.d.ts +11 -0
- package/dist/measure/rubyLift.d.ts.map +1 -0
- package/dist/measure/rubyLift.js +57 -0
- package/dist/measure/rubyLift.js.map +1 -0
- package/dist/measure/spaces.d.ts +12 -2
- package/dist/measure/spaces.d.ts.map +1 -1
- package/dist/measure/spaces.js +21 -7
- package/dist/measure/spaces.js.map +1 -1
- package/dist/measure/types.d.ts +20 -1
- package/dist/measure/types.d.ts.map +1 -1
- package/dist/measure/types.js.map +1 -1
- package/dist/measure/vertical.d.ts +104 -0
- package/dist/measure/vertical.d.ts.map +1 -0
- package/dist/measure/vertical.js +184 -0
- package/dist/measure/vertical.js.map +1 -0
- package/dist/numberWords.d.ts +3 -10
- package/dist/numberWords.d.ts.map +1 -1
- package/dist/numberWords.js +21 -9
- package/dist/numberWords.js.map +1 -1
- package/dist/numbering.d.ts +23 -5
- package/dist/numbering.d.ts.map +1 -1
- package/dist/numbering.js +99 -14
- package/dist/numbering.js.map +1 -1
- package/dist/parse/annotations.d.ts +101 -0
- package/dist/parse/annotations.d.ts.map +1 -0
- package/dist/parse/annotations.js +556 -0
- package/dist/parse/annotations.js.map +1 -0
- package/dist/parse/attrs.d.ts +48 -1
- package/dist/parse/attrs.d.ts.map +1 -1
- package/dist/parse/attrs.js +121 -8
- package/dist/parse/attrs.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +50 -24
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/index.d.ts +3 -1
- package/dist/parse/index.d.ts.map +1 -1
- package/dist/parse/index.js +1 -0
- package/dist/parse/index.js.map +1 -1
- package/dist/parse/injectSpans.d.ts +3 -1
- package/dist/parse/injectSpans.d.ts.map +1 -1
- package/dist/parse/injectSpans.js +29 -8
- package/dist/parse/injectSpans.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +38 -6
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +139 -26
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/inlineSnippet.d.ts +2 -1
- package/dist/parse/inlineSnippet.d.ts.map +1 -1
- package/dist/parse/inlineSnippet.js +6 -2
- package/dist/parse/inlineSnippet.js.map +1 -1
- package/dist/parse/orientationMarks.d.ts +49 -0
- package/dist/parse/orientationMarks.d.ts.map +1 -0
- package/dist/parse/orientationMarks.js +138 -0
- package/dist/parse/orientationMarks.js.map +1 -0
- package/dist/parse/softBreaks.d.ts +56 -0
- package/dist/parse/softBreaks.d.ts.map +1 -0
- package/dist/parse/softBreaks.js +271 -0
- package/dist/parse/softBreaks.js.map +1 -0
- package/dist/parse/sourceMapping.d.ts.map +1 -1
- package/dist/parse/sourceMapping.js +41 -0
- package/dist/parse/sourceMapping.js.map +1 -1
- package/dist/parse/types.d.ts +84 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/annotations.d.ts +56 -0
- package/dist/pipeline/annotations.d.ts.map +1 -0
- package/dist/pipeline/annotations.js +181 -0
- package/dist/pipeline/annotations.js.map +1 -0
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +158 -47
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildBlockKind.d.ts +3 -0
- package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
- package/dist/pipeline/buildBlockKind.js +5 -3
- package/dist/pipeline/buildBlockKind.js.map +1 -1
- package/dist/pipeline/buildHelpers.d.ts +36 -6
- package/dist/pipeline/buildHelpers.d.ts.map +1 -1
- package/dist/pipeline/buildHelpers.js +55 -8
- package/dist/pipeline/buildHelpers.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +4 -0
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +3 -1
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +25 -11
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/cjkGrid.d.ts +133 -0
- package/dist/pipeline/cjkGrid.d.ts.map +1 -0
- package/dist/pipeline/cjkGrid.js +191 -0
- package/dist/pipeline/cjkGrid.js.map +1 -0
- package/dist/pipeline/config.d.ts +13 -9
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +43 -18
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts +10 -0
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +174 -2
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +9 -3
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +17 -6
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +5 -2
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +123 -28
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/headingDesignCuts.d.ts.map +1 -1
- package/dist/pipeline/headingDesignCuts.js +3 -1
- package/dist/pipeline/headingDesignCuts.js.map +1 -1
- package/dist/pipeline/headingStyles.d.ts +9 -3
- package/dist/pipeline/headingStyles.d.ts.map +1 -1
- package/dist/pipeline/headingStyles.js +23 -5
- package/dist/pipeline/headingStyles.js.map +1 -1
- package/dist/pipeline/indexDirective.d.ts.map +1 -1
- package/dist/pipeline/indexDirective.js +64 -20
- package/dist/pipeline/indexDirective.js.map +1 -1
- package/dist/pipeline/indexGroups.d.ts +55 -0
- package/dist/pipeline/indexGroups.d.ts.map +1 -0
- package/dist/pipeline/indexGroups.js +151 -0
- package/dist/pipeline/indexGroups.js.map +1 -0
- package/dist/pipeline/lists.d.ts +17 -0
- package/dist/pipeline/lists.d.ts.map +1 -1
- package/dist/pipeline/lists.js +43 -51
- package/dist/pipeline/lists.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts +3 -0
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +31 -4
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/outline.d.ts.map +1 -1
- package/dist/pipeline/outline.js +13 -8
- package/dist/pipeline/outline.js.map +1 -1
- package/dist/pipeline/parts.d.ts +6 -3
- package/dist/pipeline/parts.d.ts.map +1 -1
- package/dist/pipeline/parts.js +28 -5
- package/dist/pipeline/parts.js.map +1 -1
- package/dist/pipeline/placeholders.d.ts +11 -0
- package/dist/pipeline/placeholders.d.ts.map +1 -1
- package/dist/pipeline/placeholders.js +48 -18
- package/dist/pipeline/placeholders.js.map +1 -1
- package/dist/pipeline/placement.d.ts +10 -6
- package/dist/pipeline/placement.d.ts.map +1 -1
- package/dist/pipeline/placement.js +33 -16
- package/dist/pipeline/placement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +18 -1
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +109 -21
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +6 -5
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/pipeline/verticalMetrics.d.ts +14 -0
- package/dist/pipeline/verticalMetrics.d.ts.map +1 -0
- package/dist/pipeline/verticalMetrics.js +154 -0
- package/dist/pipeline/verticalMetrics.js.map +1 -0
- package/dist/types.d.ts +344 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +396 -6
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js +49 -0
- package/dist/vdt.js.map +1 -1
- package/dist/writingMode.d.ts +156 -0
- package/dist/writingMode.d.ts.map +1 -0
- package/dist/writingMode.js +291 -0
- package/dist/writingMode.js.map +1 -0
- 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
|