@react-x11/components 0.9.0 → 0.11.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/README.md +10 -4
- package/dist/charts/node.d.ts +11 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +44 -14
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +12 -3
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +94 -25
- package/dist/charts/render.js.map +1 -1
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +27 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-editor/completion.d.ts.map +1 -1
- package/dist/code-editor/completion.js +3 -2
- package/dist/code-editor/completion.js.map +1 -1
- package/dist/code-editor/index.d.ts.map +1 -1
- package/dist/code-editor/index.js +19 -0
- package/dist/code-editor/index.js.map +1 -1
- package/dist/code-editor/node.d.ts +177 -7
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +1329 -164
- package/dist/code-editor/node.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/languages/glsl.d.ts.map +1 -1
- package/dist/code-language/languages/glsl.js +10 -2
- package/dist/code-language/languages/glsl.js.map +1 -1
- package/dist/code-language/languages/javascript.d.ts.map +1 -1
- package/dist/code-language/languages/javascript.js +18 -3
- package/dist/code-language/languages/javascript.js.map +1 -1
- package/dist/code-language/languages/json.d.ts.map +1 -1
- package/dist/code-language/languages/json.js +10 -2
- package/dist/code-language/languages/json.js.map +1 -1
- package/dist/code-language/languages/shell.d.ts.map +1 -1
- package/dist/code-language/languages/shell.js +10 -2
- package/dist/code-language/languages/shell.js.map +1 -1
- package/dist/code-language/languages/sql.d.ts.map +1 -1
- package/dist/code-language/languages/sql.js +20 -2
- package/dist/code-language/languages/sql.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/code-language/stream.d.ts +22 -0
- package/dist/code-language/stream.d.ts.map +1 -1
- package/dist/code-language/stream.js +259 -9
- package/dist/code-language/stream.js.map +1 -1
- package/dist/code-language/types.d.ts +5 -4
- package/dist/code-language/types.d.ts.map +1 -1
- package/dist/codeblock/index.d.ts +26 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +57 -4
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/draw.d.ts +50 -0
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +132 -33
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/gl/color.d.ts +13 -0
- package/dist/flow/gl/color.d.ts.map +1 -0
- package/dist/flow/gl/color.js +82 -0
- package/dist/flow/gl/color.js.map +1 -0
- package/dist/flow/gl/index.d.ts +60 -0
- package/dist/flow/gl/index.d.ts.map +1 -0
- package/dist/flow/gl/index.js +283 -0
- package/dist/flow/gl/index.js.map +1 -0
- package/dist/flow/gl/pack.d.ts +133 -0
- package/dist/flow/gl/pack.d.ts.map +1 -0
- package/dist/flow/gl/pack.js +408 -0
- package/dist/flow/gl/pack.js.map +1 -0
- package/dist/flow/gl/renderer.d.ts +95 -0
- package/dist/flow/gl/renderer.d.ts.map +1 -0
- package/dist/flow/gl/renderer.js +459 -0
- package/dist/flow/gl/renderer.js.map +1 -0
- package/dist/flow/gl/shaders.d.ts +79 -0
- package/dist/flow/gl/shaders.d.ts.map +1 -0
- package/dist/flow/gl/shaders.js +346 -0
- package/dist/flow/gl/shaders.js.map +1 -0
- package/dist/flow/gl/text.d.ts +318 -0
- package/dist/flow/gl/text.d.ts.map +1 -0
- package/dist/flow/gl/text.js +1103 -0
- package/dist/flow/gl/text.js.map +1 -0
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +619 -64
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/node.d.ts +621 -72
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +2253 -1018
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/paint.d.ts +35 -0
- package/dist/flow/paint.d.ts.map +1 -0
- package/dist/flow/paint.js +393 -0
- package/dist/flow/paint.js.map +1 -0
- package/dist/flow/paths.d.ts +14 -0
- package/dist/flow/paths.d.ts.map +1 -1
- package/dist/flow/paths.js +45 -19
- package/dist/flow/paths.js.map +1 -1
- package/dist/flow/scene.d.ts +584 -0
- package/dist/flow/scene.d.ts.map +1 -0
- package/dist/flow/scene.js +1453 -0
- package/dist/flow/scene.js.map +1 -0
- package/dist/flow/types.d.ts +111 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/html/controls.d.ts +28 -0
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +52 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/calc.d.ts +14 -0
- package/dist/html/css/calc.d.ts.map +1 -0
- package/dist/html/css/calc.js +344 -0
- package/dist/html/css/calc.js.map +1 -0
- package/dist/html/css/cascade.d.ts +262 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1373 -67
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +23 -0
- package/dist/html/css/color.d.ts.map +1 -0
- package/dist/html/css/color.js +740 -0
- package/dist/html/css/color.js.map +1 -0
- package/dist/html/css/content.d.ts +92 -0
- package/dist/html/css/content.d.ts.map +1 -0
- package/dist/html/css/content.js +388 -0
- package/dist/html/css/content.js.map +1 -0
- package/dist/html/css/counter-styles.d.ts +76 -0
- package/dist/html/css/counter-styles.d.ts.map +1 -0
- package/dist/html/css/counter-styles.js +893 -0
- package/dist/html/css/counter-styles.js.map +1 -0
- package/dist/html/css/decode.d.ts +12 -0
- package/dist/html/css/decode.d.ts.map +1 -0
- package/dist/html/css/decode.js +116 -0
- package/dist/html/css/decode.js.map +1 -0
- package/dist/html/css/parse.d.ts +110 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +1473 -180
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +501 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3729 -206
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +65 -16
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +37 -3
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +122 -48
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +52 -0
- package/dist/html/css/vars.d.ts.map +1 -0
- package/dist/html/css/vars.js +358 -0
- package/dist/html/css/vars.js.map +1 -0
- package/dist/html/dom.d.ts +30 -4
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +278 -17
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +106 -0
- package/dist/html/fonts.d.ts.map +1 -0
- package/dist/html/fonts.js +565 -0
- package/dist/html/fonts.js.map +1 -0
- package/dist/html/gif.d.ts +15 -0
- package/dist/html/gif.d.ts.map +1 -0
- package/dist/html/gif.js +229 -0
- package/dist/html/gif.js.map +1 -0
- package/dist/html/index.d.ts +50 -11
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +115 -9
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +226 -3
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +3077 -219
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +367 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1742 -182
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +58 -0
- package/dist/html/layout/cache.d.ts.map +1 -0
- package/dist/html/layout/cache.js +224 -0
- package/dist/html/layout/cache.js.map +1 -0
- package/dist/html/layout/collapse.d.ts +45 -0
- package/dist/html/layout/collapse.d.ts.map +1 -0
- package/dist/html/layout/collapse.js +246 -0
- package/dist/html/layout/collapse.js.map +1 -0
- package/dist/html/layout/css-grid.d.ts +7 -0
- package/dist/html/layout/css-grid.d.ts.map +1 -0
- package/dist/html/layout/css-grid.js +900 -0
- package/dist/html/layout/css-grid.js.map +1 -0
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1025 -89
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +45 -6
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +113 -14
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/grid-lines.d.ts +39 -0
- package/dist/html/layout/grid-lines.d.ts.map +1 -0
- package/dist/html/layout/grid-lines.js +119 -0
- package/dist/html/layout/grid-lines.js.map +1 -0
- package/dist/html/layout/grid.d.ts +53 -0
- package/dist/html/layout/grid.d.ts.map +1 -0
- package/dist/html/layout/grid.js +153 -0
- package/dist/html/layout/grid.js.map +1 -0
- package/dist/html/layout/inline.d.ts +154 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3598 -175
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/shaping.d.ts +6 -0
- package/dist/html/layout/shaping.d.ts.map +1 -0
- package/dist/html/layout/shaping.js +100 -0
- package/dist/html/layout/shaping.js.map +1 -0
- package/dist/html/layout/table.d.ts +3 -0
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +681 -175
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/layout/tracks.d.ts +44 -0
- package/dist/html/layout/tracks.d.ts.map +1 -0
- package/dist/html/layout/tracks.js +349 -0
- package/dist/html/layout/tracks.js.map +1 -0
- package/dist/html/node.d.ts +169 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1429 -137
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +92 -3
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +3635 -107
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.d.ts +60 -14
- package/dist/html/resources.d.ts.map +1 -1
- package/dist/html/resources.js +122 -36
- package/dist/html/resources.js.map +1 -1
- package/dist/html/surfaces.d.ts +23 -0
- package/dist/html/surfaces.d.ts.map +1 -0
- package/dist/html/surfaces.js +68 -0
- package/dist/html/surfaces.js.map +1 -0
- package/dist/html/svg.d.ts +80 -0
- package/dist/html/svg.d.ts.map +1 -0
- package/dist/html/svg.js +442 -0
- package/dist/html/svg.js.map +1 -0
- package/dist/html/url.d.ts +23 -0
- package/dist/html/url.d.ts.map +1 -0
- package/dist/html/url.js +80 -0
- package/dist/html/url.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/internal/collate.d.ts +3 -0
- package/dist/internal/collate.d.ts.map +1 -0
- package/dist/internal/collate.js +11 -0
- package/dist/internal/collate.js.map +1 -0
- package/dist/internal/markdown/ast.d.ts +11 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -1
- package/dist/internal/markdown/parse.d.ts.map +1 -1
- package/dist/internal/markdown/parse.js +109 -8
- package/dist/internal/markdown/parse.js.map +1 -1
- package/dist/internal/prewarm.d.ts +18 -0
- package/dist/internal/prewarm.d.ts.map +1 -0
- package/dist/internal/prewarm.js +42 -0
- package/dist/internal/prewarm.js.map +1 -0
- package/dist/internal/sdf.d.ts +20 -0
- package/dist/internal/sdf.d.ts.map +1 -0
- package/dist/internal/sdf.js +216 -0
- package/dist/internal/sdf.js.map +1 -0
- package/dist/internal/splice.d.ts +12 -0
- package/dist/internal/splice.d.ts.map +1 -0
- package/dist/internal/splice.js +23 -0
- package/dist/internal/splice.js.map +1 -0
- package/dist/internal/window.d.ts +12 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +23 -6
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +6 -1
- package/dist/maps/anchors.d.ts.map +1 -1
- package/dist/maps/anchors.js +260 -1
- package/dist/maps/anchors.js.map +1 -1
- package/dist/maps/controller.d.ts +57 -0
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +97 -18
- package/dist/maps/controller.js.map +1 -1
- package/dist/maps/gl/buckets.d.ts +5 -1
- package/dist/maps/gl/buckets.d.ts.map +1 -1
- package/dist/maps/gl/buckets.js +5 -2
- package/dist/maps/gl/buckets.js.map +1 -1
- package/dist/maps/gl/build-worker.d.ts +2 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -1
- package/dist/maps/gl/build-worker.js +1 -1
- package/dist/maps/gl/build-worker.js.map +1 -1
- package/dist/maps/gl/cover.d.ts +23 -4
- package/dist/maps/gl/cover.d.ts.map +1 -1
- package/dist/maps/gl/cover.js +61 -48
- package/dist/maps/gl/cover.js.map +1 -1
- package/dist/maps/gl/placement.d.ts +21 -8
- package/dist/maps/gl/placement.d.ts.map +1 -1
- package/dist/maps/gl/placement.js +133 -58
- package/dist/maps/gl/placement.js.map +1 -1
- package/dist/maps/gl/renderer.d.ts +36 -4
- package/dist/maps/gl/renderer.d.ts.map +1 -1
- package/dist/maps/gl/renderer.js +81 -24
- package/dist/maps/gl/renderer.js.map +1 -1
- package/dist/maps/gl/shaders.d.ts +18 -17
- package/dist/maps/gl/shaders.d.ts.map +1 -1
- package/dist/maps/gl/shaders.js +34 -32
- package/dist/maps/gl/shaders.js.map +1 -1
- package/dist/maps/gl/store.d.ts +10 -0
- package/dist/maps/gl/store.d.ts.map +1 -1
- package/dist/maps/gl/store.js +47 -4
- package/dist/maps/gl/store.js.map +1 -1
- package/dist/maps/gl/text.d.ts +153 -24
- package/dist/maps/gl/text.d.ts.map +1 -1
- package/dist/maps/gl/text.js +379 -63
- package/dist/maps/gl/text.js.map +1 -1
- package/dist/maps/gl/view.d.ts +293 -2
- package/dist/maps/gl/view.d.ts.map +1 -1
- package/dist/maps/gl/view.js +317 -82
- package/dist/maps/gl/view.js.map +1 -1
- package/dist/maps/icons.d.ts +32 -0
- package/dist/maps/icons.d.ts.map +1 -0
- package/dist/maps/icons.js +151 -0
- package/dist/maps/icons.js.map +1 -0
- package/dist/maps/index.d.ts +2 -0
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +2 -0
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +56 -4
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +213 -46
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +17 -0
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +64 -30
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/proj.d.ts +48 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +73 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/sources.d.ts +5 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +5 -3
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/style.d.ts +35 -0
- package/dist/maps/style.d.ts.map +1 -1
- package/dist/maps/style.js.map +1 -1
- package/dist/maps/styles.d.ts +12 -0
- package/dist/maps/styles.d.ts.map +1 -1
- package/dist/maps/styles.js +120 -6
- package/dist/maps/styles.js.map +1 -1
- package/dist/maps/tiles.d.ts +31 -11
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +61 -27
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +28 -4
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +209 -68
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +6 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/qml/containers.d.ts.map +1 -1
- package/dist/qml/containers.js +6 -1
- package/dist/qml/containers.js.map +1 -1
- package/dist/qml/interaction.d.ts.map +1 -1
- package/dist/qml/interaction.js +38 -5
- package/dist/qml/interaction.js.map +1 -1
- package/dist/qml/layouts.d.ts.map +1 -1
- package/dist/qml/layouts.js +21 -6
- package/dist/qml/layouts.js.map +1 -1
- package/dist/qml/qtquick.d.ts.map +1 -1
- package/dist/qml/qtquick.js +9 -2
- package/dist/qml/qtquick.js.map +1 -1
- package/dist/qml/react.d.ts.map +1 -1
- package/dist/qml/react.js +9 -7
- package/dist/qml/react.js.map +1 -1
- package/dist/reorder/clock.d.ts +7 -0
- package/dist/reorder/clock.d.ts.map +1 -0
- package/dist/reorder/clock.js +26 -0
- package/dist/reorder/clock.js.map +1 -0
- package/dist/reorder/index.d.ts +25 -3
- package/dist/reorder/index.d.ts.map +1 -1
- package/dist/reorder/index.js +64 -19
- package/dist/reorder/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts +0 -11
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +38 -9
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/keymap.d.ts.map +1 -1
- package/dist/rich-text-editor/keymap.js +2 -1
- package/dist/rich-text-editor/keymap.js.map +1 -1
- package/dist/rich-text-editor/keys.d.ts +17 -6
- package/dist/rich-text-editor/keys.d.ts.map +1 -1
- package/dist/rich-text-editor/keys.js +135 -26
- package/dist/rich-text-editor/keys.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/marks.d.ts +9 -0
- package/dist/rich-text-editor/marks.d.ts.map +1 -0
- package/dist/rich-text-editor/marks.js +83 -0
- package/dist/rich-text-editor/marks.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -1
- package/dist/rich-text-editor/render.js +28 -10
- package/dist/rich-text-editor/render.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
- package/dist/rich-text-editor/toolbar.js +1 -1
- package/dist/rich-text-editor/toolbar.js.map +1 -1
- package/dist/rich-text-editor/virtual.d.ts.map +1 -1
- package/dist/rich-text-editor/virtual.js +17 -0
- package/dist/rich-text-editor/virtual.js.map +1 -1
- package/dist/richtext/node.d.ts +46 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +187 -4
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +13 -8
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +18 -8
- package/dist/richtext/runs.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +23 -0
- package/dist/table/index.js.map +1 -1
- package/dist/table/rows.d.ts +3 -2
- package/dist/table/rows.d.ts.map +1 -1
- package/dist/table/rows.js +20 -6
- package/dist/table/rows.js.map +1 -1
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +4 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +13 -2
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +18 -3
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +103 -23
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/dist/three/canvas.d.ts.map +1 -1
- package/dist/three/canvas.js +85 -7
- package/dist/three/canvas.js.map +1 -1
- package/dist/three/events.d.ts +22 -4
- package/dist/three/events.d.ts.map +1 -1
- package/dist/three/events.js +34 -4
- package/dist/three/events.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +17 -0
- package/dist/tree/index.js.map +1 -1
- package/package.json +42 -33
- package/src/charts/node.ts +62 -26
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +36 -32
- package/src/code-editor/completion.ts +3 -2
- package/src/code-editor/index.ts +19 -0
- package/src/code-editor/node.ts +1574 -184
- package/src/code-language/index.ts +1 -1
- package/src/code-language/languages/glsl.ts +12 -2
- package/src/code-language/languages/javascript.ts +21 -4
- package/src/code-language/languages/json.ts +12 -2
- package/src/code-language/languages/shell.ts +12 -2
- package/src/code-language/languages/sql.ts +22 -2
- package/src/code-language/runs.ts +111 -1
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +68 -4
- package/src/flow/draw.ts +167 -41
- package/src/flow/gl/color.ts +85 -0
- package/src/flow/gl/index.ts +342 -0
- package/src/flow/gl/pack.ts +585 -0
- package/src/flow/gl/renderer.ts +679 -0
- package/src/flow/gl/shaders.ts +371 -0
- package/src/flow/gl/text.ts +1295 -0
- package/src/flow/index.ts +794 -67
- package/src/flow/node.ts +2496 -1256
- package/src/flow/paint.ts +511 -0
- package/src/flow/paths.ts +80 -15
- package/src/flow/scene.ts +2199 -0
- package/src/flow/types.ts +113 -1
- package/src/html/controls.ts +73 -2
- package/src/html/css/calc.ts +385 -0
- package/src/html/css/cascade.ts +1481 -62
- package/src/html/css/color.ts +755 -0
- package/src/html/css/content.ts +417 -0
- package/src/html/css/counter-styles.ts +973 -0
- package/src/html/css/decode.ts +130 -0
- package/src/html/css/parse.ts +1514 -160
- package/src/html/css/style.ts +4131 -223
- package/src/html/css/ua.ts +66 -17
- package/src/html/css/values.ts +150 -50
- package/src/html/css/vars.ts +348 -0
- package/src/html/dom.ts +284 -17
- package/src/html/fonts.ts +641 -0
- package/src/html/gif.ts +233 -0
- package/src/html/index.ts +180 -20
- package/src/html/layout/block.ts +3560 -208
- package/src/html/layout/boxes.ts +2141 -190
- package/src/html/layout/cache.ts +289 -0
- package/src/html/layout/collapse.ts +317 -0
- package/src/html/layout/css-grid.ts +1063 -0
- package/src/html/layout/flex.ts +1115 -106
- package/src/html/layout/floats.ts +128 -14
- package/src/html/layout/grid-lines.ts +141 -0
- package/src/html/layout/grid.ts +181 -0
- package/src/html/layout/inline.ts +4187 -192
- package/src/html/layout/shaping.ts +140 -0
- package/src/html/layout/table.ts +696 -186
- package/src/html/layout/tracks.ts +437 -0
- package/src/html/node.ts +1585 -156
- package/src/html/paint.ts +4582 -267
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +504 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +2 -0
- package/src/internal/codelines.ts +200 -0
- package/src/internal/collate.ts +12 -0
- package/src/internal/markdown/ast.ts +11 -0
- package/src/internal/markdown/parse.ts +154 -11
- package/src/internal/prewarm.ts +45 -0
- package/src/internal/sdf.ts +227 -0
- package/src/internal/splice.ts +25 -0
- package/src/internal/window.ts +36 -5
- package/src/maps/anchors.ts +307 -1
- package/src/maps/controller.ts +116 -31
- package/src/maps/gl/buckets.ts +5 -1
- package/src/maps/gl/build-worker.ts +13 -2
- package/src/maps/gl/cover.ts +79 -44
- package/src/maps/gl/placement.ts +221 -69
- package/src/maps/gl/renderer.ts +104 -20
- package/src/maps/gl/shaders.ts +35 -32
- package/src/maps/gl/store.ts +66 -17
- package/src/maps/gl/text.ts +444 -71
- package/src/maps/gl/view.ts +361 -96
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +4 -0
- package/src/maps/labels.ts +269 -49
- package/src/maps/node.ts +82 -49
- package/src/maps/proj.ts +87 -0
- package/src/maps/sources.ts +5 -3
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/tiles.ts +64 -31
- package/src/maps/types.ts +28 -4
- package/src/markdown/index.ts +293 -92
- package/src/markdown/spans.ts +5 -0
- package/src/qml/containers.tsx +6 -1
- package/src/qml/interaction.tsx +42 -5
- package/src/qml/layouts.tsx +21 -6
- package/src/qml/qtquick.tsx +9 -2
- package/src/qml/react.tsx +7 -5
- package/src/reorder/clock.ts +27 -0
- package/src/reorder/index.ts +100 -27
- package/src/rich-text-editor/index.ts +37 -12
- package/src/rich-text-editor/keymap.ts +1 -1
- package/src/rich-text-editor/keys.ts +135 -26
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/render.ts +49 -12
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/rich-text-editor/toolbar.ts +1 -1
- package/src/rich-text-editor/virtual.ts +25 -0
- package/src/richtext/node.ts +235 -14
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/index.ts +4 -0
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +129 -31
- package/src/terminal-output/runs.ts +16 -2
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
|
@@ -1,5 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Inline layout: text, and the atomic things that sit in the middle of it.
|
|
2
|
+
//
|
|
3
|
+
// The whole file turns on one decision — **how many times ntk's `TextLayout`
|
|
4
|
+
// is called per paragraph** — because that is where a document's first paint
|
|
5
|
+
// is won or lost.
|
|
6
|
+
//
|
|
7
|
+
// `TextLayout` does the expensive and difficult half already: UAX#14 break
|
|
8
|
+
// opportunities, OpenType shaping with a memo per inter-break segment, font
|
|
9
|
+
// fallback, bidi, and per-line UAX#9 reordering. Calling it **once** for a
|
|
10
|
+
// whole inline formatting context is therefore both the fastest and the most
|
|
11
|
+
// correct thing this can do, and it is what the common case does: a
|
|
12
|
+
// paragraph, a heading, a list item's line, a table cell.
|
|
13
|
+
//
|
|
14
|
+
// Two things break that. An **atomic** in the middle of the text — an image,
|
|
15
|
+
// an inline-block, a form control — which the text layout knows nothing
|
|
16
|
+
// about; and a **float**, which makes the available width differ line by
|
|
17
|
+
// line while `TextLayout` takes one `maxWidth`. Either drops this into a
|
|
18
|
+
// line-at-a-time loop that re-lays the remaining text per line. That loop is
|
|
19
|
+
// quadratic, and that is fine, because it *leaves* itself the moment nothing
|
|
20
|
+
// ahead can vary the width again: the tail of a long paragraph beside a
|
|
21
|
+
// floated image is one call, not one per line.
|
|
22
|
+
//
|
|
23
|
+
// The runs handed to `TextLayout` are richtext's `TextRun`s, and that is
|
|
24
|
+
// deliberate rather than incidental: ntk lets unknown span fields ride along
|
|
25
|
+
// and hands them back on the laid-out runs, so the decoration a `<span>`
|
|
26
|
+
// carries reaches paint on the same object the glyphs did — which is exactly
|
|
27
|
+
// the trick `<richtext>` is built on, and it is why paint reuses richtext's
|
|
28
|
+
// decoration pass unchanged instead of reimplementing it. What a run does
|
|
29
|
+
// *not* carry is its element: a layout outlives the parse it was made for
|
|
30
|
+
// (`TextLayoutCache`), so the element under a run is found from its text's
|
|
31
|
+
// place in the document (`LineText.spans`) instead.
|
|
32
|
+
import { codePointAtOffset, codeUnitOffsets } from '../../internal/text.js';
|
|
33
|
+
import { CONTAIN_LAYOUT, INLINE_BEFORE_ABSOLUTE, scrolls, } from '../css/style.js';
|
|
34
|
+
import { inkColor, isTransparent, resolve } from '../css/values.js';
|
|
35
|
+
import { BOX_RAISES, LINE_BOX_RAISES, isOffset, letteredAfter, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, transformInPlace, } from './boxes.js';
|
|
36
|
+
import { tableGrid } from './grid.js';
|
|
3
37
|
/**
|
|
4
38
|
* The text behind a layout, for the one caller that needs it: a selection
|
|
5
39
|
* band has to turn a document offset into a **code point** index, which is
|
|
@@ -11,6 +45,119 @@ import { inkColor, isTransparent } from '../css/values.js';
|
|
|
11
45
|
* most documents are read without using.
|
|
12
46
|
*/
|
|
13
47
|
const LAYOUT_RUNS = new WeakMap();
|
|
48
|
+
const WIDEST_WORD = new WeakMap();
|
|
49
|
+
/**
|
|
50
|
+
* The widest word between spaces in a layout's text: its runs laid out
|
|
51
|
+
* again with every space a line break, at no width limit. Where a text's
|
|
52
|
+
* only breaks are spaces that is its min-content width, and where it has
|
|
53
|
+
* others — a hyphen, a slash — more: an upper bound of it either way. The
|
|
54
|
+
* other way to ask, a layout of no width, has ntk search every word for a
|
|
55
|
+
* place to cut it, since none fits, which costs fifteen layouts of this
|
|
56
|
+
* one. Infinity where the runs are no longer kept.
|
|
57
|
+
*/
|
|
58
|
+
export function widestWord(fonts, layout, measured = true) {
|
|
59
|
+
if (!measured)
|
|
60
|
+
return longestWord(layout);
|
|
61
|
+
let widest = WIDEST_WORD.get(layout);
|
|
62
|
+
if (widest !== undefined)
|
|
63
|
+
return widest;
|
|
64
|
+
const held = LAYOUT_RUNS.get(layout);
|
|
65
|
+
if (!held || (held.length > 0 && typeof held[0] === 'number')) {
|
|
66
|
+
return Infinity;
|
|
67
|
+
}
|
|
68
|
+
const words = fonts.layout(held.map((run) => ({
|
|
69
|
+
...run,
|
|
70
|
+
text: run.text.replace(/[ \t\u200b]+/g, '\n'),
|
|
71
|
+
})), {}, {});
|
|
72
|
+
widest = 0;
|
|
73
|
+
for (const line of words.lines)
|
|
74
|
+
widest = Math.max(widest, line.width);
|
|
75
|
+
WIDEST_WORD.set(layout, widest);
|
|
76
|
+
return widest;
|
|
77
|
+
}
|
|
78
|
+
const SPACE_BREAKS = new WeakMap();
|
|
79
|
+
/**
|
|
80
|
+
* Whether a layout's text breaks only at its spaces: letters and digits of
|
|
81
|
+
* the Latin, Greek and Cyrillic scripts, and the punctuation that UAX #14
|
|
82
|
+
* never breaks after between two letters — no hyphen, slash, `!`, `?`,
|
|
83
|
+
* `|`, `}`, soft hyphen or ellipsis, and nothing of a script that breaks
|
|
84
|
+
* between its letters or by dictionary. Where it does, its widest word
|
|
85
|
+
* (`widestWord`) is exactly its min-content width.
|
|
86
|
+
*/
|
|
87
|
+
export function spaceBreaksOnly(layout) {
|
|
88
|
+
let only = SPACE_BREAKS.get(layout);
|
|
89
|
+
if (only !== undefined)
|
|
90
|
+
return only;
|
|
91
|
+
const held = LAYOUT_RUNS.get(layout);
|
|
92
|
+
only = !!held && (held.length === 0 || typeof held[0] !== 'number');
|
|
93
|
+
if (only) {
|
|
94
|
+
outer: for (const run of held) {
|
|
95
|
+
const text = run.text;
|
|
96
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
97
|
+
if (!breaksOnlyAtSpace(text.charCodeAt(i))) {
|
|
98
|
+
only = false;
|
|
99
|
+
break outer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
SPACE_BREAKS.set(layout, only);
|
|
105
|
+
return only;
|
|
106
|
+
}
|
|
107
|
+
function breaksOnlyAtSpace(c) {
|
|
108
|
+
if (c < 0x80) {
|
|
109
|
+
// `-` `/` `!` `?` `|` `}`: a break may follow each
|
|
110
|
+
return (c >= 0x20 &&
|
|
111
|
+
c !== 0x2d &&
|
|
112
|
+
c !== 0x2f &&
|
|
113
|
+
c !== 0x21 &&
|
|
114
|
+
c !== 0x3f &&
|
|
115
|
+
c !== 0x7c &&
|
|
116
|
+
c !== 0x7d &&
|
|
117
|
+
c !== 0x7f);
|
|
118
|
+
}
|
|
119
|
+
// Latin-1 but the soft hyphen and the acute accent, which a break may
|
|
120
|
+
// precede; Latin Extended, the combining marks, Greek and Cyrillic
|
|
121
|
+
if (c >= 0xa0 && c < 0x530)
|
|
122
|
+
return c !== 0xad && c !== 0xb4 && c !== 0x2c8;
|
|
123
|
+
// the curly quotes
|
|
124
|
+
return c >= 0x2018 && c <= 0x201f;
|
|
125
|
+
}
|
|
126
|
+
const LONGEST_WORD = new WeakMap();
|
|
127
|
+
/**
|
|
128
|
+
* `widestWord` estimated from the text alone, with no layout: each word's
|
|
129
|
+
* characters at half again their size, which no glyph a document sets
|
|
130
|
+
* words in is wider than, and their letter spacing. What is under it
|
|
131
|
+
* needs no measuring, and most words are.
|
|
132
|
+
*/
|
|
133
|
+
function longestWord(layout) {
|
|
134
|
+
let longest = LONGEST_WORD.get(layout);
|
|
135
|
+
if (longest !== undefined)
|
|
136
|
+
return longest;
|
|
137
|
+
const held = LAYOUT_RUNS.get(layout);
|
|
138
|
+
if (!held || (held.length > 0 && typeof held[0] === 'number')) {
|
|
139
|
+
return Infinity;
|
|
140
|
+
}
|
|
141
|
+
longest = 0;
|
|
142
|
+
let word = 0;
|
|
143
|
+
for (const run of held) {
|
|
144
|
+
const per = (run.size ?? 16) * 1.5 + Math.max(0, run.letterSpacing ?? 0);
|
|
145
|
+
const text = run.text;
|
|
146
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
147
|
+
const c = text.charCodeAt(i);
|
|
148
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x200b)
|
|
149
|
+
word = 0;
|
|
150
|
+
else if (c < 0xdc00 || c > 0xdfff) {
|
|
151
|
+
// a surrogate pair is one character
|
|
152
|
+
word += per;
|
|
153
|
+
if (word > longest)
|
|
154
|
+
longest = word;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
LONGEST_WORD.set(layout, longest);
|
|
159
|
+
return longest;
|
|
160
|
+
}
|
|
14
161
|
/** Code-unit → code-point offsets for a layout's text, built once. */
|
|
15
162
|
export function layoutOffsets(layout) {
|
|
16
163
|
const held = LAYOUT_RUNS.get(layout);
|
|
@@ -23,17 +170,131 @@ export function layoutOffsets(layout) {
|
|
|
23
170
|
return offsets;
|
|
24
171
|
}
|
|
25
172
|
const EMPTY_OFFSETS = [0];
|
|
173
|
+
function isText(item) {
|
|
174
|
+
return item.kind === 'text';
|
|
175
|
+
}
|
|
26
176
|
/**
|
|
27
177
|
* Lay out one inline formatting context. Coordinates in the result are
|
|
28
178
|
* relative to the containing block's content box; the caller translates.
|
|
29
179
|
*/
|
|
30
180
|
export function layoutInline(block, options) {
|
|
31
|
-
const
|
|
181
|
+
const result = layoutLines(block, options);
|
|
182
|
+
const clamp = options.clamp?.lines;
|
|
183
|
+
// A clamped block shows its first lines and is as tall as they are. The
|
|
184
|
+
// one-layout paths below had the engine cut them, with its ellipsis; a
|
|
185
|
+
// block laid out a line at a time — an image on a line, a float beside
|
|
186
|
+
// it — is cut here, and ends where its last line does, with none.
|
|
187
|
+
if (clamp !== undefined && result.lines.length > clamp) {
|
|
188
|
+
const lines = result.lines.slice(0, clamp);
|
|
189
|
+
const last = lines[clamp - 1];
|
|
190
|
+
let widest = 0;
|
|
191
|
+
for (const line of lines)
|
|
192
|
+
widest = Math.max(widest, line.width);
|
|
193
|
+
return {
|
|
194
|
+
lines,
|
|
195
|
+
height: last ? last.y + last.height : 0,
|
|
196
|
+
width: widest,
|
|
197
|
+
cut: true,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
function cutOf(style, clamp) {
|
|
203
|
+
// the ellipsis a clamp ends in is placed apart (`ellipsized`)
|
|
204
|
+
if (clamp)
|
|
205
|
+
return { maxLines: clamp.lines, overflow: 'clip' };
|
|
206
|
+
if (style.textOverflow === 'ellipsis' &&
|
|
207
|
+
!wraps(style) &&
|
|
208
|
+
style.overflowX !== 'visible') {
|
|
209
|
+
return { maxLines: 1, overflow: 'ellipsis', wrap: false };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function layoutLines(block, options) {
|
|
32
214
|
const items = [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
215
|
+
const statics = [];
|
|
216
|
+
const floatCount = collect(block, items, options.width, options.fonts, block.style, !!options.floatBoxes, statics);
|
|
217
|
+
const result = linesOf(block, options, items, floatCount);
|
|
218
|
+
// whichever way the lines were made, one pass over them
|
|
219
|
+
if (statics.length) {
|
|
220
|
+
staticPositions(statics, items, result.lines, block, options);
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
/** The lines of a block's inline content, gathered into `items`. */
|
|
225
|
+
function linesOf(block, options, items, floatCount) {
|
|
226
|
+
let fonts = options.fonts;
|
|
227
|
+
const placing = options.floatBoxes;
|
|
228
|
+
if (items.length === floatCount || !fonts) {
|
|
229
|
+
// no line to wait for: the floats go at the top
|
|
230
|
+
const placed = [];
|
|
231
|
+
for (const item of items) {
|
|
232
|
+
if (item.kind === 'float') {
|
|
233
|
+
placing.size(item.box);
|
|
234
|
+
placing.place(item.box, options.startY);
|
|
235
|
+
placed.push(item.box);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (!placed.length)
|
|
239
|
+
return EMPTY;
|
|
240
|
+
return { ...EMPTY, floatRow: besideLines([], placed, options.startY) };
|
|
241
|
+
}
|
|
242
|
+
// text a box's background shows through (`background-clip: text`): its
|
|
243
|
+
// layouts are made through a recorder, so paint can lay the same text out
|
|
244
|
+
// again with no ink of its own and fill it with that background
|
|
245
|
+
if (options.clipText && clipsText(block, items)) {
|
|
246
|
+
CLIPPED_TEXT.add(block);
|
|
247
|
+
fonts = recording(fonts);
|
|
248
|
+
}
|
|
249
|
+
// small capitals the face does not have, made of its capitals
|
|
250
|
+
synthesizeSmallCaps(items, fonts);
|
|
251
|
+
if (wraps(block.style))
|
|
252
|
+
holdNoWrap(items, block);
|
|
253
|
+
// one walk for what few paragraphs have: text that casts a shadow, and a
|
|
254
|
+
// tab, which only a `white-space` that keeps it leaves — so no text is
|
|
255
|
+
// searched for one where it cannot be
|
|
256
|
+
let tabbed = false;
|
|
257
|
+
let shadowed = false;
|
|
258
|
+
for (const item of items) {
|
|
259
|
+
if (item.kind !== 'text' || item.control)
|
|
260
|
+
continue;
|
|
261
|
+
const style = item.box.style;
|
|
262
|
+
if (style.textShadow)
|
|
263
|
+
shadowed = true;
|
|
264
|
+
if (!tabbed &&
|
|
265
|
+
(style.whiteSpace === 'pre' || style.whiteSpace === 'pre-wrap') &&
|
|
266
|
+
item.run.text.includes('\t')) {
|
|
267
|
+
tabbed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (shadowed)
|
|
271
|
+
SHADOWED_TEXT.add(block);
|
|
272
|
+
if (tabbed) {
|
|
273
|
+
setTabs(items, fonts, block.style, blockIndent(block, options.width));
|
|
274
|
+
}
|
|
275
|
+
// an override on the block is one on all of its inline content (CSS 2.1
|
|
276
|
+
// 9.10); its embedding or isolation is the paragraph's own direction
|
|
277
|
+
const own = block.style.unicodeBidi === 'bidi-override' ||
|
|
278
|
+
block.style.unicodeBidi === 'isolate-override'
|
|
279
|
+
? bidiControls(block.style, true)
|
|
280
|
+
: null;
|
|
281
|
+
if (own) {
|
|
282
|
+
const first = items.find(isText);
|
|
283
|
+
let last;
|
|
284
|
+
for (const item of items)
|
|
285
|
+
if (item.kind === 'text')
|
|
286
|
+
last = item;
|
|
287
|
+
if (first && last) {
|
|
288
|
+
const opening = [];
|
|
289
|
+
pushControls(opening, own[0], block, first.start);
|
|
290
|
+
items.unshift(...opening);
|
|
291
|
+
pushControls(items, own[1], block, last.start + last.length);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (options.firstLine)
|
|
295
|
+
firstLineColour(items, options.firstLine);
|
|
36
296
|
const style = block.style;
|
|
297
|
+
const wrapWords = overflowWrapOf(style, items);
|
|
37
298
|
const base = {
|
|
38
299
|
family: style.fontFamily,
|
|
39
300
|
size: style.fontSize,
|
|
@@ -42,14 +303,86 @@ export function layoutInline(block, options) {
|
|
|
42
303
|
color: style.color,
|
|
43
304
|
};
|
|
44
305
|
const lineHeightMul = lineHeightMultiplier(fonts, style);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
306
|
+
// The lines the end of the text or a forced break ends take
|
|
307
|
+
// `text-align-last` (CSS Text 3, 7.2), and a text that does not wrap is
|
|
308
|
+
// all such lines. Where they are aligned otherwise than the rest, and
|
|
309
|
+
// neither is justified — which fills a line whatever its alignment — the
|
|
310
|
+
// lines are made one at a time, each aligned its own way.
|
|
311
|
+
const lastAlign = lastAlignOf(style);
|
|
312
|
+
const align = engineAlign(wraps(style) ? style.textAlign : lastAlign);
|
|
313
|
+
const rtl = style.direction === 'rtl';
|
|
314
|
+
const alignedApart = wraps(style) &&
|
|
315
|
+
style.textAlign !== 'justify' &&
|
|
316
|
+
lastAlign !== 'justify' &&
|
|
317
|
+
alignShift(style.textAlign, rtl) !== alignShift(lastAlign, rtl);
|
|
318
|
+
const indent = blockIndent(block, options.width);
|
|
319
|
+
let hasAtomics = false;
|
|
320
|
+
let hasEdges = false;
|
|
321
|
+
let hasOffset = false;
|
|
322
|
+
let raised = false;
|
|
323
|
+
for (const item of items) {
|
|
324
|
+
if (item.kind === 'atomic')
|
|
325
|
+
hasAtomics = true;
|
|
326
|
+
else if (item.kind === 'text') {
|
|
327
|
+
// text in an inline box with a line height of its own, which an
|
|
328
|
+
// inline box of no edges has as much as one with them
|
|
329
|
+
if (!raised && fonts && inBoxOwningLeading(fonts, block, item.box)) {
|
|
330
|
+
hasOffset = true;
|
|
331
|
+
raised = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (item.kind === 'edge') {
|
|
335
|
+
hasEdges = true;
|
|
336
|
+
if (item.box.style.verticalAlign !== 'baseline' ||
|
|
337
|
+
ownsLeading(fonts, block.style, item.box.style) ||
|
|
338
|
+
tallerStrut(fonts, block.style, item.box)) {
|
|
339
|
+
hasOffset = true;
|
|
340
|
+
raised = true;
|
|
341
|
+
}
|
|
342
|
+
else if (!hasOffset && isOffset(item.box.style))
|
|
343
|
+
hasOffset = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// a float in the content is placed on the line it is on, which only the
|
|
347
|
+
// line-at-a-time loop knows, as it does whatever floats are beside it
|
|
348
|
+
const floated = floatCount > 0 ||
|
|
349
|
+
(options.floats?.intersects(options.startY, Infinity) ?? false);
|
|
350
|
+
// An inline box's edges take room on their lines. Where nothing else on
|
|
351
|
+
// the lines has to be placed a piece at a time, they go into the one
|
|
352
|
+
// layout as spacers (`spacerRun`) — which only holds where no bidi
|
|
353
|
+
// reordering can move one off the side of its box it belongs to, and no
|
|
354
|
+
// box between edges is moved afterwards, which needs its text apart.
|
|
355
|
+
const spaced = hasEdges && !hasAtomics && !hasOffset && spacersHold(style, items);
|
|
356
|
+
// a first line in fonts of its own is found a piece at a time, in them
|
|
357
|
+
const restyled = options.firstLineStyle !== undefined;
|
|
358
|
+
// and where one layout makes a line shorter than the strut, its text all
|
|
359
|
+
// smaller than the block's, the lines are made a piece at a time too,
|
|
360
|
+
// where every line holds it (`shortOfStrut`)
|
|
361
|
+
const strut = fonts ? strutOf(fonts, style) : null;
|
|
362
|
+
// and the first line holds what it is handed besides: a marker's room
|
|
363
|
+
const firstStrut = withRoom(strut, options.firstStrut);
|
|
364
|
+
let strutted = false;
|
|
365
|
+
// Inline boxes' edges as spacers in one layout, where the engine left
|
|
366
|
+
// every edge on the line of the content it belongs to (`layoutSpaced`);
|
|
367
|
+
// the lines a piece at a time, below, where it did not.
|
|
368
|
+
if (spaced && !floated && !indent && !alignedApart && !restyled) {
|
|
369
|
+
const laid = layoutSpaced(items, base, style, options.width, lineHeightMul, align, fonts);
|
|
370
|
+
if (laid && !shortOfStrut(laid.lines, strut, firstStrut))
|
|
371
|
+
return laid;
|
|
372
|
+
if (laid)
|
|
373
|
+
strutted = true;
|
|
374
|
+
}
|
|
49
375
|
// The text-only, float-free, unindented case: one call, every line — or
|
|
50
376
|
// one call per *chunk*, when the text is long and carries hard breaks.
|
|
51
|
-
if (!hasAtomics &&
|
|
52
|
-
|
|
377
|
+
fast: if (!hasAtomics &&
|
|
378
|
+
!hasEdges &&
|
|
379
|
+
!raised &&
|
|
380
|
+
!floated &&
|
|
381
|
+
!indent &&
|
|
382
|
+
!alignedApart &&
|
|
383
|
+
!restyled &&
|
|
384
|
+
!strutted) {
|
|
385
|
+
// no atomics and no edges, so everything is text
|
|
53
386
|
const textItems = items;
|
|
54
387
|
let total = 0;
|
|
55
388
|
let hasNewline = false;
|
|
@@ -60,25 +393,101 @@ export function layoutInline(block, options) {
|
|
|
60
393
|
}
|
|
61
394
|
if (!total)
|
|
62
395
|
return EMPTY;
|
|
63
|
-
|
|
64
|
-
|
|
396
|
+
// `text-overflow` cuts every line that overflows, and a layout can cut
|
|
397
|
+
// only its last: one that a forced break ends is laid out apart, a hard
|
|
398
|
+
// line at a time, or the lines after the first were lost. A clamp is
|
|
399
|
+
// the paragraph's, and stays one layout.
|
|
400
|
+
const clamp = options.clamp;
|
|
401
|
+
let cut = cutOf(style, clamp);
|
|
402
|
+
const perLine = cut !== null && !clamp && hasNewline;
|
|
403
|
+
// an embedding cannot be cut into chunks
|
|
404
|
+
if (perLine && !hasControls(items)) {
|
|
405
|
+
return layoutChunked(textItems, base, style, options.width, lineHeightMul, align, fonts, { chars: Infinity, hardLines: 1, cut });
|
|
65
406
|
}
|
|
66
|
-
|
|
67
|
-
|
|
407
|
+
if (perLine)
|
|
408
|
+
cut = null;
|
|
409
|
+
if (total > CHUNK_TRIGGER_CHARS && hasNewline && !hasControls(items)) {
|
|
410
|
+
const chunked = layoutChunked(textItems, base, style, options.width, lineHeightMul, align, fonts);
|
|
411
|
+
if (!shortOfStrut(chunked.lines, strut, firstStrut))
|
|
412
|
+
return chunked;
|
|
413
|
+
strutted = true;
|
|
414
|
+
break fast;
|
|
415
|
+
}
|
|
416
|
+
let runs = [];
|
|
417
|
+
let spans = new SpanMap();
|
|
68
418
|
for (const item of textItems) {
|
|
69
|
-
spans.add(item.
|
|
419
|
+
spans.add(item.start, item.run.text.length, item.control ? null : item.box);
|
|
70
420
|
runs.push(item.run);
|
|
71
421
|
}
|
|
72
|
-
|
|
73
|
-
|
|
422
|
+
// justified lines fill their width, so where all but the last are,
|
|
423
|
+
// the layout is aligned as those are
|
|
424
|
+
const justifies = wraps(style) && style.textJustify !== 'none';
|
|
425
|
+
const justifyRest = style.textAlign === 'justify' && justifies;
|
|
426
|
+
const justifyLast = lastAlign === 'justify' && justifies;
|
|
427
|
+
const layoutOptions = {
|
|
428
|
+
maxWidth: wraps(style) || cut ? options.width : undefined,
|
|
74
429
|
lineHeight: lineHeightMul,
|
|
75
|
-
align,
|
|
430
|
+
align: justifyRest && !justifyLast ? engineAlign(lastAlign) : align,
|
|
76
431
|
direction: style.direction,
|
|
77
|
-
|
|
432
|
+
overflowWrap: cutWrap(style, cut, wrapWords),
|
|
433
|
+
...cut,
|
|
434
|
+
};
|
|
435
|
+
let layout = fonts.layout(runs, base, layoutOptions);
|
|
78
436
|
LAYOUT_RUNS.set(layout, runs);
|
|
437
|
+
// A clamp that cut the text short says so, for the clamp point after
|
|
438
|
+
// it, and its last line ends in an ellipsis — as it does where the
|
|
439
|
+
// clamp point falls just after the lines, more of the container after
|
|
440
|
+
// them (CSS Overflow 4, 4.2).
|
|
441
|
+
let clampCut = false;
|
|
442
|
+
if (clamp) {
|
|
443
|
+
const shown = layout.lines.length;
|
|
444
|
+
clampCut =
|
|
445
|
+
shown > 0 &&
|
|
446
|
+
shown >= clamp.lines &&
|
|
447
|
+
(layout.truncated ?? inkBeyond(runs, layout.lines[shown - 1].end));
|
|
448
|
+
if (clamp.ellipsis &&
|
|
449
|
+
shown > 0 &&
|
|
450
|
+
shown === clamp.lines &&
|
|
451
|
+
(clampCut || clamp.follows)) {
|
|
452
|
+
const ended = ellipsized(fonts, textItems, runs, base, layoutOptions, layout, options.width, style);
|
|
453
|
+
layout = ended.layout;
|
|
454
|
+
runs = ended.runs;
|
|
455
|
+
spans = ended.spans;
|
|
456
|
+
LAYOUT_RUNS.set(layout, runs);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// `text-wrap: balance`: the lines broken at the narrowest width that
|
|
460
|
+
// keeps as many of them, and then set in the whole width
|
|
461
|
+
let balanced = false;
|
|
462
|
+
if (style.textWrapStyle === 'balance' &&
|
|
463
|
+
layoutOptions.maxWidth !== undefined &&
|
|
464
|
+
!cut &&
|
|
465
|
+
style.textAlign !== 'justify' &&
|
|
466
|
+
layout.lines.length > 1 &&
|
|
467
|
+
layout.lines.length <= BALANCE_LINES) {
|
|
468
|
+
const width = layoutOptions.maxWidth;
|
|
469
|
+
const at = balancedWidth(fonts, runs, base, layoutOptions, width, layout);
|
|
470
|
+
if (at < width) {
|
|
471
|
+
layout = fonts.layout(runs, base, { ...layoutOptions, maxWidth: at });
|
|
472
|
+
LAYOUT_RUNS.set(layout, runs);
|
|
473
|
+
balanced = true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
layout = justifiedLayout(fonts, runs, base, layoutOptions, layout, options.width, { rest: justifyRest, last: justifyLast }).layout;
|
|
79
477
|
const lines = [];
|
|
80
|
-
const widest = emitLayout(layout, spans, 0, 0, lines
|
|
81
|
-
|
|
478
|
+
const widest = emitLayout(layout, spans, 0, 0, lines, layoutOptions.maxWidth === undefined
|
|
479
|
+
? unwrappedPlacer(style, options.width, true)
|
|
480
|
+
: balanced
|
|
481
|
+
? unwrappedPlacer(style, options.width, false)
|
|
482
|
+
: null);
|
|
483
|
+
// a clamp and a cut are the one layout's to make
|
|
484
|
+
if (!clamp && !cut && shortOfStrut(lines, strut, firstStrut)) {
|
|
485
|
+
strutted = true;
|
|
486
|
+
break fast;
|
|
487
|
+
}
|
|
488
|
+
return clampCut
|
|
489
|
+
? { lines, height: layout.height, width: widest, cut: true }
|
|
490
|
+
: { lines, height: layout.height, width: widest };
|
|
82
491
|
}
|
|
83
492
|
// --- the general case: line at a time -------------------------------------
|
|
84
493
|
const lines = [];
|
|
@@ -87,34 +496,213 @@ export function layoutInline(block, options) {
|
|
|
87
496
|
let index = 0;
|
|
88
497
|
let offset = 0;
|
|
89
498
|
let open = openLine(indent);
|
|
90
|
-
|
|
91
|
-
|
|
499
|
+
/** Start edges waiting for the content they belong to: an inline box's
|
|
500
|
+
* opening margin, border and padding go on the line its first content
|
|
501
|
+
* goes on, so they are placed with it, never left at a line's end. */
|
|
502
|
+
let pending = [];
|
|
503
|
+
let pendingWidth = 0;
|
|
504
|
+
const placePending = (left) => {
|
|
505
|
+
if (pending.length)
|
|
506
|
+
open.left = left;
|
|
507
|
+
for (const edge of pending) {
|
|
508
|
+
const placed = {
|
|
509
|
+
box: edge.box,
|
|
510
|
+
side: 'start',
|
|
511
|
+
x: left + open.x,
|
|
512
|
+
width: edge.width,
|
|
513
|
+
};
|
|
514
|
+
open.edges.push(placed);
|
|
515
|
+
open.order.push({ kind: 'edge', at: open.x, item: placed });
|
|
516
|
+
open.x += edge.width;
|
|
517
|
+
}
|
|
518
|
+
pending = [];
|
|
519
|
+
pendingWidth = 0;
|
|
520
|
+
};
|
|
521
|
+
/** Floats met on a line they did not fit beside: they go at the top of
|
|
522
|
+
* the next line, as the line they were met on closes. */
|
|
523
|
+
let deferred = [];
|
|
524
|
+
/** Every float placed, in order, for the lines they stand beside. */
|
|
525
|
+
const floatsPlaced = [];
|
|
526
|
+
const placeDeferred = () => {
|
|
527
|
+
for (const box of deferred) {
|
|
528
|
+
options.floatBoxes.place(box, options.startY + y);
|
|
529
|
+
floatsPlaced.push(box);
|
|
530
|
+
}
|
|
531
|
+
deferred = [];
|
|
532
|
+
};
|
|
533
|
+
// How tall a line is taken to be before it is made, for its room beside
|
|
534
|
+
// the floats: the paragraph's strut, a line of its text. An item taller
|
|
535
|
+
// asks over its own height, and the line made over its whole (`close`).
|
|
536
|
+
// A 1.4em guess put a line of `line-height: 0` below floats it fitted
|
|
537
|
+
// beside.
|
|
538
|
+
const guess = strut ? strut.ascent + strut.descent : style.fontSize * 1.4;
|
|
539
|
+
const lifts = raised && fonts ? new Lifts(fonts, block.style) : null;
|
|
540
|
+
const restShift = alignShift(style.textAlign, rtl);
|
|
541
|
+
const lastShift = alignShift(lastAlign, rtl);
|
|
542
|
+
// a justified line fills its room, whatever made it a piece at a time
|
|
543
|
+
const justify = justification(style);
|
|
544
|
+
const lineJustify = fonts && (justify.rest || justify.last)
|
|
545
|
+
? {
|
|
546
|
+
fonts,
|
|
547
|
+
base,
|
|
548
|
+
options: {
|
|
549
|
+
lineHeight: lineHeightMul,
|
|
550
|
+
align: 'left',
|
|
551
|
+
direction: style.direction,
|
|
552
|
+
overflowWrap: wrapWords,
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
: null;
|
|
556
|
+
// The first line in its `::first-line` fonts, until a line is closed
|
|
557
|
+
const lineOne = options.firstLineStyle
|
|
558
|
+
? firstLineSetting(block, items, options.firstLineStyle, fonts, strut, lineJustify)
|
|
559
|
+
: null;
|
|
560
|
+
/** Close the open line; `last` where the text or a forced break ends it,
|
|
561
|
+
* which `text-align-last` aligns. */
|
|
562
|
+
const close = (last = false) => {
|
|
563
|
+
const onFirst = lineOne !== null && !lines.length;
|
|
564
|
+
const line = finishLine(open, y,
|
|
565
|
+
// the room beside the floats over the whole line box, which an
|
|
566
|
+
// inline-block can make taller than its text (CSS 2.1 9.5)
|
|
567
|
+
(height) => bandAt(options, y, Math.max(height, guess)), last || !wraps(style) ? lastShift : restShift, rtl, lines.length
|
|
568
|
+
? strut
|
|
569
|
+
: withRoom(onFirst ? lineOne.strut : strut, options.firstStrut), onFirst ? lineOne.lifts : lifts, (last || !wraps(style) ? justify.last : justify.rest)
|
|
570
|
+
? onFirst
|
|
571
|
+
? lineOne.justify
|
|
572
|
+
: lineJustify
|
|
573
|
+
: null);
|
|
92
574
|
if (!line) {
|
|
93
575
|
open = openLine(0);
|
|
576
|
+
placeDeferred();
|
|
94
577
|
return;
|
|
95
578
|
}
|
|
96
579
|
lines.push(line);
|
|
97
580
|
widest = Math.max(widest, line.width);
|
|
98
581
|
y += line.height;
|
|
99
582
|
open = openLine(0);
|
|
583
|
+
placeDeferred();
|
|
100
584
|
};
|
|
101
585
|
while (index < items.length) {
|
|
102
|
-
const band = bandAt(options, y,
|
|
586
|
+
const band = bandAt(options, y, guess);
|
|
103
587
|
const available = band.right - band.left;
|
|
104
588
|
const item = items[index];
|
|
589
|
+
const onFirst = lineOne !== null && !lines.length;
|
|
590
|
+
const lineItems = onFirst ? lineOne.items : items;
|
|
591
|
+
const lineBase = onFirst ? lineOne.base : base;
|
|
592
|
+
if (item.kind === 'float') {
|
|
593
|
+
// No higher than the top of the line it is met on (CSS 2.1 9.5.1):
|
|
594
|
+
// at that top where it fits beside what the line holds already, and
|
|
595
|
+
// what the line holds moves over for it; under the line where it
|
|
596
|
+
// does not, with the rest of the line's content still on the line.
|
|
597
|
+
// Where the line cannot break at the float, what follows it up to
|
|
598
|
+
// where it can is on the line too, and has to fit beside it as well,
|
|
599
|
+
// as browsers place one: `nowrap` text it is in the middle of runs on
|
|
600
|
+
// past it otherwise, under it.
|
|
601
|
+
const outer = options.floatBoxes.size(item.box);
|
|
602
|
+
// and after a float that waits for the next line, since none goes
|
|
603
|
+
// higher than one before it (9.5.1, rule 5)
|
|
604
|
+
let fits = !deferred.length &&
|
|
605
|
+
(isEmpty(open) || open.x + pendingWidth + outer <= available);
|
|
606
|
+
if (fits && !isEmpty(open) && !breaksAtEnd(open, style)) {
|
|
607
|
+
const after = unbreakableAfter(lineItems, index + 1, style, fonts, lineBase);
|
|
608
|
+
fits = open.x + pendingWidth + after + outer <= available;
|
|
609
|
+
}
|
|
610
|
+
if (fits) {
|
|
611
|
+
options.floatBoxes.place(item.box, options.startY + y);
|
|
612
|
+
floatsPlaced.push(item.box);
|
|
613
|
+
const left = bandAt(options, y, guess).left;
|
|
614
|
+
if (!isEmpty(open) && left !== open.left) {
|
|
615
|
+
const dx = left - open.left;
|
|
616
|
+
for (const text of open.texts)
|
|
617
|
+
text.drawX += dx;
|
|
618
|
+
for (const placed of open.atomics)
|
|
619
|
+
placed.x += dx;
|
|
620
|
+
for (const edge of open.edges)
|
|
621
|
+
edge.x += dx;
|
|
622
|
+
open.left = left;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
deferred.push(item.box);
|
|
627
|
+
}
|
|
628
|
+
index += 1;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (item.kind === 'edge') {
|
|
632
|
+
if (item.side === 'start') {
|
|
633
|
+
pending.push(item);
|
|
634
|
+
pendingWidth += item.width;
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
// an end edge follows the content it closes onto its line; it may
|
|
638
|
+
// run past the line's end, as a trailing space does, rather than
|
|
639
|
+
// leave the content it closes
|
|
640
|
+
placePending(band.left);
|
|
641
|
+
const placed = {
|
|
642
|
+
box: item.box,
|
|
643
|
+
side: 'end',
|
|
644
|
+
x: band.left + open.x,
|
|
645
|
+
width: item.width,
|
|
646
|
+
};
|
|
647
|
+
open.edges.push(placed);
|
|
648
|
+
open.order.push({ kind: 'edge', at: open.x, item: placed });
|
|
649
|
+
open.x += item.width;
|
|
650
|
+
open.left = band.left;
|
|
651
|
+
}
|
|
652
|
+
index += 1;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
105
655
|
if (item.kind === 'atomic') {
|
|
106
656
|
const box = item.box;
|
|
107
657
|
const outer = box.width + box.marginLeft + box.marginRight;
|
|
108
|
-
|
|
658
|
+
// An inline-block makes its line as tall as itself, and the line box
|
|
659
|
+
// must not run into a float over that height either (CSS 2.1 9.5):
|
|
660
|
+
// the room is what the floats leave beside all of it.
|
|
661
|
+
const tall = box.marginTop + box.height + box.marginBottom;
|
|
662
|
+
const lineTall = Math.max(guess, tall);
|
|
663
|
+
const room = tall > guess ? bandAt(options, y, tall) : band;
|
|
664
|
+
const roomWidth = room.right - room.left;
|
|
665
|
+
// a line with nothing on it keeps what comes first, indent or not:
|
|
666
|
+
// there is no break before it (a first line's `text-indent` is room
|
|
667
|
+
// taken, and no content)
|
|
668
|
+
if (open.x !== open.indent && open.x + pendingWidth + outer > roomWidth) {
|
|
109
669
|
close();
|
|
110
670
|
continue;
|
|
111
671
|
}
|
|
112
|
-
|
|
672
|
+
// an inline-block wider than the room a float left goes below it,
|
|
673
|
+
// as a word does (below)
|
|
674
|
+
if (isEmpty(open) &&
|
|
675
|
+
pendingWidth + outer > roomWidth &&
|
|
676
|
+
roomWidth < options.width) {
|
|
677
|
+
const below = belowFloats(options, y, lineTall);
|
|
678
|
+
if (below !== null) {
|
|
679
|
+
y = below;
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
placePending(room.left);
|
|
684
|
+
const placed = {
|
|
685
|
+
box,
|
|
686
|
+
x: room.left + open.x + box.marginLeft,
|
|
687
|
+
y: 0,
|
|
688
|
+
};
|
|
689
|
+
const raise = atomicRaise(fonts, box, (box.parent ?? block).style);
|
|
690
|
+
if (raise)
|
|
691
|
+
placed.raise = raise;
|
|
692
|
+
open.atomics.push(placed);
|
|
693
|
+
open.order.push({ kind: 'atomic', at: open.x, item: placed });
|
|
113
694
|
open.x += outer;
|
|
695
|
+
open.hang = 0;
|
|
696
|
+
open.left = room.left;
|
|
114
697
|
index += 1;
|
|
115
698
|
continue;
|
|
116
699
|
}
|
|
117
|
-
const
|
|
700
|
+
const plain = segmentFrom(lineItems, index, offset);
|
|
701
|
+
// measured as it will be drawn where it is justified, its spaces
|
|
702
|
+
// spaced apart (`HAIR`) — the same text, so every offset holds
|
|
703
|
+
const segment = lineJustify
|
|
704
|
+
? { ...plain, runs: spacedApart(plain.runs) }
|
|
705
|
+
: plain;
|
|
118
706
|
// `segmentFrom` always passes at least the text item it started on, so
|
|
119
707
|
// this advances even when the slice came out empty — which is what stops
|
|
120
708
|
// a zero-length tail from spinning the loop.
|
|
@@ -128,18 +716,36 @@ export function layoutInline(block, options) {
|
|
|
128
716
|
// one layout, every line of it. This is the exit that keeps the loop
|
|
129
717
|
// below from being quadratic on an ordinary paragraph: it runs line by
|
|
130
718
|
// line only while it has to.
|
|
131
|
-
|
|
719
|
+
// An inline box's edges of no width on the open line are no room it
|
|
720
|
+
// takes, and are still on it: an empty `<span>` with a tall line height
|
|
721
|
+
// before the text was left on a line of its own after it, and the
|
|
722
|
+
// text's line was as short as the paragraph's
|
|
723
|
+
// (and its lines all aligned alike, which the last is not where
|
|
724
|
+
// `text-align-last` sets it apart)
|
|
725
|
+
const tailIsPlain = !alignedApart &&
|
|
726
|
+
!onFirst &&
|
|
727
|
+
!strutted &&
|
|
728
|
+
segment.nextIndex >= items.length &&
|
|
132
729
|
open.x === 0 &&
|
|
133
730
|
!open.atomics.length &&
|
|
731
|
+
!open.edges.length &&
|
|
732
|
+
!pending.length &&
|
|
733
|
+
!deferred.length &&
|
|
134
734
|
!(options.floats?.intersects(options.startY + y, Infinity) ?? false);
|
|
135
735
|
if (tailIsPlain) {
|
|
136
|
-
const
|
|
736
|
+
const tailOptions = {
|
|
137
737
|
maxWidth: wraps(style) ? Math.max(1, available) : undefined,
|
|
138
738
|
lineHeight: lineHeightMul,
|
|
139
739
|
align,
|
|
140
740
|
direction: style.direction,
|
|
141
|
-
|
|
741
|
+
overflowWrap: wrapWords,
|
|
742
|
+
};
|
|
743
|
+
let layout = fonts.layout(segment.runs, base, tailOptions);
|
|
142
744
|
LAYOUT_RUNS.set(layout, segment.runs);
|
|
745
|
+
// the lines past the floats are justified as the lines beside them
|
|
746
|
+
// were (`finishLine`), in the room they have
|
|
747
|
+
layout = justifiedLayout(fonts, segment.runs, base, tailOptions, layout, Math.max(1, available), justify).layout;
|
|
748
|
+
const tail = [];
|
|
143
749
|
for (let i = 0; i < layout.lines.length; i += 1) {
|
|
144
750
|
const natural = layout.lines[i];
|
|
145
751
|
const text = {
|
|
@@ -150,24 +756,33 @@ export function layoutInline(block, options) {
|
|
|
150
756
|
textStart: segment.spans.documentAt(natural.start),
|
|
151
757
|
textEnd: segment.spans.documentAt(natural.end),
|
|
152
758
|
layoutStart: natural.start,
|
|
759
|
+
spans: segment.spans,
|
|
153
760
|
};
|
|
154
|
-
|
|
761
|
+
segment.spans.giveGaps(text, natural.start, natural.end);
|
|
762
|
+
tail.push({
|
|
155
763
|
x: band.left + natural.x,
|
|
156
764
|
y: y + natural.y,
|
|
157
765
|
width: natural.width,
|
|
158
766
|
height: natural.height,
|
|
159
|
-
baseline: natural.baseline,
|
|
767
|
+
baseline: natural.baseline - natural.y,
|
|
160
768
|
texts: [text],
|
|
161
769
|
textStart: text.textStart,
|
|
162
770
|
textEnd: text.textEnd,
|
|
163
771
|
atomics: [],
|
|
164
772
|
});
|
|
165
|
-
widest = Math.max(widest, natural.width);
|
|
166
773
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
774
|
+
// or a line at a time, where one of them is shorter than the strut
|
|
775
|
+
if (!shortOfStrut(tail, strut, lines.length ? null : firstStrut)) {
|
|
776
|
+
for (const line of tail) {
|
|
777
|
+
lines.push(line);
|
|
778
|
+
widest = Math.max(widest, line.width);
|
|
779
|
+
}
|
|
780
|
+
y += layout.height;
|
|
781
|
+
index = segment.nextIndex;
|
|
782
|
+
offset = 0;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
strutted = true;
|
|
171
786
|
}
|
|
172
787
|
// One line: `maxLines: 1` cuts the layout at the first break and its
|
|
173
788
|
// `truncated` flag answers "did the segment wrap" — so a line costs one
|
|
@@ -175,15 +790,29 @@ export function layoutInline(block, options) {
|
|
|
175
790
|
// re-cut (which is what an earlier shape paid, per line, beside every
|
|
176
791
|
// float). ntk's shaping memo makes the successive cuts cheap; only the
|
|
177
792
|
// line breaker re-runs.
|
|
178
|
-
const room = Math.max(1, available - open.x);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
793
|
+
const room = Math.max(1, available - open.x - pendingWidth);
|
|
794
|
+
// A float met inside a word, or inside a `nowrap` element, is no place
|
|
795
|
+
// for the line to break: the text after it up to where it may break
|
|
796
|
+
// is on this line too, so this line keeps room for it — or breaks
|
|
797
|
+
// before the word it is tied to. It overran the line otherwise, the
|
|
798
|
+
// float having gone below, and the break it could have taken passed.
|
|
799
|
+
const tied = wraps(style) &&
|
|
800
|
+
items[segment.nextIndex]?.kind === 'float' &&
|
|
801
|
+
!BREAKS_AFTER.test(segment.runs[segment.runs.length - 1].text)
|
|
802
|
+
? unbreakableAfter(lineItems, segment.nextIndex, style, fonts, lineBase)
|
|
803
|
+
: 0;
|
|
804
|
+
const fragment = fonts.layout(segment.runs, lineBase, {
|
|
805
|
+
maxWidth: wraps(style) ? Math.max(1, room - tied) : undefined,
|
|
806
|
+
lineHeight: onFirst ? lineOne.lineHeight : lineHeightMul,
|
|
807
|
+
// Never aligned by the text layout: the alignment belongs to the whole
|
|
808
|
+
// line — its text, its atomics and its inline boxes' edges together —
|
|
809
|
+
// which only this loop can see, and `finishLine` shifts it all. Laid
|
|
810
|
+
// out aligned, the first fragment of a centred line was centred alone
|
|
811
|
+
// and whatever followed it on the line was placed as though it had
|
|
812
|
+
// not been, over it.
|
|
813
|
+
align: 'left',
|
|
186
814
|
direction: style.direction,
|
|
815
|
+
overflowWrap: wrapWords,
|
|
187
816
|
maxLines: 1,
|
|
188
817
|
});
|
|
189
818
|
LAYOUT_RUNS.set(fragment, segment.runs);
|
|
@@ -193,42 +822,91 @@ export function layoutInline(block, options) {
|
|
|
193
822
|
offset = 0;
|
|
194
823
|
continue;
|
|
195
824
|
}
|
|
196
|
-
|
|
825
|
+
// Nothing fits beside the floats — the first word is wider than the
|
|
826
|
+
// room they leave, and was either run past it or cut inside itself to
|
|
827
|
+
// fit — so the line moves down to where a float ends, and tries again
|
|
828
|
+
// there (CSS 2.1 9.5). Only where a float took some of the line's width:
|
|
829
|
+
// past the floats the line has the whole of it, and a word wider still
|
|
830
|
+
// is `overflow-wrap`'s to break or to let run past.
|
|
831
|
+
if (isEmpty(open) &&
|
|
832
|
+
available < options.width &&
|
|
833
|
+
tooNarrow(segment.runs, first.end, first.width, room)) {
|
|
834
|
+
const below = belowFloats(options, y, guess);
|
|
835
|
+
if (below !== null) {
|
|
836
|
+
y = below;
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// An inline box's opening edge goes with the first word after it: where
|
|
841
|
+
// the word does not fit the room the edge leaves, both go to the next
|
|
842
|
+
// line, rather than the edge being left at this one's end. And a word
|
|
843
|
+
// that may start a line — after a space the line ends on, or after an
|
|
844
|
+
// atomic, which has a break after it (CSS Text 3, 5.1) — goes to the
|
|
845
|
+
// next line whole where it does not fit what is left of this one,
|
|
846
|
+
// rather than being broken inside itself to fit it: the text after an
|
|
847
|
+
// inline-block, or after a float it was cut at, was its first letter
|
|
848
|
+
// at the line's end and the rest on the next.
|
|
849
|
+
const startsLine = wraps(style) &&
|
|
850
|
+
(open.hang > 0 || open.order[open.order.length - 1]?.kind === 'atomic');
|
|
851
|
+
if (!isEmpty(open) &&
|
|
852
|
+
(pendingWidth > 0
|
|
853
|
+
? tooNarrow(segment.runs, first.end, first.width, room)
|
|
854
|
+
: startsLine &&
|
|
855
|
+
// run past the room — its white space is no part of the width —
|
|
856
|
+
// or cut inside itself to fit it
|
|
857
|
+
(first.width > room + 0.5 ||
|
|
858
|
+
tooNarrow(segment.runs, first.end, first.width, room)))) {
|
|
859
|
+
close();
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
placePending(band.left);
|
|
863
|
+
const placed = {
|
|
197
864
|
layout: fragment,
|
|
198
865
|
layoutLine: 0,
|
|
199
|
-
//
|
|
200
|
-
// origin is the band edge plus the cursor — subtracting `first.x`
|
|
201
|
-
// here would left-align a centred line. Mid-line continuations were
|
|
202
|
-
// laid out `left`, where `first.x` is zero and the two agree.
|
|
866
|
+
// laid out `left`, so the fragment starts at the cursor
|
|
203
867
|
drawX: band.left + open.x,
|
|
204
868
|
drawY: 0, // filled in by `finishLine`, which is where the top is known
|
|
205
869
|
textStart: segment.spans.documentAt(first.start),
|
|
206
870
|
textEnd: segment.spans.documentAt(first.end),
|
|
207
871
|
layoutStart: first.start,
|
|
872
|
+
spans: segment.spans,
|
|
873
|
+
};
|
|
874
|
+
segment.spans.giveGaps(placed, first.start, first.end);
|
|
875
|
+
open.texts.push(placed);
|
|
876
|
+
open.order.push({
|
|
877
|
+
kind: 'text',
|
|
878
|
+
at: open.x,
|
|
879
|
+
item: placed,
|
|
880
|
+
reads: readingOf(segment.runs, first.start, first.end),
|
|
208
881
|
});
|
|
209
882
|
open.x += first.width;
|
|
210
883
|
open.left = band.left;
|
|
884
|
+
open.hang = 0;
|
|
211
885
|
// Did the segment wrap? ntk answers with `truncated`. A layout that does
|
|
212
886
|
// not carry the flag — react-x11's Cocoa engine reports none — is asked
|
|
213
887
|
// the same of its line ends instead: it wrapped if anything but
|
|
214
888
|
// whitespace follows the first line. Read as "fitted", a cut fragment
|
|
215
889
|
// advanced past the whole segment, and a paragraph beside a float lost
|
|
216
|
-
// every line after its first.
|
|
217
|
-
|
|
890
|
+
// every line after its first. And a line that ends at a `<br>` is over
|
|
891
|
+
// whether or not anything followed it in this segment: what comes next
|
|
892
|
+
// — an atomic, an element's edge — is the next line's.
|
|
893
|
+
const wrapped = breakBefore(segment.runs, first.end) ||
|
|
894
|
+
(fragment.truncated ?? inkBeyond(segment.runs, first.end));
|
|
218
895
|
if (!wrapped) {
|
|
219
896
|
// It fitted: the cursor stays on this line for whatever comes next.
|
|
220
897
|
//
|
|
221
|
-
// With one correction first.
|
|
222
|
-
// right at a real line end, wrong here, where the "line"
|
|
223
|
-
// fragment and an atomic follows on the same one: `Name
|
|
224
|
-
// the space out to width zero and the control sat flush
|
|
225
|
-
//
|
|
226
|
-
//
|
|
898
|
+
// With one correction first. A text engine strips a line's trailing
|
|
899
|
+
// whitespace — right at a real line end, wrong here, where the "line"
|
|
900
|
+
// is only a fragment and an atomic follows on the same one: `Name
|
|
901
|
+
// <input>` laid the space out to width zero and the control sat flush
|
|
902
|
+
// against the label. The stripped advance is measured back and added
|
|
903
|
+
// to the cursor: what the engine strips, and no more (`hungSpaces`).
|
|
227
904
|
if (segment.nextIndex < items.length) {
|
|
905
|
+
const hung = hungSpaces(fonts, segment.runs[segment.runs.length - 1]);
|
|
228
906
|
let trailing = '';
|
|
229
907
|
for (let i = segment.runs.length - 1; i >= 0; i -= 1) {
|
|
230
908
|
const text = segment.runs[i].text;
|
|
231
|
-
const m =
|
|
909
|
+
const m = hung.exec(text);
|
|
232
910
|
if (!m)
|
|
233
911
|
break;
|
|
234
912
|
trailing = m[0] + trailing;
|
|
@@ -236,45 +914,294 @@ export function layoutInline(block, options) {
|
|
|
236
914
|
break;
|
|
237
915
|
}
|
|
238
916
|
if (trailing) {
|
|
239
|
-
|
|
917
|
+
// and noted: if what follows goes to the next line after all,
|
|
918
|
+
// these spaces end this one, where CSS removes them (16.6.1)
|
|
919
|
+
open.hang =
|
|
240
920
|
spaceAdvance(fonts, segment.runs[segment.runs.length - 1]) *
|
|
241
921
|
trailing.length;
|
|
922
|
+
open.x += open.hang;
|
|
242
923
|
}
|
|
243
924
|
}
|
|
244
925
|
index = segment.nextIndex;
|
|
245
926
|
offset = 0;
|
|
246
927
|
continue;
|
|
247
928
|
}
|
|
248
|
-
close();
|
|
929
|
+
close(breakBefore(segment.runs, first.end));
|
|
249
930
|
const advanced = advance(items, index, offset, first.end);
|
|
250
931
|
index = advanced.index;
|
|
251
932
|
offset = advanced.offset;
|
|
252
933
|
}
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
934
|
+
if (pending.length)
|
|
935
|
+
placePending(bandAt(options, y, guess).left);
|
|
936
|
+
if (open.texts.length || open.atomics.length || open.edges.length) {
|
|
937
|
+
close(true);
|
|
938
|
+
}
|
|
939
|
+
placeDeferred();
|
|
940
|
+
if (!floatsPlaced.length)
|
|
941
|
+
return { lines, height: y, width: widest };
|
|
942
|
+
const floatRow = besideLines(Number.isFinite(options.width) ? [] : lines, floatsPlaced, options.startY);
|
|
943
|
+
return { lines, height: y, width: widest, floatRow };
|
|
256
944
|
}
|
|
257
945
|
const EMPTY = { lines: [], height: 0, width: 0 };
|
|
258
|
-
/**
|
|
259
|
-
|
|
946
|
+
/**
|
|
947
|
+
* Which line each float a paragraph placed stands beside: the one whose top
|
|
948
|
+
* it went at, whose content moved over for it — added up on the line, for
|
|
949
|
+
* what the line is measured with (`LineBox.floats`). The widest row of the
|
|
950
|
+
* rest, side by side where they went at one top, is the answer. Both lines
|
|
951
|
+
* and floats come in order down the paragraph, since a float goes no
|
|
952
|
+
* higher than the one before it.
|
|
953
|
+
*
|
|
954
|
+
* The lines are handed over only where the paragraph had no width limit,
|
|
955
|
+
* where its content is measured at its widest and a float is beside a line
|
|
956
|
+
* because nothing made the line go under it. At a width, the least a
|
|
957
|
+
* paragraph can be is its widest word or float, not the two side by side
|
|
958
|
+
* (where the word ran past the room a float left it), and its floats are
|
|
959
|
+
* rows of their own.
|
|
960
|
+
*/
|
|
961
|
+
function besideLines(lines, placed, startY) {
|
|
962
|
+
let row = 0;
|
|
963
|
+
let rowTop = NaN;
|
|
964
|
+
let widest = 0;
|
|
965
|
+
let at = 0;
|
|
966
|
+
for (const box of placed) {
|
|
967
|
+
const top = box.y - box.marginTop - startY;
|
|
968
|
+
const width = box.width + box.marginLeft + box.marginRight;
|
|
969
|
+
while (at < lines.length && lines[at].y < top - 0.01)
|
|
970
|
+
at += 1;
|
|
971
|
+
const line = lines[at];
|
|
972
|
+
if (line && Math.abs(line.y - top) <= 0.01) {
|
|
973
|
+
line.floats = (line.floats ?? 0) + width;
|
|
974
|
+
}
|
|
975
|
+
else if (Math.abs(top - rowTop) <= 0.01)
|
|
976
|
+
row += width;
|
|
977
|
+
else {
|
|
978
|
+
widest = Math.max(widest, row);
|
|
979
|
+
row = width;
|
|
980
|
+
rowTop = top;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return Math.max(widest, row);
|
|
984
|
+
}
|
|
985
|
+
/** A no-break space in a paragraph's face, and its advance, per font
|
|
986
|
+
* manager and per style — which the cascade shares between every element
|
|
987
|
+
* of a kind, so a document of paragraphs asks once. */
|
|
988
|
+
const SPACERS = new WeakMap();
|
|
989
|
+
/**
|
|
990
|
+
* An inline box's edge as a run of the one layout: a no-break space in the
|
|
991
|
+
* paragraph's face, letter-spaced to the edge's width. A no-break space
|
|
992
|
+
* glues to the text on either side, so a start edge goes to whichever line
|
|
993
|
+
* the box's first word goes to, and an end edge stays with its last; a line
|
|
994
|
+
* may still break after a space before it (UAX #14, LB12a). It is drawn as
|
|
995
|
+
* nothing, and is no part of the document's text: the line's text notes
|
|
996
|
+
* where it is (`LineText.gaps`). Laid out piece by piece instead, a
|
|
997
|
+
* paragraph with an inline `<code>` in it — whose padding is an edge —
|
|
998
|
+
* cost five layouts where one does, and a document of them reflowed two
|
|
999
|
+
* and a half times slower.
|
|
1000
|
+
*/
|
|
1001
|
+
function spacerRun(fonts, style, width) {
|
|
1002
|
+
let byStyle = SPACERS.get(fonts);
|
|
1003
|
+
if (!byStyle) {
|
|
1004
|
+
byStyle = new WeakMap();
|
|
1005
|
+
SPACERS.set(fonts, byStyle);
|
|
1006
|
+
}
|
|
1007
|
+
let spacer = byStyle.get(style);
|
|
1008
|
+
if (!spacer) {
|
|
1009
|
+
const face = {
|
|
1010
|
+
family: style.fontFamily,
|
|
1011
|
+
size: style.fontSize,
|
|
1012
|
+
weight: style.fontWeight,
|
|
1013
|
+
style: style.fontStyle === 'normal'
|
|
1014
|
+
? 'normal'
|
|
1015
|
+
: 'italic',
|
|
1016
|
+
};
|
|
1017
|
+
const run = { text: '\u00a0', ...face };
|
|
1018
|
+
spacer = { run, advance: fonts.layout([run], face, {}).width };
|
|
1019
|
+
byStyle.set(style, spacer);
|
|
1020
|
+
}
|
|
1021
|
+
return { ...spacer.run, letterSpacing: width - spacer.advance };
|
|
1022
|
+
}
|
|
1023
|
+
/** Text that bidi could reorder around a spacer: any right-to-left
|
|
1024
|
+
* letter, and the explicit embedding and isolate controls. */
|
|
1025
|
+
const REORDERS = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc\u202a-\u202e\u2066-\u2069]|[\ud802\ud803\ud83a\ud83b]/;
|
|
1026
|
+
/** Whether a paragraph's edges can be spacers: it reads left to right, and
|
|
1027
|
+
* nothing in it can be reordered. */
|
|
1028
|
+
function spacersHold(style, items) {
|
|
1029
|
+
if (style.direction !== 'ltr')
|
|
1030
|
+
return false;
|
|
1031
|
+
for (const item of items) {
|
|
1032
|
+
if (item.kind === 'text' && REORDERS.test(item.run.text))
|
|
1033
|
+
return false;
|
|
1034
|
+
// A negative margin takes room back. As a spacer it is a space with
|
|
1035
|
+
// negative letter spacing, which ntk's line fill counts and CoreText's
|
|
1036
|
+
// typesetter breaks the line before: the line at a time does the sum.
|
|
1037
|
+
if (item.kind === 'edge' &&
|
|
1038
|
+
(item.box.style.direction !== 'ltr' || item.width < 0))
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* A paragraph whose inline boxes have edges, as one layout with the edges
|
|
1045
|
+
* in it as spacers (`spacerRun`), each line told where its spacers are and
|
|
1046
|
+
* where they put the edges. Null where the engine began a line with an
|
|
1047
|
+
* edge that closes a box: a break after a box's last character is after
|
|
1048
|
+
* its end edge, which stays on the line of the content it closes (CSS Text
|
|
1049
|
+
* 3, 5.1), and a line break after a space may come before a no-break space
|
|
1050
|
+
* (UAX #14, LB12a), which is what a spacer is. The lines a piece at a time
|
|
1051
|
+
* keep it there.
|
|
1052
|
+
*/
|
|
1053
|
+
function layoutSpaced(items, base, style, width, lineHeightMul, align, fonts) {
|
|
1054
|
+
const wrapWords = overflowWrapOf(style, items);
|
|
1055
|
+
const runs = [];
|
|
1056
|
+
const spans = new SpanMap();
|
|
1057
|
+
const spacers = [];
|
|
1058
|
+
let doc = 0;
|
|
1059
|
+
for (const item of items) {
|
|
1060
|
+
if (item.kind === 'text') {
|
|
1061
|
+
doc = item.start;
|
|
1062
|
+
break;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
for (const item of items) {
|
|
1066
|
+
if (item.kind === 'edge') {
|
|
1067
|
+
spacers.push({ at: spans.laidOut, edge: item });
|
|
1068
|
+
spans.add(doc, 1, null);
|
|
1069
|
+
runs.push(spacerRun(fonts, style, item.width));
|
|
1070
|
+
}
|
|
1071
|
+
else if (item.kind === 'text') {
|
|
1072
|
+
spans.add(item.start, item.run.text.length, item.control ? null : item.box);
|
|
1073
|
+
runs.push(item.run);
|
|
1074
|
+
if (!item.control)
|
|
1075
|
+
doc = item.start + item.run.text.length;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const layoutOptions = {
|
|
1079
|
+
maxWidth: wraps(style) ? width : undefined,
|
|
1080
|
+
lineHeight: lineHeightMul,
|
|
1081
|
+
align,
|
|
1082
|
+
direction: style.direction,
|
|
1083
|
+
overflowWrap: wrapWords,
|
|
1084
|
+
};
|
|
1085
|
+
let layout = fonts.layout(runs, base, layoutOptions);
|
|
1086
|
+
LAYOUT_RUNS.set(layout, runs);
|
|
1087
|
+
// justified with its spacers in it, which are edges and not spaces
|
|
1088
|
+
layout = justifiedLayout(fonts, runs, base, layoutOptions, layout, width, justification(style), new Set(spacers.map((spacer) => spacer.at))).layout;
|
|
1089
|
+
const place = wraps(style) ? null : unwrappedPlacer(style, width);
|
|
1090
|
+
let offsets = null;
|
|
1091
|
+
const lines = [];
|
|
1092
|
+
let widest = 0;
|
|
1093
|
+
let next = 0;
|
|
1094
|
+
for (let i = 0; i < layout.lines.length; i += 1) {
|
|
1095
|
+
const natural = layout.lines[i];
|
|
1096
|
+
const dx = place ? place(natural) : 0;
|
|
1097
|
+
const edges = [];
|
|
1098
|
+
while (next < spacers.length && spacers[next].at < natural.end) {
|
|
1099
|
+
const { at, edge } = spacers[next];
|
|
1100
|
+
next += 1;
|
|
1101
|
+
if (at < natural.start)
|
|
1102
|
+
continue;
|
|
1103
|
+
if (i > 0 && edge.side === 'end' && at === natural.start)
|
|
1104
|
+
return null;
|
|
1105
|
+
const run = natural.runs?.find((r) => r.start === at);
|
|
1106
|
+
const x = run
|
|
1107
|
+
? natural.x + run.x
|
|
1108
|
+
: layout.caretPosition(codePointAt((offsets ??= layoutOffsets(layout)), at)).x;
|
|
1109
|
+
edges.push({
|
|
1110
|
+
box: edge.box,
|
|
1111
|
+
side: edge.side,
|
|
1112
|
+
x: x + dx,
|
|
1113
|
+
width: edge.width,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const text = {
|
|
1117
|
+
layout,
|
|
1118
|
+
layoutLine: i,
|
|
1119
|
+
drawX: dx,
|
|
1120
|
+
drawY: 0,
|
|
1121
|
+
textStart: spans.documentAt(natural.start),
|
|
1122
|
+
textEnd: spans.documentAt(natural.end),
|
|
1123
|
+
layoutStart: natural.start,
|
|
1124
|
+
spans,
|
|
1125
|
+
};
|
|
1126
|
+
spans.giveGaps(text, natural.start, natural.end);
|
|
1127
|
+
lines.push({
|
|
1128
|
+
x: natural.x + dx,
|
|
1129
|
+
y: natural.y,
|
|
1130
|
+
width: natural.width,
|
|
1131
|
+
height: natural.height,
|
|
1132
|
+
baseline: natural.baseline - natural.y,
|
|
1133
|
+
texts: [text],
|
|
1134
|
+
textStart: text.textStart,
|
|
1135
|
+
textEnd: text.textEnd,
|
|
1136
|
+
atomics: [],
|
|
1137
|
+
...(edges.length ? { edges } : null),
|
|
1138
|
+
});
|
|
1139
|
+
widest = Math.max(widest, natural.width);
|
|
1140
|
+
}
|
|
1141
|
+
return { lines, height: layout.height, width: widest };
|
|
1142
|
+
}
|
|
1143
|
+
/** The code point a code-unit offset into a layout's text starts. */
|
|
1144
|
+
function codePointAt(offsets, units) {
|
|
1145
|
+
if (!offsets.length)
|
|
1146
|
+
return units;
|
|
1147
|
+
let lo = 0;
|
|
1148
|
+
let hi = offsets.length - 1;
|
|
1149
|
+
while (lo < hi) {
|
|
1150
|
+
const mid = (lo + hi + 1) >> 1;
|
|
1151
|
+
if (offsets[mid] <= units)
|
|
1152
|
+
lo = mid;
|
|
1153
|
+
else
|
|
1154
|
+
hi = mid - 1;
|
|
1155
|
+
}
|
|
1156
|
+
return lo;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* A code-unit offset in a line's layout as a document index, and back:
|
|
1160
|
+
* `textStart` is `layoutStart`, and every spacer before the offset is a
|
|
1161
|
+
* unit of the layout that is no text of the document's. A document offset
|
|
1162
|
+
* with a spacer right at it is past the spacer, where a caret or a range's
|
|
1163
|
+
* start goes — inside a padded box, after its padding — and before it as
|
|
1164
|
+
* a range's `end`, so a selection does not take in the padding after it.
|
|
1165
|
+
*/
|
|
1166
|
+
export function documentOffsetOf(text, units) {
|
|
1167
|
+
let doc = text.textStart + (units - text.layoutStart);
|
|
1168
|
+
for (const gap of text.gaps ?? [])
|
|
1169
|
+
if (gap < units)
|
|
1170
|
+
doc -= 1;
|
|
1171
|
+
return doc;
|
|
1172
|
+
}
|
|
1173
|
+
export function layoutOffsetOf(text, doc, end = false) {
|
|
1174
|
+
let units = text.layoutStart + (doc - text.textStart);
|
|
1175
|
+
for (const gap of text.gaps ?? []) {
|
|
1176
|
+
if (end ? gap < units : gap <= units)
|
|
1177
|
+
units += 1;
|
|
1178
|
+
}
|
|
1179
|
+
return units;
|
|
1180
|
+
}
|
|
1181
|
+
/** Append one layout's lines as LineBoxes at an offset, each moved where
|
|
1182
|
+
* `place` says; returns the widest. */
|
|
1183
|
+
function emitLayout(layout, spans, xOff, yOff, lines, place = null) {
|
|
260
1184
|
let widest = 0;
|
|
261
1185
|
for (let i = 0; i < layout.lines.length; i += 1) {
|
|
262
1186
|
const natural = layout.lines[i];
|
|
1187
|
+
const dx = place ? place(natural) : 0;
|
|
263
1188
|
const text = {
|
|
264
1189
|
layout,
|
|
265
1190
|
layoutLine: i,
|
|
266
|
-
drawX: xOff,
|
|
1191
|
+
drawX: xOff + dx,
|
|
267
1192
|
drawY: yOff,
|
|
268
1193
|
textStart: spans.documentAt(natural.start),
|
|
269
1194
|
textEnd: spans.documentAt(natural.end),
|
|
270
1195
|
layoutStart: natural.start,
|
|
1196
|
+
spans,
|
|
271
1197
|
};
|
|
1198
|
+
spans.giveGaps(text, natural.start, natural.end);
|
|
272
1199
|
lines.push({
|
|
273
|
-
x: xOff + natural.x,
|
|
1200
|
+
x: xOff + dx + natural.x,
|
|
274
1201
|
y: yOff + natural.y,
|
|
275
1202
|
width: natural.width,
|
|
276
1203
|
height: natural.height,
|
|
277
|
-
baseline: natural.baseline,
|
|
1204
|
+
baseline: natural.baseline - natural.y,
|
|
278
1205
|
texts: [text],
|
|
279
1206
|
textStart: text.textStart,
|
|
280
1207
|
textEnd: text.textEnd,
|
|
@@ -292,6 +1219,11 @@ function emitLayout(layout, spans, xOff, yOff, lines) {
|
|
|
292
1219
|
const CHUNK_TRIGGER_CHARS = 16384;
|
|
293
1220
|
const CHUNK_MAX_CHARS = 8192;
|
|
294
1221
|
const CHUNK_MAX_HARD_LINES = 64;
|
|
1222
|
+
const CHUNKS = {
|
|
1223
|
+
chars: CHUNK_MAX_CHARS,
|
|
1224
|
+
hardLines: CHUNK_MAX_HARD_LINES,
|
|
1225
|
+
cut: null,
|
|
1226
|
+
};
|
|
295
1227
|
/**
|
|
296
1228
|
* A long hard-broken text — a `<pre>` holding a log, a wall of `<br>`s — laid
|
|
297
1229
|
* out as a sequence of layouts split at newline boundaries.
|
|
@@ -305,7 +1237,8 @@ const CHUNK_MAX_HARD_LINES = 64;
|
|
|
305
1237
|
* breaking never crosses a required break), so the chunk seams are
|
|
306
1238
|
* invisible; each chunk stays a single batch a viewport can keep whole.
|
|
307
1239
|
*/
|
|
308
|
-
function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
1240
|
+
function layoutChunked(items, base, style, width, lineHeightMul, align, fonts, chunking = CHUNKS) {
|
|
1241
|
+
const wrapWords = overflowWrapOf(style, items);
|
|
309
1242
|
const lines = [];
|
|
310
1243
|
let widest = 0;
|
|
311
1244
|
let y = 0;
|
|
@@ -313,17 +1246,25 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
313
1246
|
let chunkSpans = new SpanMap();
|
|
314
1247
|
let chars = 0;
|
|
315
1248
|
let hardLines = 0;
|
|
1249
|
+
const { cut } = chunking;
|
|
1250
|
+
const place = wraps(style) || cut ? null : unwrappedPlacer(style, width);
|
|
316
1251
|
const flush = () => {
|
|
317
1252
|
if (!chunkRuns.length)
|
|
318
1253
|
return;
|
|
319
|
-
const
|
|
320
|
-
maxWidth: wraps(style) ? width : undefined,
|
|
1254
|
+
const chunkOptions = {
|
|
1255
|
+
maxWidth: wraps(style) || cut ? width : undefined,
|
|
321
1256
|
lineHeight: lineHeightMul,
|
|
322
1257
|
align,
|
|
323
1258
|
direction: style.direction,
|
|
324
|
-
|
|
1259
|
+
overflowWrap: cutWrap(style, cut, wrapWords),
|
|
1260
|
+
...cut,
|
|
1261
|
+
};
|
|
1262
|
+
let layout = fonts.layout(chunkRuns, base, chunkOptions);
|
|
325
1263
|
LAYOUT_RUNS.set(layout, chunkRuns);
|
|
326
|
-
|
|
1264
|
+
// a chunk ends at a forced break or at the paragraph's end, which is
|
|
1265
|
+
// where `justifiedRuns` takes a chunk's last line to end
|
|
1266
|
+
layout = justifiedLayout(fonts, chunkRuns, base, chunkOptions, layout, width, justification(style)).layout;
|
|
1267
|
+
widest = Math.max(widest, emitLayout(layout, chunkSpans, 0, y, lines, place));
|
|
327
1268
|
y += layout.height;
|
|
328
1269
|
chunkRuns = [];
|
|
329
1270
|
chunkSpans = new SpanMap();
|
|
@@ -332,7 +1273,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
332
1273
|
};
|
|
333
1274
|
for (const item of items) {
|
|
334
1275
|
let text = item.run.text;
|
|
335
|
-
let docStart = item.
|
|
1276
|
+
let docStart = item.start;
|
|
336
1277
|
while (text.length) {
|
|
337
1278
|
// Scan up to the budgets, remembering the last newline: the split has
|
|
338
1279
|
// to land just after one for the chunking to be exact.
|
|
@@ -341,8 +1282,8 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
341
1282
|
let lines2 = hardLines;
|
|
342
1283
|
let taken = chars;
|
|
343
1284
|
while (i < text.length &&
|
|
344
|
-
taken <
|
|
345
|
-
lines2 <
|
|
1285
|
+
taken < chunking.chars &&
|
|
1286
|
+
lines2 < chunking.hardLines) {
|
|
346
1287
|
if (text.charCodeAt(i) === 10) {
|
|
347
1288
|
lines2 += 1;
|
|
348
1289
|
lastBreak = i;
|
|
@@ -353,7 +1294,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
353
1294
|
if (i >= text.length) {
|
|
354
1295
|
// the rest of this run fits the open chunk
|
|
355
1296
|
chunkRuns.push(text === item.run.text ? item.run : { ...item.run, text });
|
|
356
|
-
chunkSpans.add(docStart, text.length);
|
|
1297
|
+
chunkSpans.add(docStart, text.length, item.box);
|
|
357
1298
|
chars = taken;
|
|
358
1299
|
hardLines = lines2;
|
|
359
1300
|
break;
|
|
@@ -369,7 +1310,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
369
1310
|
const next = text.indexOf('\n', i);
|
|
370
1311
|
const end = next < 0 ? text.length : next + 1;
|
|
371
1312
|
chunkRuns.push({ ...item.run, text: text.slice(0, end) });
|
|
372
|
-
chunkSpans.add(docStart, end);
|
|
1313
|
+
chunkSpans.add(docStart, end, item.box);
|
|
373
1314
|
flush();
|
|
374
1315
|
docStart += end;
|
|
375
1316
|
text = text.slice(end);
|
|
@@ -377,7 +1318,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
377
1318
|
}
|
|
378
1319
|
const cut = lastBreak + 1;
|
|
379
1320
|
chunkRuns.push({ ...item.run, text: text.slice(0, cut) });
|
|
380
|
-
chunkSpans.add(docStart, cut);
|
|
1321
|
+
chunkSpans.add(docStart, cut, item.box);
|
|
381
1322
|
flush();
|
|
382
1323
|
docStart += cut;
|
|
383
1324
|
text = text.slice(cut);
|
|
@@ -387,30 +1328,207 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
|
|
|
387
1328
|
return { lines, height: y, width: widest };
|
|
388
1329
|
}
|
|
389
1330
|
function openLine(indent) {
|
|
390
|
-
return {
|
|
1331
|
+
return {
|
|
1332
|
+
x: indent,
|
|
1333
|
+
left: 0,
|
|
1334
|
+
texts: [],
|
|
1335
|
+
atomics: [],
|
|
1336
|
+
edges: [],
|
|
1337
|
+
order: [],
|
|
1338
|
+
hang: 0,
|
|
1339
|
+
indent,
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
/** Whether a line may break where it has got to: after the white space it
|
|
1343
|
+
* ends on, which hangs, or after an inline-block, where its block wraps. */
|
|
1344
|
+
function breaksAtEnd(open, style) {
|
|
1345
|
+
return (wraps(style) &&
|
|
1346
|
+
(open.hang > 0 || open.order[open.order.length - 1]?.kind === 'atomic'));
|
|
1347
|
+
}
|
|
1348
|
+
/**
|
|
1349
|
+
* How wide the content from `from` on is up to where its line may break:
|
|
1350
|
+
* its text up to a space it may break at — one `holdNoWrap` left a space —
|
|
1351
|
+
* or, in a block that does not wrap, up to a line's end, with the edges of
|
|
1352
|
+
* inline boxes on the way, and an inline-block, which has a break before it
|
|
1353
|
+
* where the block wraps. Floats take no room on the line.
|
|
1354
|
+
*/
|
|
1355
|
+
function unbreakableAfter(items, from, style, fonts, base) {
|
|
1356
|
+
const wrapping = wraps(style);
|
|
1357
|
+
const runs = [];
|
|
1358
|
+
let width = 0;
|
|
1359
|
+
for (let i = from; i < items.length; i += 1) {
|
|
1360
|
+
const item = items[i];
|
|
1361
|
+
if (item.kind === 'float')
|
|
1362
|
+
continue;
|
|
1363
|
+
if (item.kind === 'edge') {
|
|
1364
|
+
width += item.width;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (item.kind === 'atomic') {
|
|
1368
|
+
if (wrapping)
|
|
1369
|
+
break;
|
|
1370
|
+
width += item.box.width + item.box.marginLeft + item.box.marginRight;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
const text = item.run.text;
|
|
1374
|
+
const stop = wrapping ? text.search(/[ \t\n]/) : text.indexOf('\n');
|
|
1375
|
+
if (stop < 0) {
|
|
1376
|
+
runs.push(item.run);
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (stop > 0)
|
|
1380
|
+
runs.push({ ...item.run, text: text.slice(0, stop) });
|
|
1381
|
+
break;
|
|
1382
|
+
}
|
|
1383
|
+
return runs.length ? width + fonts.layout(runs, base, {}).width : width;
|
|
391
1384
|
}
|
|
392
|
-
|
|
393
|
-
|
|
1385
|
+
/**
|
|
1386
|
+
* Close a line: its height and baseline from what is on it, then where it
|
|
1387
|
+
* sits — in the room `bandFor` answers for a line that tall, and aligned in
|
|
1388
|
+
* it by `shift` of the room it did not use (0 flush left, 1 flush right, ½
|
|
1389
|
+
* centred), moved onto every text, atomic and edge on it at once.
|
|
1390
|
+
*/
|
|
1391
|
+
function finishLine(open, y, bandFor, shift, rtl, strut, lifts = null, justify = null) {
|
|
1392
|
+
if (!open.texts.length && !open.atomics.length && !open.edges.length) {
|
|
394
1393
|
return null;
|
|
395
|
-
let ascent = 0;
|
|
396
|
-
let descent = 0;
|
|
397
|
-
let height = 0;
|
|
398
|
-
for (const text of open.texts) {
|
|
399
|
-
const natural = text.layout.lines[text.layoutLine];
|
|
400
|
-
ascent = Math.max(ascent, natural.baseline);
|
|
401
|
-
descent = Math.max(descent, natural.height - natural.baseline);
|
|
402
|
-
height = Math.max(height, natural.height);
|
|
403
1394
|
}
|
|
1395
|
+
const reordered = open.order.some((p) => needsOrdering(p, rtl));
|
|
1396
|
+
if (reordered)
|
|
1397
|
+
reorderLine(open, rtl);
|
|
1398
|
+
// Every line box starts with the block's strut, its face at its line
|
|
1399
|
+
// height, so a line of images alone is still as tall as `line-height`
|
|
1400
|
+
// makes a line (CSS 2.1 10.8.1). Not one with nothing on it that takes
|
|
1401
|
+
// room, which has no height at all (9.4.2), nor one that holds a block
|
|
1402
|
+
// an inline box was split around, which is no line of text.
|
|
1403
|
+
const held = strut &&
|
|
1404
|
+
(open.texts.length > 0 ||
|
|
1405
|
+
open.atomics.some((placed) => placed.box.style.display.startsWith('inline')) ||
|
|
1406
|
+
open.edges.some((edge) => edge.width !== 0 || hasEdgeOn(edge.box, edge.side)));
|
|
1407
|
+
let ascent = held ? strut.ascent : 0;
|
|
1408
|
+
let descent = held ? strut.descent : 0;
|
|
1409
|
+
// A text `vertical-align` raises takes its own box's room about where it
|
|
1410
|
+
// is raised to. A `top` or `bottom` box takes the room of all it holds
|
|
1411
|
+
// on the line about its baseline, and the line is as tall as that.
|
|
1412
|
+
const lifted = lifts ? open.texts.map((text) => lifts.of(text)) : null;
|
|
1413
|
+
let edges = null;
|
|
1414
|
+
for (let i = 0; i < open.texts.length; i += 1) {
|
|
1415
|
+
const lift = lifted?.[i];
|
|
1416
|
+
if (lift) {
|
|
1417
|
+
if (lift.edge) {
|
|
1418
|
+
edges ??= new Map();
|
|
1419
|
+
let room = edges.get(lift.edge.box);
|
|
1420
|
+
if (!room) {
|
|
1421
|
+
room = {
|
|
1422
|
+
to: lift.edge.to,
|
|
1423
|
+
ascent: lift.edge.ascent,
|
|
1424
|
+
descent: lift.edge.descent,
|
|
1425
|
+
};
|
|
1426
|
+
edges.set(lift.edge.box, room);
|
|
1427
|
+
}
|
|
1428
|
+
room.ascent = Math.max(room.ascent, lift.ascent + lift.raise);
|
|
1429
|
+
room.descent = Math.max(room.descent, lift.descent - lift.raise);
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
ascent = Math.max(ascent, lift.ascent + lift.raise);
|
|
1433
|
+
descent = Math.max(descent, lift.descent - lift.raise);
|
|
1434
|
+
}
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const natural = open.texts[i].layout.lines[open.texts[i].layoutLine];
|
|
1438
|
+
const own = natural.baseline - natural.y;
|
|
1439
|
+
ascent = Math.max(ascent, own);
|
|
1440
|
+
descent = Math.max(descent, natural.height - own);
|
|
1441
|
+
}
|
|
1442
|
+
// an inline box with no text on the line is on it all the same, as tall
|
|
1443
|
+
// as its own face and line height make it (CSS 2.1 10.8): an empty one,
|
|
1444
|
+
// or one whose text is on another line — raised, or with a line height
|
|
1445
|
+
// of its own, which an empty `<span>` made the line's no taller for.
|
|
1446
|
+
// Only on a line that is one at all, which is none for boxes of no
|
|
1447
|
+
// edges and no text (9.4.2)
|
|
1448
|
+
if (lifts && held) {
|
|
1449
|
+
for (const placed of open.edges) {
|
|
1450
|
+
if (placed.side !== 'start' || placed.box.kind !== 'inline')
|
|
1451
|
+
continue;
|
|
1452
|
+
const room = lifts.ofBox(placed.box);
|
|
1453
|
+
if (!room)
|
|
1454
|
+
continue;
|
|
1455
|
+
ascent = Math.max(ascent, room.ascent + room.raise);
|
|
1456
|
+
descent = Math.max(descent, room.descent - room.raise);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
// `top` and `bottom` boxes are set against the line's edges rather than
|
|
1460
|
+
// its baseline, so all they ask of it is to be as tall as they are
|
|
1461
|
+
let top = 0;
|
|
1462
|
+
let bottom = 0;
|
|
404
1463
|
for (const placed of open.atomics) {
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
1464
|
+
const box = placed.box;
|
|
1465
|
+
const h = box.height + box.marginTop + box.marginBottom;
|
|
1466
|
+
const va = box.style.verticalAlign;
|
|
1467
|
+
if (va === 'top' || va === 'bottom') {
|
|
1468
|
+
if (va === 'top')
|
|
1469
|
+
top = Math.max(top, h);
|
|
1470
|
+
else
|
|
1471
|
+
bottom = Math.max(bottom, h);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
// on the line's baseline by its own, raised from it by its
|
|
1475
|
+
// `vertical-align`, and below it by the rest of it
|
|
1476
|
+
const raise = placed.raise ?? 0;
|
|
1477
|
+
const b = atomicBaseline(box);
|
|
1478
|
+
ascent = Math.max(ascent, b + raise);
|
|
1479
|
+
descent = Math.max(descent, h - b - raise);
|
|
1480
|
+
}
|
|
1481
|
+
if (edges) {
|
|
1482
|
+
for (const room of edges.values()) {
|
|
1483
|
+
const h = room.ascent + room.descent;
|
|
1484
|
+
if (room.to === 'top')
|
|
1485
|
+
top = Math.max(top, h);
|
|
1486
|
+
else
|
|
1487
|
+
bottom = Math.max(bottom, h);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
// Where the baseline goes in a line one of them made taller than the
|
|
1491
|
+
// rest is left open (CSS 2.1 10.8.1). Browsers keep it where the rest
|
|
1492
|
+
// puts it, under the line's top, and only a taller `bottom` box moves it
|
|
1493
|
+
// down: an image set `top` beside a line of text has the text at its
|
|
1494
|
+
// top, not halfway down it.
|
|
1495
|
+
let height = ascent + descent;
|
|
1496
|
+
let baseline = ascent;
|
|
1497
|
+
if (bottom > height) {
|
|
1498
|
+
baseline += bottom - height;
|
|
1499
|
+
height = bottom;
|
|
1500
|
+
}
|
|
1501
|
+
height = Math.max(height, top);
|
|
1502
|
+
// Placed beside the floats at the text's height; a float the whole line
|
|
1503
|
+
// reaches moves it, and the alignment divides the room that is left.
|
|
1504
|
+
// The indent is at the line's start, which is its right in a
|
|
1505
|
+
// right-to-left paragraph: the content moves back over it there, and
|
|
1506
|
+
// the room it takes is at the other end (CSS 2.1 16.1).
|
|
1507
|
+
const band = bandFor(height);
|
|
1508
|
+
const indent = rtl ? open.indent : 0;
|
|
1509
|
+
let used = open.x - open.hang - indent;
|
|
1510
|
+
let free = band.right - indent - band.left - used;
|
|
1511
|
+
// A justified line fills its room (CSS Text 3, 7.4). Not yet one that
|
|
1512
|
+
// reads right to left, whose pieces are in visual order by now and would
|
|
1513
|
+
// take their shares from the wrong end: that is left at its start, as it
|
|
1514
|
+
// was.
|
|
1515
|
+
if (justify && !rtl && !reordered && Number.isFinite(free) && free > 0) {
|
|
1516
|
+
const grew = justifyLine(open, free, justify);
|
|
1517
|
+
used += grew;
|
|
1518
|
+
free -= grew;
|
|
1519
|
+
}
|
|
1520
|
+
let dx = band.left - open.left - indent;
|
|
1521
|
+
if (shift > 0 && Number.isFinite(free) && free > 0)
|
|
1522
|
+
dx += free * shift;
|
|
1523
|
+
if (dx) {
|
|
1524
|
+
for (const text of open.texts)
|
|
1525
|
+
text.drawX += dx;
|
|
1526
|
+
for (const placed of open.atomics)
|
|
1527
|
+
placed.x += dx;
|
|
1528
|
+
for (const edge of open.edges)
|
|
1529
|
+
edge.x += dx;
|
|
1530
|
+
open.left += dx;
|
|
411
1531
|
}
|
|
412
|
-
height = Math.max(height, ascent + descent);
|
|
413
|
-
const baseline = Math.max(ascent, (height - ascent - descent) / 2 + ascent);
|
|
414
1532
|
let textStart = Infinity;
|
|
415
1533
|
let textEnd = 0;
|
|
416
1534
|
for (const text of open.texts) {
|
|
@@ -418,63 +1536,597 @@ function finishLine(open, y) {
|
|
|
418
1536
|
textEnd = Math.max(textEnd, text.textEnd);
|
|
419
1537
|
}
|
|
420
1538
|
const line = {
|
|
421
|
-
x: open.left,
|
|
1539
|
+
x: open.left + indent,
|
|
422
1540
|
y,
|
|
423
|
-
width:
|
|
1541
|
+
width: used,
|
|
424
1542
|
height,
|
|
425
1543
|
baseline,
|
|
426
1544
|
texts: open.texts,
|
|
427
1545
|
textStart: Number.isFinite(textStart) ? textStart : 0,
|
|
428
1546
|
textEnd,
|
|
429
1547
|
atomics: open.atomics,
|
|
1548
|
+
...(open.edges.length ? { edges: open.edges } : null),
|
|
430
1549
|
};
|
|
431
1550
|
for (const placed of open.atomics)
|
|
432
|
-
placed.y = y + alignAtomic(placed
|
|
1551
|
+
placed.y = y + alignAtomic(placed, line);
|
|
433
1552
|
// Every fragment on this line shares the line's baseline, whatever its own
|
|
434
|
-
// layout thinks: that is what makes a small `<
|
|
435
|
-
// the same baseline rather than on its own.
|
|
436
|
-
|
|
1553
|
+
// layout thinks: that is what makes a small `<span>` beside body text sit
|
|
1554
|
+
// on the same baseline rather than on its own. One that `vertical-align`
|
|
1555
|
+
// raises is drawn that far above it.
|
|
1556
|
+
for (let i = 0; i < open.texts.length; i += 1) {
|
|
1557
|
+
const text = open.texts[i];
|
|
437
1558
|
const natural = text.layout.lines[text.layoutLine];
|
|
438
|
-
|
|
1559
|
+
let at = y + baseline;
|
|
1560
|
+
const lift = lifted?.[i];
|
|
1561
|
+
if (lift) {
|
|
1562
|
+
const room = lift.edge && edges.get(lift.edge.box);
|
|
1563
|
+
if (lift.edge?.to === 'top')
|
|
1564
|
+
at = y + room.ascent - lift.raise;
|
|
1565
|
+
else if (lift.edge)
|
|
1566
|
+
at = y + height - room.descent - lift.raise;
|
|
1567
|
+
else
|
|
1568
|
+
at -= lift.raise;
|
|
1569
|
+
if (at !== y + baseline) {
|
|
1570
|
+
TEXT_RAISES.set(text, y + baseline - at);
|
|
1571
|
+
SHIFTED_LINES.add(line);
|
|
1572
|
+
}
|
|
1573
|
+
// and the boxes it is in, up to the `top` or `bottom` one, whose
|
|
1574
|
+
// backgrounds are drawn from their own baselines
|
|
1575
|
+
if (lift.edge && lifts) {
|
|
1576
|
+
const owner = text.spans.boxAt?.(text.layoutStart);
|
|
1577
|
+
for (let at2 = owner?.parent; at2?.kind === 'inline'; at2 = at2.parent) {
|
|
1578
|
+
const placed = lifts.edgeOf(at2);
|
|
1579
|
+
if (!placed)
|
|
1580
|
+
break;
|
|
1581
|
+
const room = edges.get(placed.edge);
|
|
1582
|
+
if (!room)
|
|
1583
|
+
break;
|
|
1584
|
+
const own = placed.edge.style.verticalAlign === 'top'
|
|
1585
|
+
? y + room.ascent
|
|
1586
|
+
: y + height - room.descent;
|
|
1587
|
+
let raises = LINE_BOX_RAISES.get(line);
|
|
1588
|
+
if (!raises)
|
|
1589
|
+
LINE_BOX_RAISES.set(line, (raises = new Map()));
|
|
1590
|
+
raises.set(at2, y + baseline - (own - placed.raise));
|
|
1591
|
+
SHIFTED_LINES.add(line);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
text.drawY = at - natural.baseline;
|
|
439
1596
|
}
|
|
440
1597
|
return line;
|
|
441
1598
|
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Put a line's pieces in visual order. The text and the atomics first, by
|
|
1601
|
+
* UAX #9's L2 over pieces rather than characters: a fragment is in order
|
|
1602
|
+
* inside itself already — the engine laid it out bidirectionally — so what
|
|
1603
|
+
* is left to order is the fragments and the atomics between them, each at
|
|
1604
|
+
* a level — a fragment the direction its letters read in, or the
|
|
1605
|
+
* paragraph's where they read both ways, and a neutral (an atomic, a
|
|
1606
|
+
* fragment of spaces and digits) its neighbours' where they agree and the
|
|
1607
|
+
* paragraph's where they do not (N1, N2).
|
|
1608
|
+
*
|
|
1609
|
+
* Then each element's edges, innermost first, around what of it is on the
|
|
1610
|
+
* line: CSS 2.1 8.6 puts an ltr element's left margin, border and padding
|
|
1611
|
+
* on its leftmost box and a rtl one's right ones on its rightmost, which is
|
|
1612
|
+
* a matter of its `direction` rather than of which way its text reads — so
|
|
1613
|
+
* an edge is placed beside its element's pieces, never ordered among them.
|
|
1614
|
+
* Each piece keeps the room it took, and a right-to-left fragment's hung
|
|
1615
|
+
* trailing space goes to its left, where its logical end is.
|
|
1616
|
+
*/
|
|
1617
|
+
function reorderLine(open, rtl) {
|
|
1618
|
+
const order = open.order;
|
|
1619
|
+
const n = order.length;
|
|
1620
|
+
if (!n)
|
|
1621
|
+
return;
|
|
1622
|
+
const content = [];
|
|
1623
|
+
for (let i = 0; i < n; i += 1)
|
|
1624
|
+
if (order[i].kind !== 'edge')
|
|
1625
|
+
content.push(i);
|
|
1626
|
+
const strong = content.map((i) => {
|
|
1627
|
+
const p = order[i];
|
|
1628
|
+
if (p.kind !== 'text' || p.reads === null)
|
|
1629
|
+
return null;
|
|
1630
|
+
return p.reads === 'mixed' ? rtl : p.reads === 'rtl';
|
|
1631
|
+
});
|
|
1632
|
+
const strongAt = (from, step) => {
|
|
1633
|
+
for (let k = from + step; k >= 0 && k < content.length; k += step) {
|
|
1634
|
+
if (strong[k] !== null)
|
|
1635
|
+
return strong[k];
|
|
1636
|
+
}
|
|
1637
|
+
return rtl;
|
|
1638
|
+
};
|
|
1639
|
+
const isRtl = strong.map((r, k) => {
|
|
1640
|
+
if (r !== null)
|
|
1641
|
+
return r;
|
|
1642
|
+
const before = strongAt(k, -1);
|
|
1643
|
+
return before === strongAt(k, 1) ? before : rtl;
|
|
1644
|
+
});
|
|
1645
|
+
// R is level 1 in either paragraph; L is 0 in a left-to-right one and 2
|
|
1646
|
+
// in a right-to-left one
|
|
1647
|
+
const levels = isRtl.map((r) => (r ? 1 : rtl ? 2 : 0));
|
|
1648
|
+
const visual = content.map((_, k) => k);
|
|
1649
|
+
for (let level = Math.max(0, ...levels); level >= 1; level -= 1) {
|
|
1650
|
+
for (let a = 0; a < visual.length;) {
|
|
1651
|
+
if (levels[visual[a]] < level) {
|
|
1652
|
+
a += 1;
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
let b = a;
|
|
1656
|
+
while (b < visual.length && levels[visual[b]] >= level)
|
|
1657
|
+
b += 1;
|
|
1658
|
+
for (let i = a, j = b - 1; i < j; i += 1, j -= 1) {
|
|
1659
|
+
[visual[i], visual[j]] = [visual[j], visual[i]];
|
|
1660
|
+
}
|
|
1661
|
+
a = b;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const seq = visual.map((k) => content[k]);
|
|
1665
|
+
const elements = new Map();
|
|
1666
|
+
for (let i = 0; i < n; i += 1) {
|
|
1667
|
+
const p = order[i];
|
|
1668
|
+
if (p.kind !== 'edge')
|
|
1669
|
+
continue;
|
|
1670
|
+
const sides = elements.get(p.item.box) ?? { start: null, end: null };
|
|
1671
|
+
sides[p.item.side] = i;
|
|
1672
|
+
elements.set(p.item.box, sides);
|
|
1673
|
+
}
|
|
1674
|
+
const inside = [...elements].sort(([a], [b]) => depthOf(b) - depthOf(a));
|
|
1675
|
+
for (const [box, { start, end }] of inside) {
|
|
1676
|
+
// what of the element is on this line: everything between its edges,
|
|
1677
|
+
// or the line's end where one of them is on another line
|
|
1678
|
+
const from = start ?? -1;
|
|
1679
|
+
const to = end ?? n;
|
|
1680
|
+
let lo = -1;
|
|
1681
|
+
let hi = -1;
|
|
1682
|
+
for (let k = 0; k < seq.length; k += 1) {
|
|
1683
|
+
if (seq[k] > from && seq[k] < to) {
|
|
1684
|
+
if (lo < 0)
|
|
1685
|
+
lo = k;
|
|
1686
|
+
hi = k;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (lo < 0) {
|
|
1690
|
+
// Nothing of it here but its edges: they go where it is in the text,
|
|
1691
|
+
// after what comes before it, in the paragraph's direction.
|
|
1692
|
+
let at = rtl ? seq.length : 0;
|
|
1693
|
+
for (let i = from - 1; i >= 0; i -= 1) {
|
|
1694
|
+
const k = seq.indexOf(i);
|
|
1695
|
+
if (k >= 0) {
|
|
1696
|
+
at = rtl ? k : k + 1;
|
|
1697
|
+
break;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
lo = at;
|
|
1701
|
+
hi = at - 1;
|
|
1702
|
+
}
|
|
1703
|
+
const ltr = box.style.direction !== 'rtl';
|
|
1704
|
+
const left = ltr ? start : end;
|
|
1705
|
+
const right = ltr ? end : start;
|
|
1706
|
+
if (right !== null)
|
|
1707
|
+
seq.splice(hi + 1, 0, right);
|
|
1708
|
+
if (left !== null)
|
|
1709
|
+
seq.splice(lo, 0, left);
|
|
1710
|
+
}
|
|
1711
|
+
let x = order[0].at;
|
|
1712
|
+
for (const i of seq) {
|
|
1713
|
+
const piece = order[i];
|
|
1714
|
+
const room = (i + 1 < n ? order[i + 1].at : open.x) - piece.at;
|
|
1715
|
+
let dx = x - piece.at;
|
|
1716
|
+
if (piece.kind === 'text') {
|
|
1717
|
+
const natural = piece.item.layout.lines[piece.item.layoutLine];
|
|
1718
|
+
const k = content.indexOf(i);
|
|
1719
|
+
if (isRtl[k] && natural)
|
|
1720
|
+
dx += Math.max(0, room - natural.width);
|
|
1721
|
+
piece.item.drawX += dx;
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
piece.item.x += dx;
|
|
1725
|
+
}
|
|
1726
|
+
x += room;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Whether a piece can be out of place in logical order: anything on a
|
|
1730
|
+
* right-to-left line, a fragment that reads right to left, or the edge of
|
|
1731
|
+
* an element whose direction is not the line's. A line of left-to-right
|
|
1732
|
+
* text, the common one, is placed as it was built. */
|
|
1733
|
+
function needsOrdering(piece, rtl) {
|
|
1734
|
+
if (rtl)
|
|
1735
|
+
return true;
|
|
1736
|
+
if (piece.kind === 'text')
|
|
1737
|
+
return piece.reads === 'rtl';
|
|
1738
|
+
return piece.kind === 'edge' && piece.item.box.style.direction === 'rtl';
|
|
1739
|
+
}
|
|
1740
|
+
/** How deep a box is in the tree — an element inside another is deeper. */
|
|
1741
|
+
export function depthOf(box) {
|
|
1742
|
+
let depth = 0;
|
|
1743
|
+
for (let at = box.parent; at; at = at.parent)
|
|
1744
|
+
depth += 1;
|
|
1745
|
+
return depth;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Which way the text from `start` to `end` of a segment's runs reads, from
|
|
1749
|
+
* its letters. Asked of the text rather than of the engine's runs: a run of
|
|
1750
|
+
* the space before a word reads the paragraph's way wherever it starts a
|
|
1751
|
+
* fragment, which says nothing about the word, and an engine may not say
|
|
1752
|
+
* which way its runs went at all.
|
|
1753
|
+
*/
|
|
1754
|
+
function readingOf(runs, start, end) {
|
|
1755
|
+
let right = false;
|
|
1756
|
+
let left = false;
|
|
1757
|
+
let at = 0;
|
|
1758
|
+
for (const run of runs) {
|
|
1759
|
+
const length = run.text.length;
|
|
1760
|
+
const from = Math.max(start, at);
|
|
1761
|
+
const to = Math.min(end, at + length);
|
|
1762
|
+
if (to > from) {
|
|
1763
|
+
const text = to - from === length ? run.text : run.text.slice(from - at, to - at);
|
|
1764
|
+
right ||= RIGHT_TO_LEFT.test(text);
|
|
1765
|
+
left ||= LEFT_TO_RIGHT.test(text);
|
|
1766
|
+
}
|
|
1767
|
+
at += length;
|
|
1768
|
+
if (at >= end)
|
|
1769
|
+
break;
|
|
1770
|
+
}
|
|
1771
|
+
return right ? (left ? 'mixed' : 'rtl') : left ? 'ltr' : null;
|
|
1772
|
+
}
|
|
1773
|
+
/** A letter of a script written right to left — Hebrew, Arabic, Syriac,
|
|
1774
|
+
* Thaana, N'Ko and their neighbours, their presentation forms, and the
|
|
1775
|
+
* supplementary planes' right-to-left blocks — and any other letter. */
|
|
1776
|
+
const RIGHT_TO_LEFT = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}]/u;
|
|
1777
|
+
const LEFT_TO_RIGHT = /(?![\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}])\p{L}/u;
|
|
442
1778
|
/** Where an atomic's top edge sits, relative to the line box top. */
|
|
443
|
-
function alignAtomic(
|
|
444
|
-
const
|
|
1779
|
+
function alignAtomic(placed, line) {
|
|
1780
|
+
const box = placed.box;
|
|
445
1781
|
switch (box.style.verticalAlign) {
|
|
446
1782
|
case 'top':
|
|
447
1783
|
return 0;
|
|
448
1784
|
case 'bottom':
|
|
449
|
-
return line.height -
|
|
450
|
-
case 'middle':
|
|
451
|
-
return (line.height - h) / 2;
|
|
452
|
-
case 'sub':
|
|
453
|
-
return line.baseline - h + line.height * 0.1;
|
|
454
|
-
case 'super':
|
|
455
|
-
return line.baseline - h - line.height * 0.25;
|
|
1785
|
+
return line.height - (box.height + box.marginTop + box.marginBottom);
|
|
456
1786
|
default:
|
|
457
|
-
|
|
458
|
-
|
|
1787
|
+
return line.baseline - atomicBaseline(box) - (placed.raise ?? 0);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* How far an atomic's `vertical-align` raises its baseline above its
|
|
1792
|
+
* parent's (CSS 2.1 10.8.1), as `Lifts` raises an inline box's: `sub` and
|
|
1793
|
+
* `super` by a fifth and a third of the parent's font size and a pixel,
|
|
1794
|
+
* `text-top` and `text-bottom` to the edges of the parent's font, `middle`
|
|
1795
|
+
* by its middle to half the parent's x-height above the baseline, a length
|
|
1796
|
+
* by itself and a percentage of its own line height. `top` and `bottom` are
|
|
1797
|
+
* the line box's edges, and raise nothing from a baseline.
|
|
1798
|
+
*
|
|
1799
|
+
* `middle` is the parent's baseline, not the middle of the line: an image
|
|
1800
|
+
* set `middle` beside text in a line an image before it made tall sat
|
|
1801
|
+
* where that image left room, and drew the text beside it a pixel off.
|
|
1802
|
+
* Read before the line moves the atomic, as `atomicBaseline` is.
|
|
1803
|
+
*/
|
|
1804
|
+
function atomicRaise(fonts, box, parent) {
|
|
1805
|
+
const va = box.style.verticalAlign;
|
|
1806
|
+
switch (va) {
|
|
1807
|
+
case 'baseline':
|
|
1808
|
+
case 'top':
|
|
1809
|
+
case 'bottom':
|
|
1810
|
+
return 0;
|
|
1811
|
+
case 'sub':
|
|
1812
|
+
return -(parent.fontSize / 5 + parent.fontSize / 16);
|
|
1813
|
+
case 'super':
|
|
1814
|
+
return parent.fontSize / 3 + parent.fontSize / 16;
|
|
1815
|
+
}
|
|
1816
|
+
const h = box.height + box.marginTop + box.marginBottom;
|
|
1817
|
+
const b = atomicBaseline(box);
|
|
1818
|
+
switch (va) {
|
|
1819
|
+
case 'text-top':
|
|
1820
|
+
return faceExtent(fonts, parent).ascent - b;
|
|
1821
|
+
case 'text-bottom':
|
|
1822
|
+
return h - b - faceExtent(fonts, parent).descent;
|
|
1823
|
+
case 'middle':
|
|
1824
|
+
return xHeightOf(fonts, parent) / 2 + h / 2 - b;
|
|
1825
|
+
default:
|
|
1826
|
+
return typeof va === 'number'
|
|
1827
|
+
? va
|
|
1828
|
+
: resolve(va, lineHeightOf(fonts, box.style));
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Where an atomic's baseline is, from the top of its margin box (CSS 2.1
|
|
1833
|
+
* 10.8.1, 17.5.3): an inline-block's is its last line box's, an
|
|
1834
|
+
* inline-table's its first row's — its first line box's — and the bottom of
|
|
1835
|
+
* its margin box where it has none, is replaced, or clips what overflows
|
|
1836
|
+
* it. Set bottom-on-baseline, an inline-block's text sat its descent above
|
|
1837
|
+
* the text beside it, and a line holding one grew by as much.
|
|
1838
|
+
*
|
|
1839
|
+
* Read while the atomic is still where it was laid out, before the line
|
|
1840
|
+
* moves it: its lines are then at the same offset from its top as they will
|
|
1841
|
+
* be after.
|
|
1842
|
+
*/
|
|
1843
|
+
function atomicBaseline(box) {
|
|
1844
|
+
const bottom = box.height + box.marginTop + box.marginBottom;
|
|
1845
|
+
if (box.kind === 'replaced' ||
|
|
1846
|
+
scrolls(box.style) ||
|
|
1847
|
+
// layout containment keeps its baseline in (CSS Containment 2, 3.3)
|
|
1848
|
+
box.style.contain & CONTAIN_LAYOUT) {
|
|
1849
|
+
return bottom;
|
|
1850
|
+
}
|
|
1851
|
+
const baseline = box.kind === 'table'
|
|
1852
|
+
? tableBaseline(box)
|
|
1853
|
+
: // an inline flex box sits on its first (CSS Flexbox 8.5), where an
|
|
1854
|
+
// inline block sits on its last line's
|
|
1855
|
+
box.kind === 'flex'
|
|
1856
|
+
? firstBaselineIn(box)
|
|
1857
|
+
: lastBaselineIn(box);
|
|
1858
|
+
return baseline === null ? bottom : box.marginTop + (baseline - box.y);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* A flex container's first baseline, or its last (CSS Flexbox 8.5): that
|
|
1862
|
+
* of the first item on its first line that is aligned by its baseline — the
|
|
1863
|
+
* last on its last line — or where none is, of its first item, or last,
|
|
1864
|
+
* in `order`. An item gives its own, or where it has none, its border
|
|
1865
|
+
* box's bottom edge. Not its last child's: an `inline-flex` of a small
|
|
1866
|
+
* item and a big one sat on the big one's baseline. Null for a box with no
|
|
1867
|
+
* item.
|
|
1868
|
+
*/
|
|
1869
|
+
function flexBaseline(box, first) {
|
|
1870
|
+
let items = [];
|
|
1871
|
+
let reordered = false;
|
|
1872
|
+
for (const child of box.children) {
|
|
1873
|
+
if (child.outOfFlow || child.isFloat)
|
|
1874
|
+
continue;
|
|
1875
|
+
if (child.kind === 'text' || child.kind === 'break')
|
|
1876
|
+
continue;
|
|
1877
|
+
if (child.style.order !== 0)
|
|
1878
|
+
reordered = true;
|
|
1879
|
+
items.push(child);
|
|
1880
|
+
}
|
|
1881
|
+
if (!items.length)
|
|
1882
|
+
return null;
|
|
1883
|
+
if (reordered)
|
|
1884
|
+
items.sort((a, b) => a.style.order - b.style.order);
|
|
1885
|
+
if (!first)
|
|
1886
|
+
items = items.reverse();
|
|
1887
|
+
const style = box.style;
|
|
1888
|
+
if (style.flexDirection.startsWith('row') && !style.grid) {
|
|
1889
|
+
// a line ends where the next item does not go on along the main axis,
|
|
1890
|
+
// which from the last item is where it goes on
|
|
1891
|
+
const back = ((style.flexDirection === 'row-reverse') !==
|
|
1892
|
+
(style.direction === 'rtl')) !==
|
|
1893
|
+
!first;
|
|
1894
|
+
let last = NaN;
|
|
1895
|
+
for (const item of items) {
|
|
1896
|
+
if (!Number.isNaN(last) && (back ? item.x >= last : item.x <= last)) {
|
|
1897
|
+
break;
|
|
459
1898
|
}
|
|
460
|
-
|
|
1899
|
+
last = item.x;
|
|
1900
|
+
const align = item.style.alignSelf === 'auto'
|
|
1901
|
+
? style.alignItems
|
|
1902
|
+
: item.style.alignSelf;
|
|
1903
|
+
if (align === 'baseline')
|
|
1904
|
+
return itemBaselineOf(item, first, style);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return itemBaselineOf(items[0], first, style);
|
|
1908
|
+
}
|
|
1909
|
+
/** A flex item's baseline, in document coordinates: its first line's or
|
|
1910
|
+
* its last's — its bottom margin edge where it clips what it holds — or
|
|
1911
|
+
* where it has none, its border box's bottom edge, which a grid does not
|
|
1912
|
+
* take from its item, and has none of its own then (CSS Grid 1, 9). */
|
|
1913
|
+
function itemBaselineOf(item, first, container) {
|
|
1914
|
+
const found = childBaseline(item, first ? firstBaselineIn : lastBaselineIn);
|
|
1915
|
+
if (found !== null)
|
|
1916
|
+
return found;
|
|
1917
|
+
return container.grid ? null : item.y + item.height;
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* A table's baseline: its first row's (CSS 2.1 17.5.3), which is the first
|
|
1921
|
+
* line of a cell in the row, or where no cell in it has a line, the bottom
|
|
1922
|
+
* content edge of the row's lowest cell. Not the table's bottom: a table
|
|
1923
|
+
* of empty rows stands on its first one, and the rest hang below the line.
|
|
1924
|
+
*/
|
|
1925
|
+
function tableBaseline(table) {
|
|
1926
|
+
const { cells } = tableGrid(table);
|
|
1927
|
+
let lowest = -Infinity;
|
|
1928
|
+
for (const cell of cells) {
|
|
1929
|
+
if (cell.row > 0)
|
|
1930
|
+
break;
|
|
1931
|
+
const found = firstBaselineIn(cell.box);
|
|
1932
|
+
if (found !== null)
|
|
1933
|
+
return found;
|
|
1934
|
+
const b = cell.box;
|
|
1935
|
+
lowest = Math.max(lowest, b.y + b.height - b.padBottom - b.borderBottom);
|
|
461
1936
|
}
|
|
1937
|
+
return lowest === -Infinity ? null : lowest;
|
|
1938
|
+
}
|
|
1939
|
+
/**
|
|
1940
|
+
* The baseline a box's content gives it, in document coordinates: its last
|
|
1941
|
+
* line box's, or its last in-flow child's with one — and a child that clips
|
|
1942
|
+
* what overflows it gives its bottom margin edge and no deeper, whatever
|
|
1943
|
+
* text it holds (CSS Box Alignment's synthesized baseline). Null where
|
|
1944
|
+
* nothing in it has one.
|
|
1945
|
+
*/
|
|
1946
|
+
function lastBaselineIn(box) {
|
|
1947
|
+
// a flex box's is its items', in the order it lays them out
|
|
1948
|
+
if (box.kind === 'flex')
|
|
1949
|
+
return flexBaseline(box, false);
|
|
1950
|
+
if (box.lines?.length) {
|
|
1951
|
+
const line = box.lines[box.lines.length - 1];
|
|
1952
|
+
return line.y + line.baseline;
|
|
1953
|
+
}
|
|
1954
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1955
|
+
const found = childBaseline(box.children[i], lastBaselineIn);
|
|
1956
|
+
if (found !== null)
|
|
1957
|
+
return found;
|
|
1958
|
+
}
|
|
1959
|
+
return null;
|
|
1960
|
+
}
|
|
1961
|
+
/** The same, from the first line box or child: a table's baseline is its
|
|
1962
|
+
* first row's, and so is a table cell's. */
|
|
1963
|
+
export function firstBaselineIn(box) {
|
|
1964
|
+
if (box.kind === 'flex')
|
|
1965
|
+
return flexBaseline(box, true);
|
|
1966
|
+
if (box.lines?.length)
|
|
1967
|
+
return box.lines[0].y + box.lines[0].baseline;
|
|
1968
|
+
for (const child of box.children) {
|
|
1969
|
+
const found = childBaseline(child, firstBaselineIn);
|
|
1970
|
+
if (found !== null)
|
|
1971
|
+
return found;
|
|
1972
|
+
}
|
|
1973
|
+
return null;
|
|
1974
|
+
}
|
|
1975
|
+
function childBaseline(child, inside) {
|
|
1976
|
+
if (child.outOfFlow || child.isFloat || child.kind === 'replaced') {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
if (scrolls(child.style)) {
|
|
1980
|
+
return child.y + child.height + child.marginBottom;
|
|
1981
|
+
}
|
|
1982
|
+
return inside(child);
|
|
1983
|
+
}
|
|
1984
|
+
/**
|
|
1985
|
+
* The static position of each absolutely positioned box among a
|
|
1986
|
+
* paragraph's content (CSS 2.1 10.3.7, 10.6.4): where it would have been
|
|
1987
|
+
* in flow, found from the content before it. A block-level one would have
|
|
1988
|
+
* broken the line that content is on, so it goes under that line at the
|
|
1989
|
+
* line's start; an inline-level one goes on it, where the pen stood after
|
|
1990
|
+
* that content, or under it where a forced break ends the text; either goes
|
|
1991
|
+
* at the top where nothing comes before it. Taken as the block's content
|
|
1992
|
+
* top for every one of them, a box after a line of text was drawn over that
|
|
1993
|
+
* line, and a menu under its link came up on it. Not from a probe of
|
|
1994
|
+
* intrinsic width, whose room is no place for one.
|
|
1995
|
+
*/
|
|
1996
|
+
function staticPositions(statics, items, lines, block, options) {
|
|
1997
|
+
if (!Number.isFinite(options.width))
|
|
1998
|
+
return;
|
|
1999
|
+
for (const { index, box } of statics) {
|
|
2000
|
+
const inline = INLINE_BEFORE_ABSOLUTE.has(box.style);
|
|
2001
|
+
let top = lines.length ? lines[0].y : 0;
|
|
2002
|
+
let x = 0;
|
|
2003
|
+
const after = lines.length ? penAfter(items, index, lines) : null;
|
|
2004
|
+
if (after) {
|
|
2005
|
+
const { line, pen, broken } = after;
|
|
2006
|
+
if (inline && !broken) {
|
|
2007
|
+
top = line.y;
|
|
2008
|
+
x = pen;
|
|
2009
|
+
}
|
|
2010
|
+
else {
|
|
2011
|
+
top = line.y + line.height;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
box.staticPosition = {
|
|
2015
|
+
from: block,
|
|
2016
|
+
x: options.originX + x - block.x,
|
|
2017
|
+
right: options.originX + options.width - block.x,
|
|
2018
|
+
y: options.startY + top - block.y,
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
/** The line the content before `items[index]` ended on, where the pen
|
|
2023
|
+
* stood after it, and whether it ended in a forced break. Null where no
|
|
2024
|
+
* text or atomic comes before. */
|
|
2025
|
+
function penAfter(items, index, lines) {
|
|
2026
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
2027
|
+
const item = items[i];
|
|
2028
|
+
if (item.kind === 'edge') {
|
|
2029
|
+
// an inline box's margin, border or padding is content, which keeps
|
|
2030
|
+
// its line from being one of no height (CSS 2.1 9.4.2), even where
|
|
2031
|
+
// they add up to no width
|
|
2032
|
+
if (!hasEdgeOn(item.box, item.side))
|
|
2033
|
+
continue;
|
|
2034
|
+
for (const line of lines) {
|
|
2035
|
+
const placed = line.edges?.find((e) => e.box === item.box && e.side === item.side);
|
|
2036
|
+
if (placed) {
|
|
2037
|
+
return { line, pen: placed.x + placed.width, broken: false };
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
if (item.kind === 'atomic') {
|
|
2043
|
+
for (const line of lines) {
|
|
2044
|
+
const placed = line.atomics.find((a) => a.box === item.box);
|
|
2045
|
+
if (!placed)
|
|
2046
|
+
continue;
|
|
2047
|
+
const pen = placed.x + item.box.width + item.box.marginRight;
|
|
2048
|
+
return { line, pen, broken: false };
|
|
2049
|
+
}
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
if (item.kind !== 'text' || item.control)
|
|
2053
|
+
continue;
|
|
2054
|
+
const end = item.start + item.length;
|
|
2055
|
+
const line = lines.find((l) => l.textStart < end && end <= l.textEnd) ??
|
|
2056
|
+
lines[lines.length - 1];
|
|
2057
|
+
const broken = item.run.text.endsWith('\n');
|
|
2058
|
+
return { line, pen: broken ? 0 : caretX(line, end), broken };
|
|
2059
|
+
}
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
/** Whether an inline box has margin, border or padding on a side. */
|
|
2063
|
+
function hasEdgeOn(box, side) {
|
|
2064
|
+
const left = (side === 'start') !== (box.style.direction === 'rtl');
|
|
2065
|
+
return left
|
|
2066
|
+
? box.marginLeft !== 0 || box.borderLeft !== 0 || box.padLeft !== 0
|
|
2067
|
+
: box.marginRight !== 0 || box.borderRight !== 0 || box.padRight !== 0;
|
|
2068
|
+
}
|
|
2069
|
+
/** Where the caret at document offset `at` stands on a line, in the
|
|
2070
|
+
* paragraph's coordinates: the line's end where no text of it holds the
|
|
2071
|
+
* offset. */
|
|
2072
|
+
function caretX(line, at) {
|
|
2073
|
+
for (const text of line.texts) {
|
|
2074
|
+
if (at < text.textStart || at > text.textEnd)
|
|
2075
|
+
continue;
|
|
2076
|
+
const offsets = layoutOffsets(text.layout);
|
|
2077
|
+
const units = layoutOffsetOf(text, at, true);
|
|
2078
|
+
const caret = text.layout.caretPosition(codePointAtOffset(offsets, units));
|
|
2079
|
+
return text.drawX + caret.x;
|
|
2080
|
+
}
|
|
2081
|
+
return line.x + line.width;
|
|
462
2082
|
}
|
|
463
2083
|
// --- gathering --------------------------------------------------------------
|
|
464
|
-
/** Flatten an inline subtree into a stream of runs, atomics and
|
|
465
|
-
|
|
2084
|
+
/** Flatten an inline subtree into a stream of runs, atomics, breaks and the
|
|
2085
|
+
* edges of the inline boxes the text sits in. */
|
|
2086
|
+
/** Gather an inline formatting context's content into `out`, and answer
|
|
2087
|
+
* how many floats it gathered — none unless `floats` asks for them. */
|
|
2088
|
+
function collect(box, out, width, fonts, block, floats,
|
|
2089
|
+
/** Where each absolutely positioned box among the content is: before the
|
|
2090
|
+
* item at `index`. No item of the stream, which reads none of them. */
|
|
2091
|
+
statics,
|
|
2092
|
+
/** Whether the text is an inline box's that has a margin, border or
|
|
2093
|
+
* padding at a side, which parts it from the text beside it. */
|
|
2094
|
+
apart = false) {
|
|
2095
|
+
let floated = 0;
|
|
466
2096
|
for (const child of box.children) {
|
|
467
|
-
if (child.outOfFlow
|
|
2097
|
+
if (child.outOfFlow) {
|
|
2098
|
+
statics?.push({ index: out.length, box: child });
|
|
468
2099
|
continue;
|
|
2100
|
+
}
|
|
2101
|
+
if (child.isFloat) {
|
|
2102
|
+
if (floats) {
|
|
2103
|
+
out.push({ kind: 'float', box: child });
|
|
2104
|
+
floated += 1;
|
|
2105
|
+
}
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
469
2108
|
switch (child.kind) {
|
|
470
2109
|
case 'text':
|
|
471
2110
|
if (child.text) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
2111
|
+
const run = runFor(heldText(child), child.style);
|
|
2112
|
+
// shaping is broken across the edge (CSS Text 3, 7.3), and the
|
|
2113
|
+
// engine shapes a word that runs across spans shaped alike as one:
|
|
2114
|
+
// its text is shaped on its own. At both of its sides, the one
|
|
2115
|
+
// with no edge among them, as the engine takes it
|
|
2116
|
+
if (apart)
|
|
2117
|
+
run.shapeApart = true;
|
|
2118
|
+
if (child.style.wordSpacing) {
|
|
2119
|
+
wordSpaced(out, child, run, child.style.wordSpacing);
|
|
2120
|
+
}
|
|
2121
|
+
else {
|
|
2122
|
+
out.push({
|
|
2123
|
+
kind: 'text',
|
|
2124
|
+
run,
|
|
2125
|
+
box: child,
|
|
2126
|
+
length: child.text.length,
|
|
2127
|
+
start: child.textStart,
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
478
2130
|
}
|
|
479
2131
|
break;
|
|
480
2132
|
case 'break':
|
|
@@ -495,11 +2147,52 @@ function collect(box, out) {
|
|
|
495
2147
|
},
|
|
496
2148
|
box: child,
|
|
497
2149
|
length: 1,
|
|
2150
|
+
start: child.textStart,
|
|
498
2151
|
});
|
|
499
2152
|
break;
|
|
500
|
-
case 'inline':
|
|
501
|
-
|
|
2153
|
+
case 'inline': {
|
|
2154
|
+
const [start, end] = inlineEdges(child, width, fonts);
|
|
2155
|
+
// Both edges or neither, so that an element with any is bounded on
|
|
2156
|
+
// the line: its pieces are what lies between them, which is how a
|
|
2157
|
+
// right-to-left line finds them (`reorderLine`), and how a rounded
|
|
2158
|
+
// background knows the fragments that open and close it, to round
|
|
2159
|
+
// only those corners — so it gets them even at width zero.
|
|
2160
|
+
// And a box that `position: relative` moves or `vertical-align`
|
|
2161
|
+
// raises has them, so that its text is laid out apart from the text
|
|
2162
|
+
// around it, to be drawn where the box goes (`offsetInline`,
|
|
2163
|
+
// `Lifts`)
|
|
2164
|
+
// and a margin a border cancels is still an edge, which keeps its
|
|
2165
|
+
// line from being one of no height (CSS 2.1 9.4.2)
|
|
2166
|
+
const edged = start !== 0 ||
|
|
2167
|
+
end !== 0 ||
|
|
2168
|
+
hasEdgeOn(child, 'start') ||
|
|
2169
|
+
hasEdgeOn(child, 'end') ||
|
|
2170
|
+
(child.decoration !== null &&
|
|
2171
|
+
child.style.borderRadius.some((r) => r !== 0)) ||
|
|
2172
|
+
isOffset(child.style) ||
|
|
2173
|
+
child.style.verticalAlign !== 'baseline' ||
|
|
2174
|
+
(fonts !== null && ownsLeading(fonts, block, child.style));
|
|
2175
|
+
if (edged) {
|
|
2176
|
+
out.push({ kind: 'edge', box: child, side: 'start', width: start });
|
|
2177
|
+
}
|
|
2178
|
+
// `unicode-bidi` as the controls it stands for, inside the box's
|
|
2179
|
+
// edges and around its text, where it has any
|
|
2180
|
+
const controls = child.style.unicodeBidi !== 'normal' &&
|
|
2181
|
+
child.subtreeTextEnd > child.subtreeTextStart
|
|
2182
|
+
? bidiControls(child.style, false)
|
|
2183
|
+
: null;
|
|
2184
|
+
if (controls) {
|
|
2185
|
+
pushControls(out, controls[0], child, child.subtreeTextStart);
|
|
2186
|
+
}
|
|
2187
|
+
floated += collect(child, out, width, fonts, block, floats, statics, start !== 0 || end !== 0);
|
|
2188
|
+
if (controls) {
|
|
2189
|
+
pushControls(out, controls[1], child, child.subtreeTextEnd);
|
|
2190
|
+
}
|
|
2191
|
+
if (edged) {
|
|
2192
|
+
out.push({ kind: 'edge', box: child, side: 'end', width: end });
|
|
2193
|
+
}
|
|
502
2194
|
break;
|
|
2195
|
+
}
|
|
503
2196
|
default:
|
|
504
2197
|
// Everything else that reached an inline context is inline-level and
|
|
505
2198
|
// atomic: a replaced element, an `inline-block`, an `inline-table`.
|
|
@@ -507,45 +2200,885 @@ function collect(box, out) {
|
|
|
507
2200
|
break;
|
|
508
2201
|
}
|
|
509
2202
|
}
|
|
2203
|
+
return floated;
|
|
2204
|
+
}
|
|
2205
|
+
/**
|
|
2206
|
+
* What a paragraph does with a word too long for its line: cuts it where
|
|
2207
|
+
* its style or any text in it says a word may be cut — `overflow-wrap`, or
|
|
2208
|
+
* `word-break` or `line-break: anywhere`, which cut one whatever
|
|
2209
|
+
* `overflow-wrap` says (CSS Text 3, 5.2 and 5.3) — and lets it run past the
|
|
2210
|
+
* line's end where nothing does, as a browser does. The engine takes one answer for the paragraph, so a span
|
|
2211
|
+
* that breaks its words breaks the paragraph's.
|
|
2212
|
+
*/
|
|
2213
|
+
function overflowWrapOf(style, items) {
|
|
2214
|
+
if (breaksWords(style))
|
|
2215
|
+
return 'break-word';
|
|
2216
|
+
for (const item of items) {
|
|
2217
|
+
if (item.kind === 'text' && breaksWords(item.box.style)) {
|
|
2218
|
+
return 'break-word';
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return 'normal';
|
|
2222
|
+
}
|
|
2223
|
+
/** A `text-overflow` cut on a line that does not wrap puts its ellipsis at
|
|
2224
|
+
* the box's edge, inside a word where one runs past it, as a browser
|
|
2225
|
+
* does — which is the engine cutting the word there. */
|
|
2226
|
+
function cutWrap(style, cut, wrapWords) {
|
|
2227
|
+
return cut && !wraps(style) ? 'break-word' : wrapWords;
|
|
2228
|
+
}
|
|
2229
|
+
function breaksWords(style) {
|
|
2230
|
+
return (style.overflowWrap !== 'normal' ||
|
|
2231
|
+
style.wordBreak === 'break-all' ||
|
|
2232
|
+
style.wordBreak === 'break-word' ||
|
|
2233
|
+
style.lineBreakAnywhere);
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Keep the text of a `nowrap` element together where its block wraps (CSS
|
|
2237
|
+
* 2.1 16.6). Its runs share the element as their `nowrap`, which ntk makes
|
|
2238
|
+
* no break inside or between; and, for an engine that does not read it,
|
|
2239
|
+
* a space it may not break after is a no-break space, as wide and as many,
|
|
2240
|
+
* so every offset holds. Whether a line breaks between two characters is
|
|
2241
|
+
* the call of the nearest element holding both (CSS Text 3, 5.1), so the
|
|
2242
|
+
* space such an element ends on stays one to break at where the text after
|
|
2243
|
+
* it is not the same element's: text that wraps, or another `nowrap`
|
|
2244
|
+
* element's — two tags side by side, a collapsed space between them.
|
|
2245
|
+
*/
|
|
2246
|
+
function holdNoWrap(items, block) {
|
|
2247
|
+
let last = true;
|
|
2248
|
+
let nextGroup = null;
|
|
2249
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
2250
|
+
const item = items[i];
|
|
2251
|
+
if (item.kind !== 'text')
|
|
2252
|
+
continue;
|
|
2253
|
+
const group = wraps(item.box.style) ? null : noWrapGroup(item.box, block);
|
|
2254
|
+
if (group) {
|
|
2255
|
+
const text = item.run.text;
|
|
2256
|
+
let held = text;
|
|
2257
|
+
// `pre`'s are no-break spaces already (`runFor`)
|
|
2258
|
+
if (item.box.style.whiteSpace === 'nowrap' && text.includes(' ')) {
|
|
2259
|
+
held = text.replace(/ /g, '\u00a0');
|
|
2260
|
+
if (text.endsWith(' ') && (last || nextGroup !== group)) {
|
|
2261
|
+
held = held.slice(0, -1) + ' ';
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
item.run = { ...item.run, text: held, nowrap: group };
|
|
2265
|
+
}
|
|
2266
|
+
last = false;
|
|
2267
|
+
nextGroup = group;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/** The element a text that does not wrap is held together by: the outermost
|
|
2271
|
+
* one inside the block that does not wrap either. */
|
|
2272
|
+
function noWrapGroup(box, block) {
|
|
2273
|
+
let group = box;
|
|
2274
|
+
for (let up = box.parent; up && up !== block; up = up.parent) {
|
|
2275
|
+
if (wraps(up.style))
|
|
2276
|
+
break;
|
|
2277
|
+
group = up;
|
|
2278
|
+
}
|
|
2279
|
+
return group;
|
|
2280
|
+
}
|
|
2281
|
+
/** The bidi controls `unicode-bidi` stands for, opening and closing (CSS
|
|
2282
|
+
* Writing Modes 3, 2.4.2), in the direction of the box; null for `normal`.
|
|
2283
|
+
* On a block only an override opens anything: the block's embedding is
|
|
2284
|
+
* its paragraph's direction. */
|
|
2285
|
+
function bidiControls(style, block) {
|
|
2286
|
+
const rtl = style.direction === 'rtl';
|
|
2287
|
+
const override = rtl ? '\u202E' : '\u202D';
|
|
2288
|
+
switch (style.unicodeBidi) {
|
|
2289
|
+
case 'embed':
|
|
2290
|
+
return [rtl ? '\u202B' : '\u202A', '\u202C'];
|
|
2291
|
+
case 'bidi-override':
|
|
2292
|
+
return [override, '\u202C'];
|
|
2293
|
+
case 'isolate':
|
|
2294
|
+
return [rtl ? '\u2067' : '\u2066', '\u2069'];
|
|
2295
|
+
case 'isolate-override':
|
|
2296
|
+
return block
|
|
2297
|
+
? [override, '\u202C']
|
|
2298
|
+
: [(rtl ? '\u2067' : '\u2066') + override, '\u202C\u2069'];
|
|
2299
|
+
case 'plaintext':
|
|
2300
|
+
return ['\u2068', '\u2069'];
|
|
2301
|
+
default:
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
/** Bidi controls as items of their own, one a unit, at a document index
|
|
2306
|
+
* none of them takes up. */
|
|
2307
|
+
function pushControls(out, controls, box, start) {
|
|
2308
|
+
for (const control of controls) {
|
|
2309
|
+
out.push({
|
|
2310
|
+
kind: 'text',
|
|
2311
|
+
run: runFor(control, box.style),
|
|
2312
|
+
box,
|
|
2313
|
+
length: 1,
|
|
2314
|
+
start,
|
|
2315
|
+
control: true,
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
function hasControls(items) {
|
|
2320
|
+
for (const item of items)
|
|
2321
|
+
if (item.kind === 'text' && item.control)
|
|
2322
|
+
return true;
|
|
2323
|
+
return false;
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* A text box with `word-spacing`, as runs of its words and of the spaces
|
|
2327
|
+
* between them, each space run carrying the extra as letter spacing: the
|
|
2328
|
+
* engines space letters, not words, and a space's letter spacing is exactly
|
|
2329
|
+
* the room CSS adds after a word separator (16.4 — the space and the
|
|
2330
|
+
* no-break space). Only text that asks for it is split.
|
|
2331
|
+
*/
|
|
2332
|
+
function wordSpaced(out, box, run, spacing) {
|
|
2333
|
+
const text = box.text;
|
|
2334
|
+
const push = (from, to, extra) => {
|
|
2335
|
+
if (to <= from)
|
|
2336
|
+
return;
|
|
2337
|
+
out.push({
|
|
2338
|
+
kind: 'text',
|
|
2339
|
+
run: {
|
|
2340
|
+
...run,
|
|
2341
|
+
text: text.slice(from, to),
|
|
2342
|
+
...(extra ? { letterSpacing: (run.letterSpacing ?? 0) + extra } : null),
|
|
2343
|
+
},
|
|
2344
|
+
box,
|
|
2345
|
+
length: to - from,
|
|
2346
|
+
start: box.textStart + from,
|
|
2347
|
+
});
|
|
2348
|
+
};
|
|
2349
|
+
let from = 0;
|
|
2350
|
+
for (const m of text.matchAll(/[ \u00A0]+/g)) {
|
|
2351
|
+
push(from, m.index, 0);
|
|
2352
|
+
push(m.index, m.index + m[0].length, spacing);
|
|
2353
|
+
from = m.index + m[0].length;
|
|
2354
|
+
}
|
|
2355
|
+
push(from, text.length, 0);
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* An inline box's margin, border and padding, resolved onto it — percentages
|
|
2359
|
+
* of the containing block's width, as a block's are — and the widths of its
|
|
2360
|
+
* two edges on the line: the start side's before its first fragment, the
|
|
2361
|
+
* end side's after its last. One with a background or a border to paint
|
|
2362
|
+
* also notes its face's ascent and descent, the height CSS paints them over
|
|
2363
|
+
* (10.6.1, the content area), so paint needs no font.
|
|
2364
|
+
*/
|
|
2365
|
+
function inlineEdges(box, width, fonts) {
|
|
2366
|
+
const s = box.style;
|
|
2367
|
+
box.borderTop = usedBorder(s.borderTopWidth, s.borderTopStyle);
|
|
2368
|
+
box.borderRight = usedBorder(s.borderRightWidth, s.borderRightStyle);
|
|
2369
|
+
box.borderBottom = usedBorder(s.borderBottomWidth, s.borderBottomStyle);
|
|
2370
|
+
box.borderLeft = usedBorder(s.borderLeftWidth, s.borderLeftStyle);
|
|
2371
|
+
box.padTop = resolve(s.paddingTop, width);
|
|
2372
|
+
box.padRight = resolve(s.paddingRight, width);
|
|
2373
|
+
box.padBottom = resolve(s.paddingBottom, width);
|
|
2374
|
+
box.padLeft = resolve(s.paddingLeft, width);
|
|
2375
|
+
box.marginLeft = resolve(s.marginLeft, width);
|
|
2376
|
+
box.marginRight = resolve(s.marginRight, width);
|
|
2377
|
+
if (box.cut) {
|
|
2378
|
+
// a piece of a box broken around a block has no edge where it was cut
|
|
2379
|
+
const rtl = s.direction === 'rtl';
|
|
2380
|
+
if (box.cut & (rtl ? 2 : 1)) {
|
|
2381
|
+
box.marginLeft = box.borderLeft = box.padLeft = 0;
|
|
2382
|
+
}
|
|
2383
|
+
if (box.cut & (rtl ? 1 : 2)) {
|
|
2384
|
+
box.marginRight = box.borderRight = box.padRight = 0;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
box.decoration = inlineDecoration(fonts, box, s);
|
|
2388
|
+
const left = box.marginLeft + box.borderLeft + box.padLeft;
|
|
2389
|
+
const right = box.padRight + box.borderRight + box.marginRight;
|
|
2390
|
+
return s.direction === 'rtl' ? [right, left] : [left, right];
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* What an inline box draws around its text, in `s`: the extent of its face
|
|
2394
|
+
* where it has a background, a border or an outline, and null where it
|
|
2395
|
+
* draws none. Its borders are the ones `inlineEdges` just used. Shared with
|
|
2396
|
+
* a pointer move that gives a link a background (`HtmlViewNode`).
|
|
2397
|
+
*/
|
|
2398
|
+
export function inlineDecoration(fonts, box, s) {
|
|
2399
|
+
// a background is a colour, a gradient or an image: a span a gradient
|
|
2400
|
+
// underlines, a highlighter's, was painted only where it had a border
|
|
2401
|
+
const decorated = !isTransparent(s.backgroundColor) ||
|
|
2402
|
+
s.backgroundGradient !== null ||
|
|
2403
|
+
s.backgroundImage !== null ||
|
|
2404
|
+
s.backgroundImages !== null ||
|
|
2405
|
+
box.borderTop + box.borderRight + box.borderBottom + box.borderLeft > 0 ||
|
|
2406
|
+
s.outlineStyle !== 'none';
|
|
2407
|
+
return !decorated ? null : fonts ? faceExtent(fonts, s) : NO_EXTENT;
|
|
2408
|
+
}
|
|
2409
|
+
/** A border's width, or none where its style draws none. */
|
|
2410
|
+
function usedBorder(width, style) {
|
|
2411
|
+
return style === 'none' || style === 'hidden' ? 0 : width;
|
|
2412
|
+
}
|
|
2413
|
+
/** A face's ascent and descent at a style's size, kept per font manager
|
|
2414
|
+
* and shared by the boxes set in it: by the style, which the cascade
|
|
2415
|
+
* shares between every element of a kind, and by the face, which styles
|
|
2416
|
+
* that differ in anything else share. */
|
|
2417
|
+
const FACE_EXTENTS = new WeakMap();
|
|
2418
|
+
/** The extent of a decorated box where there are no fonts to ask. */
|
|
2419
|
+
const NO_EXTENT = { ascent: 0, descent: 0 };
|
|
2420
|
+
/** A style's face's ascent and descent, which an inline box's background
|
|
2421
|
+
* is painted over. */
|
|
2422
|
+
export function faceExtentOf(fonts, style) {
|
|
2423
|
+
return faceExtent(fonts, style);
|
|
2424
|
+
}
|
|
2425
|
+
function faceExtent(fonts, style) {
|
|
2426
|
+
let cache = FACE_EXTENTS.get(fonts);
|
|
2427
|
+
if (!cache) {
|
|
2428
|
+
cache = { byStyle: new WeakMap(), byFace: new Map() };
|
|
2429
|
+
FACE_EXTENTS.set(fonts, cache);
|
|
2430
|
+
}
|
|
2431
|
+
let extent = cache.byStyle.get(style);
|
|
2432
|
+
if (extent)
|
|
2433
|
+
return extent;
|
|
2434
|
+
const key = `${style.fontFamily}|${style.fontSize}|${style.fontWeight}|${style.fontStyle}`;
|
|
2435
|
+
extent = cache.byFace.get(key);
|
|
2436
|
+
if (!extent) {
|
|
2437
|
+
try {
|
|
2438
|
+
const m = fonts
|
|
2439
|
+
.match(style.fontFamily, {
|
|
2440
|
+
size: style.fontSize,
|
|
2441
|
+
weight: style.fontWeight,
|
|
2442
|
+
style: style.fontStyle,
|
|
2443
|
+
})
|
|
2444
|
+
.metrics(style.fontSize);
|
|
2445
|
+
extent = { ascent: m.ascent, descent: m.descent };
|
|
2446
|
+
}
|
|
2447
|
+
catch {
|
|
2448
|
+
extent = { ascent: style.fontSize * 0.8, descent: style.fontSize * 0.2 };
|
|
2449
|
+
}
|
|
2450
|
+
if (cache.byFace.size > 64)
|
|
2451
|
+
cache.byFace.clear();
|
|
2452
|
+
cache.byFace.set(key, extent);
|
|
2453
|
+
}
|
|
2454
|
+
cache.byStyle.set(style, extent);
|
|
2455
|
+
return extent;
|
|
2456
|
+
}
|
|
2457
|
+
/**
|
|
2458
|
+
* What a first line is made with where its `::first-line` sets its fonts
|
|
2459
|
+
* (CSS 2.1 5.12.1). Its text is in the styles its boxes have on it — each
|
|
2460
|
+
* element's own rules over what it inherits there, which takes the fonts
|
|
2461
|
+
* from `::first-line` (CSS Pseudo 4, 2.1.2), so a `<small>` is smaller
|
|
2462
|
+
* than the line's font and not than the block's — and it is laid out at
|
|
2463
|
+
* the pseudo-element's line height. The pseudo-element is an inline box
|
|
2464
|
+
* around the line's content, so its face at its line height is on the line
|
|
2465
|
+
* with the block's strut, which stays: a `::first-line` of a smaller line
|
|
2466
|
+
* height leaves the line as tall as the block's.
|
|
2467
|
+
*/
|
|
2468
|
+
function firstLineSetting(block, items, how, fonts, strut, justify) {
|
|
2469
|
+
const style = how.style;
|
|
2470
|
+
const of = (box) => box === block || !box.parent
|
|
2471
|
+
? style
|
|
2472
|
+
: styleOnFirstLine(box, of(box.parent), how.styler);
|
|
2473
|
+
// and in its `text-transform` where the line has one of its own, which
|
|
2474
|
+
// the boxes' text was not made with: a word carried from one run into
|
|
2475
|
+
// the next, as the box builder carries it (`letteredAfter`)
|
|
2476
|
+
let lettered = false;
|
|
2477
|
+
const restyled = items.map((item) => {
|
|
2478
|
+
if (item.kind === 'atomic')
|
|
2479
|
+
lettered = false;
|
|
2480
|
+
if (item.kind !== 'text')
|
|
2481
|
+
return item;
|
|
2482
|
+
const own = item.box.style;
|
|
2483
|
+
const on = of(item.box);
|
|
2484
|
+
let run = restyledRun(item.run, own, on);
|
|
2485
|
+
const transform = on.textTransform;
|
|
2486
|
+
if (!item.control &&
|
|
2487
|
+
transform !== own.textTransform &&
|
|
2488
|
+
transform !== 'none') {
|
|
2489
|
+
const text = transformInPlace(run.text, transform, lettered);
|
|
2490
|
+
if (text !== run.text)
|
|
2491
|
+
run = { ...run, text };
|
|
2492
|
+
}
|
|
2493
|
+
lettered = letteredAfter(run.text, lettered);
|
|
2494
|
+
return run === item.run ? item : { ...item, run };
|
|
2495
|
+
});
|
|
2496
|
+
const lineHeight = lineHeightMultiplier(fonts, style);
|
|
2497
|
+
const base = {
|
|
2498
|
+
family: style.fontFamily,
|
|
2499
|
+
size: style.fontSize,
|
|
2500
|
+
weight: style.fontWeight,
|
|
2501
|
+
style: style.fontStyle,
|
|
2502
|
+
color: style.color,
|
|
2503
|
+
};
|
|
2504
|
+
const own = strutOf(fonts, style);
|
|
2505
|
+
return {
|
|
2506
|
+
items: restyled,
|
|
2507
|
+
base,
|
|
2508
|
+
lineHeight,
|
|
2509
|
+
strut: strut && {
|
|
2510
|
+
ascent: Math.max(strut.ascent, own.ascent),
|
|
2511
|
+
descent: Math.max(strut.descent, own.descent),
|
|
2512
|
+
},
|
|
2513
|
+
lifts: new Lifts(fonts, style, of),
|
|
2514
|
+
justify: justify && {
|
|
2515
|
+
...justify,
|
|
2516
|
+
base,
|
|
2517
|
+
options: { ...justify.options, lineHeight },
|
|
2518
|
+
},
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
/**
|
|
2522
|
+
* Each box's style on a first line (`FirstLineStyler`), kept for the style
|
|
2523
|
+
* its parent has there: every pass asks again, and a style that is a new
|
|
2524
|
+
* object each time is a new face to every cache that keys on one.
|
|
2525
|
+
*/
|
|
2526
|
+
const ON_FIRST_LINE = new WeakMap();
|
|
2527
|
+
/** A box's style on a first line, where its parent's there is `parent`. */
|
|
2528
|
+
function styleOnFirstLine(box, parent, styler) {
|
|
2529
|
+
const known = ON_FIRST_LINE.get(box);
|
|
2530
|
+
if (known && known.parent === parent)
|
|
2531
|
+
return known.style;
|
|
2532
|
+
const style = styler(box, parent);
|
|
2533
|
+
ON_FIRST_LINE.set(box, { parent, style });
|
|
2534
|
+
return style;
|
|
2535
|
+
}
|
|
2536
|
+
/** What a run takes from its style (`runFor`). */
|
|
2537
|
+
const RUN_STYLE = [
|
|
2538
|
+
'family',
|
|
2539
|
+
'size',
|
|
2540
|
+
'weight',
|
|
2541
|
+
'style',
|
|
2542
|
+
'color',
|
|
2543
|
+
'letterSpacing',
|
|
2544
|
+
'features',
|
|
2545
|
+
'underline',
|
|
2546
|
+
'underlineStyle',
|
|
2547
|
+
'underlineOffset',
|
|
2548
|
+
'underlineThickness',
|
|
2549
|
+
'strike',
|
|
2550
|
+
];
|
|
2551
|
+
/**
|
|
2552
|
+
* A run of text in `style` rather than `own`, the style it was made in:
|
|
2553
|
+
* what it has of neither — the room a tab or `word-spacing` adds to its
|
|
2554
|
+
* letter spacing, a `nowrap` group, where its shaping is broken — kept.
|
|
2555
|
+
*/
|
|
2556
|
+
function restyledRun(run, own, style) {
|
|
2557
|
+
if (style === own)
|
|
2558
|
+
return run;
|
|
2559
|
+
const fresh = runFor(run.text, style);
|
|
2560
|
+
const out = { ...run };
|
|
2561
|
+
for (const name of RUN_STYLE) {
|
|
2562
|
+
if (fresh[name] === undefined)
|
|
2563
|
+
delete out[name];
|
|
2564
|
+
else
|
|
2565
|
+
out[name] = fresh[name];
|
|
2566
|
+
}
|
|
2567
|
+
const added = (run.letterSpacing ?? 0) - (own.letterSpacing || 0);
|
|
2568
|
+
if (added)
|
|
2569
|
+
out.letterSpacing = (out.letterSpacing ?? 0) + added;
|
|
2570
|
+
return out;
|
|
2571
|
+
}
|
|
2572
|
+
/**
|
|
2573
|
+
* The first line's text in its `::first-line` colour: the runs before the
|
|
2574
|
+
* line's end whose colour is the block's own, the one that ends past it cut
|
|
2575
|
+
* there. A run in a colour of its own — a link's — keeps it, as an element
|
|
2576
|
+
* inside the pseudo-element does. Colour moves no glyph, so the lines break
|
|
2577
|
+
* where they did.
|
|
2578
|
+
*/
|
|
2579
|
+
function firstLineColour(items, firstLine) {
|
|
2580
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
2581
|
+
const item = items[i];
|
|
2582
|
+
if (item.kind !== 'text')
|
|
2583
|
+
continue;
|
|
2584
|
+
// past the line: a run in a colour of its own may have been the one
|
|
2585
|
+
// across its end, so this is where the walk stops, not at the cut
|
|
2586
|
+
if (item.start >= firstLine.end)
|
|
2587
|
+
break;
|
|
2588
|
+
if (item.run.color !== firstLine.from)
|
|
2589
|
+
continue;
|
|
2590
|
+
const cut = firstLine.end - item.start;
|
|
2591
|
+
if (cut < item.length) {
|
|
2592
|
+
items.splice(i + 1, 0, {
|
|
2593
|
+
...item,
|
|
2594
|
+
run: { ...item.run, text: item.run.text.slice(cut) },
|
|
2595
|
+
start: item.start + cut,
|
|
2596
|
+
length: item.length - cut,
|
|
2597
|
+
});
|
|
2598
|
+
items[i] = {
|
|
2599
|
+
...item,
|
|
2600
|
+
run: {
|
|
2601
|
+
...item.run,
|
|
2602
|
+
text: item.run.text.slice(0, cut),
|
|
2603
|
+
color: firstLine.color,
|
|
2604
|
+
},
|
|
2605
|
+
length: cut,
|
|
2606
|
+
};
|
|
2607
|
+
break;
|
|
2608
|
+
}
|
|
2609
|
+
items[i] = { ...item, run: { ...item.run, color: firstLine.color } };
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
/** The `TextRun` one styled piece of text becomes: its text, and what paint
|
|
2613
|
+
* needs of its style — nothing that names a box or an element, so that a
|
|
2614
|
+
* layout made from it can be kept for the next parse (`TextLayoutCache`). */
|
|
2615
|
+
/**
|
|
2616
|
+
* A text box's text as it is laid out. `pre` keeps its spaces whole: none
|
|
2617
|
+
* is broken after, and none hangs at a line's end, where a trailing space
|
|
2618
|
+
* counts in the line's width. Laid out, they are no-break spaces, as wide
|
|
2619
|
+
* and as many, so every offset holds (CSS Text 3, 4.1.3). Kept per box: a
|
|
2620
|
+
* pass makes no new string, and the kept layouts find their text by
|
|
2621
|
+
* identity before they compare it.
|
|
2622
|
+
*/
|
|
2623
|
+
/**
|
|
2624
|
+
* Tabs a `white-space` keeps, set at their stops (CSS Text 3, 4.2): a stop
|
|
2625
|
+
* every `tab-size` spaces of the block's font from the line's start, and a
|
|
2626
|
+
* tab that would reach one less than half a `ch` on goes to the next.
|
|
2627
|
+
* Neither engine has them — ntk draws a tab a space wide, and CoreText
|
|
2628
|
+
* sets it at stops of its own every 28 points — so a tab is laid out as a
|
|
2629
|
+
* space spaced out to its stop: text as the engine sees it and not as the
|
|
2630
|
+
* document holds it, the way `heldText` holds a `pre`'s spaces. Where each
|
|
2631
|
+
* tab starts is read off one layout of the paragraph with its tabs as
|
|
2632
|
+
* spaces, and its inline boxes' edges and its atomics as the room they
|
|
2633
|
+
* take, unwrapped: a line `pre-wrap` wraps sets the tabs after the wrap as
|
|
2634
|
+
* though it had not, and a right-to-left one sets them a space wide.
|
|
2635
|
+
*
|
|
2636
|
+
* Read off each tab's run, which the tab is given of its own, and not off
|
|
2637
|
+
* a caret: CoreText puts a caret after a spaced glyph part of the way
|
|
2638
|
+
* into its spacing, so a tab after `word-spacing` was set from a place
|
|
2639
|
+
* half the spacing short of where it is drawn.
|
|
2640
|
+
*/
|
|
2641
|
+
function setTabs(items, fonts, block, indent) {
|
|
2642
|
+
// the paragraph as it will be laid out, an inline box's edges and an
|
|
2643
|
+
// atomic as the room they take, each tab a run of its own, and where
|
|
2644
|
+
// each text item starts in it
|
|
2645
|
+
const runs = [];
|
|
2646
|
+
const starts = new Map();
|
|
2647
|
+
const tabs = new Set();
|
|
2648
|
+
let units = 0;
|
|
2649
|
+
for (const item of items) {
|
|
2650
|
+
// a float is beside the line, and takes none of it
|
|
2651
|
+
if (item.kind === 'float')
|
|
2652
|
+
continue;
|
|
2653
|
+
if (item.kind !== 'text') {
|
|
2654
|
+
const box = item.box;
|
|
2655
|
+
runs.push(spacerRun(fonts, block, item.kind === 'edge'
|
|
2656
|
+
? item.width
|
|
2657
|
+
: box.width + box.marginLeft + box.marginRight));
|
|
2658
|
+
units += 1;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
starts.set(item, units);
|
|
2662
|
+
const text = item.run.text;
|
|
2663
|
+
if (item.control || !text.includes('\t')) {
|
|
2664
|
+
runs.push(item.run);
|
|
2665
|
+
units += text.length;
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
let done = 0;
|
|
2669
|
+
for (let p = text.indexOf('\t'); p >= 0; p = text.indexOf('\t', p + 1)) {
|
|
2670
|
+
if (p > done)
|
|
2671
|
+
runs.push({ ...item.run, text: text.slice(done, p) });
|
|
2672
|
+
// spaced a hair apart from its neighbours, which an engine that
|
|
2673
|
+
// merges runs alike (CoreText) would otherwise take it into
|
|
2674
|
+
tabs.add(units + p);
|
|
2675
|
+
runs.push({
|
|
2676
|
+
...item.run,
|
|
2677
|
+
text: spaceIn(item.box),
|
|
2678
|
+
letterSpacing: (item.run.letterSpacing ?? 0) + tabs.size * HAIR,
|
|
2679
|
+
});
|
|
2680
|
+
done = p + 1;
|
|
2681
|
+
}
|
|
2682
|
+
if (done < text.length)
|
|
2683
|
+
runs.push({ ...item.run, text: text.slice(done) });
|
|
2684
|
+
units += text.length;
|
|
2685
|
+
}
|
|
2686
|
+
const natural = fonts.layout(runs, fontOf(block), {
|
|
2687
|
+
direction: block.direction,
|
|
2688
|
+
});
|
|
2689
|
+
/** Where each tab starts, by its place in the layout's text. */
|
|
2690
|
+
const found = new Map();
|
|
2691
|
+
natural.lines.forEach((line, i) => {
|
|
2692
|
+
for (const run of line.runs ?? []) {
|
|
2693
|
+
if (tabs.has(run.start))
|
|
2694
|
+
found.set(run.start, { x: line.x + run.x, line: i });
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
let offsets;
|
|
2698
|
+
const space = advanceOf(fonts, fontOf(block), ' ') +
|
|
2699
|
+
block.letterSpacing +
|
|
2700
|
+
block.wordSpacing;
|
|
2701
|
+
const every = block.tabSizeIsLength ? block.tabSize : block.tabSize * space;
|
|
2702
|
+
const half = advanceOf(fonts, fontOf(block), '0') / 2;
|
|
2703
|
+
const rtl = block.direction === 'rtl';
|
|
2704
|
+
/** The room tabs have added to each line so far. */
|
|
2705
|
+
const added = new Map();
|
|
2706
|
+
const next = [];
|
|
2707
|
+
for (const item of items) {
|
|
2708
|
+
if (item.kind !== 'text' || item.control || !item.run.text.includes('\t')) {
|
|
2709
|
+
next.push(item);
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
const text = item.run.text;
|
|
2713
|
+
const at = starts.get(item);
|
|
2714
|
+
// what the space a tab is laid out as takes before it is spaced out
|
|
2715
|
+
const own = advanceOf(fonts, item.run, spaceIn(item.box)) +
|
|
2716
|
+
(item.run.letterSpacing ?? 0);
|
|
2717
|
+
const piece = (start, end, run) => {
|
|
2718
|
+
next.push({
|
|
2719
|
+
...item,
|
|
2720
|
+
run,
|
|
2721
|
+
length: end - start,
|
|
2722
|
+
start: item.start + start,
|
|
2723
|
+
});
|
|
2724
|
+
};
|
|
2725
|
+
let done = 0;
|
|
2726
|
+
for (let p = text.indexOf('\t'); p >= 0; p = text.indexOf('\t', p + 1)) {
|
|
2727
|
+
if (p > done)
|
|
2728
|
+
piece(done, p, { ...item.run, text: text.slice(done, p) });
|
|
2729
|
+
let advance = own;
|
|
2730
|
+
if (!rtl) {
|
|
2731
|
+
const unit = at + p;
|
|
2732
|
+
let caret = found.get(unit);
|
|
2733
|
+
if (!caret) {
|
|
2734
|
+
// an engine that says nothing of its runs has a caret to go by
|
|
2735
|
+
if (offsets === undefined) {
|
|
2736
|
+
const joined = runs.map((run) => run.text).join('');
|
|
2737
|
+
offsets = /[\uD800-\uDFFF]/.test(joined)
|
|
2738
|
+
? codeUnitOffsets(joined)
|
|
2739
|
+
: null;
|
|
2740
|
+
}
|
|
2741
|
+
caret = natural.caretPosition(offsets ? codePointAt(offsets, unit) : unit);
|
|
2742
|
+
}
|
|
2743
|
+
const before = added.get(caret.line) ?? 0;
|
|
2744
|
+
const x = caret.x + before + (caret.line === 0 ? indent : 0);
|
|
2745
|
+
if (every > 0) {
|
|
2746
|
+
let stop = (Math.floor(x / every) + 1) * every;
|
|
2747
|
+
if (stop - x < half)
|
|
2748
|
+
stop += every;
|
|
2749
|
+
advance = stop - x;
|
|
2750
|
+
}
|
|
2751
|
+
else
|
|
2752
|
+
advance = 0;
|
|
2753
|
+
added.set(caret.line, before + advance - own);
|
|
2754
|
+
}
|
|
2755
|
+
piece(p, p + 1, {
|
|
2756
|
+
...item.run,
|
|
2757
|
+
text: spaceIn(item.box),
|
|
2758
|
+
letterSpacing: (item.run.letterSpacing ?? 0) + advance - own,
|
|
2759
|
+
});
|
|
2760
|
+
done = p + 1;
|
|
2761
|
+
}
|
|
2762
|
+
if (done < text.length) {
|
|
2763
|
+
piece(done, text.length, { ...item.run, text: text.slice(done) });
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
items.splice(0, items.length, ...next);
|
|
2767
|
+
}
|
|
2768
|
+
/** The space a tab in a box is laid out as: a `pre`'s spaces are held
|
|
2769
|
+
* together (`heldText`), and so are its tabs. */
|
|
2770
|
+
function spaceIn(box) {
|
|
2771
|
+
return box.style.whiteSpace === 'pre' ? '\u00a0' : ' ';
|
|
2772
|
+
}
|
|
2773
|
+
/** A block's font, as its runs name it (`runFor`). */
|
|
2774
|
+
function fontOf(style) {
|
|
2775
|
+
return {
|
|
2776
|
+
family: style.fontFamily,
|
|
2777
|
+
size: style.fontSize,
|
|
2778
|
+
weight: style.fontWeight,
|
|
2779
|
+
style: style.fontStyle === 'normal' ? 'normal' : 'italic',
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
/** One character's advance in a font, kept per font manager. */
|
|
2783
|
+
function advanceOf(fonts, font, char) {
|
|
2784
|
+
let kept = ADVANCES.get(fonts);
|
|
2785
|
+
if (!kept)
|
|
2786
|
+
ADVANCES.set(fonts, (kept = new Map()));
|
|
2787
|
+
const key = `${font.family}|${font.size}|${font.weight}|${font.style}|${char}`;
|
|
2788
|
+
let advance = kept.get(key);
|
|
2789
|
+
if (advance === undefined) {
|
|
2790
|
+
const face = {
|
|
2791
|
+
family: font.family,
|
|
2792
|
+
size: font.size,
|
|
2793
|
+
weight: font.weight,
|
|
2794
|
+
style: font.style,
|
|
2795
|
+
};
|
|
2796
|
+
// between two letters, so that no engine drops it as a line's end
|
|
2797
|
+
advance =
|
|
2798
|
+
fonts.layout([{ ...face, text: `x${char}x` }], face, {}).width -
|
|
2799
|
+
fonts.layout([{ ...face, text: 'xx' }], face, {}).width;
|
|
2800
|
+
kept.set(key, advance);
|
|
2801
|
+
}
|
|
2802
|
+
return advance;
|
|
2803
|
+
}
|
|
2804
|
+
const ADVANCES = new WeakMap();
|
|
2805
|
+
function heldText(box) {
|
|
2806
|
+
if (box.style.whiteSpace !== 'pre' || !box.text.includes(' ')) {
|
|
2807
|
+
return box.text;
|
|
2808
|
+
}
|
|
2809
|
+
let held = HELD.get(box);
|
|
2810
|
+
if (held === undefined) {
|
|
2811
|
+
held = box.text.replace(/ /g, '\u00a0');
|
|
2812
|
+
HELD.set(box, held);
|
|
2813
|
+
}
|
|
2814
|
+
return held;
|
|
2815
|
+
}
|
|
2816
|
+
const HELD = new WeakMap();
|
|
2817
|
+
/**
|
|
2818
|
+
* Whether a line one layout made is shorter about its baseline than the
|
|
2819
|
+
* block's strut. Every line box starts with the strut, the block's font at
|
|
2820
|
+
* its line height (CSS 2.1 10.8.1), and a layout has none: it makes a line
|
|
2821
|
+
* as tall as the text on it, at the block's line height as a multiple of
|
|
2822
|
+
* each face's natural one. So a line of `<small>` text alone came out as
|
|
2823
|
+
* short as the small text, and overlapped what followed. A paragraph with
|
|
2824
|
+
* such a line is made a line at a time instead, where `finishLine` puts
|
|
2825
|
+
* the strut on every line; one whose lines all hold text of the block's
|
|
2826
|
+
* own face is kept as it was laid out.
|
|
2827
|
+
*/
|
|
2828
|
+
function shortOfStrut(lines, strut,
|
|
2829
|
+
/** The first line's, where it holds more: a marker's room. */
|
|
2830
|
+
first = null) {
|
|
2831
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
2832
|
+
const line = lines[i];
|
|
2833
|
+
const holds = i === 0 && first ? first : strut;
|
|
2834
|
+
if (!holds)
|
|
2835
|
+
continue;
|
|
2836
|
+
if (line.baseline < holds.ascent - 0.5 ||
|
|
2837
|
+
line.height - line.baseline < holds.descent - 0.5) {
|
|
2838
|
+
return true;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
return false;
|
|
2842
|
+
}
|
|
2843
|
+
/** A strut with room besides it about the same baseline, or the strut
|
|
2844
|
+
* where there is none. */
|
|
2845
|
+
function withRoom(strut, room) {
|
|
2846
|
+
if (!room)
|
|
2847
|
+
return strut;
|
|
2848
|
+
if (!strut)
|
|
2849
|
+
return room;
|
|
2850
|
+
return {
|
|
2851
|
+
ascent: Math.max(strut.ascent, room.ascent),
|
|
2852
|
+
descent: Math.max(strut.descent, room.descent),
|
|
2853
|
+
};
|
|
510
2854
|
}
|
|
511
|
-
/** The
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
2855
|
+
/** The share of the size a synthesized small capital is set at, rounded to
|
|
2856
|
+
* a whole pixel as Blink rounds it (`SimpleFontData`'s scaled font data):
|
|
2857
|
+
* CSS Fonts 4 (6.2) leaves the size to the user agent. */
|
|
2858
|
+
const SMALL_CAPS_SCALE = 0.7;
|
|
2859
|
+
/**
|
|
2860
|
+
* Small capitals a face does not have, made of its capitals set smaller
|
|
2861
|
+
* (CSS Fonts 4, 6.2): where `font-variant-caps` asks for `smcp` — or
|
|
2862
|
+
* `c2sc` too, for `all-small-caps` — and the face does not answer it,
|
|
2863
|
+
* each letter that would be a small capital is its capital at 70% of the
|
|
2864
|
+
* size, in a run of its own: a lower-case letter, and for `all-small-caps`
|
|
2865
|
+
* the rest of the text too. Under `small-caps` what has no case — a space,
|
|
2866
|
+
* a digit, a mark of punctuation — keeps its size, as Blink keeps it. Only
|
|
2867
|
+
* a letter whose capital is one character is made one, so every offset
|
|
2868
|
+
* holds.
|
|
2869
|
+
*/
|
|
2870
|
+
function synthesizeSmallCaps(items, fonts) {
|
|
2871
|
+
for (let i = 0; i < items.length; i++) {
|
|
2872
|
+
const item = items[i];
|
|
2873
|
+
if (item.kind !== 'text' || item.control)
|
|
2874
|
+
continue;
|
|
2875
|
+
// `font-variant-caps`'s, which is synthesized where the face has none;
|
|
2876
|
+
// not `font-feature-settings`', which asks for the feature as it is
|
|
2877
|
+
const caps = item.box.style.fontVariantCaps;
|
|
2878
|
+
const features = item.run.features;
|
|
2879
|
+
if (!caps.includes('smcp') || !features?.smcp)
|
|
2880
|
+
continue;
|
|
2881
|
+
const all = caps.includes('c2sc');
|
|
2882
|
+
if (hasSmallCaps(fonts, item.run, all))
|
|
2883
|
+
continue;
|
|
2884
|
+
const pieces = smallCapsPieces(item, all, withoutCaps(features));
|
|
2885
|
+
if (!pieces)
|
|
2886
|
+
continue;
|
|
2887
|
+
items.splice(i, 1, ...pieces);
|
|
2888
|
+
i += pieces.length - 1;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
/** A text item cut where its letters turn to synthesized small capitals
|
|
2892
|
+
* and back, or null where none does. */
|
|
2893
|
+
function smallCapsPieces(item, all, features) {
|
|
2894
|
+
const text = item.run.text;
|
|
2895
|
+
const pieces = [];
|
|
2896
|
+
let from = 0;
|
|
2897
|
+
let small = false;
|
|
2898
|
+
let capitals = '';
|
|
2899
|
+
const cut = (to) => {
|
|
2900
|
+
if (to === from)
|
|
2901
|
+
return;
|
|
2902
|
+
const run = small
|
|
2903
|
+
? {
|
|
2904
|
+
...item.run,
|
|
2905
|
+
text: capitals,
|
|
2906
|
+
size: Math.round((item.run.size ?? 16) * SMALL_CAPS_SCALE),
|
|
2907
|
+
}
|
|
2908
|
+
: { ...item.run, text: text.slice(from, to) };
|
|
2909
|
+
if (small) {
|
|
2910
|
+
if (features)
|
|
2911
|
+
run.features = features;
|
|
2912
|
+
else
|
|
2913
|
+
delete run.features;
|
|
2914
|
+
}
|
|
2915
|
+
pieces.push({ ...item, run, start: item.start + from, length: to - from });
|
|
2916
|
+
from = to;
|
|
2917
|
+
capitals = '';
|
|
2918
|
+
};
|
|
2919
|
+
let any = false;
|
|
2920
|
+
for (let at = 0; at < text.length;) {
|
|
2921
|
+
const ch = String.fromCodePoint(text.codePointAt(at));
|
|
2922
|
+
const upper = ch.toUpperCase();
|
|
2923
|
+
// everything, for `all-small-caps`: its spaces are small too
|
|
2924
|
+
const becomes = (all || upper !== ch) && upper.length === ch.length;
|
|
2925
|
+
if (becomes !== small) {
|
|
2926
|
+
cut(at);
|
|
2927
|
+
small = becomes;
|
|
2928
|
+
}
|
|
2929
|
+
if (becomes) {
|
|
2930
|
+
capitals += upper;
|
|
2931
|
+
any = true;
|
|
2932
|
+
}
|
|
2933
|
+
at += ch.length;
|
|
2934
|
+
}
|
|
2935
|
+
cut(text.length);
|
|
2936
|
+
return any ? pieces : null;
|
|
2937
|
+
}
|
|
2938
|
+
/** Features without the small capitals a synthesis stands in for, one
|
|
2939
|
+
* object for each set, as `featuresOf` keeps them. */
|
|
2940
|
+
function withoutCaps(features) {
|
|
2941
|
+
let kept = WITHOUT_CAPS.get(features);
|
|
2942
|
+
if (kept === undefined) {
|
|
2943
|
+
const out = {};
|
|
2944
|
+
for (const [tag, value] of Object.entries(features)) {
|
|
2945
|
+
if (tag !== 'smcp' && tag !== 'c2sc')
|
|
2946
|
+
out[tag] = value;
|
|
2947
|
+
}
|
|
2948
|
+
kept = Object.keys(out).length ? out : null;
|
|
2949
|
+
WITHOUT_CAPS.set(features, kept);
|
|
2950
|
+
}
|
|
2951
|
+
return kept;
|
|
2952
|
+
}
|
|
2953
|
+
const WITHOUT_CAPS = new WeakMap();
|
|
2954
|
+
/**
|
|
2955
|
+
* Whether a run's face has small capitals of its own — and capitals to
|
|
2956
|
+
* small capitals, for `all-small-caps`: the alphabet set with the feature
|
|
2957
|
+
* and without it comes out another width where it does. Asked of the
|
|
2958
|
+
* engine's layout rather than of the font's tables, which a CoreText face
|
|
2959
|
+
* does not hand over. Kept per face.
|
|
2960
|
+
*/
|
|
2961
|
+
function hasSmallCaps(fonts, run, all) {
|
|
2962
|
+
let known = SMALL_CAPS.get(fonts);
|
|
2963
|
+
if (!known)
|
|
2964
|
+
SMALL_CAPS.set(fonts, (known = new Map()));
|
|
2965
|
+
const key = `${run.family}|${run.weight}|${run.style}|${all}`;
|
|
2966
|
+
let has = known.get(key);
|
|
2967
|
+
if (has === undefined) {
|
|
2968
|
+
const face = {
|
|
2969
|
+
family: run.family,
|
|
2970
|
+
weight: run.weight,
|
|
2971
|
+
style: run.style,
|
|
2972
|
+
size: 16,
|
|
2973
|
+
};
|
|
2974
|
+
const width = (text, features) => fonts.layout([{ text, ...face, ...(features ? { features } : null) }], face, {}).width;
|
|
2975
|
+
const lower = 'abcdefghijklmnopqrstuvwxyz';
|
|
2976
|
+
has = Math.abs(width(lower, { smcp: 1 }) - width(lower, null)) > 0.01;
|
|
2977
|
+
if (has && all) {
|
|
2978
|
+
const upper = lower.toUpperCase();
|
|
2979
|
+
has = Math.abs(width(upper, { c2sc: 1 }) - width(upper, null)) > 0.01;
|
|
2980
|
+
}
|
|
2981
|
+
known.set(key, has);
|
|
2982
|
+
}
|
|
2983
|
+
return has;
|
|
2984
|
+
}
|
|
2985
|
+
const SMALL_CAPS = new WeakMap();
|
|
2986
|
+
/**
|
|
2987
|
+
* The OpenType features a style's text is shaped with: the `font-variant`
|
|
2988
|
+
* longhands' and `font-kerning`'s, then `font-feature-settings`, which has
|
|
2989
|
+
* the last word (CSS Fonts 3, 7.2). One object for each set of them, kept,
|
|
2990
|
+
* because a run's fields are compared by identity — a new object a pass
|
|
2991
|
+
* would find no layout the last pass made.
|
|
2992
|
+
*/
|
|
2993
|
+
export function featuresOf(style) {
|
|
2994
|
+
const { fontKerning: kerning, fontVariantLigatures: ligatures, fontVariantNumeric: numeric, fontVariantCaps: caps, fontVariantPosition: position, fontFeatureSettings: settings, } = style;
|
|
2995
|
+
if (!kerning && !ligatures && !numeric && !caps && !position && !settings) {
|
|
2996
|
+
return null;
|
|
2997
|
+
}
|
|
2998
|
+
const key = `${kerning}|${ligatures}|${numeric}|${caps}|${position}|${settings}`;
|
|
2999
|
+
let features = FEATURES.get(key);
|
|
3000
|
+
if (!features) {
|
|
3001
|
+
const out = {};
|
|
3002
|
+
for (const part of [
|
|
3003
|
+
kerning,
|
|
3004
|
+
ligatures,
|
|
3005
|
+
numeric,
|
|
3006
|
+
caps,
|
|
3007
|
+
position,
|
|
3008
|
+
settings,
|
|
3009
|
+
]) {
|
|
3010
|
+
if (!part)
|
|
3011
|
+
continue;
|
|
3012
|
+
for (const pair of part.split(',')) {
|
|
3013
|
+
const at = pair.indexOf('=');
|
|
3014
|
+
out[pair.slice(0, at)] = Number(pair.slice(at + 1));
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
features = Object.freeze(out);
|
|
3018
|
+
FEATURES.set(key, features);
|
|
3019
|
+
}
|
|
3020
|
+
return features;
|
|
3021
|
+
}
|
|
3022
|
+
const FEATURES = new Map();
|
|
3023
|
+
/** A run of `text` in `style`: its face, its ink and its rules. Every
|
|
3024
|
+
* paint field of a run of the document's text is this function's, which
|
|
3025
|
+
* is what lets a pointer move re-ink a paragraph without laying it out
|
|
3026
|
+
* again (`HtmlViewNode._hoverInPlace`). */
|
|
3027
|
+
export function runFor(text, style) {
|
|
516
3028
|
const run = {
|
|
517
3029
|
text,
|
|
518
3030
|
family: style.fontFamily,
|
|
519
3031
|
size: style.fontSize,
|
|
520
3032
|
weight: style.fontWeight,
|
|
521
3033
|
style: style.fontStyle === 'normal' ? 'normal' : 'italic',
|
|
522
|
-
color
|
|
3034
|
+
// the glyphs' fill, which `-webkit-text-fill-color` sets apart from
|
|
3035
|
+
// `color` — the decorations keep `color`
|
|
3036
|
+
color: style.textFillColor === null
|
|
3037
|
+
? style.color
|
|
3038
|
+
: inkColor(style.textFillColor, style.color),
|
|
3039
|
+
// set here, and made true later for an inline box's text (`collect`):
|
|
3040
|
+
// added to a run after it was made, it gave the runs of a document two
|
|
3041
|
+
// shapes where the layout cache compares them, and a pass over 600 KB
|
|
3042
|
+
// took 3 ms longer finding every paragraph's layout again
|
|
3043
|
+
shapeApart: false,
|
|
523
3044
|
};
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
run.
|
|
534
|
-
|
|
535
|
-
|
|
3045
|
+
if (style.letterSpacing)
|
|
3046
|
+
run.letterSpacing = style.letterSpacing;
|
|
3047
|
+
const features = featuresOf(style);
|
|
3048
|
+
if (features)
|
|
3049
|
+
run.features = features;
|
|
3050
|
+
// Hidden text keeps its place on the line and draws nothing, rules
|
|
3051
|
+
// included, and the text of a visible element inside it is its own run
|
|
3052
|
+
// (CSS 2.1 11.2): drawn in no ink, it is laid out as it was
|
|
3053
|
+
if (style.visibility !== 'visible') {
|
|
3054
|
+
run.color = 'transparent';
|
|
3055
|
+
return run;
|
|
3056
|
+
}
|
|
3057
|
+
// A background is not the run's: the inline box it belongs to paints it,
|
|
3058
|
+
// behind every fragment of the box — nested elements' text included — and
|
|
3059
|
+
// out to its padding (`paintInlineBoxes`).
|
|
3060
|
+
// the element's own decorations and the ones propagated to it, in the
|
|
3061
|
+
// colours of the elements that set them (`decorate`)
|
|
3062
|
+
if (style.underline) {
|
|
3063
|
+
run.underline = style.underline;
|
|
536
3064
|
// CSS's five rule styles and SGR 4's five are the same set under two
|
|
537
3065
|
// names; richtext speaks SGR's, so `solid` is `single` and `wavy` is
|
|
538
3066
|
// `curly`. The other three are spelled identically.
|
|
539
3067
|
run.underlineStyle =
|
|
540
|
-
style.
|
|
3068
|
+
style.underlineStyle === 'wavy'
|
|
541
3069
|
? 'curly'
|
|
542
|
-
: style.
|
|
3070
|
+
: style.underlineStyle === 'solid'
|
|
543
3071
|
? 'single'
|
|
544
|
-
: style.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
3072
|
+
: style.underlineStyle;
|
|
3073
|
+
if (style.underlineOffset !== null) {
|
|
3074
|
+
run.underlineOffset = style.underlineOffset;
|
|
3075
|
+
}
|
|
3076
|
+
if (style.underlineThickness !== null) {
|
|
3077
|
+
run.underlineThickness = style.underlineThickness;
|
|
3078
|
+
}
|
|
548
3079
|
}
|
|
3080
|
+
if (style.lineThrough)
|
|
3081
|
+
run.strike = style.lineThrough;
|
|
549
3082
|
return run;
|
|
550
3083
|
}
|
|
551
3084
|
/**
|
|
@@ -556,16 +3089,46 @@ function runFor(text, style, owner) {
|
|
|
556
3089
|
class SpanMap {
|
|
557
3090
|
_laid = [];
|
|
558
3091
|
_doc = [];
|
|
3092
|
+
_boxes = [];
|
|
3093
|
+
/** Where the laid-out text has a unit that is no text of the document's:
|
|
3094
|
+
* a spacer, a bidi control. */
|
|
3095
|
+
_gaps = null;
|
|
559
3096
|
laidOut = 0;
|
|
560
|
-
add(documentStart, length) {
|
|
3097
|
+
add(documentStart, length, box = null) {
|
|
561
3098
|
this._laid.push(this.laidOut);
|
|
562
3099
|
this._doc.push(documentStart);
|
|
3100
|
+
this._boxes.push(box);
|
|
3101
|
+
if (!box) {
|
|
3102
|
+
this._gaps ??= [];
|
|
3103
|
+
for (let i = 0; i < length; i += 1)
|
|
3104
|
+
this._gaps.push(this.laidOut + i);
|
|
3105
|
+
}
|
|
563
3106
|
this.laidOut += length;
|
|
564
3107
|
}
|
|
3108
|
+
/** Give a line's text the gaps in its part of the laid-out text, where
|
|
3109
|
+
* it has any. Set after the text is made rather than spread into it: a
|
|
3110
|
+
* spread in an object literal costs every line that has none. */
|
|
3111
|
+
giveGaps(text, start, end) {
|
|
3112
|
+
if (!this._gaps)
|
|
3113
|
+
return;
|
|
3114
|
+
const gaps = this._gaps.filter((at) => at >= start && at < end);
|
|
3115
|
+
if (gaps.length)
|
|
3116
|
+
text.gaps = gaps;
|
|
3117
|
+
}
|
|
3118
|
+
/** The text box whose text an offset in the laid-out text is. */
|
|
3119
|
+
boxAt(offset) {
|
|
3120
|
+
const i = this._entryAt(offset);
|
|
3121
|
+
return i < 0 ? null : this._boxes[i];
|
|
3122
|
+
}
|
|
565
3123
|
documentAt(offset) {
|
|
3124
|
+
const i = this._entryAt(offset);
|
|
3125
|
+
return i < 0 ? 0 : this._doc[i] + (offset - this._laid[i]);
|
|
3126
|
+
}
|
|
3127
|
+
/** The entry an offset falls in, or -1 when there are none. */
|
|
3128
|
+
_entryAt(offset) {
|
|
566
3129
|
const laid = this._laid;
|
|
567
3130
|
if (!laid.length)
|
|
568
|
-
return
|
|
3131
|
+
return -1;
|
|
569
3132
|
let lo = 0;
|
|
570
3133
|
let hi = laid.length - 1;
|
|
571
3134
|
while (lo < hi) {
|
|
@@ -575,7 +3138,7 @@ class SpanMap {
|
|
|
575
3138
|
else
|
|
576
3139
|
hi = mid - 1;
|
|
577
3140
|
}
|
|
578
|
-
return
|
|
3141
|
+
return lo;
|
|
579
3142
|
}
|
|
580
3143
|
}
|
|
581
3144
|
/** The maximal run of text items from `(index, offset)` to the next atomic. */
|
|
@@ -590,13 +3153,16 @@ function segmentFrom(items, index, offset) {
|
|
|
590
3153
|
break;
|
|
591
3154
|
const text = skip > 0 ? item.run.text.slice(skip) : item.run.text;
|
|
592
3155
|
if (text) {
|
|
593
|
-
spans.add(item.
|
|
3156
|
+
spans.add(item.start + skip, text.length, item.control ? null : item.box);
|
|
594
3157
|
runs.push(skip > 0 ? { ...item.run, text } : item.run);
|
|
595
3158
|
}
|
|
596
3159
|
skip = 0;
|
|
597
3160
|
}
|
|
598
3161
|
return { runs, spans, nextIndex: i };
|
|
599
3162
|
}
|
|
3163
|
+
/** A text a line may break after where its block wraps: one that ends in
|
|
3164
|
+
* white space, which a no-break space a `nowrap` element holds is not. */
|
|
3165
|
+
const BREAKS_AFTER = /[ \t\n]$/;
|
|
600
3166
|
/** Move `(index, offset)` forward by `consumed` code units of text. */
|
|
601
3167
|
function advance(items, index, offset, consumed) {
|
|
602
3168
|
let i = index;
|
|
@@ -618,8 +3184,9 @@ function advance(items, index, offset, consumed) {
|
|
|
618
3184
|
/**
|
|
619
3185
|
* One space's advance in a run's style, measured as the difference between
|
|
620
3186
|
* `x x` and `xx` — ntk strips a lone trailing space from a line, so it
|
|
621
|
-
* cannot be measured directly
|
|
622
|
-
*
|
|
3187
|
+
* cannot be measured directly — plus the run's letter spacing, which a
|
|
3188
|
+
* space takes as every other character does. Cached per font manager per
|
|
3189
|
+
* face; the measurement itself hits ntk's shaping memo.
|
|
623
3190
|
*/
|
|
624
3191
|
const SPACE_ADVANCE = new WeakMap();
|
|
625
3192
|
function spaceAdvance(fonts, run) {
|
|
@@ -628,10 +3195,11 @@ function spaceAdvance(fonts, run) {
|
|
|
628
3195
|
cache = new Map();
|
|
629
3196
|
SPACE_ADVANCE.set(fonts, cache);
|
|
630
3197
|
}
|
|
3198
|
+
const spacing = run.letterSpacing ?? 0;
|
|
631
3199
|
const key = `${run.family}|${run.size}|${run.weight}|${run.style}`;
|
|
632
3200
|
const hit = cache.get(key);
|
|
633
3201
|
if (hit !== undefined)
|
|
634
|
-
return hit;
|
|
3202
|
+
return hit + spacing;
|
|
635
3203
|
const style = {
|
|
636
3204
|
family: run.family,
|
|
637
3205
|
size: run.size,
|
|
@@ -643,7 +3211,110 @@ function spaceAdvance(fonts, run) {
|
|
|
643
3211
|
if (cache.size > 64)
|
|
644
3212
|
cache.clear();
|
|
645
3213
|
cache.set(key, advance);
|
|
646
|
-
return advance;
|
|
3214
|
+
return advance + spacing;
|
|
3215
|
+
}
|
|
3216
|
+
/** Whether the runs' joined text has a forced break just before a
|
|
3217
|
+
* code-unit offset — a line's end, which an engine counts the break in. */
|
|
3218
|
+
function breakBefore(runs, offset) {
|
|
3219
|
+
let at = 0;
|
|
3220
|
+
for (const run of runs) {
|
|
3221
|
+
const next = at + run.text.length;
|
|
3222
|
+
if (offset - 1 < next)
|
|
3223
|
+
return run.text.charCodeAt(offset - 1 - at) === 10;
|
|
3224
|
+
at = next;
|
|
3225
|
+
}
|
|
3226
|
+
return false;
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* The white space a text engine strips from a line's end, as a pattern for
|
|
3230
|
+
* the end of a run: spaces and tabs, and the no-break space too where the
|
|
3231
|
+
* engine strips that — ntk to 8.12.9 and CoreText through @windowkit/appkit
|
|
3232
|
+
* 0.15.0 do, where CSS measures it (sidorares/ntk#395, windowkit/appkit#83).
|
|
3233
|
+
* Asked of the engine once per font manager, because what the line adds back
|
|
3234
|
+
* has to be what the engine took: added back after an engine that kept it, a
|
|
3235
|
+
* no-break space before an image counted twice.
|
|
3236
|
+
*/
|
|
3237
|
+
const HUNG = new WeakMap();
|
|
3238
|
+
export function hungSpaces(fonts, run) {
|
|
3239
|
+
let hung = HUNG.get(fonts);
|
|
3240
|
+
if (!hung) {
|
|
3241
|
+
const style = { family: run.family, size: run.size };
|
|
3242
|
+
const width = (text) => fonts.layout([{ ...style, text }], style, {}).lines[0]?.width ?? 0;
|
|
3243
|
+
hung = width('x\u00A0') > width('x') ? /[ \t]+$/ : /[ \t\u00A0]+$/;
|
|
3244
|
+
HUNG.set(fonts, hung);
|
|
3245
|
+
}
|
|
3246
|
+
return hung;
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* A clamped paragraph with an ellipsis at the end of its last line (CSS
|
|
3250
|
+
* Overflow 4, 4.2): its text up to where that line breaks in the room the
|
|
3251
|
+
* ellipsis leaves it — at a soft wrap opportunity, so the words that do
|
|
3252
|
+
* not fit beside the ellipsis go to the lines the clamp hides, and inside
|
|
3253
|
+
* a word only where the line has no other — and the ellipsis after it, in
|
|
3254
|
+
* the block's own style. The engine's own ellipsis cuts inside the last
|
|
3255
|
+
* word, as `text-overflow` does.
|
|
3256
|
+
*/
|
|
3257
|
+
function ellipsized(fonts, items, runs, base, options, layout, width, style) {
|
|
3258
|
+
const mark = runFor('\u2026', style);
|
|
3259
|
+
const room = width - fonts.layout([mark], base, {}).width;
|
|
3260
|
+
const last = layout.lines[layout.lines.length - 1];
|
|
3261
|
+
let end = last.end;
|
|
3262
|
+
if (last.width > room) {
|
|
3263
|
+
const probe = fonts.layout(sliceRuns(runs, last.start, last.end), base, {
|
|
3264
|
+
...options,
|
|
3265
|
+
maxWidth: Math.max(0, room),
|
|
3266
|
+
maxLines: 1,
|
|
3267
|
+
overflow: 'clip',
|
|
3268
|
+
overflowWrap: 'break-word',
|
|
3269
|
+
});
|
|
3270
|
+
end = last.start + (probe.lines[0]?.end ?? 0);
|
|
3271
|
+
}
|
|
3272
|
+
// the text to the break, less the white space the line ends in — an
|
|
3273
|
+
// empty line keeps the break before it, and is the ellipsis alone
|
|
3274
|
+
const line = sliceRuns(runs, last.start, end);
|
|
3275
|
+
let trailing = 0;
|
|
3276
|
+
for (let i = line.length - 1; i >= 0; i -= 1) {
|
|
3277
|
+
const text = line[i].text;
|
|
3278
|
+
const trimmed = text.replace(/[ \t\n\r\f]+$/, '').length;
|
|
3279
|
+
trailing += text.length - trimmed;
|
|
3280
|
+
if (trimmed)
|
|
3281
|
+
break;
|
|
3282
|
+
}
|
|
3283
|
+
const kept = sliceRuns(runs, 0, end - trailing);
|
|
3284
|
+
// and the spans to match, the ellipsis no text of the document's
|
|
3285
|
+
const spans = new SpanMap();
|
|
3286
|
+
let at = items.length ? items[0].start : 0;
|
|
3287
|
+
let left = 0;
|
|
3288
|
+
for (const run of kept)
|
|
3289
|
+
left += run.text.length;
|
|
3290
|
+
for (const item of items) {
|
|
3291
|
+
if (left <= 0)
|
|
3292
|
+
break;
|
|
3293
|
+
const length = Math.min(item.run.text.length, left);
|
|
3294
|
+
spans.add(item.start, length, item.control ? null : item.box);
|
|
3295
|
+
left -= length;
|
|
3296
|
+
at = item.start + length;
|
|
3297
|
+
}
|
|
3298
|
+
spans.add(at, 1, null);
|
|
3299
|
+
kept.push(mark);
|
|
3300
|
+
return { layout: fonts.layout(kept, base, options), runs: kept, spans };
|
|
3301
|
+
}
|
|
3302
|
+
/** The runs' text from one code-unit offset into their joined text to
|
|
3303
|
+
* another, each piece in its own run's style. */
|
|
3304
|
+
function sliceRuns(runs, from, to) {
|
|
3305
|
+
const out = [];
|
|
3306
|
+
let at = 0;
|
|
3307
|
+
for (const run of runs) {
|
|
3308
|
+
const next = at + run.text.length;
|
|
3309
|
+
if (next > from && at < to) {
|
|
3310
|
+
const text = run.text.slice(Math.max(0, from - at), to - at);
|
|
3311
|
+
out.push(text === run.text ? run : { ...run, text });
|
|
3312
|
+
}
|
|
3313
|
+
at = next;
|
|
3314
|
+
if (at >= to)
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
3317
|
+
return out;
|
|
647
3318
|
}
|
|
648
3319
|
/** Whether anything but whitespace lies past a code-unit offset into the
|
|
649
3320
|
* runs' joined text — "did `maxLines` drop content", for a layout that
|
|
@@ -652,7 +3323,8 @@ function inkBeyond(runs, offset) {
|
|
|
652
3323
|
let at = 0;
|
|
653
3324
|
for (const run of runs) {
|
|
654
3325
|
const next = at + run.text.length;
|
|
655
|
-
if (next > offset &&
|
|
3326
|
+
if (next > offset &&
|
|
3327
|
+
/[^ \t\n\r\f]/.test(run.text.slice(Math.max(0, offset - at)))) {
|
|
656
3328
|
return true;
|
|
657
3329
|
}
|
|
658
3330
|
at = next;
|
|
@@ -663,18 +3335,419 @@ function inkBeyond(runs, offset) {
|
|
|
663
3335
|
function wraps(style) {
|
|
664
3336
|
return style.whiteSpace !== 'nowrap' && style.whiteSpace !== 'pre';
|
|
665
3337
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
3338
|
+
/** How much of a line's unused room goes before it: 0 for a line set flush
|
|
3339
|
+
* left, 1 flush right, ½ centred. `start` and `end` follow the direction;
|
|
3340
|
+
* `justify` is set as `start`, as `alignFor` sets it. */
|
|
3341
|
+
/** How far into the room a line an alignment sets goes: 0 at the left, 1
|
|
3342
|
+
* at the right; `justify` at the start, since a line not justified is
|
|
3343
|
+
* set there. */
|
|
3344
|
+
function alignShift(align, rtl) {
|
|
3345
|
+
switch (align) {
|
|
3346
|
+
case 'center':
|
|
3347
|
+
return 0.5;
|
|
3348
|
+
case 'right':
|
|
3349
|
+
return 1;
|
|
3350
|
+
case 'left':
|
|
3351
|
+
return 0;
|
|
3352
|
+
case 'end':
|
|
3353
|
+
return rtl ? 0 : 1;
|
|
3354
|
+
default:
|
|
3355
|
+
return rtl ? 1 : 0;
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
/** The blocks whose lines hold text a box's background shows through. */
|
|
3359
|
+
export const CLIPPED_TEXT = new WeakSet();
|
|
3360
|
+
/** How each of those blocks' layouts was made, to make it again. */
|
|
3361
|
+
const RECORDED = new WeakMap();
|
|
3362
|
+
const RECORDERS = new WeakMap();
|
|
3363
|
+
const INKLESS = new WeakMap();
|
|
3364
|
+
/** The fonts, noting how each layout they make was made. One recorder a
|
|
3365
|
+
* fonts object, so what is kept per fonts object is kept for it too. */
|
|
3366
|
+
function recording(fonts) {
|
|
3367
|
+
let recorder = RECORDERS.get(fonts);
|
|
3368
|
+
if (!recorder) {
|
|
3369
|
+
recorder = {
|
|
3370
|
+
layout(content, style, options) {
|
|
3371
|
+
const layout = fonts.layout(content, style, options);
|
|
3372
|
+
RECORDED.set(layout, { fonts, content, style, options });
|
|
3373
|
+
return layout;
|
|
3374
|
+
},
|
|
3375
|
+
match: (family, style) => fonts.match(family, style),
|
|
3376
|
+
};
|
|
3377
|
+
RECORDERS.set(fonts, recorder);
|
|
3378
|
+
}
|
|
3379
|
+
return recorder;
|
|
3380
|
+
}
|
|
3381
|
+
/**
|
|
3382
|
+
* A layout of the same text with no ink of its own, which an engine draws
|
|
3383
|
+
* in the context's fill — a gradient's, for text a background shows
|
|
3384
|
+
* through; the same runs, so the same glyphs where the first put them.
|
|
3385
|
+
* Null for a layout that was not recorded.
|
|
3386
|
+
*/
|
|
3387
|
+
export function inklessLayout(layout) {
|
|
3388
|
+
let inkless = INKLESS.get(layout);
|
|
3389
|
+
if (inkless !== undefined)
|
|
3390
|
+
return inkless;
|
|
3391
|
+
const made = RECORDED.get(layout);
|
|
3392
|
+
inkless = made
|
|
3393
|
+
? made.fonts.layout(made.content.map((run) => ({ ...run, color: undefined })), { ...made.style, color: undefined }, made.options)
|
|
3394
|
+
: null;
|
|
3395
|
+
INKLESS.set(layout, inkless);
|
|
3396
|
+
return inkless;
|
|
3397
|
+
}
|
|
3398
|
+
/** Whether any of a block's text is text a background shows through. */
|
|
3399
|
+
function clipsText(block, items) {
|
|
3400
|
+
for (const item of items) {
|
|
3401
|
+
if (item.kind === 'text' && !item.control && clipBoxOf(item.box, block)) {
|
|
3402
|
+
return true;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
return false;
|
|
3406
|
+
}
|
|
3407
|
+
/** The box whose background a text box's glyphs show: the nearest element
|
|
3408
|
+
* box from it up to the block its lines are in with `background-clip:
|
|
3409
|
+
* text`, or null. */
|
|
3410
|
+
export function clipBoxOf(box, block) {
|
|
3411
|
+
for (let at = box; at; at = at.parent) {
|
|
3412
|
+
if (at.kind !== 'text' && at.style.backgroundClipText)
|
|
3413
|
+
return at;
|
|
3414
|
+
if (at === block)
|
|
3415
|
+
break;
|
|
3416
|
+
}
|
|
3417
|
+
return null;
|
|
3418
|
+
}
|
|
3419
|
+
/** The most lines `text-wrap: balance` evens out, as in Chrome: a heading,
|
|
3420
|
+
* a caption, a pull quote — not a paragraph, whose last line is its own. */
|
|
3421
|
+
const BALANCE_LINES = 6;
|
|
3422
|
+
/**
|
|
3423
|
+
* The narrowest width a paragraph's text breaks into no more lines than
|
|
3424
|
+
* `layout` has (CSS Text 4, 6.2), found by halving the width between the
|
|
3425
|
+
* least that could hold them — their widths over their number — and the
|
|
3426
|
+
* width it was laid out at. The engine breaks the lines, so each step
|
|
3427
|
+
* asks it; eight steps come to within a pixel of the answer for a heading
|
|
3428
|
+
* across a page.
|
|
3429
|
+
*/
|
|
3430
|
+
function balancedWidth(fonts, runs, base, options, width, layout) {
|
|
3431
|
+
const count = layout.lines.length;
|
|
3432
|
+
let total = 0;
|
|
3433
|
+
for (const line of layout.lines)
|
|
3434
|
+
total += line.width;
|
|
3435
|
+
let lo = total / count;
|
|
3436
|
+
let hi = width;
|
|
3437
|
+
for (let step = 0; step < 8 && hi - lo > 1; step += 1) {
|
|
3438
|
+
const mid = (lo + hi) / 2;
|
|
3439
|
+
const probe = fonts.layout(runs, base, { ...options, maxWidth: mid });
|
|
3440
|
+
if (probe.lines.length <= count)
|
|
3441
|
+
hi = mid;
|
|
3442
|
+
else
|
|
3443
|
+
lo = mid;
|
|
3444
|
+
}
|
|
3445
|
+
return Math.ceil(hi);
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Where `text-align` puts the lines of a layout that was given no width to
|
|
3449
|
+
* align them in — text that does not wrap, which has no `maxWidth` — as how
|
|
3450
|
+
* far each moves: the engine aligns those lines within the widest of them,
|
|
3451
|
+
* which for a single line is no alignment at all, so a centred `<td
|
|
3452
|
+
* nowrap>` or a `white-space: nowrap` button label was set flush left. A
|
|
3453
|
+
* line too long for its box is set at its start, overflowing its end (CSS
|
|
3454
|
+
* Text 3, 7.1), which is its left in a right-to-left paragraph, as in
|
|
3455
|
+
* Blink. Null where every line is already where it belongs: flush left in
|
|
3456
|
+
* a left-to-right paragraph, or measured for a width it has not been given.
|
|
3457
|
+
*/
|
|
3458
|
+
/** How the lines of a layout made with no width to fill are placed in
|
|
3459
|
+
* the room: each is its text's last or a forced break's, and takes
|
|
3460
|
+
* `text-align-last`, but where `last` is false — a balanced paragraph's,
|
|
3461
|
+
* laid out narrower than its room. */
|
|
3462
|
+
function unwrappedPlacer(style, width, last = true) {
|
|
3463
|
+
const rtl = style.direction === 'rtl';
|
|
3464
|
+
const shift = alignShift(last ? lastAlignOf(style) : style.textAlign, rtl);
|
|
3465
|
+
if ((shift === 0 && !rtl) || !Number.isFinite(width))
|
|
3466
|
+
return null;
|
|
3467
|
+
return (line) => {
|
|
3468
|
+
const free = width - line.width;
|
|
3469
|
+
return (free >= 0 ? free * shift : rtl ? free : 0) - line.x;
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* The spacing a space is given to be laid out as one that may be spaced
|
|
3474
|
+
* apart, too little to move a glyph. It is not nothing, because neither
|
|
3475
|
+
* engine measures a space that is a spaced run of its own as it measures
|
|
3476
|
+
* the same space inside its run: ntk shapes each run apart, so the kerning
|
|
3477
|
+
* pair a space made with the letter beside it is gone (Arial's `A` and
|
|
3478
|
+
* `T` have them), and CoreText spaces a glyph with its kerning attribute,
|
|
3479
|
+
* which takes the place of the font's pairs. A line justified from a
|
|
3480
|
+
* measure of its spaces as they were came out wider than its box and
|
|
3481
|
+
* broke a word early: on macOS, in Helvetica, half of a paragraph did.
|
|
3482
|
+
*/
|
|
3483
|
+
const HAIR = 1e-6;
|
|
3484
|
+
/** The runs with each space a run of its own, spaced a hair apart. */
|
|
3485
|
+
function spacedApart(runs) {
|
|
3486
|
+
const out = [];
|
|
3487
|
+
for (const run of runs) {
|
|
3488
|
+
const text = run.text;
|
|
3489
|
+
let piece = 0;
|
|
3490
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
3491
|
+
const c = text.charCodeAt(at);
|
|
3492
|
+
if (c !== 0x20 && c !== 0xa0)
|
|
3493
|
+
continue;
|
|
3494
|
+
if (at > piece)
|
|
3495
|
+
out.push({ ...run, text: text.slice(piece, at) });
|
|
3496
|
+
out.push({
|
|
3497
|
+
...run,
|
|
3498
|
+
text: text[at],
|
|
3499
|
+
letterSpacing: (run.letterSpacing ?? 0) + HAIR,
|
|
3500
|
+
});
|
|
3501
|
+
piece = at + 1;
|
|
3502
|
+
}
|
|
3503
|
+
if (piece === 0)
|
|
3504
|
+
out.push(run);
|
|
3505
|
+
else if (piece < text.length)
|
|
3506
|
+
out.push({ ...run, text: text.slice(piece) });
|
|
3507
|
+
}
|
|
3508
|
+
return out;
|
|
3509
|
+
}
|
|
3510
|
+
/**
|
|
3511
|
+
* `text-align: justify` (CSS Text 3, 7.4), as neither engine has it: the
|
|
3512
|
+
* paragraph's runs again, with each space inside a line that is to be
|
|
3513
|
+
* justified widened by its share of what the line leaves of `width` — the
|
|
3514
|
+
* `letter-spacing` a `word-spacing` is drawn with — so the same breaks fill
|
|
3515
|
+
* their lines. A line that is the paragraph's last, or that a forced break
|
|
3516
|
+
* ends, is not justified, nor one with no space inside it; the spaces a
|
|
3517
|
+
* line ends on hang, and take no share. `skip` is the offsets of spaces
|
|
3518
|
+
* that are no word separators — an inline box's edge laid out as a spacer
|
|
3519
|
+
* (`layoutSpaced`) — which take no share either. Null where no line is
|
|
3520
|
+
* justified.
|
|
3521
|
+
*/
|
|
3522
|
+
function justifiedRuns(runs, layout, width, rest = true, last = false, skip) {
|
|
3523
|
+
const text = runs.map((run) => run.text).join('');
|
|
3524
|
+
// each space's extra, by its offset in the paragraph
|
|
3525
|
+
const extra = new Map();
|
|
3526
|
+
const lines = layout.lines;
|
|
3527
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
3528
|
+
const line = lines[i];
|
|
3529
|
+
let end = line.end;
|
|
3530
|
+
// the paragraph's last line, or one a forced break ends, is
|
|
3531
|
+
// `text-align-last`'s to justify
|
|
3532
|
+
const ends = i === lines.length - 1 || text[end - 1] === '\n' || text[end] === '\n';
|
|
3533
|
+
if (!(ends ? last : rest))
|
|
3534
|
+
continue;
|
|
3535
|
+
while (end > line.start &&
|
|
3536
|
+
(text[end - 1] === ' ' ||
|
|
3537
|
+
text[end - 1] === '\u00a0' ||
|
|
3538
|
+
!!skip?.has(end - 1)))
|
|
3539
|
+
end -= 1;
|
|
3540
|
+
const spaces = [];
|
|
3541
|
+
for (let at = line.start; at < end; at += 1) {
|
|
3542
|
+
if ((text[at] === ' ' || text[at] === '\u00a0') && !skip?.has(at)) {
|
|
3543
|
+
spaces.push(at);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
// a hair short, so that the engine breaks where it did
|
|
3547
|
+
const slack = width - line.width - 0.01;
|
|
3548
|
+
if (!spaces.length || !(slack > 0))
|
|
3549
|
+
continue;
|
|
3550
|
+
for (const at of spaces)
|
|
3551
|
+
extra.set(at, slack / spaces.length);
|
|
3552
|
+
}
|
|
3553
|
+
return extra.size ? widenedAt(runs, extra) : null;
|
|
3554
|
+
}
|
|
3555
|
+
/** The runs with the character at each offset of `extra` a run of its
|
|
3556
|
+
* own, spaced that much more. */
|
|
3557
|
+
function widenedAt(runs, extra) {
|
|
3558
|
+
const text = runs.map((run) => run.text).join('');
|
|
3559
|
+
const out = [];
|
|
3560
|
+
let from = 0;
|
|
3561
|
+
for (const run of runs) {
|
|
3562
|
+
const end = from + run.text.length;
|
|
3563
|
+
let piece = from;
|
|
3564
|
+
for (let at = from; at < end; at += 1) {
|
|
3565
|
+
const add = extra.get(at);
|
|
3566
|
+
if (add === undefined)
|
|
3567
|
+
continue;
|
|
3568
|
+
if (at > piece)
|
|
3569
|
+
out.push({ ...run, text: text.slice(piece, at) });
|
|
3570
|
+
out.push({
|
|
3571
|
+
...run,
|
|
3572
|
+
text: text[at],
|
|
3573
|
+
letterSpacing: (run.letterSpacing ?? 0) + add,
|
|
3574
|
+
});
|
|
3575
|
+
piece = at + 1;
|
|
3576
|
+
}
|
|
3577
|
+
if (end > piece)
|
|
3578
|
+
out.push({ ...run, text: text.slice(piece, end) });
|
|
3579
|
+
from = end;
|
|
3580
|
+
}
|
|
3581
|
+
return out;
|
|
3582
|
+
}
|
|
3583
|
+
/** A word separator, as justification spaces them out (CSS Text 3, 7.4):
|
|
3584
|
+
* a space, or a no-break one. */
|
|
3585
|
+
function isSeparator(code) {
|
|
3586
|
+
return code === 0x20 || code === 0xa0;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* A line made a piece at a time — beside a float, around an inline-block,
|
|
3590
|
+
* between an inline box's edges — justified where it is (CSS Text 3, 7.4):
|
|
3591
|
+
* the room it leaves, `free`, shared out equally among its word
|
|
3592
|
+
* separators, as `justifiedRuns` shares out a paragraph's. A space inside a
|
|
3593
|
+
* piece of text takes its share as letter spacing, the piece laid out again
|
|
3594
|
+
* with it; a space a piece ends on, with something after it on the line,
|
|
3595
|
+
* takes it as room before what follows. The spaces the line ends on hang
|
|
3596
|
+
* (7.3), whatever inline box closes after them, and take none. Whatever
|
|
3597
|
+
* follows a share on the line moves over by it. How much wider the line
|
|
3598
|
+
* is.
|
|
3599
|
+
*/
|
|
3600
|
+
function justifyLine(open, free, how) {
|
|
3601
|
+
const order = open.order;
|
|
3602
|
+
let lastContent = -1;
|
|
3603
|
+
for (let i = order.length - 1; i >= 0; i -= 1) {
|
|
3604
|
+
if (order[i].kind !== 'edge') {
|
|
3605
|
+
lastContent = i;
|
|
3606
|
+
break;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
const plans = new Map();
|
|
3610
|
+
let count = 0;
|
|
3611
|
+
for (let i = 0; i < order.length; i += 1) {
|
|
3612
|
+
const placed = order[i];
|
|
3613
|
+
if (placed.kind !== 'text')
|
|
3614
|
+
continue;
|
|
3615
|
+
const text = placed.item;
|
|
3616
|
+
// the runs it was laid out from, where they are still what is kept
|
|
3617
|
+
// (a selection keeps its offsets in their place)
|
|
3618
|
+
const kept = LAYOUT_RUNS.get(text.layout);
|
|
3619
|
+
const runs = kept?.length && typeof kept[0] !== 'number' ? kept : null;
|
|
3620
|
+
const line = text.layout.lines[text.layoutLine];
|
|
3621
|
+
if (!runs || !line)
|
|
3622
|
+
continue;
|
|
3623
|
+
const all = runs.map((run) => run.text).join('');
|
|
3624
|
+
let end = line.end;
|
|
3625
|
+
while (end > line.start && isSeparator(all.charCodeAt(end - 1)))
|
|
3626
|
+
end -= 1;
|
|
3627
|
+
const inside = [];
|
|
3628
|
+
for (let at = line.start; at < end; at += 1) {
|
|
3629
|
+
if (isSeparator(all.charCodeAt(at)))
|
|
3630
|
+
inside.push(at);
|
|
3631
|
+
}
|
|
3632
|
+
// the spaces it ends on, before what follows it on the line
|
|
3633
|
+
let after = 0;
|
|
3634
|
+
if (i < lastContent) {
|
|
3635
|
+
for (let at = end; at < all.length; at += 1) {
|
|
3636
|
+
if (!isSeparator(all.charCodeAt(at)))
|
|
3637
|
+
break;
|
|
3638
|
+
after += 1;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
count += inside.length + after;
|
|
3642
|
+
plans.set(text, { runs, inside, after });
|
|
3643
|
+
}
|
|
3644
|
+
if (!count)
|
|
3645
|
+
return 0;
|
|
3646
|
+
const share = free / count;
|
|
3647
|
+
let moved = 0;
|
|
3648
|
+
for (const placed of order) {
|
|
3649
|
+
if (placed.kind !== 'text') {
|
|
3650
|
+
placed.item.x += moved;
|
|
3651
|
+
continue;
|
|
3652
|
+
}
|
|
3653
|
+
const text = placed.item;
|
|
3654
|
+
text.drawX += moved;
|
|
3655
|
+
const plan = plans.get(text);
|
|
3656
|
+
if (!plan)
|
|
3657
|
+
continue;
|
|
3658
|
+
if (plan.inside.length) {
|
|
3659
|
+
const line = text.layout.lines[text.layoutLine];
|
|
3660
|
+
const widened = widenedAt(plan.runs, new Map(plan.inside.map((at) => [at, share])));
|
|
3661
|
+
// as wide as the piece is to be, and the piece alone: the text after
|
|
3662
|
+
// it did not fit beside it before it was any wider
|
|
3663
|
+
const layout = how.fonts.layout(widened, how.base, {
|
|
3664
|
+
...how.options,
|
|
3665
|
+
maxWidth: line.width + plan.inside.length * share + 0.5,
|
|
3666
|
+
maxLines: 1,
|
|
3667
|
+
});
|
|
3668
|
+
const first = layout.lines[0];
|
|
3669
|
+
if (first && first.start === line.start && first.end === line.end) {
|
|
3670
|
+
LAYOUT_RUNS.set(layout, widened);
|
|
3671
|
+
text.layout = layout;
|
|
3672
|
+
text.layoutLine = 0;
|
|
3673
|
+
moved += first.width - line.width;
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
moved += plan.after * share;
|
|
3677
|
+
}
|
|
3678
|
+
return moved;
|
|
3679
|
+
}
|
|
3680
|
+
/** Which of a paragraph's lines are justified (CSS Text 3, 7.4): those
|
|
3681
|
+
* its end or a forced break does not end, where `text-align` is
|
|
3682
|
+
* `justify`, and those it does, where `text-align-last` is — neither
|
|
3683
|
+
* where the text does not wrap, nor under `text-justify: none`. */
|
|
3684
|
+
function justification(style) {
|
|
3685
|
+
const on = wraps(style) && style.textJustify !== 'none';
|
|
3686
|
+
return {
|
|
3687
|
+
rest: on && style.textAlign === 'justify',
|
|
3688
|
+
last: on && lastAlignOf(style) === 'justify',
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
/**
|
|
3692
|
+
* A layout of `runs` whose justified lines fill `width`: measured as it
|
|
3693
|
+
* will be drawn, its spaces spaced apart (`HAIR`), then laid out again with
|
|
3694
|
+
* each space inside a justified line widened by its share
|
|
3695
|
+
* (`justifiedRuns`), which keeps the lines where they broke. The layout
|
|
3696
|
+
* that is given where no line is to be justified. Every path that lays a
|
|
3697
|
+
* paragraph out as one layout justifies through this, so that a paragraph
|
|
3698
|
+
* is justified whichever it took.
|
|
3699
|
+
*/
|
|
3700
|
+
function justifiedLayout(fonts, runs, base, options, layout, width, { rest, last }, skip) {
|
|
3701
|
+
if (!Number.isFinite(width) ||
|
|
3702
|
+
!((rest && layout.lines.length > 1) || (last && layout.lines.length > 0))) {
|
|
3703
|
+
return { layout, runs };
|
|
3704
|
+
}
|
|
3705
|
+
const spaced = spacedApart(runs);
|
|
3706
|
+
const measured = fonts.layout(spaced, base, options);
|
|
3707
|
+
const justified = justifiedRuns(spaced, measured, width, rest, last, skip);
|
|
3708
|
+
const out = justified ? fonts.layout(justified, base, options) : measured;
|
|
3709
|
+
const outRuns = justified ?? spaced;
|
|
3710
|
+
LAYOUT_RUNS.set(out, outRuns);
|
|
3711
|
+
return { layout: out, runs: outRuns };
|
|
3712
|
+
}
|
|
3713
|
+
/** An alignment as the text engine takes it. It has no justification:
|
|
3714
|
+
* `start` is closer than a silent left on an RTL paragraph, and closer
|
|
3715
|
+
* than a ragged-right lie about what was drawn — justified lines are
|
|
3716
|
+
* spaced to fill their width before they get here (`justifiedRuns`). */
|
|
3717
|
+
function engineAlign(align) {
|
|
3718
|
+
return align === 'justify' || align === 'auto' ? 'start' : align;
|
|
3719
|
+
}
|
|
3720
|
+
/** How the lines the text or a forced break ends are aligned (CSS Text 3,
|
|
3721
|
+
* 7.2): `text-align-last`, or where that is `auto`, `text-align`, but
|
|
3722
|
+
* `justify`'s, which leaves them at the start. */
|
|
3723
|
+
function lastAlignOf(style) {
|
|
3724
|
+
const last = style.textAlignLast;
|
|
3725
|
+
if (last !== 'auto')
|
|
3726
|
+
return last;
|
|
669
3727
|
return style.textAlign === 'justify' ? 'start' : style.textAlign;
|
|
670
3728
|
}
|
|
3729
|
+
/**
|
|
3730
|
+
* The `text-indent` a block's first line takes: none where that line is
|
|
3731
|
+
* not its element's first formatted line (CSS 2.1 16.1). An anonymous
|
|
3732
|
+
* block's is only where the block is its parent's first child: the text
|
|
3733
|
+
* after a `<div>` in a `<span>`, or after a paragraph in a `<div>`, starts
|
|
3734
|
+
* a line of no indent.
|
|
3735
|
+
*/
|
|
3736
|
+
function blockIndent(block, width) {
|
|
3737
|
+
if (!block.el && block.parent && block.parent.children[0] !== block) {
|
|
3738
|
+
return 0;
|
|
3739
|
+
}
|
|
3740
|
+
return indentOf(block.style, width);
|
|
3741
|
+
}
|
|
671
3742
|
function indentOf(style, width) {
|
|
672
3743
|
const indent = style.textIndent;
|
|
673
3744
|
if (typeof indent === 'number')
|
|
674
3745
|
return indent;
|
|
675
3746
|
if (indent === 'auto')
|
|
676
3747
|
return 0;
|
|
677
|
-
|
|
3748
|
+
// of zero where the width is not known yet, as when measuring the
|
|
3749
|
+
// content's width: a `calc()` keeps its pixels (CSS Sizing 3 5.2.1)
|
|
3750
|
+
return resolve(indent, Number.isFinite(width) ? width : 0);
|
|
678
3751
|
}
|
|
679
3752
|
/**
|
|
680
3753
|
* CSS's `line-height` expressed as ntk's multiplier.
|
|
@@ -684,8 +3757,14 @@ function indentOf(style, width) {
|
|
|
684
3757
|
* rather than passing the CSS number through is the difference between
|
|
685
3758
|
* `line-height: 1.5` meaning 1.5 × 16px and it meaning 1.5 × 19px, which is
|
|
686
3759
|
* a visibly looser document than the author asked for.
|
|
3760
|
+
*
|
|
3761
|
+
* `line-height: 0` is legal and means what it says — lines of no height,
|
|
3762
|
+
* the glyphs overflowing evenly — and ntk's half-leading does exactly that
|
|
3763
|
+
* with a multiplier of 0. The floor is a hair above it rather than the 0.1
|
|
3764
|
+
* it was, which set such lines a tenth of a line apart: the Cocoa engine
|
|
3765
|
+
* takes a zero multiple for none at all.
|
|
687
3766
|
*/
|
|
688
|
-
function lineHeightMultiplier(fonts, style) {
|
|
3767
|
+
export function lineHeightMultiplier(fonts, style) {
|
|
689
3768
|
if (style.lineHeight === 'normal')
|
|
690
3769
|
return 1;
|
|
691
3770
|
const target = style.lineHeightIsLength
|
|
@@ -694,25 +3773,369 @@ function lineHeightMultiplier(fonts, style) {
|
|
|
694
3773
|
const natural = naturalLineHeight(fonts, style);
|
|
695
3774
|
if (!natural)
|
|
696
3775
|
return 1;
|
|
697
|
-
return Math.max(
|
|
3776
|
+
return Math.max(1e-4, target / natural);
|
|
698
3777
|
}
|
|
3778
|
+
/**
|
|
3779
|
+
* The font's natural line height in a style, which CSS's number form is
|
|
3780
|
+
* converted against. Kept per font manager per style, like a space's
|
|
3781
|
+
* advance: a pass asks once a paragraph — 8,800 times at 600 KB — and on
|
|
3782
|
+
* CoreText every answer was a call to the native side. Found by the style
|
|
3783
|
+
* object first, as `faceExtent` is: the paragraphs of a pass share their
|
|
3784
|
+
* styles, and spelling a face into a string key for each was a millisecond
|
|
3785
|
+
* of every pass over a long document.
|
|
3786
|
+
*/
|
|
3787
|
+
const NATURAL_LINE_HEIGHT = new WeakMap();
|
|
699
3788
|
function naturalLineHeight(fonts, style) {
|
|
3789
|
+
let cache = NATURAL_LINE_HEIGHT.get(fonts);
|
|
3790
|
+
if (!cache) {
|
|
3791
|
+
cache = { byStyle: new WeakMap(), byFace: new Map() };
|
|
3792
|
+
NATURAL_LINE_HEIGHT.set(fonts, cache);
|
|
3793
|
+
}
|
|
3794
|
+
const known = cache.byStyle.get(style);
|
|
3795
|
+
if (known !== undefined)
|
|
3796
|
+
return known;
|
|
3797
|
+
const key = `${style.fontFamily}|${style.fontSize}|${style.fontWeight}|${style.fontStyle}`;
|
|
3798
|
+
let height = cache.byFace.get(key);
|
|
3799
|
+
if (height === undefined) {
|
|
3800
|
+
try {
|
|
3801
|
+
const font = fonts.match(style.fontFamily, {
|
|
3802
|
+
size: style.fontSize,
|
|
3803
|
+
weight: style.fontWeight,
|
|
3804
|
+
style: style.fontStyle,
|
|
3805
|
+
});
|
|
3806
|
+
height = font.metrics(style.fontSize).lineHeight;
|
|
3807
|
+
}
|
|
3808
|
+
catch {
|
|
3809
|
+
height = style.fontSize * 1.2;
|
|
3810
|
+
}
|
|
3811
|
+
if (cache.byFace.size > 64)
|
|
3812
|
+
cache.byFace.clear();
|
|
3813
|
+
cache.byFace.set(key, height);
|
|
3814
|
+
}
|
|
3815
|
+
cache.byStyle.set(style, height);
|
|
3816
|
+
return height;
|
|
3817
|
+
}
|
|
3818
|
+
/**
|
|
3819
|
+
* A block's strut: the ascent and descent of a line of nothing in its face,
|
|
3820
|
+
* the half-leading its `line-height` adds shared above and below, as the
|
|
3821
|
+
* text engines set a line (CSS 2.1 10.8.1).
|
|
3822
|
+
*/
|
|
3823
|
+
export function strutOf(fonts, style) {
|
|
3824
|
+
// text set at no size takes no room, and CoreText reads a size of 0 as
|
|
3825
|
+
// its default twelve points
|
|
3826
|
+
if (!(style.fontSize > 0))
|
|
3827
|
+
return NO_EXTENT;
|
|
3828
|
+
const face = faceExtent(fonts, style);
|
|
3829
|
+
const target = style.lineHeight === 'normal'
|
|
3830
|
+
? naturalLineHeight(fonts, style)
|
|
3831
|
+
: style.lineHeightIsLength
|
|
3832
|
+
? style.lineHeight
|
|
3833
|
+
: style.lineHeight * style.fontSize;
|
|
3834
|
+
const half = (target - face.ascent - face.descent) / 2;
|
|
3835
|
+
return { ascent: face.ascent + half, descent: face.descent + half };
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* The raises `vertical-align` gives a paragraph's inline boxes (CSS 2.1
|
|
3839
|
+
* 10.8.1): each from its parent's baseline, so a box inside a raised box is
|
|
3840
|
+
* raised with it. `sub` and `super` by a fifth and a third of the parent's
|
|
3841
|
+
* font size and a pixel, as browsers set them; `text-top` and `text-bottom`
|
|
3842
|
+
* to the edges of the parent's font; `middle` by its middle to half the
|
|
3843
|
+
* parent's x-height above its baseline; a length by itself and a
|
|
3844
|
+
* percentage of the box's own line height. Worked out once a box a pass.
|
|
3845
|
+
*/
|
|
3846
|
+
class Lifts {
|
|
3847
|
+
fonts;
|
|
3848
|
+
block;
|
|
3849
|
+
styleOf;
|
|
3850
|
+
_boxes = new Map();
|
|
3851
|
+
constructor(fonts, block,
|
|
3852
|
+
/** A box's style on the lines these are for: its own, or the one it
|
|
3853
|
+
* has on a first line (`firstLineSetting`). */
|
|
3854
|
+
styleOf = ownStyle) {
|
|
3855
|
+
this.fonts = fonts;
|
|
3856
|
+
this.block = block;
|
|
3857
|
+
this.styleOf = styleOf;
|
|
3858
|
+
}
|
|
3859
|
+
/** Where a text goes, or null for one that is on the line's baseline and
|
|
3860
|
+
* in no box with a line height of its own. */
|
|
3861
|
+
of(text) {
|
|
3862
|
+
const owner = text.spans.boxAt?.(text.layoutStart);
|
|
3863
|
+
const parent = owner?.parent;
|
|
3864
|
+
if (!owner || !parent || parent.kind !== 'inline')
|
|
3865
|
+
return null;
|
|
3866
|
+
const { raise, edge, lead } = this._box(parent);
|
|
3867
|
+
if (!raise && !edge && !lead)
|
|
3868
|
+
return null;
|
|
3869
|
+
// the room of every box whose text is in it: no raised box starts or
|
|
3870
|
+
// ends inside a fragment, but one it holds may be set larger
|
|
3871
|
+
const own = { ...strutOf(this.fonts, this.styleOf(owner)) };
|
|
3872
|
+
// and of the inline boxes around it, each about its own baseline: each
|
|
3873
|
+
// is on the line with its line height, which the text's may not reach.
|
|
3874
|
+
// Not only where one sets a line height of its own: a raised `<sup>`
|
|
3875
|
+
// holding a smaller `<a>` — every footnote mark — is as tall on the
|
|
3876
|
+
// line as its own font and line height make it (CSS 2.1 10.8)
|
|
3877
|
+
if (lead || raise) {
|
|
3878
|
+
for (let at = parent; at?.kind === 'inline'; at = at.parent) {
|
|
3879
|
+
const box = this._box(at);
|
|
3880
|
+
const room = strutOf(this.fonts, this.styleOf(at));
|
|
3881
|
+
own.ascent = Math.max(own.ascent, room.ascent + box.raise - raise);
|
|
3882
|
+
own.descent = Math.max(own.descent, room.descent - box.raise + raise);
|
|
3883
|
+
if (box.edge === at)
|
|
3884
|
+
break;
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
3888
|
+
let seen = this.styleOf(owner);
|
|
3889
|
+
for (const run of natural?.runs ?? []) {
|
|
3890
|
+
const at = text.spans.boxAt?.(run.start);
|
|
3891
|
+
const style = at && this.styleOf(at);
|
|
3892
|
+
if (!style || style === seen)
|
|
3893
|
+
continue;
|
|
3894
|
+
seen = style;
|
|
3895
|
+
const room = strutOf(this.fonts, style);
|
|
3896
|
+
own.ascent = Math.max(own.ascent, room.ascent);
|
|
3897
|
+
own.descent = Math.max(own.descent, room.descent);
|
|
3898
|
+
}
|
|
3899
|
+
let to = null;
|
|
3900
|
+
if (edge) {
|
|
3901
|
+
const room = strutOf(this.fonts, this.styleOf(edge));
|
|
3902
|
+
to = {
|
|
3903
|
+
box: edge,
|
|
3904
|
+
to: this.styleOf(edge).verticalAlign === 'top' ? 'top' : 'bottom',
|
|
3905
|
+
ascent: room.ascent,
|
|
3906
|
+
descent: room.descent,
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
return { raise, ascent: own.ascent, descent: own.descent, edge: to };
|
|
3910
|
+
}
|
|
3911
|
+
/** Where an inline box's own room is about the line's baseline — its
|
|
3912
|
+
* face at its line height, raised as it is — or null for one on the
|
|
3913
|
+
* baseline, or one the line's edge sets. */
|
|
3914
|
+
ofBox(box) {
|
|
3915
|
+
const { raise, edge, lead } = this._box(box);
|
|
3916
|
+
if (edge)
|
|
3917
|
+
return null;
|
|
3918
|
+
if (!raise && !lead && !tallerStrut(this.fonts, this.block, box)) {
|
|
3919
|
+
return null;
|
|
3920
|
+
}
|
|
3921
|
+
const room = strutOf(this.fonts, this.styleOf(box));
|
|
3922
|
+
return { raise, ascent: room.ascent, descent: room.descent };
|
|
3923
|
+
}
|
|
3924
|
+
/** The `top` or `bottom` box a box is in, itself included, and how far
|
|
3925
|
+
* its baseline is raised from that box's; null outside one. */
|
|
3926
|
+
edgeOf(box) {
|
|
3927
|
+
const lift = this._box(box);
|
|
3928
|
+
return lift.edge ? { edge: lift.edge, raise: lift.raise } : null;
|
|
3929
|
+
}
|
|
3930
|
+
_box(box) {
|
|
3931
|
+
if (box.kind !== 'inline' || !box.parent)
|
|
3932
|
+
return NO_LIFT;
|
|
3933
|
+
const known = this._boxes.get(box);
|
|
3934
|
+
if (known)
|
|
3935
|
+
return known;
|
|
3936
|
+
const va = this.styleOf(box).verticalAlign;
|
|
3937
|
+
const own = ownsLeading(this.fonts, this.block, this.styleOf(box));
|
|
3938
|
+
let lift;
|
|
3939
|
+
if (va === 'top' || va === 'bottom') {
|
|
3940
|
+
// what is in it is raised from its baseline, which the line's edge
|
|
3941
|
+
// sets, whatever is around it
|
|
3942
|
+
lift = { raise: 0, edge: box, lead: own };
|
|
3943
|
+
}
|
|
3944
|
+
else {
|
|
3945
|
+
const up = this._box(box.parent);
|
|
3946
|
+
lift = {
|
|
3947
|
+
raise: up.raise + this._own(box, box.parent),
|
|
3948
|
+
edge: up.edge,
|
|
3949
|
+
lead: own || up.lead,
|
|
3950
|
+
};
|
|
3951
|
+
if (lift.raise && !lift.edge)
|
|
3952
|
+
BOX_RAISES.set(box, lift.raise);
|
|
3953
|
+
}
|
|
3954
|
+
this._boxes.set(box, lift);
|
|
3955
|
+
return lift;
|
|
3956
|
+
}
|
|
3957
|
+
/** How far a box's own `vertical-align` raises it from its parent's
|
|
3958
|
+
* baseline. */
|
|
3959
|
+
_own(box, parent) {
|
|
3960
|
+
const va = this.styleOf(box).verticalAlign;
|
|
3961
|
+
const size = this.styleOf(parent).fontSize;
|
|
3962
|
+
switch (va) {
|
|
3963
|
+
case 'baseline':
|
|
3964
|
+
case 'top':
|
|
3965
|
+
case 'bottom':
|
|
3966
|
+
return 0;
|
|
3967
|
+
case 'sub':
|
|
3968
|
+
return -(size / 5 + size / 16);
|
|
3969
|
+
case 'super':
|
|
3970
|
+
return size / 3 + size / 16;
|
|
3971
|
+
case 'text-top':
|
|
3972
|
+
return (faceExtent(this.fonts, this.styleOf(parent)).ascent -
|
|
3973
|
+
strutOf(this.fonts, this.styleOf(box)).ascent);
|
|
3974
|
+
case 'text-bottom':
|
|
3975
|
+
return (strutOf(this.fonts, this.styleOf(box)).descent -
|
|
3976
|
+
faceExtent(this.fonts, this.styleOf(parent)).descent);
|
|
3977
|
+
case 'middle': {
|
|
3978
|
+
const own = strutOf(this.fonts, this.styleOf(box));
|
|
3979
|
+
return (xHeightOf(this.fonts, this.styleOf(parent)) / 2 -
|
|
3980
|
+
(own.ascent - own.descent) / 2);
|
|
3981
|
+
}
|
|
3982
|
+
default:
|
|
3983
|
+
return typeof va === 'number'
|
|
3984
|
+
? va
|
|
3985
|
+
: resolve(va, lineHeightOf(this.fonts, this.styleOf(box)));
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
const ownStyle = (box) => box.style;
|
|
3990
|
+
const NO_LIFT = { raise: 0, edge: null, lead: false };
|
|
3991
|
+
/**
|
|
3992
|
+
* Whether an inline box's own line height sets its line otherwise than the
|
|
3993
|
+
* one a paragraph's layout gives its text. That layout sets every run at
|
|
3994
|
+
* the block's line height, as a multiple of the run's font's natural one;
|
|
3995
|
+
* CSS gives each inline box its own, and the line box holds them all (CSS
|
|
3996
|
+
* 2.1 10.8.1). So a box whose own is more makes its line taller, and a box
|
|
3997
|
+
* whose own is less makes it shorter than the multiple does, where the
|
|
3998
|
+
* multiple is more than the block's line height too: a larger face under
|
|
3999
|
+
* a `line-height` length, which every inline box inherits as that length —
|
|
4000
|
+
* `font: 11px/15px` on the body and a 14px heading run inline, which the
|
|
4001
|
+
* multiple set 19px tall on a line CSS makes 16.5. One whose own is less
|
|
4002
|
+
* but which the block's line height holds anyway, a `<code>` in a font
|
|
4003
|
+
* with taller natural lines, is left to the one layout: its lines come out
|
|
4004
|
+
* the same, and the line at a time costs a long document dear.
|
|
4005
|
+
*
|
|
4006
|
+
* Asked of every inline box in every paragraph, so it has to cost nothing
|
|
4007
|
+
* for the ones that are nothing: a box with its block's line height, family
|
|
4008
|
+
* and size is past at once, and any other answer is kept on its style.
|
|
4009
|
+
*/
|
|
4010
|
+
function ownsLeading(fonts, block, style) {
|
|
4011
|
+
// a bold or an italic face keeps its family's line metrics, and a
|
|
4012
|
+
// paragraph is thick with <strong>, <em> and <a>
|
|
4013
|
+
if (style.lineHeight === block.lineHeight &&
|
|
4014
|
+
style.lineHeightIsLength === block.lineHeightIsLength &&
|
|
4015
|
+
style.fontSize === block.fontSize &&
|
|
4016
|
+
style.fontFamily === block.fontFamily) {
|
|
4017
|
+
return false;
|
|
4018
|
+
}
|
|
4019
|
+
const known = LEADS.get(style);
|
|
4020
|
+
if (known && known.block === block && known.fonts === fonts) {
|
|
4021
|
+
return known.own;
|
|
4022
|
+
}
|
|
4023
|
+
const multiple = lineHeightMultiplier(fonts, block) * naturalLineHeight(fonts, style);
|
|
4024
|
+
const height = lineHeightOf(fonts, style);
|
|
4025
|
+
const own = height > multiple + 0.5 ||
|
|
4026
|
+
multiple > Math.max(height, lineHeightOf(fonts, block)) + 0.5;
|
|
4027
|
+
LEADS.set(style, { fonts, block, own });
|
|
4028
|
+
return own;
|
|
4029
|
+
}
|
|
4030
|
+
/** Whether a text is in an inline box that owns its leading
|
|
4031
|
+
* (`ownsLeading`), below the block it is laid out in. */
|
|
4032
|
+
function inBoxOwningLeading(fonts, block, text) {
|
|
4033
|
+
for (let at = text.parent; at && at !== block; at = at.parent) {
|
|
4034
|
+
if (at.kind !== 'inline')
|
|
4035
|
+
break;
|
|
4036
|
+
if (ownsLeading(fonts, block.style, at.style))
|
|
4037
|
+
return true;
|
|
4038
|
+
}
|
|
4039
|
+
return false;
|
|
4040
|
+
}
|
|
4041
|
+
const LEADS = new WeakMap();
|
|
4042
|
+
/**
|
|
4043
|
+
* Whether an inline box with no text is taller on its line than the
|
|
4044
|
+
* paragraph's own line height: what it holds there, which the engine,
|
|
4045
|
+
* which lays out text, gives no room (CSS 2.1 10.8). A box with text is
|
|
4046
|
+
* given its face's room with the text, at the paragraph's leading, which
|
|
4047
|
+
* `ownsLeading` compares its own line height with; asked of one with none,
|
|
4048
|
+
* the border of an empty `<span>` in a large face was drawn over the text
|
|
4049
|
+
* above its line, which was as short as the paragraph's. Not asked of a
|
|
4050
|
+
* box with text, which the one-layout paths lay out whole: a padded
|
|
4051
|
+
* `<code>` in a paragraph would have been laid out a line at a time.
|
|
4052
|
+
*/
|
|
4053
|
+
function tallerStrut(fonts, block, box) {
|
|
4054
|
+
if (box.subtreeTextEnd > box.subtreeTextStart)
|
|
4055
|
+
return false;
|
|
4056
|
+
const style = box.style;
|
|
4057
|
+
if (style.fontSize === block.fontSize &&
|
|
4058
|
+
style.lineHeight === block.lineHeight) {
|
|
4059
|
+
return false;
|
|
4060
|
+
}
|
|
4061
|
+
const own = strutOf(fonts, style);
|
|
4062
|
+
const theirs = strutOf(fonts, block);
|
|
4063
|
+
return own.ascent + own.descent > theirs.ascent + theirs.descent + 0.5;
|
|
4064
|
+
}
|
|
4065
|
+
/** A style's line height in pixels. */
|
|
4066
|
+
function lineHeightOf(fonts, style) {
|
|
4067
|
+
if (style.lineHeight === 'normal')
|
|
4068
|
+
return naturalLineHeight(fonts, style);
|
|
4069
|
+
return style.lineHeightIsLength
|
|
4070
|
+
? style.lineHeight
|
|
4071
|
+
: style.lineHeight * style.fontSize;
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* A style's x-height, where its font says, or half its size, as `ex` takes
|
|
4075
|
+
* it (CSS 2.1 4.3.2). A face whose OS/2 table is older than version 2 —
|
|
4076
|
+
* DejaVu's — states none, and the engine's answer is then NaN, which is a
|
|
4077
|
+
* number: taken for one, it put an image set `middle` nowhere at all.
|
|
4078
|
+
*/
|
|
4079
|
+
function xHeightOf(fonts, style) {
|
|
700
4080
|
try {
|
|
701
|
-
const
|
|
4081
|
+
const metrics = fonts
|
|
4082
|
+
.match(style.fontFamily, {
|
|
702
4083
|
size: style.fontSize,
|
|
703
4084
|
weight: style.fontWeight,
|
|
704
4085
|
style: style.fontStyle,
|
|
705
|
-
})
|
|
706
|
-
|
|
4086
|
+
})
|
|
4087
|
+
.metrics(style.fontSize);
|
|
4088
|
+
const x = metrics.xHeight;
|
|
4089
|
+
if (typeof x === 'number' && x > 0)
|
|
4090
|
+
return x;
|
|
707
4091
|
}
|
|
708
4092
|
catch {
|
|
709
|
-
|
|
4093
|
+
// no font to ask
|
|
4094
|
+
}
|
|
4095
|
+
return style.fontSize / 2;
|
|
4096
|
+
}
|
|
4097
|
+
/** Whether a line has nothing on it yet. */
|
|
4098
|
+
function isEmpty(open) {
|
|
4099
|
+
return open.x === open.indent && !open.texts.length && !open.atomics.length;
|
|
4100
|
+
}
|
|
4101
|
+
/** Where a line at `y` next has more room, in the block's own space: the
|
|
4102
|
+
* nearest bottom edge of a float beside it, or null when none is. */
|
|
4103
|
+
function belowFloats(options, y, height) {
|
|
4104
|
+
const edge = options.floats?.nextEdgeBelow(options.startY + y, height);
|
|
4105
|
+
return edge == null ? null : Math.max(y + 1, edge - options.startY);
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* Whether a line's first word did not fit its room: the line runs past the
|
|
4109
|
+
* room — a line's width leaves out the white space it ends on, which hangs,
|
|
4110
|
+
* so this is the word, kept whole where `overflow-wrap` says so — or it
|
|
4111
|
+
* ends inside a word — between two letters or digits, where no break is
|
|
4112
|
+
* allowed, so only a line too narrow for the word put one there. Ideographs
|
|
4113
|
+
* and kana break between any two, and are not counted as a word's letters.
|
|
4114
|
+
*/
|
|
4115
|
+
function tooNarrow(runs, end, width, room) {
|
|
4116
|
+
let at = 0;
|
|
4117
|
+
let before = '';
|
|
4118
|
+
let after = '';
|
|
4119
|
+
for (const run of runs) {
|
|
4120
|
+
const text = run.text;
|
|
4121
|
+
if (end > at && end <= at + text.length)
|
|
4122
|
+
before = text[end - at - 1];
|
|
4123
|
+
if (end >= at && end < at + text.length)
|
|
4124
|
+
after = text[end - at];
|
|
4125
|
+
at += text.length;
|
|
710
4126
|
}
|
|
4127
|
+
if (WORD_CHAR.test(before) && WORD_CHAR.test(after))
|
|
4128
|
+
return true;
|
|
4129
|
+
return width > room + 0.5 && before !== '';
|
|
711
4130
|
}
|
|
4131
|
+
const WORD_CHAR = /^(?![\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])[\p{L}\p{N}\p{M}]$/u;
|
|
712
4132
|
function bandAt(options, y, height) {
|
|
713
4133
|
if (!options.floats)
|
|
714
4134
|
return { left: 0, right: options.width };
|
|
715
|
-
|
|
4135
|
+
// Floats narrow the block's own line, where they reach into it; the
|
|
4136
|
+
// formatting context's edges are not this block's, and a block wider than
|
|
4137
|
+
// its context — a fixed width in a narrow body — keeps its lines whole.
|
|
4138
|
+
const band = options.floats.bandAt(options.startY + y, height, options.originX, options.originX + options.width);
|
|
716
4139
|
return {
|
|
717
4140
|
left: Math.max(0, band.left - options.originX),
|
|
718
4141
|
right: Math.min(options.width, band.right - options.originX),
|