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