@react-x11/components 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/dist/charts/node.d.ts +11 -1
- package/dist/charts/node.d.ts.map +1 -1
- package/dist/charts/node.js +44 -14
- package/dist/charts/node.js.map +1 -1
- package/dist/charts/render.d.ts +12 -3
- package/dist/charts/render.d.ts.map +1 -1
- package/dist/charts/render.js +94 -25
- package/dist/charts/render.js.map +1 -1
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +27 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-editor/completion.d.ts.map +1 -1
- package/dist/code-editor/completion.js +3 -2
- package/dist/code-editor/completion.js.map +1 -1
- package/dist/code-editor/index.d.ts.map +1 -1
- package/dist/code-editor/index.js +19 -0
- package/dist/code-editor/index.js.map +1 -1
- package/dist/code-editor/node.d.ts +177 -7
- package/dist/code-editor/node.d.ts.map +1 -1
- package/dist/code-editor/node.js +1329 -164
- package/dist/code-editor/node.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/languages/glsl.d.ts.map +1 -1
- package/dist/code-language/languages/glsl.js +10 -2
- package/dist/code-language/languages/glsl.js.map +1 -1
- package/dist/code-language/languages/javascript.d.ts.map +1 -1
- package/dist/code-language/languages/javascript.js +18 -3
- package/dist/code-language/languages/javascript.js.map +1 -1
- package/dist/code-language/languages/json.d.ts.map +1 -1
- package/dist/code-language/languages/json.js +10 -2
- package/dist/code-language/languages/json.js.map +1 -1
- package/dist/code-language/languages/shell.d.ts.map +1 -1
- package/dist/code-language/languages/shell.js +10 -2
- package/dist/code-language/languages/shell.js.map +1 -1
- package/dist/code-language/languages/sql.d.ts.map +1 -1
- package/dist/code-language/languages/sql.js +20 -2
- package/dist/code-language/languages/sql.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/code-language/stream.d.ts +22 -0
- package/dist/code-language/stream.d.ts.map +1 -1
- package/dist/code-language/stream.js +259 -9
- package/dist/code-language/stream.js.map +1 -1
- package/dist/code-language/types.d.ts +5 -4
- package/dist/code-language/types.d.ts.map +1 -1
- package/dist/codeblock/index.d.ts +26 -2
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +57 -4
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/draw.d.ts +50 -0
- package/dist/flow/draw.d.ts.map +1 -1
- package/dist/flow/draw.js +132 -33
- package/dist/flow/draw.js.map +1 -1
- package/dist/flow/gl/color.d.ts +13 -0
- package/dist/flow/gl/color.d.ts.map +1 -0
- package/dist/flow/gl/color.js +82 -0
- package/dist/flow/gl/color.js.map +1 -0
- package/dist/flow/gl/index.d.ts +60 -0
- package/dist/flow/gl/index.d.ts.map +1 -0
- package/dist/flow/gl/index.js +283 -0
- package/dist/flow/gl/index.js.map +1 -0
- package/dist/flow/gl/pack.d.ts +133 -0
- package/dist/flow/gl/pack.d.ts.map +1 -0
- package/dist/flow/gl/pack.js +408 -0
- package/dist/flow/gl/pack.js.map +1 -0
- package/dist/flow/gl/renderer.d.ts +95 -0
- package/dist/flow/gl/renderer.d.ts.map +1 -0
- package/dist/flow/gl/renderer.js +459 -0
- package/dist/flow/gl/renderer.js.map +1 -0
- package/dist/flow/gl/shaders.d.ts +79 -0
- package/dist/flow/gl/shaders.d.ts.map +1 -0
- package/dist/flow/gl/shaders.js +346 -0
- package/dist/flow/gl/shaders.js.map +1 -0
- package/dist/flow/gl/text.d.ts +318 -0
- package/dist/flow/gl/text.d.ts.map +1 -0
- package/dist/flow/gl/text.js +1103 -0
- package/dist/flow/gl/text.js.map +1 -0
- package/dist/flow/index.d.ts +1 -1
- package/dist/flow/index.d.ts.map +1 -1
- package/dist/flow/index.js +619 -64
- package/dist/flow/index.js.map +1 -1
- package/dist/flow/node.d.ts +621 -72
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +2253 -1018
- package/dist/flow/node.js.map +1 -1
- package/dist/flow/paint.d.ts +35 -0
- package/dist/flow/paint.d.ts.map +1 -0
- package/dist/flow/paint.js +393 -0
- package/dist/flow/paint.js.map +1 -0
- package/dist/flow/paths.d.ts +14 -0
- package/dist/flow/paths.d.ts.map +1 -1
- package/dist/flow/paths.js +45 -19
- package/dist/flow/paths.js.map +1 -1
- package/dist/flow/scene.d.ts +584 -0
- package/dist/flow/scene.d.ts.map +1 -0
- package/dist/flow/scene.js +1453 -0
- package/dist/flow/scene.js.map +1 -0
- package/dist/flow/types.d.ts +111 -1
- package/dist/flow/types.d.ts.map +1 -1
- package/dist/html/controls.d.ts +28 -0
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +52 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/calc.d.ts +14 -0
- package/dist/html/css/calc.d.ts.map +1 -0
- package/dist/html/css/calc.js +344 -0
- package/dist/html/css/calc.js.map +1 -0
- package/dist/html/css/cascade.d.ts +262 -6
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +1373 -67
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +23 -0
- package/dist/html/css/color.d.ts.map +1 -0
- package/dist/html/css/color.js +740 -0
- package/dist/html/css/color.js.map +1 -0
- package/dist/html/css/content.d.ts +92 -0
- package/dist/html/css/content.d.ts.map +1 -0
- package/dist/html/css/content.js +388 -0
- package/dist/html/css/content.js.map +1 -0
- package/dist/html/css/counter-styles.d.ts +76 -0
- package/dist/html/css/counter-styles.d.ts.map +1 -0
- package/dist/html/css/counter-styles.js +893 -0
- package/dist/html/css/counter-styles.js.map +1 -0
- package/dist/html/css/decode.d.ts +12 -0
- package/dist/html/css/decode.d.ts.map +1 -0
- package/dist/html/css/decode.js +116 -0
- package/dist/html/css/decode.js.map +1 -0
- package/dist/html/css/parse.d.ts +110 -10
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +1473 -180
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +501 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +3729 -206
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +65 -16
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +37 -3
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +122 -48
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +52 -0
- package/dist/html/css/vars.d.ts.map +1 -0
- package/dist/html/css/vars.js +358 -0
- package/dist/html/css/vars.js.map +1 -0
- package/dist/html/dom.d.ts +30 -4
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +278 -17
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +106 -0
- package/dist/html/fonts.d.ts.map +1 -0
- package/dist/html/fonts.js +565 -0
- package/dist/html/fonts.js.map +1 -0
- package/dist/html/gif.d.ts +15 -0
- package/dist/html/gif.d.ts.map +1 -0
- package/dist/html/gif.js +229 -0
- package/dist/html/gif.js.map +1 -0
- package/dist/html/index.d.ts +50 -11
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +115 -9
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +226 -3
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +3077 -219
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +367 -17
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +1742 -182
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +58 -0
- package/dist/html/layout/cache.d.ts.map +1 -0
- package/dist/html/layout/cache.js +224 -0
- package/dist/html/layout/cache.js.map +1 -0
- package/dist/html/layout/collapse.d.ts +45 -0
- package/dist/html/layout/collapse.d.ts.map +1 -0
- package/dist/html/layout/collapse.js +246 -0
- package/dist/html/layout/collapse.js.map +1 -0
- package/dist/html/layout/css-grid.d.ts +7 -0
- package/dist/html/layout/css-grid.d.ts.map +1 -0
- package/dist/html/layout/css-grid.js +900 -0
- package/dist/html/layout/css-grid.js.map +1 -0
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +1025 -89
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +45 -6
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +113 -14
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/grid-lines.d.ts +39 -0
- package/dist/html/layout/grid-lines.d.ts.map +1 -0
- package/dist/html/layout/grid-lines.js +119 -0
- package/dist/html/layout/grid-lines.js.map +1 -0
- package/dist/html/layout/grid.d.ts +53 -0
- package/dist/html/layout/grid.d.ts.map +1 -0
- package/dist/html/layout/grid.js +153 -0
- package/dist/html/layout/grid.js.map +1 -0
- package/dist/html/layout/inline.d.ts +154 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +3598 -175
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/shaping.d.ts +6 -0
- package/dist/html/layout/shaping.d.ts.map +1 -0
- package/dist/html/layout/shaping.js +100 -0
- package/dist/html/layout/shaping.js.map +1 -0
- package/dist/html/layout/table.d.ts +3 -0
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +681 -175
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/layout/tracks.d.ts +44 -0
- package/dist/html/layout/tracks.d.ts.map +1 -0
- package/dist/html/layout/tracks.js +349 -0
- package/dist/html/layout/tracks.js.map +1 -0
- package/dist/html/node.d.ts +169 -4
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +1429 -137
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +92 -3
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +3635 -107
- package/dist/html/paint.js.map +1 -1
- package/dist/html/resources.d.ts +60 -14
- package/dist/html/resources.d.ts.map +1 -1
- package/dist/html/resources.js +122 -36
- package/dist/html/resources.js.map +1 -1
- package/dist/html/surfaces.d.ts +23 -0
- package/dist/html/surfaces.d.ts.map +1 -0
- package/dist/html/surfaces.js +68 -0
- package/dist/html/surfaces.js.map +1 -0
- package/dist/html/svg.d.ts +80 -0
- package/dist/html/svg.d.ts.map +1 -0
- package/dist/html/svg.js +442 -0
- package/dist/html/svg.js.map +1 -0
- package/dist/html/url.d.ts +23 -0
- package/dist/html/url.d.ts.map +1 -0
- package/dist/html/url.js +80 -0
- package/dist/html/url.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/internal/collate.d.ts +3 -0
- package/dist/internal/collate.d.ts.map +1 -0
- package/dist/internal/collate.js +11 -0
- package/dist/internal/collate.js.map +1 -0
- package/dist/internal/markdown/ast.d.ts +11 -0
- package/dist/internal/markdown/ast.d.ts.map +1 -1
- package/dist/internal/markdown/parse.d.ts.map +1 -1
- package/dist/internal/markdown/parse.js +109 -8
- package/dist/internal/markdown/parse.js.map +1 -1
- package/dist/internal/prewarm.d.ts +18 -0
- package/dist/internal/prewarm.d.ts.map +1 -0
- package/dist/internal/prewarm.js +42 -0
- package/dist/internal/prewarm.js.map +1 -0
- package/dist/internal/sdf.d.ts +20 -0
- package/dist/internal/sdf.d.ts.map +1 -0
- package/dist/internal/sdf.js +216 -0
- package/dist/internal/sdf.js.map +1 -0
- package/dist/internal/splice.d.ts +12 -0
- package/dist/internal/splice.d.ts.map +1 -0
- package/dist/internal/splice.js +23 -0
- package/dist/internal/splice.js.map +1 -0
- package/dist/internal/window.d.ts +12 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +23 -6
- package/dist/internal/window.js.map +1 -1
- package/dist/maps/anchors.d.ts +6 -1
- package/dist/maps/anchors.d.ts.map +1 -1
- package/dist/maps/anchors.js +260 -1
- package/dist/maps/anchors.js.map +1 -1
- package/dist/maps/controller.d.ts +57 -0
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +97 -18
- package/dist/maps/controller.js.map +1 -1
- package/dist/maps/gl/buckets.d.ts +5 -1
- package/dist/maps/gl/buckets.d.ts.map +1 -1
- package/dist/maps/gl/buckets.js +5 -2
- package/dist/maps/gl/buckets.js.map +1 -1
- package/dist/maps/gl/build-worker.d.ts +2 -0
- package/dist/maps/gl/build-worker.d.ts.map +1 -1
- package/dist/maps/gl/build-worker.js +1 -1
- package/dist/maps/gl/build-worker.js.map +1 -1
- package/dist/maps/gl/cover.d.ts +23 -4
- package/dist/maps/gl/cover.d.ts.map +1 -1
- package/dist/maps/gl/cover.js +61 -48
- package/dist/maps/gl/cover.js.map +1 -1
- package/dist/maps/gl/placement.d.ts +21 -8
- package/dist/maps/gl/placement.d.ts.map +1 -1
- package/dist/maps/gl/placement.js +133 -58
- package/dist/maps/gl/placement.js.map +1 -1
- package/dist/maps/gl/renderer.d.ts +36 -4
- package/dist/maps/gl/renderer.d.ts.map +1 -1
- package/dist/maps/gl/renderer.js +81 -24
- package/dist/maps/gl/renderer.js.map +1 -1
- package/dist/maps/gl/shaders.d.ts +18 -17
- package/dist/maps/gl/shaders.d.ts.map +1 -1
- package/dist/maps/gl/shaders.js +34 -32
- package/dist/maps/gl/shaders.js.map +1 -1
- package/dist/maps/gl/store.d.ts +10 -0
- package/dist/maps/gl/store.d.ts.map +1 -1
- package/dist/maps/gl/store.js +47 -4
- package/dist/maps/gl/store.js.map +1 -1
- package/dist/maps/gl/text.d.ts +153 -24
- package/dist/maps/gl/text.d.ts.map +1 -1
- package/dist/maps/gl/text.js +379 -63
- package/dist/maps/gl/text.js.map +1 -1
- package/dist/maps/gl/view.d.ts +293 -2
- package/dist/maps/gl/view.d.ts.map +1 -1
- package/dist/maps/gl/view.js +317 -82
- package/dist/maps/gl/view.js.map +1 -1
- package/dist/maps/icons.d.ts +32 -0
- package/dist/maps/icons.d.ts.map +1 -0
- package/dist/maps/icons.js +151 -0
- package/dist/maps/icons.js.map +1 -0
- package/dist/maps/index.d.ts +2 -0
- package/dist/maps/index.d.ts.map +1 -1
- package/dist/maps/index.js +2 -0
- package/dist/maps/index.js.map +1 -1
- package/dist/maps/labels.d.ts +56 -4
- package/dist/maps/labels.d.ts.map +1 -1
- package/dist/maps/labels.js +213 -46
- package/dist/maps/labels.js.map +1 -1
- package/dist/maps/node.d.ts +17 -0
- package/dist/maps/node.d.ts.map +1 -1
- package/dist/maps/node.js +64 -30
- package/dist/maps/node.js.map +1 -1
- package/dist/maps/proj.d.ts +48 -0
- package/dist/maps/proj.d.ts.map +1 -1
- package/dist/maps/proj.js +73 -0
- package/dist/maps/proj.js.map +1 -1
- package/dist/maps/sources.d.ts +5 -3
- package/dist/maps/sources.d.ts.map +1 -1
- package/dist/maps/sources.js +5 -3
- package/dist/maps/sources.js.map +1 -1
- package/dist/maps/style.d.ts +35 -0
- package/dist/maps/style.d.ts.map +1 -1
- package/dist/maps/style.js.map +1 -1
- package/dist/maps/styles.d.ts +12 -0
- package/dist/maps/styles.d.ts.map +1 -1
- package/dist/maps/styles.js +120 -6
- package/dist/maps/styles.js.map +1 -1
- package/dist/maps/tiles.d.ts +31 -11
- package/dist/maps/tiles.d.ts.map +1 -1
- package/dist/maps/tiles.js +61 -27
- package/dist/maps/tiles.js.map +1 -1
- package/dist/maps/types.d.ts +28 -4
- package/dist/maps/types.d.ts.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +209 -68
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/spans.d.ts.map +1 -1
- package/dist/markdown/spans.js +6 -0
- package/dist/markdown/spans.js.map +1 -1
- package/dist/qml/containers.d.ts.map +1 -1
- package/dist/qml/containers.js +6 -1
- package/dist/qml/containers.js.map +1 -1
- package/dist/qml/interaction.d.ts.map +1 -1
- package/dist/qml/interaction.js +38 -5
- package/dist/qml/interaction.js.map +1 -1
- package/dist/qml/layouts.d.ts.map +1 -1
- package/dist/qml/layouts.js +21 -6
- package/dist/qml/layouts.js.map +1 -1
- package/dist/qml/qtquick.d.ts.map +1 -1
- package/dist/qml/qtquick.js +9 -2
- package/dist/qml/qtquick.js.map +1 -1
- package/dist/qml/react.d.ts.map +1 -1
- package/dist/qml/react.js +9 -7
- package/dist/qml/react.js.map +1 -1
- package/dist/reorder/clock.d.ts +7 -0
- package/dist/reorder/clock.d.ts.map +1 -0
- package/dist/reorder/clock.js +26 -0
- package/dist/reorder/clock.js.map +1 -0
- package/dist/reorder/index.d.ts +25 -3
- package/dist/reorder/index.d.ts.map +1 -1
- package/dist/reorder/index.js +64 -19
- package/dist/reorder/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts +0 -11
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +38 -9
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/keymap.d.ts.map +1 -1
- package/dist/rich-text-editor/keymap.js +2 -1
- package/dist/rich-text-editor/keymap.js.map +1 -1
- package/dist/rich-text-editor/keys.d.ts +17 -6
- package/dist/rich-text-editor/keys.d.ts.map +1 -1
- package/dist/rich-text-editor/keys.js +135 -26
- package/dist/rich-text-editor/keys.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/marks.d.ts +9 -0
- package/dist/rich-text-editor/marks.d.ts.map +1 -0
- package/dist/rich-text-editor/marks.js +83 -0
- package/dist/rich-text-editor/marks.js.map +1 -0
- package/dist/rich-text-editor/render.d.ts.map +1 -1
- package/dist/rich-text-editor/render.js +28 -10
- package/dist/rich-text-editor/render.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
- package/dist/rich-text-editor/toolbar.js +1 -1
- package/dist/rich-text-editor/toolbar.js.map +1 -1
- package/dist/rich-text-editor/virtual.d.ts.map +1 -1
- package/dist/rich-text-editor/virtual.js +17 -0
- package/dist/rich-text-editor/virtual.js.map +1 -1
- package/dist/richtext/node.d.ts +46 -2
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +187 -4
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +13 -8
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +18 -8
- package/dist/richtext/runs.js.map +1 -1
- package/dist/table/index.d.ts.map +1 -1
- package/dist/table/index.js +23 -0
- package/dist/table/index.js.map +1 -1
- package/dist/table/rows.d.ts +3 -2
- package/dist/table/rows.d.ts.map +1 -1
- package/dist/table/rows.js +20 -6
- package/dist/table/rows.js.map +1 -1
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +4 -0
- package/dist/terminal/index.js.map +1 -1
- package/dist/terminal/vt/pty.d.ts +13 -2
- package/dist/terminal/vt/pty.d.ts.map +1 -1
- package/dist/terminal/vt/pty.js +18 -3
- package/dist/terminal/vt/pty.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +103 -23
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/dist/three/canvas.d.ts.map +1 -1
- package/dist/three/canvas.js +85 -7
- package/dist/three/canvas.js.map +1 -1
- package/dist/three/events.d.ts +22 -4
- package/dist/three/events.d.ts.map +1 -1
- package/dist/three/events.js +34 -4
- package/dist/three/events.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +17 -0
- package/dist/tree/index.js.map +1 -1
- package/package.json +42 -33
- package/src/charts/node.ts +62 -26
- package/src/charts/render.ts +120 -30
- package/src/code/index.ts +36 -32
- package/src/code-editor/completion.ts +3 -2
- package/src/code-editor/index.ts +19 -0
- package/src/code-editor/node.ts +1574 -184
- package/src/code-language/index.ts +1 -1
- package/src/code-language/languages/glsl.ts +12 -2
- package/src/code-language/languages/javascript.ts +21 -4
- package/src/code-language/languages/json.ts +12 -2
- package/src/code-language/languages/shell.ts +12 -2
- package/src/code-language/languages/sql.ts +22 -2
- package/src/code-language/runs.ts +111 -1
- package/src/code-language/stream.ts +257 -17
- package/src/code-language/types.ts +5 -4
- package/src/codeblock/index.ts +68 -4
- package/src/flow/draw.ts +167 -41
- package/src/flow/gl/color.ts +85 -0
- package/src/flow/gl/index.ts +342 -0
- package/src/flow/gl/pack.ts +585 -0
- package/src/flow/gl/renderer.ts +679 -0
- package/src/flow/gl/shaders.ts +371 -0
- package/src/flow/gl/text.ts +1295 -0
- package/src/flow/index.ts +794 -67
- package/src/flow/node.ts +2496 -1256
- package/src/flow/paint.ts +511 -0
- package/src/flow/paths.ts +80 -15
- package/src/flow/scene.ts +2199 -0
- package/src/flow/types.ts +113 -1
- package/src/html/controls.ts +73 -2
- package/src/html/css/calc.ts +385 -0
- package/src/html/css/cascade.ts +1481 -62
- package/src/html/css/color.ts +755 -0
- package/src/html/css/content.ts +417 -0
- package/src/html/css/counter-styles.ts +973 -0
- package/src/html/css/decode.ts +130 -0
- package/src/html/css/parse.ts +1514 -160
- package/src/html/css/style.ts +4131 -223
- package/src/html/css/ua.ts +66 -17
- package/src/html/css/values.ts +150 -50
- package/src/html/css/vars.ts +348 -0
- package/src/html/dom.ts +284 -17
- package/src/html/fonts.ts +641 -0
- package/src/html/gif.ts +233 -0
- package/src/html/index.ts +180 -20
- package/src/html/layout/block.ts +3560 -208
- package/src/html/layout/boxes.ts +2141 -190
- package/src/html/layout/cache.ts +289 -0
- package/src/html/layout/collapse.ts +317 -0
- package/src/html/layout/css-grid.ts +1063 -0
- package/src/html/layout/flex.ts +1115 -106
- package/src/html/layout/floats.ts +128 -14
- package/src/html/layout/grid-lines.ts +141 -0
- package/src/html/layout/grid.ts +181 -0
- package/src/html/layout/inline.ts +4187 -192
- package/src/html/layout/shaping.ts +140 -0
- package/src/html/layout/table.ts +696 -186
- package/src/html/layout/tracks.ts +437 -0
- package/src/html/node.ts +1585 -156
- package/src/html/paint.ts +4582 -267
- package/src/html/resources.ts +161 -50
- package/src/html/surfaces.ts +81 -0
- package/src/html/svg.ts +504 -0
- package/src/html/url.ts +86 -0
- package/src/index.ts +2 -0
- package/src/internal/codelines.ts +200 -0
- package/src/internal/collate.ts +12 -0
- package/src/internal/markdown/ast.ts +11 -0
- package/src/internal/markdown/parse.ts +154 -11
- package/src/internal/prewarm.ts +45 -0
- package/src/internal/sdf.ts +227 -0
- package/src/internal/splice.ts +25 -0
- package/src/internal/window.ts +36 -5
- package/src/maps/anchors.ts +307 -1
- package/src/maps/controller.ts +116 -31
- package/src/maps/gl/buckets.ts +5 -1
- package/src/maps/gl/build-worker.ts +13 -2
- package/src/maps/gl/cover.ts +79 -44
- package/src/maps/gl/placement.ts +221 -69
- package/src/maps/gl/renderer.ts +104 -20
- package/src/maps/gl/shaders.ts +35 -32
- package/src/maps/gl/store.ts +66 -17
- package/src/maps/gl/text.ts +444 -71
- package/src/maps/gl/view.ts +361 -96
- package/src/maps/icons.ts +183 -0
- package/src/maps/index.ts +4 -0
- package/src/maps/labels.ts +269 -49
- package/src/maps/node.ts +82 -49
- package/src/maps/proj.ts +87 -0
- package/src/maps/sources.ts +5 -3
- package/src/maps/style.ts +32 -0
- package/src/maps/styles.ts +215 -4
- package/src/maps/tiles.ts +64 -31
- package/src/maps/types.ts +28 -4
- package/src/markdown/index.ts +293 -92
- package/src/markdown/spans.ts +5 -0
- package/src/qml/containers.tsx +6 -1
- package/src/qml/interaction.tsx +42 -5
- package/src/qml/layouts.tsx +21 -6
- package/src/qml/qtquick.tsx +9 -2
- package/src/qml/react.tsx +7 -5
- package/src/reorder/clock.ts +27 -0
- package/src/reorder/index.ts +100 -27
- package/src/rich-text-editor/index.ts +37 -12
- package/src/rich-text-editor/keymap.ts +1 -1
- package/src/rich-text-editor/keys.ts +135 -26
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/marks.ts +101 -0
- package/src/rich-text-editor/render.ts +49 -12
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/rich-text-editor/toolbar.ts +1 -1
- package/src/rich-text-editor/virtual.ts +25 -0
- package/src/richtext/node.ts +235 -14
- package/src/richtext/runs.ts +23 -12
- package/src/table/index.ts +29 -0
- package/src/table/rows.ts +18 -8
- package/src/terminal/index.ts +4 -0
- package/src/terminal/vt/pty.ts +16 -2
- package/src/terminal-output/index.ts +129 -31
- package/src/terminal-output/runs.ts +16 -2
- package/src/three/canvas.ts +102 -8
- package/src/three/events.ts +49 -9
- package/src/tree/index.ts +19 -0
package/src/html/layout/block.ts
CHANGED
|
@@ -10,21 +10,86 @@
|
|
|
10
10
|
// Float coordinates are absolute for the same reason, which is what lets the
|
|
11
11
|
// inline pass ask "how wide is the line at this y" without knowing whose
|
|
12
12
|
// formatting context it is inside.
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
AUTO,
|
|
15
|
+
gapOf,
|
|
16
|
+
isPct,
|
|
17
|
+
isTransparent,
|
|
18
|
+
resolve,
|
|
19
|
+
resolveOrNull,
|
|
20
|
+
} from '../css/values.js';
|
|
14
21
|
import type { Len } from '../css/values.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
22
|
+
import type {
|
|
23
|
+
BorderStyle,
|
|
24
|
+
ComputedStyle,
|
|
25
|
+
ContentSize,
|
|
26
|
+
GridLine,
|
|
27
|
+
} from '../css/style.js';
|
|
28
|
+
import {
|
|
29
|
+
CONTAIN_INLINE_SIZE,
|
|
30
|
+
CONTAIN_LAYOUT,
|
|
31
|
+
CONTAIN_PAINT,
|
|
32
|
+
CONTAIN_SIZE,
|
|
33
|
+
scrolls,
|
|
34
|
+
} from '../css/style.js';
|
|
35
|
+
import { lineOf, spanToName } from './grid-lines.js';
|
|
36
|
+
import type { GridLines } from './grid-lines.js';
|
|
37
|
+
import {
|
|
38
|
+
BOX_RAISES,
|
|
39
|
+
Box,
|
|
40
|
+
CLAMPED,
|
|
41
|
+
CUT_BLOCKS,
|
|
42
|
+
GRID_TRACKS,
|
|
43
|
+
FIRST_LINE,
|
|
44
|
+
INLINE_OFFSETS,
|
|
45
|
+
MOVED_OFF_LINES,
|
|
46
|
+
LINE_BOX_RAISES,
|
|
47
|
+
SHIFTED_LINES,
|
|
48
|
+
TEXT_SHIFTS,
|
|
49
|
+
isBlank,
|
|
50
|
+
} from './boxes.js';
|
|
51
|
+
import type {
|
|
52
|
+
BoxTree,
|
|
53
|
+
FirstLineStyler,
|
|
54
|
+
Intrinsic,
|
|
55
|
+
InlineDecoration,
|
|
56
|
+
LineBox,
|
|
57
|
+
MarginStrut,
|
|
58
|
+
Marker,
|
|
59
|
+
} from './boxes.js';
|
|
17
60
|
import { FloatContext } from './floats.js';
|
|
18
|
-
import {
|
|
19
|
-
|
|
61
|
+
import {
|
|
62
|
+
faceExtentOf,
|
|
63
|
+
layoutInline,
|
|
64
|
+
lineHeightMultiplier,
|
|
65
|
+
spaceBreaksOnly,
|
|
66
|
+
strutOf,
|
|
67
|
+
widestWord,
|
|
68
|
+
} from './inline.js';
|
|
69
|
+
import type { FontsLike, InlineOptions, InlineResult } from './inline.js';
|
|
70
|
+
import type { TextRun } from '../../richtext/index.js';
|
|
20
71
|
import { layoutFlex } from './flex.js';
|
|
21
|
-
import { layoutTable } from './table.js';
|
|
22
|
-
import {
|
|
72
|
+
import { finishCaptions, layoutTable } from './table.js';
|
|
73
|
+
import { collapseEdges } from './collapse.js';
|
|
74
|
+
import { computePaintBounds, hoistNegative, stackLayers } from '../paint.js';
|
|
23
75
|
|
|
24
76
|
export interface LayoutContext {
|
|
25
77
|
fonts: FontsLike | null;
|
|
78
|
+
/** Whether any block has a `::first-line` (`BoxTree.firstLine`). */
|
|
79
|
+
firstLine?: boolean;
|
|
80
|
+
/** Each box's style on a first line (`BoxTree.firstLineStyler`). */
|
|
81
|
+
firstLineStyler?: FirstLineStyler | null;
|
|
82
|
+
/** Whether a float or an out-of-flow box is in an inline box
|
|
83
|
+
* (`BoxTree.nestedOutOfLine`). */
|
|
84
|
+
nestedOutOfLine?: boolean;
|
|
85
|
+
/** Whether any box paints its background through its text
|
|
86
|
+
* (`BoxTree.clipText`). */
|
|
87
|
+
clipText?: boolean;
|
|
26
88
|
viewportWidth: number;
|
|
27
89
|
viewportHeight: number;
|
|
90
|
+
/** Set once a box is placed against the initial containing block, whose
|
|
91
|
+
* height is the viewport's (`LayoutResult.readsViewportHeight`). */
|
|
92
|
+
readViewportHeight?: boolean;
|
|
28
93
|
/** Out-of-flow boxes, collected in flow order and laid out afterwards —
|
|
29
94
|
* an absolutely positioned box may be positioned against an ancestor
|
|
30
95
|
* whose size is not known until its in-flow content has been laid out. */
|
|
@@ -41,11 +106,48 @@ export interface LayoutContext {
|
|
|
41
106
|
* exception would be.
|
|
42
107
|
*/
|
|
43
108
|
layoutSubtree(box: Box, width: number): void;
|
|
109
|
+
/** How many flex boxes' Yoga passes are running, one inside the last's
|
|
110
|
+
* measure (`layoutFlex`). */
|
|
111
|
+
flexDepth?: number;
|
|
112
|
+
/** The clamp of the line-clamp container whose formatting context is
|
|
113
|
+
* being laid out, if any (`layoutInternals`). */
|
|
114
|
+
clamp?: Clamp | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A line-clamp container's clamp, carried through its formatting context as
|
|
119
|
+
* the lines in it are laid out (CSS Overflow 4, 5.2 and 5.3.1). Only the
|
|
120
|
+
* lines of that formatting context count: a box that makes one of its own
|
|
121
|
+
* is laid out with none, and counts as a block.
|
|
122
|
+
*/
|
|
123
|
+
interface Clamp {
|
|
124
|
+
/** The line-clamp container. */
|
|
125
|
+
box: Box;
|
|
126
|
+
/** How many more lines show before the clamp point. */
|
|
127
|
+
left: number;
|
|
128
|
+
/** Whether the clamp point is passed: every box after it in the flow is
|
|
129
|
+
* invisible, and takes no room (`CLAMPED`). */
|
|
130
|
+
done: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* For `line-clamp: auto`, whose clamp point is after as many lines as
|
|
133
|
+
* the box's height holds: the first pass lays the content out whole and
|
|
134
|
+
* counts the lines that end above `floor` — the box's content bottom, in
|
|
135
|
+
* document coordinates — with what closes below each. `over` once one
|
|
136
|
+
* does not; null where the lines are cut rather than counted.
|
|
137
|
+
*/
|
|
138
|
+
fit: { floor: number; lines: number; over: boolean } | null;
|
|
44
139
|
}
|
|
45
140
|
|
|
46
141
|
export interface LayoutResult {
|
|
47
142
|
width: number;
|
|
48
143
|
height: number;
|
|
144
|
+
/**
|
|
145
|
+
* Whether the layout read the viewport's height: a percentage height on
|
|
146
|
+
* the root element, or a box positioned against the initial containing
|
|
147
|
+
* block. A document that did lays out differently in a viewport of
|
|
148
|
+
* another height, and one that did not — most — lays out the same.
|
|
149
|
+
*/
|
|
150
|
+
readsViewportHeight: boolean;
|
|
49
151
|
}
|
|
50
152
|
|
|
51
153
|
/** Lay the whole document out at a width. */
|
|
@@ -61,6 +163,10 @@ export function layoutDocument(
|
|
|
61
163
|
viewportHeight,
|
|
62
164
|
positioned: [],
|
|
63
165
|
layoutSubtree: (box, width) => layoutSubtree(box, ctx, width),
|
|
166
|
+
firstLine: tree.firstLine,
|
|
167
|
+
firstLineStyler: tree.firstLineStyler,
|
|
168
|
+
nestedOutOfLine: tree.nestedOutOfLine,
|
|
169
|
+
clipText: tree.clipText,
|
|
64
170
|
};
|
|
65
171
|
const root = tree.root;
|
|
66
172
|
root.x = 0;
|
|
@@ -68,11 +174,17 @@ export function layoutDocument(
|
|
|
68
174
|
root.width = viewportWidth;
|
|
69
175
|
// The root box stands in for `<body>` when the document has none (see
|
|
70
176
|
// `Cascade.rootStyle`), so it may carry a margin — and a margin on the
|
|
71
|
-
// initial containing block has nothing to
|
|
72
|
-
//
|
|
73
|
-
//
|
|
177
|
+
// initial containing block has nothing to sit inside. Folded into the
|
|
178
|
+
// padding at the sides and the bottom, which keeps `contentX` the one
|
|
179
|
+
// thing layout reads. Not at the top: a body's top margin collapses with
|
|
180
|
+
// its first block's, `<html>` being the formatting context's root and
|
|
181
|
+
// `<body>` not, so it is handed to the flow as the margin already pending
|
|
182
|
+
// there — or `<p>hi</p>` would stand 8px lower than the same paragraph in
|
|
183
|
+
// `<body>`.
|
|
74
184
|
resolveEdges(root, viewportWidth);
|
|
75
|
-
|
|
185
|
+
const body = fragmentBodyHeight(tree, viewportHeight);
|
|
186
|
+
const leading = root.marginTop;
|
|
187
|
+
const trailing = root.marginBottom;
|
|
76
188
|
root.padRight += root.marginRight;
|
|
77
189
|
root.padBottom += root.marginBottom;
|
|
78
190
|
root.padLeft += root.marginLeft;
|
|
@@ -80,13 +192,40 @@ export function layoutDocument(
|
|
|
80
192
|
root.marginRight = 0;
|
|
81
193
|
root.marginBottom = 0;
|
|
82
194
|
root.marginLeft = 0;
|
|
195
|
+
if (body) handPercentBase(root, body.inner);
|
|
83
196
|
|
|
84
197
|
const contentWidth = Math.max(0, viewportWidth - root.horizontalExtra);
|
|
85
198
|
const floats = new FloatContext(root.contentX, root.contentX + contentWidth);
|
|
86
|
-
|
|
199
|
+
// the initial containing block is the viewport's size, which a percentage
|
|
200
|
+
// height on the root element resolves against (CSS 2.1 10.1, 10.5); a
|
|
201
|
+
// fragment has no root element, and the box standing in for its body has
|
|
202
|
+
// the body's `auto` height to give
|
|
203
|
+
let readsViewportHeight = body?.viewport === true;
|
|
204
|
+
for (const child of root.children) {
|
|
205
|
+
if (child.el?.name !== 'html') continue;
|
|
206
|
+
child.percentHeightBase = viewportHeight;
|
|
207
|
+
const { height, minHeight, maxHeight } = child.style;
|
|
208
|
+
if (
|
|
209
|
+
isPct(height) ||
|
|
210
|
+
isPct(minHeight) ||
|
|
211
|
+
(maxHeight !== 'none' && isPct(maxHeight))
|
|
212
|
+
) {
|
|
213
|
+
readsViewportHeight = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const flow = layoutChildren(
|
|
217
|
+
root,
|
|
218
|
+
ctx,
|
|
219
|
+
floats,
|
|
220
|
+
root.contentY,
|
|
221
|
+
contentWidth,
|
|
222
|
+
leading,
|
|
223
|
+
);
|
|
87
224
|
const floatBottom =
|
|
88
225
|
floats.bottom === -Infinity ? 0 : floats.bottom - root.contentY;
|
|
89
|
-
root.height =
|
|
226
|
+
root.height = body
|
|
227
|
+
? leading + body.outer + trailing
|
|
228
|
+
: Math.max(flow.height, floatBottom) + root.verticalExtra;
|
|
90
229
|
|
|
91
230
|
// Positioned boxes last, and in the order they were found, so a later one
|
|
92
231
|
// can be positioned against an earlier one's resolved rectangle.
|
|
@@ -99,32 +238,478 @@ export function layoutDocument(
|
|
|
99
238
|
// against where it *was* — which is the whole of what makes it relative —
|
|
100
239
|
// and the ink bounds are computed after that, so culling sees where boxes
|
|
101
240
|
// ended up rather than where they were laid out.
|
|
102
|
-
applyRelativeOffsets(root);
|
|
103
|
-
computePaintBounds(root);
|
|
241
|
+
if (tree.relative) applyRelativeOffsets(root);
|
|
242
|
+
const reach = computePaintBounds(root, tree.movedInline);
|
|
243
|
+
if (tree.negative) hoistNegative(root);
|
|
244
|
+
stackLayers(root);
|
|
104
245
|
|
|
105
|
-
|
|
246
|
+
// The document is as tall as what overflows the root, not the root: an
|
|
247
|
+
// `html, body { height: 100% }` a window tall holds a message longer than
|
|
248
|
+
// the window, and the element sizes to all of it.
|
|
249
|
+
let bottom = Math.max(root.height, reach);
|
|
106
250
|
for (const { box } of ctx.positioned) {
|
|
107
251
|
if (box.style.position !== 'fixed')
|
|
108
252
|
bottom = Math.max(bottom, box.y + box.height);
|
|
109
253
|
}
|
|
110
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
width: viewportWidth,
|
|
256
|
+
height: bottom,
|
|
257
|
+
readsViewportHeight: readsViewportHeight || ctx.readViewportHeight === true,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* How tall the body a fragment's root box stands in for says it is, where
|
|
263
|
+
* that is definite (CSS 2.1 10.5): a length, `100vh` among them, or a
|
|
264
|
+
* percentage of the `<html>` implied around it where that has a height of
|
|
265
|
+
* its own — a length, or a percentage of the viewport, as `html, body {
|
|
266
|
+
* height: 100% }` fills a window. Its border box, and its content box,
|
|
267
|
+
* which what it holds takes its percentages of; `viewport` where the
|
|
268
|
+
* answer read the viewport's height. Null where the document has an
|
|
269
|
+
* `<html>` or a `<body>`, and where the body's height is `auto`, and the
|
|
270
|
+
* box is as tall as what is in it. Measured before its margins are folded
|
|
271
|
+
* into its padding.
|
|
272
|
+
*/
|
|
273
|
+
function fragmentBodyHeight(
|
|
274
|
+
tree: BoxTree,
|
|
275
|
+
viewportHeight: number,
|
|
276
|
+
): { outer: number; inner: number; viewport: boolean } | null {
|
|
277
|
+
const html = tree.impliedHtml;
|
|
278
|
+
if (!html) return null;
|
|
279
|
+
const root = tree.root;
|
|
280
|
+
const style = root.style;
|
|
281
|
+
const base = resolveOrNull(html.height, viewportHeight);
|
|
282
|
+
const set = resolveOrNull(style.height, base ?? NaN);
|
|
283
|
+
if (set === null) return null;
|
|
284
|
+
root.percentHeightBase = base ?? NaN;
|
|
285
|
+
const extra = root.verticalExtra;
|
|
286
|
+
const outer = clampHeight(
|
|
287
|
+
root,
|
|
288
|
+
style.boxSizing === 'border-box' ? Math.max(set, extra) : set + extra,
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
outer,
|
|
292
|
+
inner: Math.max(0, outer - extra),
|
|
293
|
+
viewport: isPct(style.height) && isPct(html.height),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** No margin at all, shared: most joins leave a strut as it was. */
|
|
298
|
+
const NO_MARGIN: MarginStrut = Object.freeze({ pos: 0, neg: 0 });
|
|
299
|
+
|
|
300
|
+
/** One margin, as a strut to collapse others with. */
|
|
301
|
+
function marginStrut(margin: number): MarginStrut {
|
|
302
|
+
if (margin > 0) return { pos: margin, neg: 0 };
|
|
303
|
+
if (margin < 0) return { pos: 0, neg: margin };
|
|
304
|
+
return NO_MARGIN;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* A strut with one more margin collapsed into it: the largest positive
|
|
309
|
+
* and the most negative stay apart, and add up only where the margin is
|
|
310
|
+
* read (`marginOf`) — CSS 8.3.1's rule, whole. A `-8px` pull still works
|
|
311
|
+
* against a `40px` push and comes out at `32`, and a third margin after
|
|
312
|
+
* them collapses with the two, not with their sum.
|
|
313
|
+
*/
|
|
314
|
+
function join(strut: MarginStrut, margin: number): MarginStrut {
|
|
315
|
+
if (margin > strut.pos) return { pos: margin, neg: strut.neg };
|
|
316
|
+
if (margin < strut.neg) return { pos: strut.pos, neg: margin };
|
|
317
|
+
return strut;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Two sets of adjoining margins collapsed into one. */
|
|
321
|
+
function joinStruts(a: MarginStrut, b: MarginStrut): MarginStrut {
|
|
322
|
+
if (b.pos <= a.pos && b.neg >= a.neg) return a;
|
|
323
|
+
if (a.pos <= b.pos && a.neg >= b.neg) return b;
|
|
324
|
+
return { pos: Math.max(a.pos, b.pos), neg: Math.min(a.neg, b.neg) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The margin a strut comes to. */
|
|
328
|
+
function marginOf(strut: MarginStrut): number {
|
|
329
|
+
return strut.pos + strut.neg;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** A box's bottom margin as the next sibling's collapses with it, with
|
|
333
|
+
* what came out through its bottom edge where anything did. */
|
|
334
|
+
function bottomOf(box: Box): MarginStrut {
|
|
335
|
+
return box.bottomStrut ?? marginStrut(box.marginBottom);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A block's top margin as its placement uses it: its own, collapsed with
|
|
340
|
+
* every margin that adjoins it (CSS 2.1 8.3.1). That is its first in-flow
|
|
341
|
+
* child's while nothing parts them — no top border, no top padding, no
|
|
342
|
+
* formatting context of its own, no line of text first — and that child's
|
|
343
|
+
* first child's, down the chain; and past a child that is empty, its bottom
|
|
344
|
+
* margin and the next child's top, because an empty block's two margins
|
|
345
|
+
* adjoin each other. The children the chain went through are marked, so
|
|
346
|
+
* their own layout does not apply the margin a second time. Without it
|
|
347
|
+
* `<div><p>` stood a paragraph's margin lower than `<p>`, and a `<div>`
|
|
348
|
+
* holding only an absolute image stood the rest of a document a body's
|
|
349
|
+
* margin lower than a browser does.
|
|
350
|
+
*/
|
|
351
|
+
function collapsedTopMargin(
|
|
352
|
+
box: Box,
|
|
353
|
+
containingWidth: number,
|
|
354
|
+
floats: FloatContext | null = null,
|
|
355
|
+
y = 0,
|
|
356
|
+
pending: MarginStrut = NO_MARGIN,
|
|
357
|
+
): MarginStrut {
|
|
358
|
+
floatsPassed = false;
|
|
359
|
+
floatsLeft = 0;
|
|
360
|
+
floatsRight = 0;
|
|
361
|
+
passedLeft = false;
|
|
362
|
+
passedRight = false;
|
|
363
|
+
walkFloats = floats;
|
|
364
|
+
walkY = y;
|
|
365
|
+
walkPending = pending;
|
|
366
|
+
return absorbChildren(box, containingWidth, marginStrut(box.marginTop));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Whether the last `absorbChildren` went through every child in flow of
|
|
370
|
+
* its box, each empty and absorbed whole — which is what lets the caller
|
|
371
|
+
* take that box's bottom margin too. Read straight after the call. */
|
|
372
|
+
let throughAll = false;
|
|
373
|
+
/** Whether the walk has passed a float, and how wide the floats it passed
|
|
374
|
+
* on each side are, their margin boxes as their styles state them — NaN
|
|
375
|
+
* once one does not. Such a float is placed where the margin ends, so it
|
|
376
|
+
* moves with the margin, and a new formatting context the margin reaches
|
|
377
|
+
* next has to be able to sit beside it. */
|
|
378
|
+
let floatsPassed = false;
|
|
379
|
+
let floatsLeft = 0;
|
|
380
|
+
let floatsRight = 0;
|
|
381
|
+
/** Which sides the floats the walk passed are on. */
|
|
382
|
+
let passedLeft = false;
|
|
383
|
+
let passedRight = false;
|
|
384
|
+
/** Where the margin the walk collapses starts, and the floats placed before
|
|
385
|
+
* it: what says whether a cleared child in it has clearance. */
|
|
386
|
+
let walkFloats: FloatContext | null = null;
|
|
387
|
+
let walkY = 0;
|
|
388
|
+
let walkPending: MarginStrut = NO_MARGIN;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Collapse into `margin` the margins of `at`'s children that adjoin its top
|
|
392
|
+
* edge, marking each child it takes, and return it. The walk stops at the
|
|
393
|
+
* first child something parts from the margin — a line, a clearance, a
|
|
394
|
+
* border, padding or height, a formatting context of its own — and counts
|
|
395
|
+
* a child empty only where its layout is sure to agree, since this runs
|
|
396
|
+
* before anything is laid out and the parent is placed by what it answers:
|
|
397
|
+
* an inline box with an edge, or a percentage, stops it where it might have
|
|
398
|
+
* gone on.
|
|
399
|
+
*/
|
|
400
|
+
function absorbChildren(
|
|
401
|
+
at: Box,
|
|
402
|
+
width: number,
|
|
403
|
+
margin: MarginStrut,
|
|
404
|
+
): MarginStrut {
|
|
405
|
+
let whole = true;
|
|
406
|
+
/** `at`'s content width, worked out when a child first needs it: most
|
|
407
|
+
* blocks start with text, and never do. */
|
|
408
|
+
let inner = -1;
|
|
409
|
+
for (const child of at.children) {
|
|
410
|
+
if (child.outOfFlow) continue;
|
|
411
|
+
if (child.kind === 'text') {
|
|
412
|
+
if (makesNoLine(child)) continue;
|
|
413
|
+
// a line box stops a margin
|
|
414
|
+
whole = false;
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
const floated = child.isFloat;
|
|
418
|
+
const inLine =
|
|
419
|
+
!floated &&
|
|
420
|
+
(child.kind === 'inline' ||
|
|
421
|
+
child.kind === 'break' ||
|
|
422
|
+
isInlineLevel(child));
|
|
423
|
+
if (inLine) {
|
|
424
|
+
const made = phantom(child);
|
|
425
|
+
if (made === 0) {
|
|
426
|
+
whole = false;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
if (made === 1) continue;
|
|
430
|
+
}
|
|
431
|
+
if (inner < 0) {
|
|
432
|
+
if (!topOpen(at)) {
|
|
433
|
+
whole = false;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
inner = Math.max(0, blockWidth(at, width) - at.horizontalExtra);
|
|
437
|
+
}
|
|
438
|
+
if (floated) {
|
|
439
|
+
passFloat(child, inner);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (inLine) {
|
|
443
|
+
passFloatsIn(child, inner);
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
resolveEdges(child, inner);
|
|
447
|
+
/** The margin that collapses up through the child's top edge, where
|
|
448
|
+
* it had to be worked out before the child was taken. */
|
|
449
|
+
let through: MarginStrut | null = null;
|
|
450
|
+
if (child.style.clear !== 'none') {
|
|
451
|
+
// Where it would be with `clear: none` is where its top margin and
|
|
452
|
+
// every one that comes up through its top edge put it: a large
|
|
453
|
+
// margin inside it takes it past the floats as surely as its own
|
|
454
|
+
// (CSS 2.1 9.5.2). The walk into it is undone where it has
|
|
455
|
+
// clearance, which it passes no float for.
|
|
456
|
+
const passed = passedFloats(child.style.clear);
|
|
457
|
+
const walked = passed ? null : saveWalk();
|
|
458
|
+
if (!passed) {
|
|
459
|
+
through = absorbChildren(child, inner, join(margin, child.marginTop));
|
|
460
|
+
}
|
|
461
|
+
if (passed || hasClearance(child, through!)) {
|
|
462
|
+
if (walked) restoreWalk(walked);
|
|
463
|
+
child.topAbsorbed = 0;
|
|
464
|
+
whole = false;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (
|
|
469
|
+
floatsPassed &&
|
|
470
|
+
(child.kind === 'replaced' || establishesBFC(child)) &&
|
|
471
|
+
!fitsBeside(child, inner)
|
|
472
|
+
) {
|
|
473
|
+
// it would be pushed below the floats the margin carries, so it
|
|
474
|
+
// separates from them as clearance does: the margin stops above it,
|
|
475
|
+
// and it sits under the floats rather than a margin under them
|
|
476
|
+
child.topAbsorbed = 3;
|
|
477
|
+
whole = false;
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
margin =
|
|
481
|
+
through ?? absorbChildren(child, inner, join(margin, child.marginTop));
|
|
482
|
+
if (!throughAll || !bottomOpen(child)) {
|
|
483
|
+
child.topAbsorbed = 1;
|
|
484
|
+
whole = false;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
child.topAbsorbed = 2;
|
|
488
|
+
margin = join(margin, child.marginBottom);
|
|
489
|
+
}
|
|
490
|
+
throughAll = whole && (inner >= 0 || topOpen(at));
|
|
491
|
+
return margin;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Whether a box's top margin adjoins its first child's: a block, with no
|
|
495
|
+
* border or padding at the top and no formatting context of its own. */
|
|
496
|
+
function topOpen(box: Box): boolean {
|
|
497
|
+
return (
|
|
498
|
+
box.kind === 'block' &&
|
|
499
|
+
box.borderTop === 0 &&
|
|
500
|
+
box.padTop === 0 &&
|
|
501
|
+
!establishesBFC(box)
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Whether nothing at an empty block's bottom holds its margins apart: no
|
|
506
|
+
* border, padding or height there — nor a height its ratio gives it from
|
|
507
|
+
* its width — and no marker to stand a line tall. A percentage says no,
|
|
508
|
+
* whatever it resolves to. */
|
|
509
|
+
function bottomOpen(box: Box): boolean {
|
|
510
|
+
const { height, minHeight, aspectRatio } = box.style;
|
|
511
|
+
return (
|
|
512
|
+
box.borderBottom === 0 &&
|
|
513
|
+
box.padBottom === 0 &&
|
|
514
|
+
!box.marker &&
|
|
515
|
+
(height === 0 ||
|
|
516
|
+
(height === AUTO &&
|
|
517
|
+
!aspectRatio &&
|
|
518
|
+
box.style.heightKeyword !== 'stretch')) &&
|
|
519
|
+
(minHeight === AUTO || minHeight === 0) &&
|
|
520
|
+
box.style.minHeightKeyword !== 'stretch'
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Whether a text makes no line of its own: white space that collapses
|
|
525
|
+
* away. A no-break space is not white space to CSS, though `\s` says it
|
|
526
|
+
* is, and white space that is kept makes a line. */
|
|
527
|
+
function makesNoLine(text: Box): boolean {
|
|
528
|
+
const ws = text.style.whiteSpace;
|
|
529
|
+
return (ws === 'normal' || ws === 'nowrap') && COLLAPSIBLE.test(text.text);
|
|
530
|
+
}
|
|
531
|
+
const COLLAPSIBLE = /^[ \t\n\r\f]*$/;
|
|
532
|
+
|
|
533
|
+
/** Whether an inline box makes no line either — nothing in it that does,
|
|
534
|
+
* and no margin, border or padding on any side: the phantom line of CSS
|
|
535
|
+
* 2.1 9.4.2, which a margin collapses through. `0` where it makes one,
|
|
536
|
+
* `1` where it does not, and `2` where it does not and holds a float, which
|
|
537
|
+
* the walk has to pass. The children go first: most inline boxes hold text,
|
|
538
|
+
* and that answers without a look at their edges. */
|
|
539
|
+
function phantom(box: Box): 0 | 1 | 2 {
|
|
540
|
+
if (box.kind !== 'inline' || box.style.display !== 'inline') return 0;
|
|
541
|
+
let made: 1 | 2 = 1;
|
|
542
|
+
for (const child of box.children) {
|
|
543
|
+
if (child.outOfFlow) continue;
|
|
544
|
+
if (child.isFloat) {
|
|
545
|
+
made = 2;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (child.kind === 'text') {
|
|
549
|
+
if (makesNoLine(child)) continue;
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
const inner = phantom(child);
|
|
553
|
+
if (inner === 0) return 0;
|
|
554
|
+
if (inner === 2) made = 2;
|
|
555
|
+
}
|
|
556
|
+
const style = box.style;
|
|
557
|
+
return noLength(style.marginLeft) &&
|
|
558
|
+
noLength(style.marginRight) &&
|
|
559
|
+
noLength(style.marginTop) &&
|
|
560
|
+
noLength(style.marginBottom) &&
|
|
561
|
+
noLength(style.paddingLeft) &&
|
|
562
|
+
noLength(style.paddingRight) &&
|
|
563
|
+
noLength(style.paddingTop) &&
|
|
564
|
+
noLength(style.paddingBottom) &&
|
|
565
|
+
!hasBorder(style.borderLeftStyle, style.borderLeftWidth) &&
|
|
566
|
+
!hasBorder(style.borderRightStyle, style.borderRightWidth) &&
|
|
567
|
+
!hasBorder(style.borderTopStyle, style.borderTopWidth) &&
|
|
568
|
+
!hasBorder(style.borderBottomStyle, style.borderBottomWidth)
|
|
569
|
+
? made
|
|
570
|
+
: 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Pass the floats in a phantom inline box, at any depth. */
|
|
574
|
+
function passFloatsIn(box: Box, width: number): void {
|
|
575
|
+
for (const child of box.children) {
|
|
576
|
+
if (child.isFloat) passFloat(child, width);
|
|
577
|
+
else if (child.kind === 'inline') passFloatsIn(child, width);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function noLength(len: Len): boolean {
|
|
582
|
+
return len === 0 || len === AUTO;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function hasBorder(style: BorderStyle, width: number): boolean {
|
|
586
|
+
return style !== 'none' && style !== 'hidden' && width !== 0;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Count a float the walk passes toward the room a new formatting context
|
|
590
|
+
* after it would have beside it. */
|
|
591
|
+
function passFloat(float: Box, width: number): void {
|
|
592
|
+
resolveEdges(float, width);
|
|
593
|
+
floatsPassed = true;
|
|
594
|
+
if (float.style.float === 'right') {
|
|
595
|
+
floatsRight += statedOuterWidth(float, width);
|
|
596
|
+
passedRight = true;
|
|
597
|
+
} else {
|
|
598
|
+
floatsLeft += statedOuterWidth(float, width);
|
|
599
|
+
passedLeft = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Whether the walk passed a float that a `clear` clears, which the margin
|
|
604
|
+
* carries down with it: a child that clears one has clearance whatever
|
|
605
|
+
* its margin is. */
|
|
606
|
+
function passedFloats(clear: Box['style']['clear']): boolean {
|
|
607
|
+
return (
|
|
608
|
+
((clear === 'left' || clear === 'both') && passedLeft) ||
|
|
609
|
+
((clear === 'right' || clear === 'both') && passedRight)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Whether a cleared child in the margin the walk collapses has clearance
|
|
615
|
+
* from a float placed before the walk, which parts it from that margin
|
|
616
|
+
* (CSS 2.1 9.5.2): where `through`, the margin that collapses up through
|
|
617
|
+
* its top edge, does not take it below the float already. A child with none
|
|
618
|
+
* is like any other, and the margin goes on up through it: stopped there, a
|
|
619
|
+
* large margin under a float was spent inside its parent, whose background
|
|
620
|
+
* showed above it.
|
|
621
|
+
*/
|
|
622
|
+
function hasClearance(child: Box, through: MarginStrut): boolean {
|
|
623
|
+
if (!walkFloats) return true;
|
|
624
|
+
const floor = walkFloats.clearance(child.style.clear);
|
|
625
|
+
if (floor === -Infinity) return false;
|
|
626
|
+
return walkY + marginOf(joinStruts(walkPending, through)) < floor;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** What the walk has passed, to be put back where a look ahead into a
|
|
630
|
+
* child is undone. */
|
|
631
|
+
function saveWalk(): [boolean, number, number, boolean, boolean] {
|
|
632
|
+
return [floatsPassed, floatsLeft, floatsRight, passedLeft, passedRight];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function restoreWalk([passed, left, right, onLeft, onRight]: ReturnType<
|
|
636
|
+
typeof saveWalk
|
|
637
|
+
>): void {
|
|
638
|
+
floatsPassed = passed;
|
|
639
|
+
floatsLeft = left;
|
|
640
|
+
floatsRight = right;
|
|
641
|
+
passedLeft = onLeft;
|
|
642
|
+
passedRight = onRight;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Whether a new formatting context fits beside the floats the walk passed
|
|
646
|
+
* (CSS 2.1 9.5), as `layoutBesideFloats` places it: where its width and
|
|
647
|
+
* theirs are stated, whether its border box, set in from its margins or
|
|
648
|
+
* the floats, ends before them or the room does; where the floats' are
|
|
649
|
+
* not, yes, and a box of `width: auto` there shrinks to the room it has. */
|
|
650
|
+
function fitsBeside(box: Box, width: number): boolean {
|
|
651
|
+
const stated =
|
|
652
|
+
statedOuterWidth(box, width) - box.marginLeft - box.marginRight;
|
|
653
|
+
const lo = floatsLeft ? floatsLeft : -Infinity;
|
|
654
|
+
const hi = floatsRight ? width - floatsRight : Infinity;
|
|
655
|
+
const [start, end] = besideFloats(box, 0, width, lo, hi);
|
|
656
|
+
const across = Number.isNaN(stated)
|
|
657
|
+
? Math.max(box.horizontalExtra, end - start)
|
|
658
|
+
: stated;
|
|
659
|
+
return box.parent?.style.direction === 'rtl'
|
|
660
|
+
? !(end - across < Math.max(Math.min(0, box.marginLeft), lo) - 0.01)
|
|
661
|
+
: !(
|
|
662
|
+
start + across >
|
|
663
|
+
Math.min(width - Math.min(0, box.marginRight), hi) + 0.01
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** A box's margin-box width as its style states it, before it is laid out:
|
|
668
|
+
* NaN for `width: auto`, which only layout can answer. */
|
|
669
|
+
function statedOuterWidth(box: Box, containingWidth: number): number {
|
|
670
|
+
const { width, boxSizing } = box.style;
|
|
671
|
+
if (width === AUTO) return NaN;
|
|
672
|
+
const set = resolve(width, containingWidth, NaN);
|
|
673
|
+
const border =
|
|
674
|
+
boxSizing === 'border-box'
|
|
675
|
+
? Math.max(set, box.horizontalExtra)
|
|
676
|
+
: set + box.horizontalExtra;
|
|
677
|
+
return border + box.marginLeft + box.marginRight;
|
|
111
678
|
}
|
|
112
679
|
|
|
113
680
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
681
|
+
* Whether a box's top and bottom margins collapse through it: a block with
|
|
682
|
+
* no height, no border or padding, no line in it and no formatting context
|
|
683
|
+
* of its own, whose blocks are all the same (CSS 2.1 8.3.1). An empty
|
|
684
|
+
* `<div>` between two paragraphs is the case: its margins and theirs are
|
|
685
|
+
* one margin, where they were stacked.
|
|
118
686
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
687
|
+
function collapsesThrough(box: Box): boolean {
|
|
688
|
+
if (box.kind !== 'block' || box.height !== 0) return false;
|
|
689
|
+
if (box.lines?.length || establishesBFC(box)) return false;
|
|
690
|
+
const min = resolveOrNull(box.style.minHeight, box.percentHeightBase);
|
|
691
|
+
if (min !== null && min > 0) return false;
|
|
692
|
+
for (const child of box.children) {
|
|
693
|
+
if (child.outOfFlow || child.isFloat) continue;
|
|
694
|
+
// what is in line made a line or did not, and the lines said which
|
|
695
|
+
if (
|
|
696
|
+
child.kind === 'text' ||
|
|
697
|
+
child.kind === 'inline' ||
|
|
698
|
+
child.kind === 'break' ||
|
|
699
|
+
isInlineLevel(child)
|
|
700
|
+
) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (!collapsesThrough(child)) return false;
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
121
706
|
}
|
|
122
707
|
|
|
123
708
|
/** What a box's children came to: their height, and the margin still hanging
|
|
124
709
|
* past the last of them when the box's own bottom edge does not stop it. */
|
|
125
710
|
interface FlowResult {
|
|
126
711
|
height: number;
|
|
127
|
-
hanging:
|
|
712
|
+
hanging: MarginStrut;
|
|
128
713
|
}
|
|
129
714
|
|
|
130
715
|
/**
|
|
@@ -137,28 +722,84 @@ function layoutChildren(
|
|
|
137
722
|
floats: FloatContext,
|
|
138
723
|
contentTop: number,
|
|
139
724
|
contentWidth: number,
|
|
725
|
+
leading = 0,
|
|
140
726
|
): FlowResult {
|
|
141
727
|
const contentLeft = box.contentX;
|
|
142
728
|
if (establishesInlineContext(box)) {
|
|
729
|
+
// a line box stops a margin: the leading one is spent above the lines
|
|
143
730
|
const height = layoutInlineContent(
|
|
144
731
|
box,
|
|
145
732
|
ctx,
|
|
146
733
|
floats,
|
|
147
|
-
contentTop,
|
|
734
|
+
contentTop + leading,
|
|
148
735
|
contentWidth,
|
|
149
736
|
contentLeft,
|
|
150
737
|
);
|
|
151
|
-
return { height, hanging:
|
|
738
|
+
return { height: height + leading, hanging: NO_MARGIN };
|
|
152
739
|
}
|
|
153
740
|
|
|
154
741
|
let y = contentTop;
|
|
155
|
-
/** The margin left hanging by the previous sibling,
|
|
156
|
-
|
|
742
|
+
/** The margin left hanging by the previous sibling — or, before the first
|
|
743
|
+
* child, the one `leading` hands down — for collapsing. */
|
|
744
|
+
let pendingMargin = marginStrut(leading);
|
|
745
|
+
/** How many floats were placed before this box's content began, while
|
|
746
|
+
* nothing has yet fixed where the content is: a margin that collapses
|
|
747
|
+
* up through its top edge would take the ones placed since down with
|
|
748
|
+
* it (`clearanceSince`). Infinity once a child in flow has fixed it,
|
|
749
|
+
* and from the start where a border, padding or a formatting context
|
|
750
|
+
* of its own does. */
|
|
751
|
+
let openFloats = topOpen(box) ? floats.count : Infinity;
|
|
157
752
|
let first = true;
|
|
753
|
+
/** Whether every child in flow so far was absorbed whole into this box's
|
|
754
|
+
* top margin: the walk that marked the next child reached it only
|
|
755
|
+
* through them, so past any other its mark is an earlier pass's. Asked
|
|
756
|
+
* of the box only once a child carries a mark, which few do. */
|
|
757
|
+
let open: boolean | null = null;
|
|
758
|
+
/** Whether the margin still hanging collapsed with a top margin that has
|
|
759
|
+
* clearance, an empty block cleared past a float's, and the margin that
|
|
760
|
+
* makes: the block's own two and what the empty blocks after it add to
|
|
761
|
+
* them. It stays in this box rather than escape its bottom (CSS 2.1
|
|
762
|
+
* 8.3.1, 10.6.3). */
|
|
763
|
+
let cleared = false;
|
|
764
|
+
let afterClear = NO_MARGIN;
|
|
765
|
+
/** The cleared block's own top margin: its top border edge is that far
|
|
766
|
+
* inside the margin its margins and the ones after it collapse to. */
|
|
767
|
+
let clearTop = 0;
|
|
768
|
+
// this box's first formatted line is its first child's in flow (CSS 2.1
|
|
769
|
+
// 5.12.1), which its `::first-line` is handed to
|
|
770
|
+
let firstLine = ctx.firstLine ? firstLineOf(box) : null;
|
|
771
|
+
// and the room a list item's marker takes on it (`MARKER_ROOM`)
|
|
772
|
+
let marked = MARKER_ROOM.get(box) ?? null;
|
|
773
|
+
const clamp = ctx.clamp ?? null;
|
|
158
774
|
|
|
159
775
|
for (const child of box.children) {
|
|
160
|
-
if (child.kind === 'text' &&
|
|
776
|
+
if (child.kind === 'text' && isBlank(child.text)) continue;
|
|
777
|
+
// Past a line-clamp container's clamp point, a box in flow or floating
|
|
778
|
+
// is invisible and takes no room (CSS Overflow 4, 5.3.1); a positioned
|
|
779
|
+
// box keeps its place, invisible only where its containing block is.
|
|
780
|
+
if (clamp !== null && !child.outOfFlow) {
|
|
781
|
+
if (clamp.done) {
|
|
782
|
+
hideClamped(child);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
CLAMPED.delete(child);
|
|
786
|
+
}
|
|
787
|
+
if (firstLine && !child.outOfFlow && !child.isFloat) {
|
|
788
|
+
if (!FIRST_LINE.has(child)) HANDED.set(child, firstLine);
|
|
789
|
+
firstLine = null;
|
|
790
|
+
}
|
|
791
|
+
if (marked && !child.outOfFlow && !child.isFloat) {
|
|
792
|
+
MARKER_ROOM.set(child, marked);
|
|
793
|
+
marked = null;
|
|
794
|
+
}
|
|
161
795
|
if (child.outOfFlow) {
|
|
796
|
+
placeStatic(
|
|
797
|
+
child,
|
|
798
|
+
box,
|
|
799
|
+
contentLeft,
|
|
800
|
+
contentWidth,
|
|
801
|
+
y + marginOf(pendingMargin),
|
|
802
|
+
);
|
|
162
803
|
ctx.positioned.push({
|
|
163
804
|
box: child,
|
|
164
805
|
containing: containingBlockFor(child) ?? box,
|
|
@@ -166,21 +807,109 @@ function layoutChildren(
|
|
|
166
807
|
continue;
|
|
167
808
|
}
|
|
168
809
|
if (child.isFloat) {
|
|
169
|
-
layoutFloat(
|
|
810
|
+
layoutFloat(
|
|
811
|
+
child,
|
|
812
|
+
ctx,
|
|
813
|
+
floats,
|
|
814
|
+
y + marginOf(pendingMargin),
|
|
815
|
+
contentLeft,
|
|
816
|
+
contentWidth,
|
|
817
|
+
);
|
|
170
818
|
continue;
|
|
171
819
|
}
|
|
172
820
|
|
|
173
821
|
resolveEdges(child, contentWidth);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
822
|
+
// A child whose margin this box already spent — collapsed through its
|
|
823
|
+
// top edge — sits at the content top; any other brings its own margin,
|
|
824
|
+
// collapsed with its first descendants' where nothing parts them.
|
|
825
|
+
let absorbed = child.topAbsorbed;
|
|
826
|
+
if (absorbed !== 0 && !(open ??= topOpen(box))) absorbed = 0;
|
|
827
|
+
const top = absorbed
|
|
828
|
+
? NO_MARGIN
|
|
829
|
+
: collapsedTopMargin(child, contentWidth, floats, y, pendingMargin);
|
|
830
|
+
const collapsed = joinStruts(pendingMargin, top);
|
|
831
|
+
// After an empty block cleared past a float, what collapses with its
|
|
832
|
+
// margins is placed from its top border edge, less its own top
|
|
833
|
+
// margin, which is above that edge: the depth the margin they make
|
|
834
|
+
// reaches below it (CSS 2.1 8.3.1), as the box's bottom is below it
|
|
835
|
+
// where the block is the last in it
|
|
836
|
+
const at: number = cleared
|
|
837
|
+
? y + Math.max(0, marginOf(joinStruts(afterClear, top)) - clearTop)
|
|
838
|
+
: y + marginOf(collapsed);
|
|
839
|
+
let childY = at;
|
|
840
|
+
const clear = child.style.clear;
|
|
841
|
+
const clearance = floats.clearance(clear);
|
|
842
|
+
if (clearance > -Infinity) {
|
|
843
|
+
// A float placed before anything fixed where this box's content is
|
|
844
|
+
// goes down with the margin where `clear` is none, so the child is
|
|
845
|
+
// never below it there: it has clearance, and its border edge goes
|
|
846
|
+
// under the floats whatever its margin is, which may take it back
|
|
847
|
+
// up (CSS 2.1 9.5.2, as the browsers read it). A margin under a
|
|
848
|
+
// float in an empty block put a block that cleared it the margin
|
|
849
|
+
// below the float, and its parent's background showed between.
|
|
850
|
+
// And one the walk that placed this box reached through every child
|
|
851
|
+
// before it, and stopped at for its clearance, has it too: where
|
|
852
|
+
// it would have been with `clear: none`, its margin collapsed up
|
|
853
|
+
// through this box's top, is above the floats, so its border edge
|
|
854
|
+
// goes under them, and its own margin, held apart there, may not
|
|
855
|
+
// take it lower. The clearance is negative then.
|
|
856
|
+
if (
|
|
857
|
+
(openFloats !== Infinity &&
|
|
858
|
+
floats.clearanceSince(clear, openFloats) > -Infinity) ||
|
|
859
|
+
(child.topAbsorbed === 0 && (open ?? topOpen(box)))
|
|
860
|
+
) {
|
|
861
|
+
childY = clearance;
|
|
862
|
+
} else if (clearance > childY) childY = clearance;
|
|
863
|
+
}
|
|
179
864
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
865
|
+
const counted = clamp?.fit?.lines ?? 0;
|
|
866
|
+
if (
|
|
867
|
+
!floats.isEmpty &&
|
|
868
|
+
(child.kind === 'replaced' || establishesBFC(child))
|
|
869
|
+
) {
|
|
870
|
+
layoutBesideFloats(child, ctx, floats, contentLeft, childY, contentWidth);
|
|
871
|
+
} else {
|
|
872
|
+
layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
|
|
873
|
+
}
|
|
874
|
+
// a block with none of the flow's lines in it — an image, a box of a
|
|
875
|
+
// formatting context of its own — puts the clamp point of an `auto`
|
|
876
|
+
// clamp before it where it ends below the room
|
|
877
|
+
const fit = clamp?.fit;
|
|
878
|
+
if (fit && !fit.over && fit.lines === counted) {
|
|
879
|
+
const bottom =
|
|
880
|
+
child.y +
|
|
881
|
+
child.height -
|
|
882
|
+
child.padBottom -
|
|
883
|
+
child.borderBottom +
|
|
884
|
+
closingBelow(child, clamp!.box);
|
|
885
|
+
if (bottom > fit.floor) fit.over = true;
|
|
886
|
+
}
|
|
183
887
|
first = false;
|
|
888
|
+
const moved = childY !== at;
|
|
889
|
+
if (!moved && collapsesThrough(child)) {
|
|
890
|
+
// nothing in it parts its margins: they and the ones either side of
|
|
891
|
+
// it are one (CSS 2.1 8.3.1), still hanging for what comes next —
|
|
892
|
+
// or, where the walk took them all into this box's top margin, spent
|
|
893
|
+
if (absorbed === 2) {
|
|
894
|
+
pendingMargin = collapsed;
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
open = false;
|
|
898
|
+
pendingMargin = joinStruts(collapsed, bottomOf(child));
|
|
899
|
+
if (cleared) {
|
|
900
|
+
afterClear = joinStruts(joinStruts(afterClear, top), bottomOf(child));
|
|
901
|
+
}
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
open = false;
|
|
905
|
+
y = child.y + child.height;
|
|
906
|
+
pendingMargin = bottomOf(child);
|
|
907
|
+
openFloats = Infinity;
|
|
908
|
+
// cleared, and empty: its margins collapse together, and what follows
|
|
909
|
+
// collapses with them, a top margin that has clearance (CSS 2.1 8.3.1)
|
|
910
|
+
cleared = moved && collapsesThrough(child);
|
|
911
|
+
afterClear = cleared ? joinStruts(top, bottomOf(child)) : NO_MARGIN;
|
|
912
|
+
clearTop = cleared ? marginOf(top) : 0;
|
|
184
913
|
}
|
|
185
914
|
|
|
186
915
|
// The last child's bottom margin collapses through the parent's bottom
|
|
@@ -190,16 +919,164 @@ function layoutChildren(
|
|
|
190
919
|
// escapes, and the caller merges it into the box's own bottom margin so
|
|
191
920
|
// the next sibling still sees it. Dropping it here was the bug that made
|
|
192
921
|
// `<div><p>…</p></div><p>…</p>` set the two paragraphs solid.
|
|
922
|
+
// A percentage of a height that depends on content computes to `auto`
|
|
923
|
+
// (CSS 2.1 10.5), and lets it through as `auto` does: a `height: 100%`
|
|
924
|
+
// wrapper in an `auto` body kept its last child's margin inside it
|
|
193
925
|
if (
|
|
194
926
|
!first &&
|
|
927
|
+
!cleared &&
|
|
195
928
|
!box.borderBottom &&
|
|
196
929
|
!box.padBottom &&
|
|
197
|
-
box
|
|
930
|
+
specifiedHeight(box) === null &&
|
|
198
931
|
!establishesBFC(box)
|
|
199
932
|
) {
|
|
200
|
-
|
|
933
|
+
const height = y - contentTop;
|
|
934
|
+
// A minimum height that makes the box taller parts the margin from its
|
|
935
|
+
// bottom, and spends it: the margin neither escapes nor makes the box
|
|
936
|
+
// taller still. 8.3.1 has it collapse only through a box of no
|
|
937
|
+
// `min-height`; browsers part it only where the minimum is what sets
|
|
938
|
+
// the height, and the suite has it so. A maximum leaves it to collapse
|
|
939
|
+
// through, as 8.3.1 has it, where the suite has a test either way
|
|
940
|
+
const parted =
|
|
941
|
+
box.style.minHeight !== AUTO &&
|
|
942
|
+
clampHeight(box, height + box.verticalExtra) > height + box.verticalExtra;
|
|
943
|
+
return { height, hanging: parted ? NO_MARGIN : pendingMargin };
|
|
944
|
+
}
|
|
945
|
+
// The cleared block's top border edge is where it would be with a border
|
|
946
|
+
// at its bottom, its top margin's depth inside the margin they all make:
|
|
947
|
+
// the box ends where that margin does, below the edge by what exceeds
|
|
948
|
+
// the top margin (8.3.1, 10.6.3)
|
|
949
|
+
if (cleared) {
|
|
950
|
+
return {
|
|
951
|
+
height: y + Math.max(0, marginOf(afterClear) - clearTop) - contentTop,
|
|
952
|
+
hanging: NO_MARGIN,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
height: y + marginOf(pendingMargin) - contentTop,
|
|
957
|
+
hanging: NO_MARGIN,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* A block that makes its own formatting context — a table, a box that
|
|
963
|
+
* clips its overflow — or a block-level image does not flow round the
|
|
964
|
+
* floats beside it: it is a rectangle beside them (CSS 2.1 9.5), in the
|
|
965
|
+
* room they leave at its top, or lower down where it does not fit there.
|
|
966
|
+
* An image floated left with an `overflow: hidden` block of text beside it
|
|
967
|
+
* is the layout this is for; the block ran under the image.
|
|
968
|
+
*/
|
|
969
|
+
function layoutBesideFloats(
|
|
970
|
+
box: Box,
|
|
971
|
+
ctx: LayoutContext,
|
|
972
|
+
floats: FloatContext,
|
|
973
|
+
contentLeft: number,
|
|
974
|
+
y: number,
|
|
975
|
+
contentWidth: number,
|
|
976
|
+
): void {
|
|
977
|
+
const right = contentLeft + contentWidth;
|
|
978
|
+
const style = box.style;
|
|
979
|
+
// auto margins, and HTML's alignment, place the box in the room the
|
|
980
|
+
// floats leave, as browsers do
|
|
981
|
+
const inRoom =
|
|
982
|
+
style.marginLeft === AUTO ||
|
|
983
|
+
style.marginRight === AUTO ||
|
|
984
|
+
!!box.parent?.style.alignBlocks;
|
|
985
|
+
const rtl = box.parent?.style.direction === 'rtl';
|
|
986
|
+
// Otherwise its margins are its containing block's, and may overlap a
|
|
987
|
+
// float on their own side; its border box may not. So a column beside a
|
|
988
|
+
// 200px sidebar with a margin of 220px starts 220px in, not 420px. A
|
|
989
|
+
// float is looked for as far out as a negative margin takes the box
|
|
990
|
+
resolveEdges(box, contentWidth);
|
|
991
|
+
const reachLeft = inRoom
|
|
992
|
+
? contentLeft
|
|
993
|
+
: Math.min(contentLeft, contentLeft + box.marginLeft);
|
|
994
|
+
const reachRight = inRoom ? right : Math.max(right, right - box.marginRight);
|
|
995
|
+
let at = y;
|
|
996
|
+
for (let tries = 0; tries < 64; tries += 1) {
|
|
997
|
+
let band = floats.bandAt(at, 1, reachLeft, reachRight, !inRoom);
|
|
998
|
+
let fits = false;
|
|
999
|
+
// The room is what the floats leave over the box's whole height, not at
|
|
1000
|
+
// its top: a float that starts lower down, beside the box, narrows it
|
|
1001
|
+
// too (CSS 2.1 9.5). Known only once the box is laid out, so a box
|
|
1002
|
+
// that runs into one is laid out again in what is left.
|
|
1003
|
+
for (let settle = 0; settle < 8; settle += 1) {
|
|
1004
|
+
const room = band.right - band.left;
|
|
1005
|
+
const lo = band.left > reachLeft ? band.left : -Infinity;
|
|
1006
|
+
const hi = band.right < reachRight ? band.right : Infinity;
|
|
1007
|
+
if (inRoom) layoutBlockLevel(box, ctx, floats, band.left, at, room);
|
|
1008
|
+
else {
|
|
1009
|
+
const [start, end] = besideFloats(box, contentLeft, right, lo, hi);
|
|
1010
|
+
layoutBlockLevel(
|
|
1011
|
+
box,
|
|
1012
|
+
ctx,
|
|
1013
|
+
floats,
|
|
1014
|
+
start - box.marginLeft,
|
|
1015
|
+
at,
|
|
1016
|
+
end - start + box.marginLeft + box.marginRight,
|
|
1017
|
+
contentWidth,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
// its border box, from its top: its margins may overlap a float, and
|
|
1021
|
+
// a negative top margin takes the box up past one it then meets
|
|
1022
|
+
const over = floats.bandAt(
|
|
1023
|
+
at,
|
|
1024
|
+
box.height,
|
|
1025
|
+
reachLeft,
|
|
1026
|
+
reachRight,
|
|
1027
|
+
!inRoom,
|
|
1028
|
+
);
|
|
1029
|
+
if (over.left > band.left + 0.5 || over.right < band.right - 0.5) {
|
|
1030
|
+
band = over;
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
// What is too wide overflows at the end of the line, as far as a
|
|
1034
|
+
// negative margin there takes it, but no further into a float or
|
|
1035
|
+
// past the room; at the start it would run into the floats, and
|
|
1036
|
+
// waits below them (the suite's floats-wrap-bfc-with-margin tests)
|
|
1037
|
+
if (inRoom) {
|
|
1038
|
+
fits = box.marginLeft + box.width + box.marginRight <= room + 0.5;
|
|
1039
|
+
} else if (rtl) {
|
|
1040
|
+
const limit = Math.max(contentLeft + Math.min(0, box.marginLeft), lo);
|
|
1041
|
+
fits = box.x >= limit - 0.5;
|
|
1042
|
+
} else {
|
|
1043
|
+
const limit = Math.min(right - Math.min(0, box.marginRight), hi);
|
|
1044
|
+
fits = box.x + box.width <= limit + 0.5;
|
|
1045
|
+
}
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
if (fits) return;
|
|
1049
|
+
// under the float it meets, which may start below its top
|
|
1050
|
+
const below = floats.nextEdgeBelow(at, Math.max(1, box.height));
|
|
1051
|
+
if (below === null || below <= at) break;
|
|
1052
|
+
at = below;
|
|
201
1053
|
}
|
|
202
|
-
|
|
1054
|
+
layoutBlockLevel(box, ctx, floats, contentLeft, at, contentWidth);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* The left and right edges a block with a formatting context of its own
|
|
1059
|
+
* has for its border box beside floats that reach `lo` from the left and
|
|
1060
|
+
* `hi` from the right (infinite where none does): each margin set in from
|
|
1061
|
+
* its containing block's edge, or the float's where that is further in.
|
|
1062
|
+
*/
|
|
1063
|
+
function besideFloats(
|
|
1064
|
+
box: Box,
|
|
1065
|
+
left: number,
|
|
1066
|
+
right: number,
|
|
1067
|
+
lo: number,
|
|
1068
|
+
hi: number,
|
|
1069
|
+
): [number, number] {
|
|
1070
|
+
// Where a float narrows the containing block's room on either side, a
|
|
1071
|
+
// negative margin takes the box no further out than the containing
|
|
1072
|
+
// block's edge, as every engine has it; where none does, the margins
|
|
1073
|
+
// are the containing block's in full. The suite's
|
|
1074
|
+
// floats-wrap-bfc-with-margin-006 to -009 propose otherwise, and pass in
|
|
1075
|
+
// no browser.
|
|
1076
|
+
const beside = lo > left || hi < right;
|
|
1077
|
+
const ml = beside ? Math.max(0, box.marginLeft) : box.marginLeft;
|
|
1078
|
+
const mr = beside ? Math.max(0, box.marginRight) : box.marginRight;
|
|
1079
|
+
return [Math.max(left + ml, lo), Math.min(right - mr, hi)];
|
|
203
1080
|
}
|
|
204
1081
|
|
|
205
1082
|
function layoutInlineContent(
|
|
@@ -210,28 +1087,103 @@ function layoutInlineContent(
|
|
|
210
1087
|
contentWidth: number,
|
|
211
1088
|
contentLeft: number,
|
|
212
1089
|
): number {
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
} else if (child.isFloat) {
|
|
222
|
-
layoutFloat(child, ctx, floats, contentTop, contentWidth);
|
|
223
|
-
}
|
|
1090
|
+
// a line-clamp container whose clamp point is at its start shows none of
|
|
1091
|
+
// its own lines
|
|
1092
|
+
const clamp = ctx.clamp ?? null;
|
|
1093
|
+
if (clamp !== null && (clamp.done || clamp.left <= 0)) {
|
|
1094
|
+
box.lines = [];
|
|
1095
|
+
box.floatRow = 0;
|
|
1096
|
+
return 0;
|
|
224
1097
|
}
|
|
1098
|
+
// An absolute box's static position is the block's; a float is placed
|
|
1099
|
+
// on the line it is on, as the lines reach it (`floatBoxes`).
|
|
1100
|
+
const floated = placeOutOfLine(
|
|
1101
|
+
box,
|
|
1102
|
+
box,
|
|
1103
|
+
ctx,
|
|
1104
|
+
contentTop,
|
|
1105
|
+
contentWidth,
|
|
1106
|
+
contentLeft,
|
|
1107
|
+
);
|
|
225
1108
|
// Atomics have to be sized before the line breaker can place them.
|
|
226
1109
|
sizeAtomics(box, ctx, contentWidth);
|
|
227
1110
|
|
|
228
|
-
const
|
|
1111
|
+
const options = {
|
|
229
1112
|
fonts: ctx.fonts,
|
|
230
1113
|
width: contentWidth,
|
|
231
1114
|
startY: contentTop,
|
|
232
1115
|
floats,
|
|
233
1116
|
originX: contentLeft,
|
|
234
|
-
|
|
1117
|
+
clipText: ctx.clipText,
|
|
1118
|
+
firstStrut: MARKER_ROOM.get(box),
|
|
1119
|
+
floatBoxes: floated
|
|
1120
|
+
? {
|
|
1121
|
+
size: (child: Box) => sizeFloat(child, ctx, contentWidth),
|
|
1122
|
+
place: (child: Box, y: number) =>
|
|
1123
|
+
placeFloat(child, floats, y, contentLeft, contentWidth),
|
|
1124
|
+
}
|
|
1125
|
+
: undefined,
|
|
1126
|
+
// the lines the clamp leaves, where it cuts rather than counts
|
|
1127
|
+
clamp:
|
|
1128
|
+
clamp !== null && clamp.fit === null && clamp.left !== Infinity
|
|
1129
|
+
? {
|
|
1130
|
+
lines: clamp.left,
|
|
1131
|
+
ellipsis: box.style.blockEllipsis,
|
|
1132
|
+
follows: followsInFlow(box, clamp.box),
|
|
1133
|
+
}
|
|
1134
|
+
: undefined,
|
|
1135
|
+
};
|
|
1136
|
+
const firstLine = ctx.firstLine ? firstLineOf(box) : null;
|
|
1137
|
+
// A `::first-line` that sets the line's fonts moves where it breaks, so
|
|
1138
|
+
// the line is found in them (`InlineOptions.firstLineStyle`); its text
|
|
1139
|
+
// takes the pseudo-element's colour there too
|
|
1140
|
+
const restyled =
|
|
1141
|
+
firstLine && ctx.firstLineStyler && setsFonts(firstLine, box.style)
|
|
1142
|
+
? { style: firstLine, styler: ctx.firstLineStyler }
|
|
1143
|
+
: undefined;
|
|
1144
|
+
let result = layoutInline(
|
|
1145
|
+
box,
|
|
1146
|
+
restyled ? { ...options, firstLineStyle: restyled } : options,
|
|
1147
|
+
);
|
|
1148
|
+
if (
|
|
1149
|
+
firstLine &&
|
|
1150
|
+
!restyled &&
|
|
1151
|
+
firstLine.color !== box.style.color &&
|
|
1152
|
+
result.lines.length
|
|
1153
|
+
) {
|
|
1154
|
+
// the first line's text in its `::first-line` colour, laid out again
|
|
1155
|
+
// with its runs cut where the line ends (CSS 2.1 5.12.1), beside the
|
|
1156
|
+
// floats the first pass placed — and measured with them
|
|
1157
|
+
const placed = result;
|
|
1158
|
+
result = layoutInline(box, {
|
|
1159
|
+
...options,
|
|
1160
|
+
floatBoxes: undefined,
|
|
1161
|
+
firstLine: {
|
|
1162
|
+
color: firstLine.color,
|
|
1163
|
+
from: box.style.color,
|
|
1164
|
+
end: result.lines[0].textEnd,
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
if (placed.lines.length === result.lines.length) {
|
|
1168
|
+
placed.lines.forEach((line, i) => {
|
|
1169
|
+
if (line.floats) result.lines[i].floats = line.floats;
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
result.floatRow = placed.floatRow;
|
|
1173
|
+
}
|
|
1174
|
+
box.floatRow = result.floatRow ?? 0;
|
|
1175
|
+
if (clamp !== null) countLines(clamp, box, result, contentTop, options.clamp);
|
|
1176
|
+
if (
|
|
1177
|
+
firstLine &&
|
|
1178
|
+
ctx.fonts &&
|
|
1179
|
+
result.lines.length &&
|
|
1180
|
+
!isTransparent(firstLine.backgroundColor)
|
|
1181
|
+
) {
|
|
1182
|
+
result.lines[0].background = {
|
|
1183
|
+
style: firstLine,
|
|
1184
|
+
...faceExtentOf(ctx.fonts, firstLine),
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
235
1187
|
// The inline pass works in content-box coordinates; move the result into
|
|
236
1188
|
// document space now, so nothing below this line has to know the
|
|
237
1189
|
// difference.
|
|
@@ -247,11 +1199,151 @@ function layoutInlineContent(
|
|
|
247
1199
|
text.drawX += contentLeft;
|
|
248
1200
|
text.drawY += contentTop;
|
|
249
1201
|
}
|
|
1202
|
+
for (const edge of line.edges ?? []) edge.x += contentLeft;
|
|
250
1203
|
}
|
|
251
1204
|
box.lines = result.lines;
|
|
252
1205
|
return result.height;
|
|
253
1206
|
}
|
|
254
1207
|
|
|
1208
|
+
/**
|
|
1209
|
+
* A block's lines, counted against the clamp: the ones left, or for an
|
|
1210
|
+
* `auto` clamp's first pass, the ones that end above its floor. The clamp
|
|
1211
|
+
* point is passed where they were cut, or where they were all the lines
|
|
1212
|
+
* left and more of the container follows them.
|
|
1213
|
+
*/
|
|
1214
|
+
function countLines(
|
|
1215
|
+
clamp: Clamp,
|
|
1216
|
+
box: Box,
|
|
1217
|
+
result: InlineResult,
|
|
1218
|
+
contentTop: number,
|
|
1219
|
+
cut: InlineOptions['clamp'],
|
|
1220
|
+
): void {
|
|
1221
|
+
const fit = clamp.fit;
|
|
1222
|
+
if (fit !== null) {
|
|
1223
|
+
if (fit.over) return;
|
|
1224
|
+
const below = closingBelow(box, clamp.box);
|
|
1225
|
+
for (const line of result.lines) {
|
|
1226
|
+
if (contentTop + line.y + line.height + below > fit.floor) {
|
|
1227
|
+
fit.over = true;
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
fit.lines += 1;
|
|
1231
|
+
}
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
clamp.left -= result.lines.length;
|
|
1235
|
+
if (result.cut || (clamp.left <= 0 && cut?.follows)) clamp.done = true;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** Mark a box past a clamp point, and take from everything in it the lines
|
|
1239
|
+
* a layout before gave it: a hidden box is not laid out, and nothing —
|
|
1240
|
+
* a selection, a caret — may find text in it where it stood. */
|
|
1241
|
+
function hideClamped(box: Box): void {
|
|
1242
|
+
CLAMPED.add(box);
|
|
1243
|
+
const stack = [box];
|
|
1244
|
+
while (stack.length) {
|
|
1245
|
+
const at = stack.pop()!;
|
|
1246
|
+
at.lines = null;
|
|
1247
|
+
for (const child of at.children) stack.push(child);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* How far below a box's content a line-clamp container's content ends,
|
|
1253
|
+
* where the clamp point is just after it: the bottom padding, border and
|
|
1254
|
+
* margin of the box and of each block around it in the container, the
|
|
1255
|
+
* margins collapsed where nothing parts them.
|
|
1256
|
+
*/
|
|
1257
|
+
function closingBelow(box: Box, container: Box): number {
|
|
1258
|
+
let below = 0;
|
|
1259
|
+
let margin = 0;
|
|
1260
|
+
for (let at: Box | null = box; at && at !== container; at = at.parent) {
|
|
1261
|
+
const edge = at.padBottom + at.borderBottom;
|
|
1262
|
+
if (edge) {
|
|
1263
|
+
below += margin + edge;
|
|
1264
|
+
margin = 0;
|
|
1265
|
+
}
|
|
1266
|
+
margin = Math.max(margin, at.marginBottom);
|
|
1267
|
+
}
|
|
1268
|
+
return below + margin;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Whether anything in flow follows a block in a line-clamp container — a
|
|
1273
|
+
* block after it, or after a block around it. Then a clamp point is just
|
|
1274
|
+
* after its last line, which ends in an ellipsis (CSS Overflow 4, 4.2);
|
|
1275
|
+
* where nothing does, the lines end where the content does.
|
|
1276
|
+
*/
|
|
1277
|
+
function followsInFlow(box: Box, container: Box): boolean {
|
|
1278
|
+
for (let at = box; at !== container && at.parent; at = at.parent) {
|
|
1279
|
+
const siblings = at.parent.children;
|
|
1280
|
+
for (let i = siblings.indexOf(at) + 1; i < siblings.length; i += 1) {
|
|
1281
|
+
const next = siblings[i];
|
|
1282
|
+
if (next.outOfFlow || next.isFloat || makesNothing(next)) continue;
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/** Whether a box in flow makes neither a line nor a height of its own: a
|
|
1290
|
+
* block of phantom lines — the rest of an inline box a block broke — with
|
|
1291
|
+
* no edge or height to part it, which puts no clamp point after the line
|
|
1292
|
+
* before it. */
|
|
1293
|
+
function makesNothing(box: Box): boolean {
|
|
1294
|
+
if (box.kind === 'text') return makesNoLine(box);
|
|
1295
|
+
if (box.kind === 'inline') return phantom(box) !== 0;
|
|
1296
|
+
if (box.kind !== 'block' || establishesBFC(box)) return false;
|
|
1297
|
+
const style = box.style;
|
|
1298
|
+
if (
|
|
1299
|
+
style.height !== AUTO ||
|
|
1300
|
+
style.minHeight !== AUTO ||
|
|
1301
|
+
!noLength(style.paddingTop) ||
|
|
1302
|
+
!noLength(style.paddingBottom) ||
|
|
1303
|
+
hasBorder(style.borderTopStyle, style.borderTopWidth) ||
|
|
1304
|
+
hasBorder(style.borderBottomStyle, style.borderBottomWidth)
|
|
1305
|
+
) {
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
for (const child of box.children) {
|
|
1309
|
+
if (child.outOfFlow || child.isFloat) continue;
|
|
1310
|
+
if (!makesNothing(child)) return false;
|
|
1311
|
+
}
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/** Whether a `::first-line` style sets its line's text otherwise than its
|
|
1316
|
+
* block's: in another face, size or line height, spaced apart, or in
|
|
1317
|
+
* capitals. */
|
|
1318
|
+
function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
|
|
1319
|
+
return (
|
|
1320
|
+
line.fontFamily !== block.fontFamily ||
|
|
1321
|
+
line.fontSize !== block.fontSize ||
|
|
1322
|
+
line.fontWeight !== block.fontWeight ||
|
|
1323
|
+
line.fontStyle !== block.fontStyle ||
|
|
1324
|
+
line.lineHeight !== block.lineHeight ||
|
|
1325
|
+
line.lineHeightIsLength !== block.lineHeightIsLength ||
|
|
1326
|
+
line.letterSpacing !== block.letterSpacing ||
|
|
1327
|
+
line.fontVariantCaps !== block.fontVariantCaps ||
|
|
1328
|
+
line.fontVariantNumeric !== block.fontVariantNumeric ||
|
|
1329
|
+
line.fontVariantLigatures !== block.fontVariantLigatures ||
|
|
1330
|
+
line.fontVariantPosition !== block.fontVariantPosition ||
|
|
1331
|
+
line.fontKerning !== block.fontKerning ||
|
|
1332
|
+
line.fontFeatureSettings !== block.fontFeatureSettings ||
|
|
1333
|
+
line.textTransform !== block.textTransform
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/** `::first-line` styles handed down to the first child in flow of a box
|
|
1338
|
+
* that has one, whose first line is the box's (`layoutChildren`). */
|
|
1339
|
+
const HANDED = new WeakMap<Box, ComputedStyle>();
|
|
1340
|
+
|
|
1341
|
+
/** The `::first-line` style a box's first line takes: its own, or one an
|
|
1342
|
+
* ancestor handed it. */
|
|
1343
|
+
function firstLineOf(box: Box): ComputedStyle | null {
|
|
1344
|
+
return FIRST_LINE.get(box) ?? HANDED.get(box) ?? null;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
255
1347
|
/** Size every atomic in an inline context. */
|
|
256
1348
|
function sizeAtomics(box: Box, ctx: LayoutContext, contentWidth: number): void {
|
|
257
1349
|
for (const child of box.children) {
|
|
@@ -278,9 +1370,123 @@ function layoutAtomic(
|
|
|
278
1370
|
return;
|
|
279
1371
|
}
|
|
280
1372
|
const width = shrinkToFitWidth(box, ctx, availableWidth);
|
|
1373
|
+
layoutOwn(box, ctx, width);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Lay out a box that sizes itself — a float, an inline-block, an absolute
|
|
1378
|
+
* box — at its own width, where nothing outside it reaches in: or, where
|
|
1379
|
+
* this pass has already laid it out at that width with the same edges,
|
|
1380
|
+
* leave it as it was and move it back to where a layout puts it.
|
|
1381
|
+
*
|
|
1382
|
+
* Measuring a box's content lays its content out, and so a float in a
|
|
1383
|
+
* float was laid out twice a level — as its parent measured, and as its
|
|
1384
|
+
* parent was laid out at the width it measured — and a hundred of them,
|
|
1385
|
+
* the markup of a broken page, never finished. What a caller does to the
|
|
1386
|
+
* box after its layout is move it, and stretch its height, which is put
|
|
1387
|
+
* back; its own, and its content's, are the layout's.
|
|
1388
|
+
*/
|
|
1389
|
+
function layoutOwn(box: Box, ctx: LayoutContext, width: number): void {
|
|
1390
|
+
const laid = LAID_OWN.get(box);
|
|
1391
|
+
if (
|
|
1392
|
+
laid !== undefined &&
|
|
1393
|
+
laid.ctx === ctx &&
|
|
1394
|
+
// and nothing, a probe of its content, has laid it out since
|
|
1395
|
+
laid.serial === box.layoutSerial &&
|
|
1396
|
+
laid.width === width &&
|
|
1397
|
+
// NaN where no height is a base, which `===` would never find again
|
|
1398
|
+
Object.is(laid.base, box.percentHeightBase) &&
|
|
1399
|
+
laid.padTop === box.padTop &&
|
|
1400
|
+
laid.padRight === box.padRight &&
|
|
1401
|
+
laid.padBottom === box.padBottom &&
|
|
1402
|
+
laid.padLeft === box.padLeft &&
|
|
1403
|
+
// and was put somewhere it can be moved back from: a right float in a
|
|
1404
|
+
// content measured at no width limit stands at an infinite x, and
|
|
1405
|
+
// moved back by it, came out at NaN
|
|
1406
|
+
Number.isFinite(box.x) &&
|
|
1407
|
+
Number.isFinite(box.y)
|
|
1408
|
+
) {
|
|
1409
|
+
box.height = laid.height;
|
|
1410
|
+
moveTo(box, 0, 0);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
281
1413
|
layoutInternals(box, ctx, width, 0, 0);
|
|
1414
|
+
LAID_OWN.set(box, {
|
|
1415
|
+
ctx,
|
|
1416
|
+
serial: box.layoutSerial,
|
|
1417
|
+
width,
|
|
1418
|
+
base: box.percentHeightBase,
|
|
1419
|
+
padTop: box.padTop,
|
|
1420
|
+
padRight: box.padRight,
|
|
1421
|
+
padBottom: box.padBottom,
|
|
1422
|
+
padLeft: box.padLeft,
|
|
1423
|
+
height: box.height,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** Every box's layout counted, for `Box.layoutSerial`. */
|
|
1428
|
+
let layoutSerial = 0;
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* Whether a box's layout is one this pass made at `width`, with the same
|
|
1432
|
+
* edges and the same height to take percentages of, and nothing has laid
|
|
1433
|
+
* it out since (`keep`): what a table in a table in a table is asked for
|
|
1434
|
+
* again at every level, and at the same width each time. Its height is put
|
|
1435
|
+
* back, where a caller may have stretched it; where it stands is the
|
|
1436
|
+
* caller's to move it to.
|
|
1437
|
+
*/
|
|
1438
|
+
function kept(box: Box, ctx: LayoutContext, width: number): boolean {
|
|
1439
|
+
const laid = LAID_OWN.get(box);
|
|
1440
|
+
if (
|
|
1441
|
+
laid === undefined ||
|
|
1442
|
+
laid.ctx !== ctx ||
|
|
1443
|
+
laid.serial !== box.layoutSerial ||
|
|
1444
|
+
laid.width !== width ||
|
|
1445
|
+
!Object.is(laid.base, box.percentHeightBase) ||
|
|
1446
|
+
laid.padTop !== box.padTop ||
|
|
1447
|
+
laid.padRight !== box.padRight ||
|
|
1448
|
+
laid.padBottom !== box.padBottom ||
|
|
1449
|
+
laid.padLeft !== box.padLeft ||
|
|
1450
|
+
!Number.isFinite(box.x) ||
|
|
1451
|
+
!Number.isFinite(box.y)
|
|
1452
|
+
) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
box.height = laid.height;
|
|
1456
|
+
return true;
|
|
282
1457
|
}
|
|
283
1458
|
|
|
1459
|
+
/** Note the layout a box was just given, at `width`, for `kept`. */
|
|
1460
|
+
function keep(box: Box, ctx: LayoutContext, width: number): void {
|
|
1461
|
+
LAID_OWN.set(box, {
|
|
1462
|
+
ctx,
|
|
1463
|
+
serial: box.layoutSerial,
|
|
1464
|
+
width,
|
|
1465
|
+
base: box.percentHeightBase,
|
|
1466
|
+
padTop: box.padTop,
|
|
1467
|
+
padRight: box.padRight,
|
|
1468
|
+
padBottom: box.padBottom,
|
|
1469
|
+
padLeft: box.padLeft,
|
|
1470
|
+
height: box.height,
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** What `layoutOwn` laid each box out at, and in which pass. */
|
|
1475
|
+
const LAID_OWN = new WeakMap<
|
|
1476
|
+
Box,
|
|
1477
|
+
{
|
|
1478
|
+
ctx: LayoutContext;
|
|
1479
|
+
serial: number;
|
|
1480
|
+
width: number;
|
|
1481
|
+
base: number;
|
|
1482
|
+
padTop: number;
|
|
1483
|
+
padRight: number;
|
|
1484
|
+
padBottom: number;
|
|
1485
|
+
padLeft: number;
|
|
1486
|
+
height: number;
|
|
1487
|
+
}
|
|
1488
|
+
>();
|
|
1489
|
+
|
|
284
1490
|
function layoutBlockLevel(
|
|
285
1491
|
box: Box,
|
|
286
1492
|
ctx: LayoutContext,
|
|
@@ -288,39 +1494,114 @@ function layoutBlockLevel(
|
|
|
288
1494
|
contentLeft: number,
|
|
289
1495
|
y: number,
|
|
290
1496
|
containingWidth: number,
|
|
1497
|
+
/** What percentages are of: the containing block's width, where the
|
|
1498
|
+
* room is narrower than it beside floats */
|
|
1499
|
+
percentBase = containingWidth,
|
|
291
1500
|
): void {
|
|
292
|
-
resolveEdges(box,
|
|
1501
|
+
resolveEdges(box, percentBase);
|
|
293
1502
|
|
|
294
1503
|
if (box.kind === 'replaced') {
|
|
295
|
-
sizeReplaced(box, containingWidth);
|
|
1504
|
+
sizeReplaced(box, percentBase, containingWidth);
|
|
296
1505
|
placeBlock(box, contentLeft, y, containingWidth);
|
|
297
1506
|
return;
|
|
298
1507
|
}
|
|
299
1508
|
|
|
300
|
-
const width = blockWidth(box, containingWidth);
|
|
1509
|
+
const width = blockWidth(box, containingWidth, percentBase, ctx);
|
|
1510
|
+
if (box.kind === 'table' && kept(box, ctx, width)) {
|
|
1511
|
+
// laid out at this width earlier in the pass, and not since: put where
|
|
1512
|
+
// a layout would put it, and moved there whole
|
|
1513
|
+
const laid = box.width;
|
|
1514
|
+
const x = box.x;
|
|
1515
|
+
const top = box.y;
|
|
1516
|
+
box.width = width;
|
|
1517
|
+
placeBlock(box, contentLeft, y, containingWidth);
|
|
1518
|
+
if (laid !== width && Number.isFinite(containingWidth)) {
|
|
1519
|
+
box.width = laid;
|
|
1520
|
+
placeBlock(box, contentLeft, y, containingWidth);
|
|
1521
|
+
}
|
|
1522
|
+
box.width = laid;
|
|
1523
|
+
const to = { x: box.x, y: box.y };
|
|
1524
|
+
box.x = x;
|
|
1525
|
+
box.y = top;
|
|
1526
|
+
moveTo(box, to.x, to.y);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
301
1529
|
box.width = width;
|
|
302
1530
|
placeBlock(box, contentLeft, y, containingWidth);
|
|
303
1531
|
layoutInternals(box, ctx, width, box.x, box.y, outerFloats);
|
|
1532
|
+
if (
|
|
1533
|
+
box.kind === 'table' &&
|
|
1534
|
+
box.width !== width &&
|
|
1535
|
+
Number.isFinite(containingWidth)
|
|
1536
|
+
) {
|
|
1537
|
+
// a table shrinks to its columns once it is laid out, where it was
|
|
1538
|
+
// placed as wide as its room: placed again, its auto margins centre it
|
|
1539
|
+
// — in a room that has a width, and not in the unbounded one a
|
|
1540
|
+
// shrink-to-fit probe lays it out in
|
|
1541
|
+
const x = box.x;
|
|
1542
|
+
placeBlock(box, contentLeft, y, containingWidth);
|
|
1543
|
+
const dx = box.x - x;
|
|
1544
|
+
box.x = x;
|
|
1545
|
+
translate(box, dx, 0);
|
|
1546
|
+
}
|
|
1547
|
+
if (box.kind === 'table') keep(box, ctx, width);
|
|
304
1548
|
}
|
|
305
1549
|
|
|
306
1550
|
/**
|
|
307
|
-
* Lay a box out
|
|
308
|
-
*
|
|
1551
|
+
* Lay a block-level box out as a block in a containing block of a width,
|
|
1552
|
+
* at its own `width` or at the room its margins leave, with the offset its
|
|
1553
|
+
* margins give it; the caller moves it into place. What a table's caption
|
|
1554
|
+
* is, in the box around the table.
|
|
1555
|
+
*/
|
|
1556
|
+
export function layoutBlockIn(
|
|
1557
|
+
box: Box,
|
|
1558
|
+
ctx: LayoutContext,
|
|
1559
|
+
containingWidth: number,
|
|
1560
|
+
): void {
|
|
1561
|
+
resolveEdges(box, containingWidth);
|
|
1562
|
+
const width = blockWidth(box, containingWidth, containingWidth, ctx);
|
|
1563
|
+
box.width = width;
|
|
1564
|
+
placeBlock(box, 0, 0, containingWidth);
|
|
1565
|
+
layoutInternals(box, ctx, width, box.x, 0);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* The width a min-content probe lays a box out at: none at all. Every line
|
|
1570
|
+
* breaks at every opportunity there, and the widest is the longest word —
|
|
1571
|
+
* which is what both text engines answer at zero; react-x11's CoreText
|
|
1572
|
+
* engine answers it at zero only, and at a pixel breaks inside words, so a
|
|
1573
|
+
* probe a pixel wide gave every table cell on macOS a min-content a
|
|
1574
|
+
* character or two wide.
|
|
1575
|
+
*/
|
|
1576
|
+
export const MIN_CONTENT_PROBE = 0;
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Lay a box out at a width and report the widest thing it drew.
|
|
1580
|
+
*
|
|
309
1581
|
* This is what a table column asks twice — once unbounded for max-content,
|
|
310
1582
|
* once at a hair's width for min-content — and it exists rather than the
|
|
311
1583
|
* caller reading `box.width` because a `width: auto` box laid out unbounded
|
|
312
1584
|
* *is* unbounded: it fills its containing block, and its containing block was
|
|
313
1585
|
* `Infinity`. The answer has to come from what the content came to, which is
|
|
314
1586
|
* exactly what `intrinsicWidth` walks.
|
|
1587
|
+
*
|
|
1588
|
+
* A box with a width of its own answers with it, as its contribution to
|
|
1589
|
+
* whatever is sizing it — unless `content` asks for what the content
|
|
1590
|
+
* alone came to, which is how a table cell's is taken: its `width` is
|
|
1591
|
+
* weighed apart, and answered with the width the probe laid it out at,
|
|
1592
|
+
* a cell set to 10% measured no wider than its padding.
|
|
315
1593
|
*/
|
|
316
1594
|
export function measureIntrinsicWidth(
|
|
317
1595
|
box: Box,
|
|
318
1596
|
ctx: LayoutContext,
|
|
319
1597
|
available: number,
|
|
1598
|
+
content = false,
|
|
320
1599
|
): number {
|
|
321
1600
|
ctx.layoutSubtree(box, available);
|
|
322
1601
|
const specified = box.style.width;
|
|
323
|
-
if (specified !== AUTO && Number.isFinite(box.width))
|
|
1602
|
+
if (!content && specified !== AUTO && Number.isFinite(box.width)) {
|
|
1603
|
+
return box.width;
|
|
1604
|
+
}
|
|
324
1605
|
return intrinsicWidth(box) + box.horizontalExtra;
|
|
325
1606
|
}
|
|
326
1607
|
|
|
@@ -349,6 +1630,9 @@ function layoutSubtree(box: Box, ctx: LayoutContext, width: number): void {
|
|
|
349
1630
|
* float context, passed only when this box does *not* establish one of its
|
|
350
1631
|
* own — which is the difference between text flowing beside a float that
|
|
351
1632
|
* started in an earlier sibling and text that starts below it.
|
|
1633
|
+
*
|
|
1634
|
+
* A line-clamp container's clamp is carried through its formatting context
|
|
1635
|
+
* (`Clamp`), and a box that makes one of its own is laid out outside it.
|
|
352
1636
|
*/
|
|
353
1637
|
function layoutInternals(
|
|
354
1638
|
box: Box,
|
|
@@ -358,10 +1642,85 @@ function layoutInternals(
|
|
|
358
1642
|
y: number,
|
|
359
1643
|
outerFloats?: FloatContext,
|
|
360
1644
|
): void {
|
|
1645
|
+
const outer = ctx.clamp ?? null;
|
|
1646
|
+
const lines = box.style.lineClamp;
|
|
1647
|
+
if (lines === null && (outer === null || !establishesBFC(box))) {
|
|
1648
|
+
layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (lines === null) {
|
|
1652
|
+
ctx.clamp = null;
|
|
1653
|
+
layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
|
|
1654
|
+
} else {
|
|
1655
|
+
layoutClamped(box, ctx, lines, borderBoxWidth, x, y, outerFloats);
|
|
1656
|
+
}
|
|
1657
|
+
ctx.clamp = outer;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* A line-clamp container, laid out with its clamp. `line-clamp: auto`'s
|
|
1662
|
+
* clamp point is after the lines its height holds, where it has a height
|
|
1663
|
+
* of its own (CSS Overflow 4, 5.3.1): the content is laid out whole to
|
|
1664
|
+
* count them, and again cut after them, where they are not all of it.
|
|
1665
|
+
*/
|
|
1666
|
+
function layoutClamped(
|
|
1667
|
+
box: Box,
|
|
1668
|
+
ctx: LayoutContext,
|
|
1669
|
+
lines: number,
|
|
1670
|
+
borderBoxWidth: number,
|
|
1671
|
+
x: number,
|
|
1672
|
+
y: number,
|
|
1673
|
+
outerFloats?: FloatContext,
|
|
1674
|
+
): void {
|
|
1675
|
+
const floor = lines === Infinity ? autoFloor(box, y) : Infinity;
|
|
1676
|
+
if (floor === Infinity) {
|
|
1677
|
+
// counted against the number, or where `auto` has no height to fill,
|
|
1678
|
+
// against none: the boxes a clamp hid before show again
|
|
1679
|
+
ctx.clamp = { box, left: lines, done: false, fit: null };
|
|
1680
|
+
layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
const fit = { floor, lines: 0, over: false };
|
|
1684
|
+
ctx.clamp = { box, left: Infinity, done: false, fit };
|
|
1685
|
+
const positioned = ctx.positioned.length;
|
|
1686
|
+
layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
|
|
1687
|
+
if (!fit.over) return;
|
|
1688
|
+
ctx.positioned.length = positioned;
|
|
1689
|
+
ctx.clamp = { box, left: fit.lines, done: fit.lines === 0, fit: null };
|
|
1690
|
+
layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/** Where an `auto` clamp's lines have to end by, in document coordinates:
|
|
1694
|
+
* the content bottom of a box as tall as its `height`, or `max-height`,
|
|
1695
|
+
* lets it be — Infinity where neither is set. */
|
|
1696
|
+
function autoFloor(box: Box, y: number): number {
|
|
1697
|
+
const set = resolveOrNull(box.style.height, box.percentHeightBase);
|
|
1698
|
+
const outer =
|
|
1699
|
+
set === null
|
|
1700
|
+
? Infinity
|
|
1701
|
+
: box.style.boxSizing === 'border-box'
|
|
1702
|
+
? Math.max(set, box.verticalExtra)
|
|
1703
|
+
: set + box.verticalExtra;
|
|
1704
|
+
const height = clampHeight(box, outer);
|
|
1705
|
+
return Number.isFinite(height)
|
|
1706
|
+
? y + height - box.padBottom - box.borderBottom
|
|
1707
|
+
: Infinity;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function layoutBox(
|
|
1711
|
+
box: Box,
|
|
1712
|
+
ctx: LayoutContext,
|
|
1713
|
+
borderBoxWidth: number,
|
|
1714
|
+
x: number,
|
|
1715
|
+
y: number,
|
|
1716
|
+
outerFloats?: FloatContext,
|
|
1717
|
+
): void {
|
|
1718
|
+
box.layoutSerial = ++layoutSerial;
|
|
361
1719
|
box.x = x;
|
|
362
1720
|
box.y = y;
|
|
363
1721
|
box.width = borderBoxWidth;
|
|
364
1722
|
const contentWidth = box.contentWidth;
|
|
1723
|
+
givePercentBase(box, percentBaseInside(box));
|
|
365
1724
|
|
|
366
1725
|
if (box.kind === 'flex') {
|
|
367
1726
|
const height = layoutFlex(box, ctx, contentWidth);
|
|
@@ -371,6 +1730,7 @@ function layoutInternals(
|
|
|
371
1730
|
if (box.kind === 'table') {
|
|
372
1731
|
const height = layoutTable(box, ctx, contentWidth);
|
|
373
1732
|
finishHeight(box, height);
|
|
1733
|
+
finishCaptions(box);
|
|
374
1734
|
return;
|
|
375
1735
|
}
|
|
376
1736
|
|
|
@@ -378,24 +1738,96 @@ function layoutInternals(
|
|
|
378
1738
|
const floats = ownFloats
|
|
379
1739
|
? new FloatContext(box.contentX, box.contentX + contentWidth)
|
|
380
1740
|
: outerFloats;
|
|
1741
|
+
// the line a pass before gave a marker, which this one may not
|
|
1742
|
+
if (box.marker) box.lines = null;
|
|
1743
|
+
if (box.marker && ctx.fonts) {
|
|
1744
|
+
const room = markerRoom(box, box.marker, ctx.fonts);
|
|
1745
|
+
if (room) MARKER_ROOM.set(box, room);
|
|
1746
|
+
else MARKER_ROOM.delete(box);
|
|
1747
|
+
}
|
|
381
1748
|
const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
|
|
1749
|
+
// A list item with a marker and no line holds one, the marker's, a line
|
|
1750
|
+
// of its own face tall: an empty `<li>` is a line tall in a browser, and
|
|
1751
|
+
// an inline-block around one sits on its marker's baseline.
|
|
1752
|
+
let height = flow.height;
|
|
1753
|
+
if (box.marker && ctx.fonts && !firstLineIn(box)) {
|
|
1754
|
+
const own = strutOf(ctx.fonts, box.style);
|
|
1755
|
+
// as tall as the marker on it, as a first line with text is
|
|
1756
|
+
const room = MARKER_ROOM.get(box);
|
|
1757
|
+
const strut = room
|
|
1758
|
+
? {
|
|
1759
|
+
ascent: Math.max(own.ascent, room.ascent),
|
|
1760
|
+
descent: Math.max(own.descent, room.descent),
|
|
1761
|
+
}
|
|
1762
|
+
: own;
|
|
1763
|
+
const line = strut.ascent + strut.descent;
|
|
1764
|
+
box.lines = [
|
|
1765
|
+
{
|
|
1766
|
+
x: box.contentX,
|
|
1767
|
+
y: box.contentY,
|
|
1768
|
+
width: 0,
|
|
1769
|
+
height: line,
|
|
1770
|
+
baseline: strut.ascent,
|
|
1771
|
+
texts: [],
|
|
1772
|
+
textStart: box.subtreeTextStart,
|
|
1773
|
+
textEnd: box.subtreeTextStart,
|
|
1774
|
+
atomics: [],
|
|
1775
|
+
},
|
|
1776
|
+
];
|
|
1777
|
+
height = Math.max(height, line);
|
|
1778
|
+
}
|
|
382
1779
|
// A box that establishes a formatting context contains its own floats, so
|
|
383
1780
|
// it has to be at least as tall as they are. One that does not, does not —
|
|
384
1781
|
// that is the classic "collapsed parent" every author has met.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1782
|
+
// Past a clamp point, a line-clamp container clips its floats rather
|
|
1783
|
+
// than holding them (CSS Overflow 4, 5.3.1).
|
|
1784
|
+
const clamped = ctx.clamp?.box === box && ctx.clamp.done;
|
|
1785
|
+
const withFloats =
|
|
1786
|
+
ownFloats && !clamped
|
|
1787
|
+
? Math.max(
|
|
1788
|
+
height,
|
|
1789
|
+
floats.bottom === -Infinity ? 0 : floats.bottom - box.contentY,
|
|
1790
|
+
)
|
|
1791
|
+
: height;
|
|
391
1792
|
finishHeight(box, withFloats);
|
|
392
1793
|
// The margin that escaped through this box's bottom edge becomes part of
|
|
393
1794
|
// its own: the parent's flow loop reads `child.marginBottom` for the next
|
|
394
1795
|
// sibling's collapse, which is exactly where an escaped margin goes.
|
|
395
|
-
|
|
396
|
-
|
|
1796
|
+
// Kept as a strut too, for the next sibling's margins to collapse with
|
|
1797
|
+
// all of the ones it came to rather than with their sum.
|
|
1798
|
+
if (flow.hanging.pos || flow.hanging.neg) {
|
|
1799
|
+
const bottom = join(flow.hanging, box.marginBottom);
|
|
1800
|
+
box.bottomStrut = bottom;
|
|
1801
|
+
box.marginBottom = marginOf(bottom);
|
|
1802
|
+
} else box.bottomStrut = null;
|
|
1803
|
+
if (box.marker) layoutMarker(box, box.marker, ctx);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/**
|
|
1807
|
+
* The room a list item's marker takes about its first line's baseline,
|
|
1808
|
+
* handed to the box that line is in: an image's height above it, its
|
|
1809
|
+
* bottom on the baseline as an inline image's is, and a marker in a face
|
|
1810
|
+
* of its own — a `::marker` rule's — its face at its line height. CSS 2.1
|
|
1811
|
+
* (12.5.1) leaves where an outside marker goes to the user agent; Blink
|
|
1812
|
+
* and Gecko both set it on the item's first line and make the line as tall
|
|
1813
|
+
* as it, and design 032's bullets, images taller than its 10px text, set
|
|
1814
|
+
* every item's first line lower than ours did.
|
|
1815
|
+
*/
|
|
1816
|
+
const MARKER_ROOM = new WeakMap<Box, InlineDecoration>();
|
|
1817
|
+
|
|
1818
|
+
function markerRoom(
|
|
1819
|
+
box: Box,
|
|
1820
|
+
marker: Marker,
|
|
1821
|
+
fonts: FontsLike,
|
|
1822
|
+
): InlineDecoration | null {
|
|
1823
|
+
if (marker.image) {
|
|
1824
|
+
return marker.image.height > 0
|
|
1825
|
+
? { ascent: marker.image.height, descent: 0 }
|
|
1826
|
+
: null;
|
|
397
1827
|
}
|
|
398
|
-
|
|
1828
|
+
return marker.style && marker.style !== box.style
|
|
1829
|
+
? strutOf(fonts, marker.style)
|
|
1830
|
+
: null;
|
|
399
1831
|
}
|
|
400
1832
|
|
|
401
1833
|
/** The first line box anywhere under a box, in layout order. */
|
|
@@ -421,41 +1853,468 @@ function firstLineIn(box: Box): LineBox | null {
|
|
|
421
1853
|
* content: it must not join the selection, or copying a list would paste a
|
|
422
1854
|
* bullet before every line.
|
|
423
1855
|
*/
|
|
424
|
-
|
|
1856
|
+
const MARKER_GAPS = new WeakMap<FontsLike, Map<string, number>>();
|
|
1857
|
+
|
|
1858
|
+
/**
|
|
1859
|
+
* The gap after a marker, a space's width in its face: kept per face and
|
|
1860
|
+
* fonts, since a list is a column of markers set alike, and laying the
|
|
1861
|
+
* space out for each of them was most of what a long document's markers
|
|
1862
|
+
* cost.
|
|
1863
|
+
*/
|
|
1864
|
+
function markerGap(fonts: FontsLike, face: Omit<TextRun, 'text'>): number {
|
|
1865
|
+
let gaps = MARKER_GAPS.get(fonts);
|
|
1866
|
+
if (!gaps) MARKER_GAPS.set(fonts, (gaps = new Map()));
|
|
1867
|
+
const key = `${face.family}\u0001${face.size}\u0001${String(face.weight ?? '')}\u0001${String(face.style ?? '')}`;
|
|
1868
|
+
let gap = gaps.get(key);
|
|
1869
|
+
if (gap === undefined) {
|
|
1870
|
+
gap = fonts.layout([{ text: '\u00a0', ...face }], face, {}).width;
|
|
1871
|
+
gaps.set(key, gap);
|
|
1872
|
+
}
|
|
1873
|
+
return gap;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function layoutMarker(box: Box, marker: Marker, ctx: LayoutContext): void {
|
|
425
1877
|
const fonts = ctx.fonts;
|
|
426
1878
|
if (!fonts) return;
|
|
427
1879
|
const style = box.style;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
1880
|
+
if (marker.image) {
|
|
1881
|
+
// an image: its bottom on the first line's baseline, as an inline
|
|
1882
|
+
// image's is, and the gap a bullet has before the content
|
|
1883
|
+
const first = firstLineIn(box);
|
|
1884
|
+
marker.y = first
|
|
1885
|
+
? first.y + first.baseline - marker.image.height
|
|
1886
|
+
: box.contentY;
|
|
1887
|
+
const gap = Math.round(style.fontSize * 0.4);
|
|
1888
|
+
// at the start of the line, which is its right in a right-to-left item
|
|
1889
|
+
marker.x =
|
|
1890
|
+
style.direction === 'rtl'
|
|
1891
|
+
? box.contentX + box.contentWidth + gap
|
|
1892
|
+
: box.contentX - gap - marker.image.width;
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
// in its own style where a `::marker` rule gives it one — the colour and
|
|
1896
|
+
// the face a bullet or a number is set in — and the item's where not
|
|
1897
|
+
const set = marker.style ?? style;
|
|
1898
|
+
const face = {
|
|
1899
|
+
family: set.fontFamily,
|
|
1900
|
+
size: set.fontSize,
|
|
1901
|
+
color: set.color,
|
|
1902
|
+
...(marker.style
|
|
1903
|
+
? {
|
|
1904
|
+
weight: set.fontWeight,
|
|
1905
|
+
style:
|
|
1906
|
+
set.fontStyle === 'normal'
|
|
1907
|
+
? ('normal' as const)
|
|
1908
|
+
: ('italic' as const),
|
|
1909
|
+
}
|
|
1910
|
+
: null),
|
|
1911
|
+
};
|
|
1912
|
+
// set in a line of the item's own height, so that where the item has no
|
|
1913
|
+
// line of its own the marker stands where its first would have been
|
|
1914
|
+
// in the item's direction, which puts a number's full stop on its left
|
|
1915
|
+
// in a right-to-left list, as the bidi algorithm has it
|
|
1916
|
+
const layout = fonts.layout([{ text: marker.text, ...face }], face, {
|
|
1917
|
+
lineHeight: lineHeightMultiplier(fonts, style),
|
|
1918
|
+
direction: style.direction,
|
|
1919
|
+
});
|
|
1920
|
+
marker.layout = layout;
|
|
1921
|
+
// a space between it and the content — the one its style's suffix ends
|
|
1922
|
+
// in, or its own text's — in its own face: a line measures without it
|
|
1923
|
+
const gap =
|
|
1924
|
+
marker.flush && !/\s$/.test(marker.text) ? 0 : markerGap(fonts, face);
|
|
442
1925
|
// The marker sits on the first line of the item's *content*, which is not
|
|
443
1926
|
// always the item's own: an `<li>` holding a paragraph, or one holding text
|
|
444
1927
|
// and a nested list, has its inline content in an anonymous block. Looking
|
|
445
1928
|
// only at `box.lines` puts the marker of every such item at the content
|
|
446
|
-
// top, which reads as a missing bullet rather than a misplaced one.
|
|
1929
|
+
// top, which reads as a missing bullet rather than a misplaced one. An
|
|
1930
|
+
// empty item is still a list item, and its marker a line at its top.
|
|
447
1931
|
const first = firstLineIn(box);
|
|
448
|
-
const baselineY = first ? first.y + first.baseline : box.contentY;
|
|
449
1932
|
const own = layout.lines[0];
|
|
450
|
-
|
|
451
|
-
|
|
1933
|
+
marker.y = first
|
|
1934
|
+
? first.y + first.baseline - (own ? own.baseline : style.fontSize)
|
|
1935
|
+
: box.contentY;
|
|
1936
|
+
marker.x =
|
|
452
1937
|
style.listStylePosition === 'inside'
|
|
453
1938
|
? box.contentX
|
|
454
|
-
:
|
|
1939
|
+
: style.direction === 'rtl'
|
|
1940
|
+
? box.contentX + box.contentWidth + gap
|
|
1941
|
+
: box.contentX - gap - layout.width;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/**
|
|
1945
|
+
* The height a percentage `height` inside a box resolves against: its own
|
|
1946
|
+
* content height, where that is set rather than grown from its content —
|
|
1947
|
+
* a length, or a percentage that itself resolved (CSS 2.1 10.5). NaN, and
|
|
1948
|
+
* so `auto`, where it is not. An anonymous box is no containing block for
|
|
1949
|
+
* this (9.2.1.1) and hands on its parent's.
|
|
1950
|
+
*
|
|
1951
|
+
* The root element's is the viewport's height, so `html, body { height:
|
|
1952
|
+
* 100% }` — which mail sets as often as not — is a window tall, as in a
|
|
1953
|
+
* browser; the document is as tall as what overflows it, so a message
|
|
1954
|
+
* longer than the window is not cut off (`layoutDocument`).
|
|
1955
|
+
*/
|
|
1956
|
+
export function percentBaseInside(box: Box): number {
|
|
1957
|
+
const flexed = FLEXED_HEIGHT.get(box);
|
|
1958
|
+
if (flexed !== undefined) return flexed;
|
|
1959
|
+
if (!box.el && !box.pseudo) return box.percentHeightBase;
|
|
1960
|
+
const resolved = specifiedHeight(box);
|
|
1961
|
+
if (resolved === null) {
|
|
1962
|
+
// a height `aspect-ratio` gives from a width is as definite as the width
|
|
1963
|
+
const ratio = ratioHeight(box);
|
|
1964
|
+
if (ratio === null) return NaN;
|
|
1965
|
+
return Math.max(
|
|
1966
|
+
0,
|
|
1967
|
+
clampHeight(box, ratio + box.verticalExtra) - box.verticalExtra,
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
const set = Math.max(0, resolved);
|
|
1971
|
+
const borderBox =
|
|
1972
|
+
box.style.boxSizing === 'border-box'
|
|
1973
|
+
? Math.max(set, box.verticalExtra)
|
|
1974
|
+
: set + box.verticalExtra;
|
|
1975
|
+
return Math.max(0, clampHeight(box, borderBox) - box.verticalExtra);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
/** Whether anything in a box takes a percentage of a height: what a height
|
|
1979
|
+
* a flex or grid layout makes definite changes. Kept for the tree's life. */
|
|
1980
|
+
export function percentHeightsIn(box: Box): boolean {
|
|
1981
|
+
let found = PERCENT_HEIGHTS.get(box);
|
|
1982
|
+
if (found === undefined) {
|
|
1983
|
+
found = false;
|
|
1984
|
+
// a column's basis is a height too
|
|
1985
|
+
const column =
|
|
1986
|
+
box.kind === 'flex' &&
|
|
1987
|
+
!box.style.grid &&
|
|
1988
|
+
box.style.flexDirection.startsWith('column');
|
|
1989
|
+
for (const child of box.children) {
|
|
1990
|
+
const style = child.style;
|
|
1991
|
+
if (
|
|
1992
|
+
isPct(style.height) ||
|
|
1993
|
+
isPct(style.minHeight) ||
|
|
1994
|
+
(style.maxHeight !== 'none' && isPct(style.maxHeight)) ||
|
|
1995
|
+
(column && style.flexBasis !== 'content' && isPct(style.flexBasis)) ||
|
|
1996
|
+
percentHeightsIn(child)
|
|
1997
|
+
) {
|
|
1998
|
+
found = true;
|
|
1999
|
+
break;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
PERCENT_HEIGHTS.set(box, found);
|
|
2003
|
+
}
|
|
2004
|
+
return found;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
const PERCENT_HEIGHTS = new WeakMap<Box, boolean>();
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* A box's own height, where it has one, as `box-sizing` measures it: a
|
|
2011
|
+
* length, a percentage of a height that resolves, or `stretch`'s, and else
|
|
2012
|
+
* none, which is `auto`.
|
|
2013
|
+
*/
|
|
2014
|
+
function specifiedHeight(box: Box): number | null {
|
|
2015
|
+
const style = box.style;
|
|
2016
|
+
if (style.heightKeyword === 'stretch') {
|
|
2017
|
+
const outer = stretchHeight(box);
|
|
2018
|
+
if (outer === null) return null;
|
|
2019
|
+
return style.boxSizing === 'border-box'
|
|
2020
|
+
? outer
|
|
2021
|
+
: Math.max(0, outer - box.verticalExtra);
|
|
2022
|
+
}
|
|
2023
|
+
return resolveOrNull(style.height, box.percentHeightBase);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/** Where an absolutely positioned box's `stretch` height starts in its
|
|
2027
|
+
* containing block: its `top`, or its static position where it has
|
|
2028
|
+
* neither offset (`layoutPositioned`). */
|
|
2029
|
+
const STRETCH_TOP = new WeakMap<Box, number>();
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* A replaced box's `stretch` sizes, which `sizeReplaced` knows nothing of:
|
|
2033
|
+
* the width it was given for one across, the height its offsets leave for
|
|
2034
|
+
* one down, and through its ratio, the other where that is `auto`.
|
|
2035
|
+
*/
|
|
2036
|
+
function stretchReplaced(
|
|
2037
|
+
box: Box,
|
|
2038
|
+
across: number | null,
|
|
2039
|
+
containingWidth: number,
|
|
2040
|
+
): void {
|
|
2041
|
+
const style = box.style;
|
|
2042
|
+
const down = style.heightKeyword === 'stretch' ? stretchHeight(box) : null;
|
|
2043
|
+
if (across === null && down === null) return;
|
|
2044
|
+
if (across !== null) box.width = across;
|
|
2045
|
+
if (down !== null) box.height = clampHeight(box, down);
|
|
2046
|
+
if (across !== null && down === null && style.height === AUTO) {
|
|
2047
|
+
const tall = heightThroughRatio(box, across);
|
|
2048
|
+
if (tall !== null) box.height = clampHeight(box, tall);
|
|
2049
|
+
} else if (down !== null && across === null && style.width === AUTO) {
|
|
2050
|
+
const wide = widthThroughRatio(box, box.height);
|
|
2051
|
+
if (wide !== null) box.width = clampWidth(box, wide, containingWidth);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* The border-box height `stretch` makes of a box (CSS Sizing 3, 4.2): what
|
|
2057
|
+
* its margins leave of the height its percentages are of, where that is
|
|
2058
|
+
* definite, and else none. A margin of an in-flow block that its parent's
|
|
2059
|
+
* border or padding does not part from the parent's edge, in a parent that
|
|
2060
|
+
* is no formatting context of its own, counts as none, as the margin would
|
|
2061
|
+
* collapse with the parent's.
|
|
2062
|
+
*/
|
|
2063
|
+
function stretchHeight(box: Box): number | null {
|
|
2064
|
+
const base = box.percentHeightBase;
|
|
2065
|
+
if (!Number.isFinite(base)) return null;
|
|
2066
|
+
const style = box.style;
|
|
2067
|
+
if (style.position === 'absolute' || style.position === 'fixed') {
|
|
2068
|
+
// what its offsets leave of its containing block, an `auto` one none
|
|
2069
|
+
// — or its static position, where both are (`layoutPositioned`)
|
|
2070
|
+
const top = STRETCH_TOP.get(box) ?? resolveOrNull(style.top, base) ?? 0;
|
|
2071
|
+
const bottom = resolveOrNull(style.bottom, base) ?? 0;
|
|
2072
|
+
return Math.max(
|
|
2073
|
+
box.verticalExtra,
|
|
2074
|
+
base - top - bottom - box.marginTop - box.marginBottom,
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
const parent = box.parent;
|
|
2078
|
+
const inFlow =
|
|
2079
|
+
parent !== null &&
|
|
2080
|
+
!box.outOfFlow &&
|
|
2081
|
+
!box.isFloat &&
|
|
2082
|
+
box.kind !== 'replaced' &&
|
|
2083
|
+
!isInlineLevel(box) &&
|
|
2084
|
+
!establishesBFC(parent);
|
|
2085
|
+
const top = inFlow && !parent.borderTop && !parent.padTop ? 0 : box.marginTop;
|
|
2086
|
+
const bottom =
|
|
2087
|
+
inFlow && !parent.borderBottom && !parent.padBottom ? 0 : box.marginBottom;
|
|
2088
|
+
return Math.max(box.verticalExtra, base - top - bottom);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* The content height a flex layout made definite for an item — a stretched
|
|
2093
|
+
* item's, a flexed one's in a container of a definite height (CSS Flexbox
|
|
2094
|
+
* 9.8) — which what is in it takes its percentages of. Set by `flex.ts`
|
|
2095
|
+
* for the item's own layout alone.
|
|
2096
|
+
*/
|
|
2097
|
+
export const FLEXED_HEIGHT = new WeakMap<Box, number>();
|
|
2098
|
+
|
|
2099
|
+
/**
|
|
2100
|
+
* Hand a box's children the height their percentages resolve against —
|
|
2101
|
+
* through inline boxes, which are no containing block, to the inline-block
|
|
2102
|
+
* or image in them. Nearly every box has none to give, and its children
|
|
2103
|
+
* hold NaN until it has, so it visits them only when there is a height, or
|
|
2104
|
+
* was one on the last pass: visiting every child of every block on every
|
|
2105
|
+
* pass cost more than laying out a table.
|
|
2106
|
+
*/
|
|
2107
|
+
function givePercentBase(box: Box, base: number): void {
|
|
2108
|
+
const given = !Number.isNaN(base);
|
|
2109
|
+
if (!given && !box.gavePercentBase) return;
|
|
2110
|
+
box.gavePercentBase = given;
|
|
2111
|
+
handPercentBase(box, base);
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
function handPercentBase(box: Box, base: number): void {
|
|
2115
|
+
for (const child of box.children) {
|
|
2116
|
+
child.percentHeightBase = base;
|
|
2117
|
+
if (child.kind === 'inline') handPercentBase(child, base);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* The content height `aspect-ratio` gives a box whose height is `auto`, from
|
|
2123
|
+
* its width (CSS Sizing 4, 5.1): the ratio is of the box its `box-sizing`
|
|
2124
|
+
* names, which is the border box in everything Tailwind writes. Null where
|
|
2125
|
+
* it has none, or a height of its own; a replaced box is sized apart.
|
|
2126
|
+
*/
|
|
2127
|
+
export function ratioHeight(box: Box): number | null {
|
|
2128
|
+
const ratio = boxRatio(box);
|
|
2129
|
+
if (!ratio) return null;
|
|
2130
|
+
if (resolveOrNull(box.style.height, box.percentHeightBase) !== null) {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
return ratio.border
|
|
2134
|
+
? Math.max(0, box.width / ratio.ratio - box.verticalExtra)
|
|
2135
|
+
: Math.max(0, box.contentWidth / ratio.ratio);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/**
|
|
2139
|
+
* A box's `aspect-ratio` where it is not replaced, and whether it is of its
|
|
2140
|
+
* border box — a ratio written alone under `box-sizing: border-box` — or
|
|
2141
|
+
* of its content box, as one written `auto` with it is whatever the
|
|
2142
|
+
* `box-sizing` (CSS Sizing 4, 5.1).
|
|
2143
|
+
*/
|
|
2144
|
+
export function boxRatio(box: Box): { ratio: number; border: boolean } | null {
|
|
2145
|
+
const aspect = box.style.aspectRatio;
|
|
2146
|
+
if (!aspect || box.kind === 'replaced' || !(aspect.ratio > 0)) return null;
|
|
2147
|
+
return {
|
|
2148
|
+
ratio: aspect.ratio,
|
|
2149
|
+
border: box.style.boxSizing === 'border-box' && !aspect.auto,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** A border-box height through a box's ratio into its border-box width,
|
|
2154
|
+
* or null where the box has no ratio of its own. */
|
|
2155
|
+
export function widthFromHeight(box: Box, height: number): number | null {
|
|
2156
|
+
const ratio = boxRatio(box);
|
|
2157
|
+
return ratio ? acrossRatio(box, ratio, height) : null;
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/** A replaced element's ratio: its `aspect-ratio`, unless that says `auto`
|
|
2161
|
+
* and it has one of its own, as `sizeReplaced` takes it. */
|
|
2162
|
+
export function replacedRatio(box: Box): number {
|
|
2163
|
+
const own = box.intrinsic;
|
|
2164
|
+
const aspect = box.style.aspectRatio;
|
|
2165
|
+
const natural = own ? own.ratio : 0;
|
|
2166
|
+
return aspect && !(aspect.auto && natural > 0) ? aspect.ratio : natural;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/** A border-box height through any box's ratio into its border-box width
|
|
2170
|
+
* — a replaced element's is of its content box — or null where it has
|
|
2171
|
+
* none. */
|
|
2172
|
+
export function widthThroughRatio(box: Box, height: number): number | null {
|
|
2173
|
+
if (box.kind !== 'replaced') return widthFromHeight(box, height);
|
|
2174
|
+
const ratio = replacedRatio(box);
|
|
2175
|
+
return ratio > 0
|
|
2176
|
+
? Math.max(0, height - box.verticalExtra) * ratio + box.horizontalExtra
|
|
2177
|
+
: null;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/** A border-box width through any box's ratio into its border-box height,
|
|
2181
|
+
* or null where it has none. */
|
|
2182
|
+
export function heightThroughRatio(box: Box, width: number): number | null {
|
|
2183
|
+
if (box.kind !== 'replaced') return heightFromWidth(box, width);
|
|
2184
|
+
const ratio = replacedRatio(box);
|
|
2185
|
+
return ratio > 0
|
|
2186
|
+
? Math.max(0, width - box.horizontalExtra) / ratio + box.verticalExtra
|
|
2187
|
+
: null;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/** A border-box width through a box's ratio into its border-box height,
|
|
2191
|
+
* or null where the box has no ratio of its own. */
|
|
2192
|
+
export function heightFromWidth(box: Box, width: number): number | null {
|
|
2193
|
+
const ratio = boxRatio(box);
|
|
2194
|
+
if (!ratio) return null;
|
|
2195
|
+
return ratio.border
|
|
2196
|
+
? width / ratio.ratio
|
|
2197
|
+
: Math.max(0, width - box.horizontalExtra) / ratio.ratio +
|
|
2198
|
+
box.verticalExtra;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
/** A border-box height through a box's ratio into its border-box width. */
|
|
2202
|
+
function acrossRatio(
|
|
2203
|
+
box: Box,
|
|
2204
|
+
ratio: { ratio: number; border: boolean },
|
|
2205
|
+
height: number,
|
|
2206
|
+
): number {
|
|
2207
|
+
return ratio.border
|
|
2208
|
+
? height * ratio.ratio
|
|
2209
|
+
: Math.max(0, height - box.verticalExtra) * ratio.ratio +
|
|
2210
|
+
box.horizontalExtra;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
/**
|
|
2214
|
+
* The border-box width `aspect-ratio` gives a box whose width is `auto`
|
|
2215
|
+
* from a height of its own, within its least and greatest heights (CSS
|
|
2216
|
+
* Sizing 4, 5.1): a block is that wide, not as wide as its room — and so
|
|
2217
|
+
* is one whose width is its content's, `min-content` or `fit-content`, as
|
|
2218
|
+
* its content is that height through its ratio (5.3). Null where it has
|
|
2219
|
+
* no ratio, no height or a length for a width.
|
|
2220
|
+
*/
|
|
2221
|
+
export function ratioWidth(box: Box): number | null {
|
|
2222
|
+
const ratio = boxRatio(box);
|
|
2223
|
+
const style = box.style;
|
|
2224
|
+
if (!ratio || style.width !== AUTO) return null;
|
|
2225
|
+
const set = resolveOrNull(style.height, box.percentHeightBase);
|
|
2226
|
+
if (set === null) return null;
|
|
2227
|
+
const outer = clampHeight(
|
|
2228
|
+
box,
|
|
2229
|
+
style.boxSizing === 'border-box'
|
|
2230
|
+
? Math.max(set, box.verticalExtra)
|
|
2231
|
+
: set + box.verticalExtra,
|
|
2232
|
+
);
|
|
2233
|
+
return acrossRatio(box, ratio, outer);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* A width its ratio gave a box, no narrower than its content at its
|
|
2238
|
+
* narrowest where its least width is `auto` and it shows what overflows
|
|
2239
|
+
* it — the automatic minimum of a box with a ratio (5.2) — though no
|
|
2240
|
+
* wider for that than its greatest width.
|
|
2241
|
+
*/
|
|
2242
|
+
function ratioMinimum(
|
|
2243
|
+
box: Box,
|
|
2244
|
+
width: number,
|
|
2245
|
+
containingWidth: number,
|
|
2246
|
+
ctx?: LayoutContext,
|
|
2247
|
+
): number {
|
|
2248
|
+
const style = box.style;
|
|
2249
|
+
if (
|
|
2250
|
+
!ctx ||
|
|
2251
|
+
style.minWidth !== AUTO ||
|
|
2252
|
+
style.minWidthKeyword ||
|
|
2253
|
+
scrolls(style)
|
|
2254
|
+
) {
|
|
2255
|
+
return width;
|
|
2256
|
+
}
|
|
2257
|
+
if (box.intrinsicMinContent < 0) {
|
|
2258
|
+
const saved = box.lines;
|
|
2259
|
+
box.intrinsicMinContent = measureIntrinsicWidth(
|
|
2260
|
+
box,
|
|
2261
|
+
ctx,
|
|
2262
|
+
MIN_CONTENT_PROBE,
|
|
2263
|
+
);
|
|
2264
|
+
box.lines = saved;
|
|
2265
|
+
resolveEdges(box, containingWidth);
|
|
2266
|
+
}
|
|
2267
|
+
const max =
|
|
2268
|
+
style.maxWidth === 'none'
|
|
2269
|
+
? Infinity
|
|
2270
|
+
: (resolveOrNull(style.maxWidth, containingWidth) ?? Infinity) +
|
|
2271
|
+
(style.boxSizing === 'border-box' ? 0 : box.horizontalExtra);
|
|
2272
|
+
return Math.max(width, Math.min(box.intrinsicMinContent, max));
|
|
455
2273
|
}
|
|
456
2274
|
|
|
2275
|
+
/** A table cell's height as its content came to, border box, apart from a
|
|
2276
|
+
* height it sets: what `vertical-align` moves in the cell (`table.ts`). */
|
|
2277
|
+
export const CELL_CONTENT = new WeakMap<Box, number>();
|
|
2278
|
+
|
|
457
2279
|
function finishHeight(box: Box, contentHeight: number): void {
|
|
458
|
-
|
|
2280
|
+
// as though it held nothing, under size containment
|
|
2281
|
+
if (contained(box, CONTAIN_SIZE)) {
|
|
2282
|
+
contentHeight = box.style.containIntrinsicHeight ?? 0;
|
|
2283
|
+
}
|
|
2284
|
+
if (box.kind === 'table-cell') {
|
|
2285
|
+
CELL_CONTENT.set(box, contentHeight + box.verticalExtra);
|
|
2286
|
+
}
|
|
2287
|
+
const set = specifiedHeight(box);
|
|
2288
|
+
// at least zero: a `calc()` may come to less
|
|
2289
|
+
const specified = set === null ? null : Math.max(0, set);
|
|
2290
|
+
if (specified !== null && box.kind === 'table') {
|
|
2291
|
+
// A table's height is a least one, which `layoutTable` has shared out
|
|
2292
|
+
// to its rows: one set shorter than they are leaves them their height
|
|
2293
|
+
// (CSS 2.1 17.5.3). Taken as the table's, it ended its background
|
|
2294
|
+
// over its rows, and a float after it went up beside them.
|
|
2295
|
+
const outer =
|
|
2296
|
+
box.style.boxSizing === 'border-box'
|
|
2297
|
+
? specified
|
|
2298
|
+
: specified + box.verticalExtra;
|
|
2299
|
+
box.height = Math.max(
|
|
2300
|
+
clampHeight(box, outer),
|
|
2301
|
+
contentHeight + box.verticalExtra,
|
|
2302
|
+
);
|
|
2303
|
+
return;
|
|
2304
|
+
}
|
|
2305
|
+
if (specified === null && box.style.aspectRatio) {
|
|
2306
|
+
const ratio = ratioHeight(box);
|
|
2307
|
+
if (ratio !== null) {
|
|
2308
|
+
// the ratio's height, grown to what the box holds unless it clips it
|
|
2309
|
+
// or has a least height of its own (5.2: the automatic minimum of a
|
|
2310
|
+
// box with a ratio is its content, and `min-height: 0` is none)
|
|
2311
|
+
const automatic =
|
|
2312
|
+
box.style.minHeight === AUTO &&
|
|
2313
|
+
box.style.minHeightKeyword === null &&
|
|
2314
|
+
!scrolls(box.style);
|
|
2315
|
+
contentHeight = automatic ? Math.max(ratio, contentHeight) : ratio;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
459
2318
|
const height = specified ?? contentHeight;
|
|
460
2319
|
const borderBox =
|
|
461
2320
|
box.style.boxSizing === 'border-box' && specified !== null
|
|
@@ -464,13 +2323,26 @@ function finishHeight(box: Box, contentHeight: number): void {
|
|
|
464
2323
|
box.height = clampHeight(box, borderBox);
|
|
465
2324
|
}
|
|
466
2325
|
|
|
467
|
-
function clampHeight(box: Box, height: number): number {
|
|
2326
|
+
export function clampHeight(box: Box, height: number): number {
|
|
468
2327
|
let out = height;
|
|
469
|
-
const
|
|
2328
|
+
const base = box.percentHeightBase;
|
|
2329
|
+
// a percentage of a height nothing sets is zero for a minimum (CSS 2.1
|
|
2330
|
+
// 10.7), which leaves a `calc()` its pixels; so is `stretch`, which is a
|
|
2331
|
+
// border-box height where it is one
|
|
2332
|
+
if (box.style.maxHeightKeyword === 'stretch') {
|
|
2333
|
+
out = Math.min(out, stretchHeight(box) ?? Infinity);
|
|
2334
|
+
}
|
|
2335
|
+
if (box.style.minHeightKeyword === 'stretch') {
|
|
2336
|
+
out = Math.max(out, stretchHeight(box) ?? 0);
|
|
2337
|
+
}
|
|
2338
|
+
const min =
|
|
2339
|
+
box.style.minHeight === AUTO
|
|
2340
|
+
? 0
|
|
2341
|
+
: resolve(box.style.minHeight, Number.isFinite(base) ? base : 0);
|
|
470
2342
|
const max =
|
|
471
2343
|
box.style.maxHeight === 'none'
|
|
472
2344
|
? null
|
|
473
|
-
: resolveOrNull(box.style.maxHeight,
|
|
2345
|
+
: resolveOrNull(box.style.maxHeight, base);
|
|
474
2346
|
if (max !== null)
|
|
475
2347
|
out = Math.min(
|
|
476
2348
|
out,
|
|
@@ -495,31 +2367,147 @@ function placeBlock(
|
|
|
495
2367
|
const leftAuto = style.marginLeft === AUTO;
|
|
496
2368
|
const rightAuto = style.marginRight === AUTO;
|
|
497
2369
|
const slack = containingWidth - box.width - box.marginLeft - box.marginRight;
|
|
2370
|
+
// what the width and the margins do not add up to goes to the margin at
|
|
2371
|
+
// the end: the right one, or in a right-to-left containing block the
|
|
2372
|
+
// left one, and a box too wide overflows there (CSS 2.1 10.3.3)
|
|
2373
|
+
const rtl = box.parent?.style.direction === 'rtl';
|
|
2374
|
+
// an alignment around it places a box its margins do not (HTML's
|
|
2375
|
+
// `<center>` and `align`)
|
|
2376
|
+
const aligned = leftAuto || rightAuto ? null : box.parent?.style.alignBlocks;
|
|
498
2377
|
let left = contentLeft + box.marginLeft;
|
|
2378
|
+
// In the unbounded room a shrink-to-fit probe lays a box out in, there is
|
|
2379
|
+
// no slack to share: half of an infinite one put an auto-margined box at
|
|
2380
|
+
// x = Infinity, and the pass after moved it from there by a finite amount
|
|
2381
|
+
// — NaN, which went up every ink bound above it and left a whole article
|
|
2382
|
+
// unpainted.
|
|
2383
|
+
if (!Number.isFinite(slack)) {
|
|
2384
|
+
box.x = left;
|
|
2385
|
+
box.y = y;
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
499
2388
|
if (slack > 0) {
|
|
500
2389
|
if (leftAuto && rightAuto) left = contentLeft + slack / 2 + box.marginLeft;
|
|
501
|
-
else if (
|
|
2390
|
+
else if (aligned) {
|
|
2391
|
+
if (aligned === 'center') left += slack / 2;
|
|
2392
|
+
else if (aligned === 'right') left += slack;
|
|
2393
|
+
} else if (leftAuto || (rtl && !rightAuto)) {
|
|
2394
|
+
left = contentLeft + slack + box.marginLeft;
|
|
2395
|
+
}
|
|
2396
|
+
} else if (rtl) {
|
|
2397
|
+
left = contentLeft + slack + box.marginLeft;
|
|
502
2398
|
}
|
|
503
2399
|
box.x = left;
|
|
504
2400
|
box.y = y;
|
|
505
2401
|
}
|
|
506
2402
|
|
|
507
|
-
/** The border-box width of an in-flow block-level box
|
|
508
|
-
|
|
2403
|
+
/** The border-box width of an in-flow block-level box: the room its
|
|
2404
|
+
* margins leave, a length, or an intrinsic size where `ctx` is there to
|
|
2405
|
+
* measure it with. */
|
|
2406
|
+
function blockWidth(
|
|
2407
|
+
box: Box,
|
|
2408
|
+
containingWidth: number,
|
|
2409
|
+
percentBase = containingWidth,
|
|
2410
|
+
ctx?: LayoutContext,
|
|
2411
|
+
): number {
|
|
509
2412
|
const style = box.style;
|
|
510
2413
|
const available = containingWidth - box.marginLeft - box.marginRight;
|
|
511
2414
|
if (style.width === AUTO) {
|
|
512
|
-
|
|
2415
|
+
// a width its height gives it through its ratio
|
|
2416
|
+
const fromRatio = ratioWidth(box);
|
|
2417
|
+
if (fromRatio !== null) {
|
|
2418
|
+
const clamped = clampWidth(box, fromRatio, percentBase, ctx);
|
|
2419
|
+
return ratioMinimum(box, clamped, percentBase, ctx);
|
|
2420
|
+
}
|
|
2421
|
+
const room = Math.max(0, available);
|
|
2422
|
+
const width =
|
|
2423
|
+
style.widthKeyword && ctx
|
|
2424
|
+
? contentSizedWidth(box, ctx, style.widthKeyword, room, percentBase)
|
|
2425
|
+
: room;
|
|
2426
|
+
return clampWidth(
|
|
2427
|
+
box,
|
|
2428
|
+
transferredWidth(box, width),
|
|
2429
|
+
percentBase,
|
|
2430
|
+
ctx,
|
|
2431
|
+
containingWidth,
|
|
2432
|
+
);
|
|
513
2433
|
}
|
|
514
|
-
|
|
2434
|
+
// at least zero: a `calc()` may come to less
|
|
2435
|
+
const specified = Math.max(0, resolve(style.width, percentBase, 0));
|
|
515
2436
|
const borderBox =
|
|
516
2437
|
style.boxSizing === 'border-box'
|
|
517
2438
|
? Math.max(specified, box.horizontalExtra)
|
|
518
2439
|
: specified + box.horizontalExtra;
|
|
519
|
-
return clampWidth(box, borderBox, containingWidth);
|
|
2440
|
+
return clampWidth(box, borderBox, percentBase, ctx, containingWidth);
|
|
520
2441
|
}
|
|
521
2442
|
|
|
522
|
-
|
|
2443
|
+
/**
|
|
2444
|
+
* A width a box with a ratio and no height of its own works out for itself
|
|
2445
|
+
* within the least and greatest widths its least and greatest heights make
|
|
2446
|
+
* through its ratio (CSS Sizing 4, 5.2) — those of its own winning where
|
|
2447
|
+
* the two disagree, as `clampWidth` after this makes them. A block the
|
|
2448
|
+
* room would stretch is no wider than its greatest height lets it be. A
|
|
2449
|
+
* width a flex box or a grid gives an item is not, which is theirs.
|
|
2450
|
+
*/
|
|
2451
|
+
export function transferredWidth(box: Box, width: number): number {
|
|
2452
|
+
const style = box.style;
|
|
2453
|
+
const ratio = boxRatio(box);
|
|
2454
|
+
if (!ratio || resolveOrNull(style.height, box.percentHeightBase) !== null) {
|
|
2455
|
+
return width;
|
|
2456
|
+
}
|
|
2457
|
+
const heightOf = (len: Len) => {
|
|
2458
|
+
const px = resolveOrNull(len, box.percentHeightBase);
|
|
2459
|
+
if (px === null || !Number.isFinite(px)) return null;
|
|
2460
|
+
return style.boxSizing === 'border-box'
|
|
2461
|
+
? Math.max(px, box.verticalExtra)
|
|
2462
|
+
: px + box.verticalExtra;
|
|
2463
|
+
};
|
|
2464
|
+
const maxH = style.maxHeight === 'none' ? null : heightOf(style.maxHeight);
|
|
2465
|
+
const minH = style.minHeight === AUTO ? null : heightOf(style.minHeight);
|
|
2466
|
+
let out = width;
|
|
2467
|
+
if (maxH !== null) out = Math.min(out, acrossRatio(box, ratio, maxH));
|
|
2468
|
+
if (minH !== null) out = Math.max(out, acrossRatio(box, ratio, minH));
|
|
2469
|
+
return out;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* A border-box height within what a box's ratio makes of its least and
|
|
2474
|
+
* greatest widths: how a flex item's content size suggestion down a
|
|
2475
|
+
* column is held (CSS Flexbox 4.5). Unchanged where it has no ratio.
|
|
2476
|
+
*/
|
|
2477
|
+
export function transferredHeight(
|
|
2478
|
+
box: Box,
|
|
2479
|
+
height: number,
|
|
2480
|
+
containingWidth: number,
|
|
2481
|
+
): number {
|
|
2482
|
+
const ratio = boxRatio(box);
|
|
2483
|
+
if (!ratio) return height;
|
|
2484
|
+
const style = box.style;
|
|
2485
|
+
const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
|
|
2486
|
+
const down = (width: number) => heightFromWidth(box, width + extra)!;
|
|
2487
|
+
let out = height;
|
|
2488
|
+
if (style.maxWidth !== 'none') {
|
|
2489
|
+
const most = resolveOrNull(style.maxWidth, containingWidth);
|
|
2490
|
+
if (most !== null) out = Math.min(out, down(most));
|
|
2491
|
+
}
|
|
2492
|
+
const least =
|
|
2493
|
+
style.minWidth === AUTO
|
|
2494
|
+
? null
|
|
2495
|
+
: resolveOrNull(style.minWidth, containingWidth);
|
|
2496
|
+
if (least !== null && least > 0) out = Math.max(out, down(least));
|
|
2497
|
+
return out;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/** A width within `min-width` and `max-width` — the minimum winning — the
|
|
2501
|
+
* intrinsic ones among them where `ctx` is there to measure them. */
|
|
2502
|
+
export function clampWidth(
|
|
2503
|
+
box: Box,
|
|
2504
|
+
width: number,
|
|
2505
|
+
containingWidth: number,
|
|
2506
|
+
ctx?: LayoutContext,
|
|
2507
|
+
/** The room a `stretch` limit fills, where it is less than the
|
|
2508
|
+
* containing block beside floats. */
|
|
2509
|
+
room = containingWidth,
|
|
2510
|
+
): number {
|
|
523
2511
|
const style = box.style;
|
|
524
2512
|
const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
|
|
525
2513
|
let out = width;
|
|
@@ -528,11 +2516,108 @@ function clampWidth(box: Box, width: number, containingWidth: number): number {
|
|
|
528
2516
|
? null
|
|
529
2517
|
: resolveOrNull(style.maxWidth, containingWidth);
|
|
530
2518
|
if (max !== null) out = Math.min(out, max + extra);
|
|
531
|
-
|
|
2519
|
+
if (style.maxWidthKeyword && ctx) {
|
|
2520
|
+
out = Math.min(
|
|
2521
|
+
out,
|
|
2522
|
+
keywordWidth(box, ctx, style.maxWidthKeyword, containingWidth, room),
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
// `auto` is 0 but for a flex item, which `flex.ts` answers
|
|
2526
|
+
const min =
|
|
2527
|
+
style.minWidth === AUTO
|
|
2528
|
+
? 0
|
|
2529
|
+
: resolveOrNull(style.minWidth, containingWidth);
|
|
532
2530
|
if (min !== null) out = Math.max(out, min + extra);
|
|
2531
|
+
if (style.minWidthKeyword && ctx) {
|
|
2532
|
+
out = Math.max(
|
|
2533
|
+
out,
|
|
2534
|
+
keywordWidth(box, ctx, style.minWidthKeyword, containingWidth, room),
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
533
2537
|
return Math.max(0, out);
|
|
534
2538
|
}
|
|
535
2539
|
|
|
2540
|
+
/** An intrinsic `min-width` or `max-width` in the room a box's margins
|
|
2541
|
+
* leave it. Apart from `clampWidth`, which every block is sized through,
|
|
2542
|
+
* so that it makes nothing for the ones that have none. */
|
|
2543
|
+
function keywordWidth(
|
|
2544
|
+
box: Box,
|
|
2545
|
+
ctx: LayoutContext,
|
|
2546
|
+
keyword: ContentSize,
|
|
2547
|
+
containingWidth: number,
|
|
2548
|
+
stretchRoom = containingWidth,
|
|
2549
|
+
): number {
|
|
2550
|
+
const of = keyword === 'stretch' ? stretchRoom : containingWidth;
|
|
2551
|
+
const room = Math.max(0, of - box.marginLeft - box.marginRight);
|
|
2552
|
+
return contentSizedWidth(box, ctx, keyword, room, containingWidth);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/**
|
|
2556
|
+
* A box's border-box width from an intrinsic size (CSS Sizing 3, 3.1): its
|
|
2557
|
+
* max-content width, its min-content width, or `fit-content` — the
|
|
2558
|
+
* max-content width where `available` holds it, and `available` where it
|
|
2559
|
+
* does not, but never less than the min-content width. Each is measured
|
|
2560
|
+
* once a box, as a table cell's are, and by laying the box out on its own,
|
|
2561
|
+
* which resolves its edges against nothing: they are resolved again
|
|
2562
|
+
* against `percentBase`, as its caller had them.
|
|
2563
|
+
*/
|
|
2564
|
+
export function contentSizedWidth(
|
|
2565
|
+
box: Box,
|
|
2566
|
+
ctx: LayoutContext,
|
|
2567
|
+
size: ContentSize,
|
|
2568
|
+
available: number,
|
|
2569
|
+
percentBase: number,
|
|
2570
|
+
): number {
|
|
2571
|
+
// `stretch` is the room, whatever the content (CSS Sizing 4)
|
|
2572
|
+
if (size === 'stretch') return Math.max(0, available);
|
|
2573
|
+
// a height of its own through a ratio is its content's width at any size
|
|
2574
|
+
const fromRatio = ratioWidth(box);
|
|
2575
|
+
if (fromRatio !== null) return fromRatio;
|
|
2576
|
+
// the content's sizes, whatever width the box has of its own: a
|
|
2577
|
+
// `min-width: max-content` beside a `width` is its content's widest,
|
|
2578
|
+
// and a probe of the box at no width answered the probe's width
|
|
2579
|
+
let probed = false;
|
|
2580
|
+
if (box.intrinsicMaxContent < 0) {
|
|
2581
|
+
box.intrinsicMaxContent = measureIntrinsicWidth(box, ctx, Infinity, true);
|
|
2582
|
+
probed = true;
|
|
2583
|
+
}
|
|
2584
|
+
if (size !== 'max-content' && box.intrinsicMinContent < 0) {
|
|
2585
|
+
box.intrinsicMinContent = measureIntrinsicWidth(
|
|
2586
|
+
box,
|
|
2587
|
+
ctx,
|
|
2588
|
+
MIN_CONTENT_PROBE,
|
|
2589
|
+
true,
|
|
2590
|
+
);
|
|
2591
|
+
probed = true;
|
|
2592
|
+
}
|
|
2593
|
+
if (probed) resolveEdges(box, percentBase);
|
|
2594
|
+
const max = box.intrinsicMaxContent;
|
|
2595
|
+
if (size === 'max-content') return max;
|
|
2596
|
+
const min = box.intrinsicMinContent;
|
|
2597
|
+
if (size === 'min-content') return min;
|
|
2598
|
+
return Math.min(
|
|
2599
|
+
max,
|
|
2600
|
+
Math.max(min, fitRoom(box, size, available, percentBase)),
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
/** The room `fit-content` fits a box's content in: what its margins leave,
|
|
2605
|
+
* or `fit-content()`'s argument as a width of its own, of `percentBase`
|
|
2606
|
+
* where it is a percentage — an indefinite one leaving the content its
|
|
2607
|
+
* widest. */
|
|
2608
|
+
function fitRoom(
|
|
2609
|
+
box: Box,
|
|
2610
|
+
size: ContentSize,
|
|
2611
|
+
available: number,
|
|
2612
|
+
percentBase: number,
|
|
2613
|
+
): number {
|
|
2614
|
+
if (typeof size !== 'object') return available;
|
|
2615
|
+
const fit = Math.max(0, resolve(size.fit, percentBase, 0));
|
|
2616
|
+
return box.style.boxSizing === 'border-box'
|
|
2617
|
+
? Math.max(fit, box.horizontalExtra)
|
|
2618
|
+
: fit + box.horizontalExtra;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
536
2621
|
/**
|
|
537
2622
|
* Shrink-to-fit: the width a float, an inline-block or a positioned box with
|
|
538
2623
|
* `width: auto` takes.
|
|
@@ -549,27 +2634,228 @@ function shrinkToFitWidth(
|
|
|
549
2634
|
box: Box,
|
|
550
2635
|
ctx: LayoutContext,
|
|
551
2636
|
available: number,
|
|
2637
|
+
/** How far into the containing block the box's room starts: an
|
|
2638
|
+
* absolute box's offset, or its static position. */
|
|
2639
|
+
offset = 0,
|
|
552
2640
|
): number {
|
|
553
2641
|
const style = box.style;
|
|
554
2642
|
if (style.width !== AUTO) {
|
|
555
|
-
const specified = resolve(style.width, available, 0);
|
|
2643
|
+
const specified = Math.max(0, resolve(style.width, available, 0));
|
|
556
2644
|
const borderBox =
|
|
557
2645
|
style.boxSizing === 'border-box'
|
|
558
2646
|
? Math.max(specified, box.horizontalExtra)
|
|
559
2647
|
: specified + box.horizontalExtra;
|
|
560
|
-
return clampWidth(box, borderBox, available);
|
|
2648
|
+
return clampWidth(box, borderBox, available, ctx);
|
|
2649
|
+
}
|
|
2650
|
+
// `stretch` fills the room its margins leave, as a block's `auto` does
|
|
2651
|
+
if (style.widthKeyword === 'stretch') {
|
|
2652
|
+
const room = available - offset - box.marginLeft - box.marginRight;
|
|
2653
|
+
return clampWidth(box, Math.max(0, room), available, ctx);
|
|
2654
|
+
}
|
|
2655
|
+
// a width its height gives it through its ratio, as a block's
|
|
2656
|
+
const fromRatio = ratioWidth(box);
|
|
2657
|
+
if (fromRatio !== null) {
|
|
2658
|
+
const clamped = clampWidth(box, fromRatio, available, ctx);
|
|
2659
|
+
return ratioMinimum(box, clamped, available, ctx);
|
|
2660
|
+
}
|
|
2661
|
+
// `fit-content` is what shrink-to-fit is; the other two are not bounded
|
|
2662
|
+
// by the room, or not by the longest line, and `fit-content()` by a room
|
|
2663
|
+
// of its own
|
|
2664
|
+
const keyword = style.widthKeyword;
|
|
2665
|
+
if (keyword !== null && keyword !== 'fit-content') {
|
|
2666
|
+
const width = contentSizedWidth(box, ctx, keyword, available, available);
|
|
2667
|
+
return clampWidth(box, transferredWidth(box, width), available, ctx);
|
|
561
2668
|
}
|
|
562
2669
|
const probe = new FloatContext(0, Infinity);
|
|
563
2670
|
const saved = box.lines;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
2671
|
+
// Its content's width with no limit on it is the same at every width, so
|
|
2672
|
+
// it is probed once in the box's life, as a cell's is. Probed every time
|
|
2673
|
+
// it was asked, a float in a float was probed as often as there were
|
|
2674
|
+
// floats around it, and each probe laid out all of the floats inside.
|
|
2675
|
+
let content = PREFERRED.get(box);
|
|
2676
|
+
let probed = false;
|
|
2677
|
+
if (content === undefined) {
|
|
2678
|
+
layoutInternals(box, ctx, Infinity, 0, 0, probe);
|
|
2679
|
+
content = intrinsicWidth(box);
|
|
2680
|
+
PREFERRED.set(box, content);
|
|
2681
|
+
probed = true;
|
|
2682
|
+
}
|
|
2683
|
+
const preferred = content + box.horizontalExtra;
|
|
2684
|
+
// the room is the containing block's, less the box's margins and where
|
|
2685
|
+
// it starts (CSS 2.1 10.3.5, 10.3.7, 10.3.9): a float with side margins
|
|
2686
|
+
// was as wide as its containing block, and stood out of it by them
|
|
2687
|
+
const room = Math.max(
|
|
2688
|
+
0,
|
|
2689
|
+
available - offset - box.marginLeft - box.marginRight,
|
|
571
2690
|
);
|
|
572
|
-
|
|
2691
|
+
let width = Math.max(preferred, 0);
|
|
2692
|
+
if (
|
|
2693
|
+
width > room &&
|
|
2694
|
+
box.intrinsicMinContent < 0 &&
|
|
2695
|
+
ctx.fonts !== null &&
|
|
2696
|
+
!probed
|
|
2697
|
+
) {
|
|
2698
|
+
// the words the floor is read from are the probe's
|
|
2699
|
+
layoutInternals(box, ctx, Infinity, 0, 0, probe);
|
|
2700
|
+
}
|
|
2701
|
+
// and where its content does not fit the room, its longest word is the
|
|
2702
|
+
// least it comes to: `min(max(min-content, room), max-content)`. That
|
|
2703
|
+
// matters only for a word wider than the room, which the widest word
|
|
2704
|
+
// between spaces bounds, from the probe just laid out; only past the
|
|
2705
|
+
// room is the min-content width measured — a second probe, at a width
|
|
2706
|
+
// every line breaks at — and it is kept on the box, as a cell's is
|
|
2707
|
+
const fonts = ctx.fonts;
|
|
2708
|
+
const floored =
|
|
2709
|
+
width > room &&
|
|
2710
|
+
box.intrinsicMinContent < 0 &&
|
|
2711
|
+
fonts !== null &&
|
|
2712
|
+
wordBound(box, fonts, false) + box.horizontalExtra > room &&
|
|
2713
|
+
wordBound(box, fonts, true) + box.horizontalExtra > room;
|
|
2714
|
+
box.lines = saved;
|
|
2715
|
+
if (floored) {
|
|
2716
|
+
box.intrinsicMinContent = measureIntrinsicWidth(
|
|
2717
|
+
box,
|
|
2718
|
+
ctx,
|
|
2719
|
+
MIN_CONTENT_PROBE,
|
|
2720
|
+
);
|
|
2721
|
+
box.lines = saved;
|
|
2722
|
+
// the probe resolved the box's edges against the width it was given
|
|
2723
|
+
resolveEdges(box, available);
|
|
2724
|
+
}
|
|
2725
|
+
if (width > room)
|
|
2726
|
+
width = Math.min(width, Math.max(box.intrinsicMinContent, room));
|
|
2727
|
+
return clampWidth(box, transferredWidth(box, width), available, ctx);
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/** A shrink-to-fit box's content width at no width limit, the once
|
|
2731
|
+
* (`shrinkToFitWidth`); a box's lifetime is the cache's, as for
|
|
2732
|
+
* `Box.intrinsicMaxContent`. */
|
|
2733
|
+
const PREFERRED = new WeakMap<Box, number>();
|
|
2734
|
+
|
|
2735
|
+
/**
|
|
2736
|
+
* A box's min-content width as its words give it, laid out at no width
|
|
2737
|
+
* limit, and null where they may not: blocks and plain text that breaks
|
|
2738
|
+
* only at its spaces (`spaceBreaksOnly`) are as narrow as their widest
|
|
2739
|
+
* word (`widestWord`) with the edges of the blocks between, which is what
|
|
2740
|
+
* the probe finds (`MIN_CONTENT_PROBE`) — for a fifteenth of the cost on
|
|
2741
|
+
* ntk, which searches every word of that probe for a place to cut it, as
|
|
2742
|
+
* none fits in no width at all. Anything else
|
|
2743
|
+
* is left to the probe: another kind of break, an inline box's edges, a
|
|
2744
|
+
* box on a line, a float, a table or a flex box, text that does not wrap
|
|
2745
|
+
* or is indented, a block with widths of its own.
|
|
2746
|
+
*/
|
|
2747
|
+
export function exactMinContent(box: Box, fonts: FontsLike): number | null {
|
|
2748
|
+
if (hasWidths(box.style) || contained(box, CONTAIN_WIDTH)) return null;
|
|
2749
|
+
const inner = exactWords(box, fonts);
|
|
2750
|
+
return inner === null ? null : inner + box.horizontalExtra;
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/** Whether a style sets its box's width, or its least or greatest width. */
|
|
2754
|
+
function hasWidths(style: ComputedStyle): boolean {
|
|
2755
|
+
return (
|
|
2756
|
+
style.width !== AUTO ||
|
|
2757
|
+
style.widthKeyword !== null ||
|
|
2758
|
+
(style.minWidth !== 0 && style.minWidth !== AUTO) ||
|
|
2759
|
+
style.minWidthKeyword !== null ||
|
|
2760
|
+
style.maxWidth !== 'none' ||
|
|
2761
|
+
style.maxWidthKeyword !== null
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
function exactWords(box: Box, fonts: FontsLike): number | null {
|
|
2766
|
+
const style = box.style;
|
|
2767
|
+
if (style.whiteSpace !== 'normal' || style.textIndent !== 0) return null;
|
|
2768
|
+
let widest = 0;
|
|
2769
|
+
if (box.lines) {
|
|
2770
|
+
let last: object | null = null;
|
|
2771
|
+
for (const line of box.lines) {
|
|
2772
|
+
if (line.atomics.length || line.edges?.length) return null;
|
|
2773
|
+
for (const text of line.texts) {
|
|
2774
|
+
if (text.layout === last) continue;
|
|
2775
|
+
last = text.layout;
|
|
2776
|
+
if (!spaceBreaksOnly(text.layout)) return null;
|
|
2777
|
+
widest = Math.max(widest, widestWord(fonts, text.layout));
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
for (const child of box.children) {
|
|
2782
|
+
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
2783
|
+
if (child.kind === 'inline') {
|
|
2784
|
+
if (!plainInline(child)) return null;
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
if (child.kind !== 'block' || child.isFloat) return null;
|
|
2788
|
+
if (child.outOfFlow) continue;
|
|
2789
|
+
if (hasWidths(child.style)) return null;
|
|
2790
|
+
const inner = exactWords(child, fonts);
|
|
2791
|
+
if (inner === null) return null;
|
|
2792
|
+
widest = Math.max(
|
|
2793
|
+
widest,
|
|
2794
|
+
inner + child.horizontalExtra + child.marginLeft + child.marginRight,
|
|
2795
|
+
);
|
|
2796
|
+
}
|
|
2797
|
+
return widest;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
/** Whether an inline box and what is in it are text that wraps. */
|
|
2801
|
+
function plainInline(box: Box): boolean {
|
|
2802
|
+
if (box.style.whiteSpace !== 'normal') return false;
|
|
2803
|
+
for (const child of box.children) {
|
|
2804
|
+
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
2805
|
+
if (child.kind !== 'inline' || !plainInline(child)) return false;
|
|
2806
|
+
}
|
|
2807
|
+
return true;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* An upper bound of a laid-out block's min-content width inside its edges:
|
|
2812
|
+
* the widest word of its lines (`widestWord`, measured or estimated from
|
|
2813
|
+
* its characters), the widest box on them, and the same of each block in
|
|
2814
|
+
* it with that block's edges. A table, a flex
|
|
2815
|
+
* box and anything else not laid out as blocks and lines is as wide as the
|
|
2816
|
+
* probe made it, which is more than it has to be; a box that is itself one
|
|
2817
|
+
* has no bound.
|
|
2818
|
+
*/
|
|
2819
|
+
function wordBound(box: Box, fonts: FontsLike, measured: boolean): number {
|
|
2820
|
+
if (box.kind !== 'block') return Infinity;
|
|
2821
|
+
let widest = 0;
|
|
2822
|
+
// a line that does not wrap is one word: a `nowrap` tooltip in a narrow
|
|
2823
|
+
// containing block was cut to its width, the rest of the line out of it
|
|
2824
|
+
const unbroken =
|
|
2825
|
+
box.style.whiteSpace === 'nowrap' || box.style.whiteSpace === 'pre';
|
|
2826
|
+
if (box.lines) {
|
|
2827
|
+
let last: object | null = null;
|
|
2828
|
+
for (const line of box.lines) {
|
|
2829
|
+
if (unbroken) widest = Math.max(widest, line.width);
|
|
2830
|
+
for (const text of line.texts) {
|
|
2831
|
+
if (text.layout === last) continue;
|
|
2832
|
+
last = text.layout;
|
|
2833
|
+
widest = Math.max(widest, widestWord(fonts, text.layout, measured));
|
|
2834
|
+
}
|
|
2835
|
+
for (const placed of line.atomics) {
|
|
2836
|
+
const b = placed.box;
|
|
2837
|
+
widest = Math.max(widest, b.width + b.marginLeft + b.marginRight);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
for (const child of box.children) {
|
|
2842
|
+
if (child.outOfFlow) continue;
|
|
2843
|
+
if (
|
|
2844
|
+
child.kind !== 'block' &&
|
|
2845
|
+
child.kind !== 'flex' &&
|
|
2846
|
+
child.kind !== 'table' &&
|
|
2847
|
+
child.kind !== 'replaced'
|
|
2848
|
+
)
|
|
2849
|
+
continue;
|
|
2850
|
+
const edges = child.marginLeft + child.marginRight;
|
|
2851
|
+
const inner =
|
|
2852
|
+
child.kind === 'block' && child.style.width === AUTO
|
|
2853
|
+
? wordBound(child, fonts, measured) + child.horizontalExtra
|
|
2854
|
+
: child.width;
|
|
2855
|
+
widest = Math.max(widest, inner + edges);
|
|
2856
|
+
}
|
|
2857
|
+
return widest;
|
|
2858
|
+
}
|
|
573
2859
|
|
|
574
2860
|
/**
|
|
575
2861
|
* The widest thing a laid-out box drew — its max-content width.
|
|
@@ -580,88 +2866,364 @@ function shrinkToFitWidth(
|
|
|
580
2866
|
* Skipping the non-finite ones is what stops the probe from answering
|
|
581
2867
|
* `Infinity` for every box that contains a paragraph.
|
|
582
2868
|
*/
|
|
583
|
-
function intrinsicWidth(box: Box): number {
|
|
2869
|
+
export function intrinsicWidth(box: Box): number {
|
|
2870
|
+
// under size containment, as though it held nothing: the size
|
|
2871
|
+
// `contain-intrinsic-size` gives it, or none (CSS Containment 2, 3.2)
|
|
2872
|
+
if (contained(box, CONTAIN_WIDTH)) {
|
|
2873
|
+
return box.style.containIntrinsicWidth ?? 0;
|
|
2874
|
+
}
|
|
2875
|
+
// a grid laid out at no width limit has its columns at their widest, and
|
|
2876
|
+
// is as wide as they are — an item that runs past its column makes it no
|
|
2877
|
+
// wider — or, where it has none, as where its items end
|
|
2878
|
+
if (box.style.grid) {
|
|
2879
|
+
const cols = GRID_TRACKS.get(box)?.cols;
|
|
2880
|
+
if (cols?.length) return cols[cols.length - 1][1] - cols[0][0];
|
|
2881
|
+
let right = 0;
|
|
2882
|
+
for (const child of box.children) {
|
|
2883
|
+
if (child.outOfFlow || child.kind === 'text') continue;
|
|
2884
|
+
right = Math.max(
|
|
2885
|
+
right,
|
|
2886
|
+
child.x - box.contentX + child.width + child.marginRight,
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
return right;
|
|
2890
|
+
}
|
|
584
2891
|
let widest = 0;
|
|
585
|
-
|
|
586
|
-
|
|
2892
|
+
// A row of flex items is as wide as all of them side by side, and gaps
|
|
2893
|
+
// between them; any other box is as wide as the widest thing in it
|
|
2894
|
+
const display = box.style.display;
|
|
2895
|
+
const row =
|
|
2896
|
+
(display === 'flex' || display === 'inline-flex') &&
|
|
2897
|
+
box.style.flexDirection.startsWith('row');
|
|
2898
|
+
let total = 0;
|
|
2899
|
+
let items = 0;
|
|
2900
|
+
const lines = box.lines;
|
|
2901
|
+
if (lines) {
|
|
2902
|
+
// a float met in a line stands beside what the line holds, at its
|
|
2903
|
+
// width, as does a row of them beside no line
|
|
2904
|
+
for (const line of lines) {
|
|
2905
|
+
widest = Math.max(widest, line.width + (line.floats ?? 0));
|
|
2906
|
+
}
|
|
2907
|
+
widest = Math.max(widest, box.floatRow);
|
|
587
2908
|
}
|
|
2909
|
+
// Floats among blocks stand side by side too, as many as come together,
|
|
2910
|
+
// where each block in flow is a line of its own; one with a formatting
|
|
2911
|
+
// context of its own stands beside them (as Blink measures them). Taken
|
|
2912
|
+
// for the widest of them, a floated menu's items were one under another.
|
|
2913
|
+
// That is at the box's widest, laid out at no width limit; at its
|
|
2914
|
+
// narrowest each is as narrow as it can be, and one under another.
|
|
2915
|
+
const sideBySide = !Number.isFinite(box.width);
|
|
2916
|
+
let left = 0;
|
|
2917
|
+
let right = 0;
|
|
588
2918
|
for (const child of box.children) {
|
|
589
2919
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
590
2920
|
if (child.outOfFlow) continue;
|
|
2921
|
+
// what sits in the lines is measured with them, indent and all: an
|
|
2922
|
+
// inline-block after a negative `text-indent` ends where its line does
|
|
2923
|
+
if (lines && (isInlineLevel(child) || child.isFloat)) continue;
|
|
591
2924
|
const margins = child.marginLeft + child.marginRight;
|
|
592
|
-
const inner = intrinsicWidth(child) + child.horizontalExtra + margins;
|
|
593
2925
|
const own = Number.isFinite(child.width) ? child.width + margins : 0;
|
|
594
|
-
|
|
2926
|
+
const style = child.style;
|
|
2927
|
+
// a minimum's percentage is of zero here (CSS Sizing 3 5.2.1), which
|
|
2928
|
+
// leaves a `calc()` its pixels
|
|
2929
|
+
const min =
|
|
2930
|
+
style.minWidth === 0 || style.minWidth === AUTO
|
|
2931
|
+
? 0
|
|
2932
|
+
: resolve(style.minWidth, 0) + contentExtra(child);
|
|
2933
|
+
let contribution: number;
|
|
2934
|
+
// A width of its own is what a child contributes, whatever its content
|
|
2935
|
+
// does past it (CSS Sizing 3 5.1); a percentage one is cyclic here and
|
|
2936
|
+
// counts as `auto`, so its content decides.
|
|
2937
|
+
if (typeof style.width === 'number') {
|
|
2938
|
+
contribution = Math.max(own, min + margins);
|
|
2939
|
+
} else {
|
|
2940
|
+
let inner = intrinsicWidth(child) + child.horizontalExtra;
|
|
2941
|
+
if (typeof style.maxWidth === 'number') {
|
|
2942
|
+
inner = Math.min(inner, style.maxWidth + contentExtra(child));
|
|
2943
|
+
}
|
|
2944
|
+
if (min > inner) inner = min;
|
|
2945
|
+
contribution = Math.max(inner + margins, own);
|
|
2946
|
+
}
|
|
2947
|
+
if (row) {
|
|
2948
|
+
total += contribution;
|
|
2949
|
+
items += 1;
|
|
2950
|
+
} else if (!sideBySide) widest = Math.max(widest, contribution);
|
|
2951
|
+
else if (child.isFloat) {
|
|
2952
|
+
const clear = style.clear;
|
|
2953
|
+
if (clear !== 'none') {
|
|
2954
|
+
widest = Math.max(widest, left + right);
|
|
2955
|
+
if (clear !== 'right') left = 0;
|
|
2956
|
+
if (clear !== 'left') right = 0;
|
|
2957
|
+
}
|
|
2958
|
+
if (style.float === 'right') right += contribution;
|
|
2959
|
+
else left += contribution;
|
|
2960
|
+
} else {
|
|
2961
|
+
const beside = establishesBFC(child) ? left + right : 0;
|
|
2962
|
+
widest = Math.max(widest, beside + contribution, left + right);
|
|
2963
|
+
left = 0;
|
|
2964
|
+
right = 0;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
widest = Math.max(widest, left + right);
|
|
2968
|
+
if (row && items) {
|
|
2969
|
+
const gap = gapOf(box.style.columnGap, NaN);
|
|
2970
|
+
widest = Math.max(widest, total + gap * (items - 1));
|
|
595
2971
|
}
|
|
596
2972
|
return widest;
|
|
597
2973
|
}
|
|
598
2974
|
|
|
599
|
-
/**
|
|
600
|
-
*
|
|
601
|
-
function
|
|
2975
|
+
/** What a box's own width leaves out of its border box: its padding and
|
|
2976
|
+
* border, unless `box-sizing` puts them in. */
|
|
2977
|
+
function contentExtra(box: Box): number {
|
|
2978
|
+
return box.style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* A replaced box's size, from its style and what it has of an intrinsic
|
|
2983
|
+
* width, height and ratio (CSS 2.1 10.3.2 and 10.6.2), within its minimum
|
|
2984
|
+
* and maximum (10.4, 10.7).
|
|
2985
|
+
*
|
|
2986
|
+
* An image has all three, a frame and a control their default size and no
|
|
2987
|
+
* ratio — so the axis the style does not set keeps its own, and a text field
|
|
2988
|
+
* `width: 100%` is not stretched to twice its height. An SVG may have any of
|
|
2989
|
+
* them: a height and nothing else is 300 pixels wide, a ratio and nothing
|
|
2990
|
+
* else fills its containing block, and nothing at all is 300 by 150 — the
|
|
2991
|
+
* default object size, which the builder leaves in `Intrinsic` for an axis
|
|
2992
|
+
* its `missing` names.
|
|
2993
|
+
*
|
|
2994
|
+
* A limit applied to one axis carries to the other through the ratio: an
|
|
2995
|
+
* image `max-width: 100%` narrower than itself keeps its proportions, where
|
|
2996
|
+
* clamping each axis alone squashed it — the `<img width="600">` of every
|
|
2997
|
+
* mail template, in a narrow column.
|
|
2998
|
+
*/
|
|
2999
|
+
function sizeReplaced(
|
|
3000
|
+
box: Box,
|
|
3001
|
+
containingWidth: number,
|
|
3002
|
+
/** The room `stretch` fills: the containing block's width, or less of
|
|
3003
|
+
* it beside floats (CSS Sizing 3, 4.2). */
|
|
3004
|
+
stretchRoom = containingWidth,
|
|
3005
|
+
): void {
|
|
602
3006
|
const style = box.style;
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
3007
|
+
const base = box.percentHeightBase;
|
|
3008
|
+
// everything below is the content box's; a `border-box` length is not
|
|
3009
|
+
const hx = style.boxSizing === 'border-box' ? box.horizontalExtra : 0;
|
|
3010
|
+
const vx = style.boxSizing === 'border-box' ? box.verticalExtra : 0;
|
|
3011
|
+
const across = (len: Len): number | null => {
|
|
3012
|
+
const v = resolveOrNull(len, containingWidth);
|
|
3013
|
+
return v === null ? null : Math.max(0, v - hx);
|
|
3014
|
+
};
|
|
3015
|
+
const down = (len: Len): number | null => {
|
|
3016
|
+
const v = resolveOrNull(len, base);
|
|
3017
|
+
return v === null ? null : Math.max(0, v - vx);
|
|
3018
|
+
};
|
|
3019
|
+
// `stretch` is what the margins leave of the containing block, as a
|
|
3020
|
+
// content box, and of its height where that is definite
|
|
3021
|
+
const stretchedHeight =
|
|
3022
|
+
style.heightKeyword === 'stretch' ? stretchHeight(box) : null;
|
|
3023
|
+
const stretchedWidth = Number.isFinite(stretchRoom)
|
|
3024
|
+
? Math.max(
|
|
3025
|
+
0,
|
|
3026
|
+
stretchRoom - box.marginLeft - box.marginRight - box.horizontalExtra,
|
|
3027
|
+
)
|
|
3028
|
+
: null;
|
|
3029
|
+
const width =
|
|
3030
|
+
style.widthKeyword === 'stretch' && stretchedWidth !== null
|
|
3031
|
+
? stretchedWidth
|
|
3032
|
+
: across(style.width);
|
|
3033
|
+
const height =
|
|
3034
|
+
stretchedHeight !== null
|
|
3035
|
+
? Math.max(0, stretchedHeight - box.verticalExtra)
|
|
3036
|
+
: down(style.height);
|
|
3037
|
+
const minW =
|
|
3038
|
+
(style.minWidthKeyword === 'stretch' ? stretchedWidth : null) ??
|
|
3039
|
+
across(style.minWidth) ??
|
|
3040
|
+
0;
|
|
3041
|
+
const minH = down(style.minHeight) ?? 0;
|
|
3042
|
+
// a maximum below the minimum is the minimum (10.4)
|
|
3043
|
+
const maxW = Math.max(
|
|
3044
|
+
minW,
|
|
3045
|
+
style.maxWidthKeyword === 'stretch' && stretchedWidth !== null
|
|
3046
|
+
? stretchedWidth
|
|
3047
|
+
: style.maxWidth === 'none'
|
|
3048
|
+
? Infinity
|
|
3049
|
+
: (across(style.maxWidth) ?? Infinity),
|
|
3050
|
+
);
|
|
3051
|
+
const maxH = Math.max(
|
|
3052
|
+
minH,
|
|
3053
|
+
style.maxHeight === 'none' ? Infinity : (down(style.maxHeight) ?? Infinity),
|
|
3054
|
+
);
|
|
3055
|
+
// under size containment, as though it had no size or ratio of its own
|
|
3056
|
+
// but the one `contain-intrinsic-size` gives it (CSS Containment 2, 3.2)
|
|
3057
|
+
const own = contained(box, CONTAIN_SIZE)
|
|
3058
|
+
? {
|
|
3059
|
+
width: style.containIntrinsicWidth ?? 0,
|
|
3060
|
+
height: style.containIntrinsicHeight ?? 0,
|
|
3061
|
+
ratio: 0,
|
|
3062
|
+
missing: 0,
|
|
3063
|
+
}
|
|
3064
|
+
: (box.intrinsic ?? NO_INTRINSIC);
|
|
3065
|
+
const iw = own.missing & 1 ? null : own.width;
|
|
3066
|
+
const ih = own.missing & 2 ? null : own.height;
|
|
3067
|
+
// `aspect-ratio` over its own, unless written `auto` and it has one
|
|
3068
|
+
const aspect = style.aspectRatio;
|
|
3069
|
+
const ratio =
|
|
3070
|
+
aspect && !(aspect.auto && own.ratio > 0) ? aspect.ratio : own.ratio;
|
|
3071
|
+
// the width a block would have here: what a ratio with no size fills, and
|
|
3072
|
+
// an `hr`, whose whole appearance is its border across the line
|
|
3073
|
+
const room = (): number => {
|
|
3074
|
+
const w =
|
|
3075
|
+
containingWidth - box.marginLeft - box.marginRight - box.horizontalExtra;
|
|
3076
|
+
return Number.isFinite(w) ? Math.max(0, w) : 0;
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
let w: number;
|
|
3080
|
+
let h: number;
|
|
3081
|
+
if (width === null && height === null && ratio > 0) {
|
|
3082
|
+
// both auto, with a ratio: the size it asks for, then the table in 10.4,
|
|
3083
|
+
// which moves both axes together
|
|
3084
|
+
if (iw !== null) {
|
|
3085
|
+
w = iw;
|
|
3086
|
+
h = ih ?? iw / ratio;
|
|
3087
|
+
} else if (ih !== null) {
|
|
3088
|
+
h = ih;
|
|
3089
|
+
w = ih * ratio;
|
|
3090
|
+
} else {
|
|
3091
|
+
// a ratio and no size: CSS 2.1 leaves it undefined and suggests the
|
|
3092
|
+
// width a block would have, which is what browsers do — or, where the
|
|
3093
|
+
// containing block waits on this box, the default object size
|
|
3094
|
+
w = Number.isFinite(containingWidth) ? room() : own.width;
|
|
3095
|
+
h = w / ratio;
|
|
3096
|
+
}
|
|
3097
|
+
[w, h] = constrained(w, h, minW, maxW, minH, maxH);
|
|
3098
|
+
} else {
|
|
3099
|
+
// one axis set, or no ratio: the width, clamped, and then the height
|
|
3100
|
+
// from the width that was used (10.3.2's "the rules above are applied
|
|
3101
|
+
// again" for a limit)
|
|
3102
|
+
const usedHeight = height === null ? null : clamp(height, minH, maxH);
|
|
3103
|
+
if (width !== null) w = width;
|
|
3104
|
+
else if (usedHeight !== null && ratio > 0) w = usedHeight * ratio;
|
|
3105
|
+
else if (box.replaced === 'hr') w = room();
|
|
3106
|
+
else w = own.width;
|
|
3107
|
+
w = clamp(w, minW, maxW);
|
|
3108
|
+
if (usedHeight !== null) h = usedHeight;
|
|
3109
|
+
else if (ratio > 0) h = clamp(w / ratio, minH, maxH);
|
|
3110
|
+
else h = clamp(own.height, minH, maxH);
|
|
634
3111
|
}
|
|
3112
|
+
box.width = w + box.horizontalExtra;
|
|
3113
|
+
box.height = h + box.verticalExtra;
|
|
635
3114
|
}
|
|
636
3115
|
|
|
637
|
-
/**
|
|
3116
|
+
/** What a replaced box with nothing to say about its size has: `hr`. */
|
|
3117
|
+
const NO_INTRINSIC: Intrinsic = { width: 0, height: 0, missing: 0, ratio: 0 };
|
|
3118
|
+
|
|
3119
|
+
function clamp(value: number, min: number, max: number): number {
|
|
3120
|
+
return Math.max(min, Math.min(value, max));
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* CSS 2.1 10.4's table, for a replaced box sized by its ratio alone: a
|
|
3125
|
+
* limit on either axis is met by scaling both, and where the two axes' limits
|
|
3126
|
+
* disagree, the one that asks for more change wins.
|
|
3127
|
+
*/
|
|
3128
|
+
function constrained(
|
|
3129
|
+
w: number,
|
|
3130
|
+
h: number,
|
|
3131
|
+
minW: number,
|
|
3132
|
+
maxW: number,
|
|
3133
|
+
minH: number,
|
|
3134
|
+
maxH: number,
|
|
3135
|
+
): [number, number] {
|
|
3136
|
+
if (!(w > 0 && h > 0)) return [clamp(w, minW, maxW), clamp(h, minH, maxH)];
|
|
3137
|
+
if (w > maxW && h > maxH) {
|
|
3138
|
+
return maxW / w <= maxH / h
|
|
3139
|
+
? [maxW, Math.max(minH, (maxW * h) / w)]
|
|
3140
|
+
: [Math.max(minW, (maxH * w) / h), maxH];
|
|
3141
|
+
}
|
|
3142
|
+
if (w < minW && h < minH) {
|
|
3143
|
+
return minW / w <= minH / h
|
|
3144
|
+
? [Math.min(maxW, (minH * w) / h), minH]
|
|
3145
|
+
: [minW, Math.min(maxH, (minW * h) / w)];
|
|
3146
|
+
}
|
|
3147
|
+
if (w < minW && h > maxH) return [minW, maxH];
|
|
3148
|
+
if (w > maxW && h < minH) return [maxW, minH];
|
|
3149
|
+
if (w > maxW) return [maxW, Math.max((maxW * h) / w, minH)];
|
|
3150
|
+
if (w < minW) return [minW, Math.min((minW * h) / w, maxH)];
|
|
3151
|
+
if (h > maxH) return [Math.max((maxH * w) / h, minW), maxH];
|
|
3152
|
+
if (h < minH) return [Math.min((minH * w) / h, maxW), minH];
|
|
3153
|
+
return [w, h];
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
/**
|
|
3157
|
+
* Place and size a float, and register it with the formatting context.
|
|
3158
|
+
*
|
|
3159
|
+
* The formatting context holds every float in it, and its band is as wide
|
|
3160
|
+
* as its root; a float is placed within its own containing block's content
|
|
3161
|
+
* box, which may be narrower (CSS 2.1 9.5.1, rules 1 and 7). Floats placed
|
|
3162
|
+
* earlier still push it over wherever they reach into that box. Placed in
|
|
3163
|
+
* the formatting context's band, a float in a nested block sat at the
|
|
3164
|
+
* root's edge — outside its parent's padding and the body's margin.
|
|
3165
|
+
*/
|
|
638
3166
|
function layoutFloat(
|
|
639
3167
|
box: Box,
|
|
640
3168
|
ctx: LayoutContext,
|
|
641
3169
|
floats: FloatContext,
|
|
642
3170
|
y: number,
|
|
3171
|
+
containingLeft: number,
|
|
643
3172
|
containingWidth: number,
|
|
644
3173
|
): void {
|
|
3174
|
+
sizeFloat(box, ctx, containingWidth);
|
|
3175
|
+
placeFloat(box, floats, y, containingLeft, containingWidth);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/** Lay a float out at its own width, and answer its outer width. */
|
|
3179
|
+
function sizeFloat(
|
|
3180
|
+
box: Box,
|
|
3181
|
+
ctx: LayoutContext,
|
|
3182
|
+
containingWidth: number,
|
|
3183
|
+
): number {
|
|
645
3184
|
resolveEdges(box, containingWidth);
|
|
646
3185
|
if (box.kind === 'replaced') sizeReplaced(box, containingWidth);
|
|
647
|
-
else
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
3186
|
+
else layoutOwn(box, ctx, shrinkToFitWidth(box, ctx, containingWidth));
|
|
3187
|
+
return box.width + box.marginLeft + box.marginRight;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
/** Put a float that is laid out as high as it goes from `y`, and to its
|
|
3191
|
+
* side, and note it in the float context. */
|
|
3192
|
+
function placeFloat(
|
|
3193
|
+
box: Box,
|
|
3194
|
+
floats: FloatContext,
|
|
3195
|
+
y: number,
|
|
3196
|
+
containingLeft: number,
|
|
3197
|
+
containingWidth: number,
|
|
3198
|
+
): void {
|
|
651
3199
|
const outerWidth = box.width + box.marginLeft + box.marginRight;
|
|
652
3200
|
const clearance = floats.clearance(box.style.clear);
|
|
653
3201
|
const from = Math.max(y, clearance === -Infinity ? y : clearance);
|
|
3202
|
+
const left = containingLeft;
|
|
3203
|
+
const right = containingLeft + containingWidth;
|
|
654
3204
|
const top = floats.placeAt(
|
|
655
3205
|
from,
|
|
656
3206
|
outerWidth,
|
|
657
3207
|
box.style.float === 'right' ? 'right' : 'left',
|
|
3208
|
+
left,
|
|
3209
|
+
right,
|
|
658
3210
|
);
|
|
659
|
-
const band = floats.bandAt(top, 1);
|
|
3211
|
+
const band = floats.bandAt(top, 1, left, right);
|
|
660
3212
|
const x =
|
|
661
3213
|
box.style.float === 'right'
|
|
662
3214
|
? band.right - outerWidth + box.marginLeft
|
|
663
3215
|
: band.left + box.marginLeft;
|
|
664
|
-
|
|
3216
|
+
// A right float in content measured at no width limit stands at an
|
|
3217
|
+
// infinite x, where its box is not put: a box at infinity cannot be moved
|
|
3218
|
+
// back, so everything in it was laid out again at its next layout — at
|
|
3219
|
+
// every level of a nest of such floats, the innermost as many times as
|
|
3220
|
+
// there were levels. What a measure reads of it is its width, and the
|
|
3221
|
+
// float context keeps where it stands.
|
|
3222
|
+
moveTo(
|
|
3223
|
+
box,
|
|
3224
|
+
Number.isFinite(x) ? x : band.left + box.marginLeft,
|
|
3225
|
+
top + box.marginTop,
|
|
3226
|
+
);
|
|
665
3227
|
floats.add({
|
|
666
3228
|
left: x - box.marginLeft,
|
|
667
3229
|
right: x - box.marginLeft + outerWidth,
|
|
@@ -672,21 +3234,26 @@ function layoutFloat(
|
|
|
672
3234
|
}
|
|
673
3235
|
|
|
674
3236
|
/**
|
|
675
|
-
* An absolutely positioned box, against its containing block
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
* block's content top rather than where its markup sat. That is the one
|
|
680
|
-
* deliberate simplification in positioning, and it is invisible for the
|
|
681
|
-
* overwhelmingly common `position: absolute` with an explicit offset, which
|
|
682
|
-
* is how a badge, a tooltip and an overlay are all written.
|
|
3237
|
+
* An absolutely positioned box, against its containing block: a box's
|
|
3238
|
+
* padding box, or an inline box's fragments (`inlineContainingBlock`). An
|
|
3239
|
+
* axis with neither offset takes the static position, where the box would
|
|
3240
|
+
* have been in flow (`placeStatic`, and `staticPositions` in a line).
|
|
683
3241
|
*/
|
|
684
3242
|
function layoutPositioned(box: Box, containing: Box, ctx: LayoutContext): void {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const
|
|
688
|
-
|
|
3243
|
+
// a grid's is the box's grid area (CSS Grid 1, 9.1), whether the box is
|
|
3244
|
+
// the grid's child or deeper in it
|
|
3245
|
+
const area =
|
|
3246
|
+
containing.kind === 'inline' ? null : gridArea(box, containing, ctx);
|
|
3247
|
+
const {
|
|
3248
|
+
x: cbX,
|
|
3249
|
+
y: cbY,
|
|
3250
|
+
width: cbWidth,
|
|
3251
|
+
height: cbHeight,
|
|
3252
|
+
} = containing.kind === 'inline'
|
|
3253
|
+
? inlineContainingBlock(containing, box, ctx)
|
|
3254
|
+
: (area ?? containingRect(containing, ctx));
|
|
689
3255
|
resolveEdges(box, cbWidth);
|
|
3256
|
+
box.percentHeightBase = cbHeight;
|
|
690
3257
|
|
|
691
3258
|
const style = box.style;
|
|
692
3259
|
const left = resolveOrNull(style.left, cbWidth);
|
|
@@ -694,58 +3261,701 @@ function layoutPositioned(box: Box, containing: Box, ctx: LayoutContext): void {
|
|
|
694
3261
|
const top = resolveOrNull(style.top, cbHeight);
|
|
695
3262
|
const bottom = resolveOrNull(style.bottom, cbHeight);
|
|
696
3263
|
|
|
3264
|
+
// with neither offset on an axis, the box is where the flow would have
|
|
3265
|
+
// put it (CSS 2.1 10.3.7, 10.6.4): against its start edge, which is the
|
|
3266
|
+
// right one in a right-to-left flow. A grid's own child is aligned in its
|
|
3267
|
+
// grid area instead, where that is its containing block; a box deeper in
|
|
3268
|
+
// the grid is where its own flow put it.
|
|
3269
|
+
const inArea =
|
|
3270
|
+
area !== null &&
|
|
3271
|
+
box.staticPosition?.inside !== undefined &&
|
|
3272
|
+
box.staticPosition.from === containing;
|
|
3273
|
+
const at = inArea ? null : box.staticPosition;
|
|
3274
|
+
const rtl = (at?.from ?? containing).style.direction === 'rtl';
|
|
3275
|
+
// where the box is as a flex box's or a grid's one item, aligned in it —
|
|
3276
|
+
// or in its grid area
|
|
3277
|
+
const room = inArea
|
|
3278
|
+
? {
|
|
3279
|
+
x: cbX,
|
|
3280
|
+
y: cbY,
|
|
3281
|
+
width: cbWidth,
|
|
3282
|
+
height: cbHeight,
|
|
3283
|
+
...staticAlignment(box, containing, false),
|
|
3284
|
+
}
|
|
3285
|
+
: at?.inside
|
|
3286
|
+
? { ...insideRect(at.from, at.inside.box), ...at.inside }
|
|
3287
|
+
: null;
|
|
3288
|
+
// and a shrink-to-fit box's room starts at the offset on the side it has
|
|
3289
|
+
// one, or at its static position: `left: 50%` leaves it half the width
|
|
3290
|
+
const offset =
|
|
3291
|
+
left ??
|
|
3292
|
+
right ??
|
|
3293
|
+
(!at
|
|
3294
|
+
? 0
|
|
3295
|
+
: rtl
|
|
3296
|
+
? cbX + cbWidth - (at.from.x + at.right)
|
|
3297
|
+
: at.from.x + at.x - cbX);
|
|
3298
|
+
|
|
697
3299
|
let width: number;
|
|
3300
|
+
let stretched = NaN;
|
|
698
3301
|
if (style.width !== AUTO) {
|
|
699
|
-
width = blockWidth(box, cbWidth);
|
|
3302
|
+
width = blockWidth(box, cbWidth, cbWidth, ctx);
|
|
3303
|
+
} else if (style.widthKeyword === 'stretch') {
|
|
3304
|
+
// what its offsets and margins leave of its containing block — an
|
|
3305
|
+
// `auto` offset leaving all it has, but where both are, the start is
|
|
3306
|
+
// its static position (CSS Position 3, 4.1)
|
|
3307
|
+
const start = left ?? (right === null ? offset : 0);
|
|
3308
|
+
const room =
|
|
3309
|
+
cbWidth - start - (right ?? 0) - box.marginLeft - box.marginRight;
|
|
3310
|
+
width = clampWidth(box, Math.max(0, room), cbWidth, ctx);
|
|
3311
|
+
} else if (style.widthKeyword || ratioWidth(box) !== null) {
|
|
3312
|
+
// an intrinsic size, which both offsets do not stretch, or a height of
|
|
3313
|
+
// its own through its ratio
|
|
3314
|
+
width = shrinkToFitWidth(box, ctx, cbWidth, offset);
|
|
3315
|
+
} else if (
|
|
3316
|
+
boxRatio(box) &&
|
|
3317
|
+
(left === null || right === null) &&
|
|
3318
|
+
top !== null &&
|
|
3319
|
+
bottom !== null
|
|
3320
|
+
) {
|
|
3321
|
+
// the height its offsets leave it through its ratio, where its width
|
|
3322
|
+
// has no pair of them to stretch it (CSS Sizing 4, 5.1)
|
|
3323
|
+
const fill = clampHeight(
|
|
3324
|
+
box,
|
|
3325
|
+
Math.max(0, cbHeight - top - bottom - box.marginTop - box.marginBottom),
|
|
3326
|
+
);
|
|
3327
|
+
width = ratioMinimum(
|
|
3328
|
+
box,
|
|
3329
|
+
clampWidth(box, widthFromHeight(box, fill)!, cbWidth, ctx),
|
|
3330
|
+
cbWidth,
|
|
3331
|
+
ctx,
|
|
3332
|
+
);
|
|
700
3333
|
} else if (left !== null && right !== null) {
|
|
701
|
-
|
|
3334
|
+
stretched = Math.max(
|
|
702
3335
|
0,
|
|
703
3336
|
cbWidth - left - right - box.marginLeft - box.marginRight,
|
|
704
3337
|
);
|
|
3338
|
+
// within its least and greatest width, which make it a width as one
|
|
3339
|
+
// set would, and the margins' rules run again with it (CSS 2.1 10.4) —
|
|
3340
|
+
// and those of its height, through a ratio (CSS Sizing 4, 5.1)
|
|
3341
|
+
width = clampWidth(box, transferredWidth(box, stretched), cbWidth, ctx);
|
|
705
3342
|
} else {
|
|
706
|
-
width = shrinkToFitWidth(box, ctx, cbWidth);
|
|
3343
|
+
width = shrinkToFitWidth(box, ctx, cbWidth, offset);
|
|
707
3344
|
}
|
|
708
3345
|
|
|
709
|
-
|
|
710
|
-
|
|
3346
|
+
// `stretch` down: what its offsets leave, from its static position where
|
|
3347
|
+
// it has neither
|
|
3348
|
+
if (
|
|
3349
|
+
style.heightKeyword === 'stretch' ||
|
|
3350
|
+
style.minHeightKeyword === 'stretch'
|
|
3351
|
+
) {
|
|
3352
|
+
STRETCH_TOP.set(
|
|
3353
|
+
box,
|
|
3354
|
+
top ?? (bottom === null && at ? at.from.y + at.y - cbY : 0),
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
if (box.kind === 'replaced') {
|
|
3358
|
+
sizeReplaced(box, cbWidth);
|
|
3359
|
+
stretchReplaced(
|
|
3360
|
+
box,
|
|
3361
|
+
style.widthKeyword === 'stretch' ? width : null,
|
|
3362
|
+
cbWidth,
|
|
3363
|
+
);
|
|
3364
|
+
} else layoutOwn(box, ctx, width);
|
|
711
3365
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
3366
|
+
let x: number;
|
|
3367
|
+
if (
|
|
3368
|
+
left !== null &&
|
|
3369
|
+
right !== null &&
|
|
3370
|
+
(style.width !== AUTO ||
|
|
3371
|
+
style.widthKeyword !== null ||
|
|
3372
|
+
box.kind === 'replaced' ||
|
|
3373
|
+
width !== stretched)
|
|
3374
|
+
) {
|
|
3375
|
+
// Both offsets and a width: what is left over goes to the margins that
|
|
3376
|
+
// are `auto`, shared where both are, and where none is, the end offset
|
|
3377
|
+
// gives way (10.3.7, 10.3.8).
|
|
3378
|
+
const rest = cbWidth - left - right - box.width;
|
|
3379
|
+
const autoLeft = style.marginLeft === AUTO;
|
|
3380
|
+
const autoRight = style.marginRight === AUTO;
|
|
3381
|
+
const endRtl = containing.style.direction === 'rtl';
|
|
3382
|
+
if (autoLeft && autoRight) {
|
|
3383
|
+
if (rest >= 0) {
|
|
3384
|
+
box.marginLeft = rest / 2;
|
|
3385
|
+
box.marginRight = rest / 2;
|
|
3386
|
+
} else if (endRtl) {
|
|
3387
|
+
box.marginRight = 0;
|
|
3388
|
+
box.marginLeft = rest;
|
|
3389
|
+
} else {
|
|
3390
|
+
box.marginLeft = 0;
|
|
3391
|
+
box.marginRight = rest;
|
|
3392
|
+
}
|
|
3393
|
+
} else if (autoLeft) {
|
|
3394
|
+
box.marginLeft = rest - box.marginRight;
|
|
3395
|
+
} else if (autoRight) {
|
|
3396
|
+
box.marginRight = rest - box.marginLeft;
|
|
3397
|
+
}
|
|
3398
|
+
x =
|
|
3399
|
+
endRtl && !autoLeft && !autoRight
|
|
716
3400
|
? cbX + cbWidth - right - box.width - box.marginRight
|
|
717
|
-
: cbX;
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
3401
|
+
: cbX + left + box.marginLeft;
|
|
3402
|
+
} else {
|
|
3403
|
+
x =
|
|
3404
|
+
left !== null
|
|
3405
|
+
? cbX + left + box.marginLeft
|
|
3406
|
+
: right !== null
|
|
3407
|
+
? cbX + cbWidth - right - box.width - box.marginRight
|
|
3408
|
+
: room
|
|
3409
|
+
? alignedIn(
|
|
3410
|
+
room.x,
|
|
3411
|
+
room.width,
|
|
3412
|
+
room.across,
|
|
3413
|
+
box.width + box.marginLeft + box.marginRight,
|
|
3414
|
+
rtl,
|
|
3415
|
+
) + box.marginLeft
|
|
3416
|
+
: rtl
|
|
3417
|
+
? (at ? at.from.x + at.right : cbX + cbWidth) -
|
|
3418
|
+
box.width -
|
|
3419
|
+
box.marginRight
|
|
3420
|
+
: (at ? at.from.x + at.x : cbX) + box.marginLeft;
|
|
3421
|
+
}
|
|
3422
|
+
let y: number;
|
|
3423
|
+
if (top !== null && bottom !== null) {
|
|
3424
|
+
const fill = Math.max(
|
|
3425
|
+
0,
|
|
3426
|
+
cbHeight - top - bottom - box.marginTop - box.marginBottom,
|
|
3427
|
+
);
|
|
3428
|
+
// — a box with a ratio has its height from its width
|
|
3429
|
+
const stretches =
|
|
3430
|
+
style.height === AUTO && box.kind !== 'replaced' && !boxRatio(box);
|
|
3431
|
+
// both offsets and no height: the box fills what they leave, its
|
|
3432
|
+
// `auto` margins nothing (10.6.4, rule 5) — unless `min-height` or
|
|
3433
|
+
// `max-height` moves that, which makes it a height like one set, and
|
|
3434
|
+
// the rules run again with it (10.7)
|
|
3435
|
+
if (stretches) box.height = clampHeight(box, fill);
|
|
3436
|
+
if (!stretches || box.height !== fill) {
|
|
3437
|
+
// and a height: the `auto` margins share the rest, and where none
|
|
3438
|
+
// is `auto`, `bottom` gives way (10.6.4, 10.6.5)
|
|
3439
|
+
const rest = cbHeight - top - bottom - box.height;
|
|
3440
|
+
const autoTop = style.marginTop === AUTO;
|
|
3441
|
+
const autoBottom = style.marginBottom === AUTO;
|
|
3442
|
+
if (autoTop && autoBottom) {
|
|
3443
|
+
box.marginTop = rest / 2;
|
|
3444
|
+
box.marginBottom = rest / 2;
|
|
3445
|
+
} else if (autoTop) {
|
|
3446
|
+
box.marginTop = rest - box.marginBottom;
|
|
3447
|
+
} else if (autoBottom) {
|
|
3448
|
+
box.marginBottom = rest - box.marginTop;
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
y = cbY + top + box.marginTop;
|
|
3452
|
+
} else {
|
|
3453
|
+
y =
|
|
3454
|
+
top !== null
|
|
3455
|
+
? cbY + top + box.marginTop
|
|
3456
|
+
: bottom !== null
|
|
3457
|
+
? cbY + cbHeight - bottom - box.height - box.marginBottom
|
|
3458
|
+
: room
|
|
3459
|
+
? alignedIn(
|
|
3460
|
+
room.y,
|
|
3461
|
+
room.height,
|
|
3462
|
+
room.down,
|
|
3463
|
+
box.height + box.marginTop + box.marginBottom,
|
|
3464
|
+
false,
|
|
3465
|
+
) + box.marginTop
|
|
3466
|
+
: (at ? at.from.y + at.y : cbY) + box.marginTop;
|
|
3467
|
+
}
|
|
724
3468
|
moveTo(box, x, y);
|
|
725
3469
|
}
|
|
726
3470
|
|
|
3471
|
+
/** A box's content box or its padding box, as its layout left them. */
|
|
3472
|
+
function insideRect(
|
|
3473
|
+
box: Box,
|
|
3474
|
+
which: 'content' | 'padding',
|
|
3475
|
+
): { x: number; y: number; width: number; height: number } {
|
|
3476
|
+
if (which === 'content') {
|
|
3477
|
+
return {
|
|
3478
|
+
x: box.contentX,
|
|
3479
|
+
y: box.contentY,
|
|
3480
|
+
width: box.contentWidth,
|
|
3481
|
+
height: Math.max(0, box.height - box.verticalExtra),
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
return {
|
|
3485
|
+
x: box.x + box.borderLeft,
|
|
3486
|
+
y: box.y + box.borderTop,
|
|
3487
|
+
width: Math.max(0, box.width - box.borderLeft - box.borderRight),
|
|
3488
|
+
height: Math.max(0, box.height - box.borderTop - box.borderBottom),
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
/** Where a margin box of a size starts in a room, `along` of the free room
|
|
3493
|
+
* before it from the room's start — its right end where `rtl`. */
|
|
3494
|
+
function alignedIn(
|
|
3495
|
+
start: number,
|
|
3496
|
+
size: number,
|
|
3497
|
+
along: number,
|
|
3498
|
+
outer: number,
|
|
3499
|
+
rtl: boolean,
|
|
3500
|
+
): number {
|
|
3501
|
+
const free = size - outer;
|
|
3502
|
+
return rtl ? start + size - outer - along * free : start + along * free;
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
/**
|
|
3506
|
+
* The grid area a box positioned in a grid takes for its containing block
|
|
3507
|
+
* (CSS Grid 1, 9.1): between the lines its placement names, where the grid
|
|
3508
|
+
* has them, and the grid's padding edge where a line is `auto`, is no line
|
|
3509
|
+
* of the grid, or is only a `span`. Null for a box whose containing block
|
|
3510
|
+
* is no grid.
|
|
3511
|
+
*/
|
|
3512
|
+
function gridArea(
|
|
3513
|
+
box: Box,
|
|
3514
|
+
containing: Box,
|
|
3515
|
+
ctx: LayoutContext,
|
|
3516
|
+
): CbRect | null {
|
|
3517
|
+
const tracks = GRID_TRACKS.get(containing);
|
|
3518
|
+
if (!tracks) return null;
|
|
3519
|
+
const style = box.style;
|
|
3520
|
+
const pad = containingRect(containing, ctx);
|
|
3521
|
+
const [x, width] = areaSpan(
|
|
3522
|
+
style.gridColumnStart,
|
|
3523
|
+
style.gridColumnEnd,
|
|
3524
|
+
tracks.cols,
|
|
3525
|
+
tracks.colLines,
|
|
3526
|
+
containing.contentX,
|
|
3527
|
+
pad.x,
|
|
3528
|
+
pad.width,
|
|
3529
|
+
);
|
|
3530
|
+
const [y, height] = areaSpan(
|
|
3531
|
+
style.gridRowStart,
|
|
3532
|
+
style.gridRowEnd,
|
|
3533
|
+
tracks.rows,
|
|
3534
|
+
tracks.rowLines,
|
|
3535
|
+
containing.contentY,
|
|
3536
|
+
pad.y,
|
|
3537
|
+
pad.height,
|
|
3538
|
+
);
|
|
3539
|
+
return { x, y, width, height };
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
/** One axis of a grid area: its start and its size in document
|
|
3543
|
+
* coordinates. A line is a track's start where it starts one and a
|
|
3544
|
+
* track's end where it ends one, so a gap is in neither; a line by name
|
|
3545
|
+
* is the one the placement would take. */
|
|
3546
|
+
function areaSpan(
|
|
3547
|
+
start: GridLine,
|
|
3548
|
+
end: GridLine,
|
|
3549
|
+
tracks: readonly [number, number][],
|
|
3550
|
+
lines: GridLines,
|
|
3551
|
+
origin: number,
|
|
3552
|
+
edge: number,
|
|
3553
|
+
size: number,
|
|
3554
|
+
): [number, number] {
|
|
3555
|
+
const count = tracks.length;
|
|
3556
|
+
// a line the grid has, or null for auto, a span and one it has not
|
|
3557
|
+
const at = (line: GridLine, side: 'start' | 'end'): number | null => {
|
|
3558
|
+
const index = lineOf(lines, line, side);
|
|
3559
|
+
return index !== null && count > 0 && index >= 0 && index <= count
|
|
3560
|
+
? index
|
|
3561
|
+
: null;
|
|
3562
|
+
};
|
|
3563
|
+
const spanOf = (line: GridLine) => (line && 'span' in line ? line : null);
|
|
3564
|
+
let from = at(start, 'start');
|
|
3565
|
+
let to = at(end, 'end');
|
|
3566
|
+
const startSpan = spanOf(start);
|
|
3567
|
+
const endSpan = spanOf(end);
|
|
3568
|
+
if (from === null && to !== null && startSpan) {
|
|
3569
|
+
const f =
|
|
3570
|
+
startSpan.name !== undefined
|
|
3571
|
+
? spanToName(lines, startSpan.name, startSpan.span, to, false)
|
|
3572
|
+
: to - startSpan.span;
|
|
3573
|
+
from = f >= 0 ? f : null;
|
|
3574
|
+
}
|
|
3575
|
+
if (to === null && from !== null && endSpan) {
|
|
3576
|
+
const t =
|
|
3577
|
+
endSpan.name !== undefined
|
|
3578
|
+
? spanToName(lines, endSpan.name, endSpan.span, from, true)
|
|
3579
|
+
: from + endSpan.span;
|
|
3580
|
+
to = t <= count ? t : null;
|
|
3581
|
+
}
|
|
3582
|
+
if (from !== null && to !== null && to < from) [from, to] = [to, from];
|
|
3583
|
+
const a =
|
|
3584
|
+
from === null
|
|
3585
|
+
? edge
|
|
3586
|
+
: origin + (from < count ? tracks[from][0] : tracks[from - 1][1]);
|
|
3587
|
+
const b =
|
|
3588
|
+
to === null || to === from
|
|
3589
|
+
? edge + size
|
|
3590
|
+
: origin + (to >= 1 ? tracks[to - 1][1] : tracks[0][0]);
|
|
3591
|
+
return [a, Math.max(0, b - a)];
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
/** A containing block's rectangle, in document coordinates. */
|
|
3595
|
+
interface CbRect {
|
|
3596
|
+
x: number;
|
|
3597
|
+
y: number;
|
|
3598
|
+
width: number;
|
|
3599
|
+
height: number;
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
/**
|
|
3603
|
+
* The containing block a box that lays itself out gives: its padding box
|
|
3604
|
+
* (CSS 2.1 10.1), not its content box — `left: 0` in a padded box is at
|
|
3605
|
+
* its padding edge — and with nothing positioned around it the initial
|
|
3606
|
+
* one, as tall as the viewport rather than as the document, which a fixed
|
|
3607
|
+
* box's is too.
|
|
3608
|
+
*/
|
|
3609
|
+
function containingRect(containing: Box, ctx: LayoutContext): CbRect {
|
|
3610
|
+
// a layout that reads the viewport's height lays out differently in
|
|
3611
|
+
// another (`LayoutResult.readsViewportHeight`)
|
|
3612
|
+
if (!containing.parent) ctx.readViewportHeight = true;
|
|
3613
|
+
return {
|
|
3614
|
+
x: containing.x + containing.borderLeft,
|
|
3615
|
+
y: containing.y + containing.captionTop + containing.borderTop,
|
|
3616
|
+
width: Math.max(
|
|
3617
|
+
0,
|
|
3618
|
+
containing.width - containing.borderLeft - containing.borderRight,
|
|
3619
|
+
),
|
|
3620
|
+
height: !containing.parent
|
|
3621
|
+
? ctx.viewportHeight
|
|
3622
|
+
: Math.max(
|
|
3623
|
+
0,
|
|
3624
|
+
containing.height -
|
|
3625
|
+
containing.captionTop -
|
|
3626
|
+
containing.captionBottom -
|
|
3627
|
+
containing.borderTop -
|
|
3628
|
+
containing.borderBottom,
|
|
3629
|
+
),
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
/**
|
|
3634
|
+
* The containing block a `position: relative` inline box gives (CSS 2.1
|
|
3635
|
+
* 10.1, item 4): from the padding edge its first fragment starts at to the
|
|
3636
|
+
* one its last ends at, over the top of the first and the bottom of the
|
|
3637
|
+
* last. The box lays nothing out of its own — its fragments are on its
|
|
3638
|
+
* block's lines — so read as a box that does, it was a rectangle of no size
|
|
3639
|
+
* at the page's corner, and a tooltip under a positioned link came up
|
|
3640
|
+
* there. A box with no fragment at all stands where the box inside it
|
|
3641
|
+
* would have been in flow.
|
|
3642
|
+
*/
|
|
3643
|
+
function inlineContainingBlock(
|
|
3644
|
+
inline: Box,
|
|
3645
|
+
box: Box,
|
|
3646
|
+
ctx: LayoutContext,
|
|
3647
|
+
): CbRect {
|
|
3648
|
+
let block = inline.parent;
|
|
3649
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
3650
|
+
const face = ctx.fonts ? faceExtentOf(ctx.fonts, inline.style) : null;
|
|
3651
|
+
let first: CbRect | null = null;
|
|
3652
|
+
let last: CbRect | null = null;
|
|
3653
|
+
for (const line of block?.lines ?? []) {
|
|
3654
|
+
const span = spanOn(line, inline);
|
|
3655
|
+
if (!span) continue;
|
|
3656
|
+
let top = line.y;
|
|
3657
|
+
let bottom = line.y + line.height;
|
|
3658
|
+
if (face) {
|
|
3659
|
+
const baseline =
|
|
3660
|
+
line.y +
|
|
3661
|
+
line.baseline -
|
|
3662
|
+
(LINE_BOX_RAISES.get(line)?.get(inline) ?? BOX_RAISES.get(inline) ?? 0);
|
|
3663
|
+
top = baseline - face.ascent - inline.padTop;
|
|
3664
|
+
bottom = baseline + face.descent + inline.padBottom;
|
|
3665
|
+
}
|
|
3666
|
+
const rect = {
|
|
3667
|
+
x: span[0],
|
|
3668
|
+
y: top,
|
|
3669
|
+
width: span[1] - span[0],
|
|
3670
|
+
height: bottom - top,
|
|
3671
|
+
};
|
|
3672
|
+
first ??= rect;
|
|
3673
|
+
last = rect;
|
|
3674
|
+
}
|
|
3675
|
+
if (!first || !last) {
|
|
3676
|
+
const at = box.staticPosition;
|
|
3677
|
+
const x = at ? at.from.x + at.x : 0;
|
|
3678
|
+
const y = at ? at.from.y + at.y : 0;
|
|
3679
|
+
const height = face ? face.ascent + face.descent : 0;
|
|
3680
|
+
return { x, y, width: 0, height };
|
|
3681
|
+
}
|
|
3682
|
+
const rtl = inline.style.direction === 'rtl';
|
|
3683
|
+
const start = rtl ? first.x + first.width : first.x;
|
|
3684
|
+
const end = rtl ? last.x : last.x + last.width;
|
|
3685
|
+
let x = Math.min(start, end);
|
|
3686
|
+
let width = Math.abs(end - start);
|
|
3687
|
+
if (rtl ? end > start : end < start) {
|
|
3688
|
+
// the last fragment ends before the first begins, which CSS 2.1 leaves
|
|
3689
|
+
// undefined: both, then, as a browser takes them
|
|
3690
|
+
x = Math.min(first.x, last.x);
|
|
3691
|
+
width = Math.max(first.x + first.width, last.x + last.width) - x;
|
|
3692
|
+
}
|
|
3693
|
+
return { x, y: first.y, width, height: last.y + last.height - first.y };
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/** How far an inline box's padding box reaches across a line, left and
|
|
3697
|
+
* right; null where nothing of it is on the line. */
|
|
3698
|
+
function spanOn(line: LineBox, inline: Box): [number, number] | null {
|
|
3699
|
+
let left = Infinity;
|
|
3700
|
+
let right = -Infinity;
|
|
3701
|
+
for (const text of line.texts) {
|
|
3702
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
3703
|
+
const boxAt = text.spans.boxAt;
|
|
3704
|
+
if (!natural || !boxAt) continue;
|
|
3705
|
+
const x = text.drawX + natural.x;
|
|
3706
|
+
for (const run of natural.runs) {
|
|
3707
|
+
const owner = boxAt.call(text.spans, run.start);
|
|
3708
|
+
if (!owner || !holds(inline, owner)) continue;
|
|
3709
|
+
left = Math.min(left, x + run.x);
|
|
3710
|
+
right = Math.max(right, x + run.x + run.width);
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
for (const placed of line.atomics) {
|
|
3714
|
+
if (!holds(inline, placed.box)) continue;
|
|
3715
|
+
left = Math.min(left, placed.x - placed.box.marginLeft);
|
|
3716
|
+
right = Math.max(
|
|
3717
|
+
right,
|
|
3718
|
+
placed.x + placed.box.width + placed.box.marginRight,
|
|
3719
|
+
);
|
|
3720
|
+
}
|
|
3721
|
+
for (const edge of line.edges ?? []) {
|
|
3722
|
+
if (edge.box !== inline) {
|
|
3723
|
+
if (!holds(inline, edge.box)) continue;
|
|
3724
|
+
left = Math.min(left, edge.x);
|
|
3725
|
+
right = Math.max(right, edge.x + edge.width);
|
|
3726
|
+
continue;
|
|
3727
|
+
}
|
|
3728
|
+
// its own: the padding edge, inside the margin and the border
|
|
3729
|
+
const onLeft =
|
|
3730
|
+
(edge.side === 'start') !== (inline.style.direction === 'rtl');
|
|
3731
|
+
if (onLeft) {
|
|
3732
|
+
const at = edge.x + inline.marginLeft + inline.borderLeft;
|
|
3733
|
+
left = Math.min(left, at);
|
|
3734
|
+
right = Math.max(right, at);
|
|
3735
|
+
} else {
|
|
3736
|
+
const at = edge.x + edge.width - inline.marginRight - inline.borderRight;
|
|
3737
|
+
left = Math.min(left, at);
|
|
3738
|
+
right = Math.max(right, at);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return left <= right ? [left, right] : null;
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
/**
|
|
3745
|
+
* Lay out the floats in a block's inline content and give its positioned
|
|
3746
|
+
* boxes their static positions — at any depth of inline box, which lays
|
|
3747
|
+
* nothing out of its own: a float or an absolute box inside a `<span>` is
|
|
3748
|
+
* the paragraph's as much as one beside it. Met only among the block's own
|
|
3749
|
+
* children, one in an inline box was never laid out at all, and stood at
|
|
3750
|
+
* the page's corner with no size.
|
|
3751
|
+
*/
|
|
3752
|
+
/** Give the absolute boxes in a block's inline content their static
|
|
3753
|
+
* position, and answer whether the content holds a float. */
|
|
3754
|
+
function placeOutOfLine(
|
|
3755
|
+
parent: Box,
|
|
3756
|
+
block: Box,
|
|
3757
|
+
ctx: LayoutContext,
|
|
3758
|
+
contentTop: number,
|
|
3759
|
+
contentWidth: number,
|
|
3760
|
+
contentLeft: number,
|
|
3761
|
+
): boolean {
|
|
3762
|
+
let floated = false;
|
|
3763
|
+
for (const child of parent.children) {
|
|
3764
|
+
if (child.outOfFlow) {
|
|
3765
|
+
placeStatic(child, block, contentLeft, contentWidth, contentTop);
|
|
3766
|
+
ctx.positioned.push({
|
|
3767
|
+
box: child,
|
|
3768
|
+
containing: containingBlockFor(child) ?? block,
|
|
3769
|
+
});
|
|
3770
|
+
} else if (child.isFloat) {
|
|
3771
|
+
floated = true;
|
|
3772
|
+
} else if (child.kind === 'inline' && ctx.nestedOutOfLine) {
|
|
3773
|
+
const inner = placeOutOfLine(
|
|
3774
|
+
child,
|
|
3775
|
+
block,
|
|
3776
|
+
ctx,
|
|
3777
|
+
contentTop,
|
|
3778
|
+
contentWidth,
|
|
3779
|
+
contentLeft,
|
|
3780
|
+
);
|
|
3781
|
+
floated ||= inner;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
return floated;
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
/** Where an out-of-flow box would have gone in its parent's flow: its
|
|
3788
|
+
* margin edge's, at either side, kept from the parent's corner, which may
|
|
3789
|
+
* yet move. */
|
|
3790
|
+
function placeStatic(
|
|
3791
|
+
box: Box,
|
|
3792
|
+
parent: Box,
|
|
3793
|
+
x: number,
|
|
3794
|
+
width: number,
|
|
3795
|
+
y: number,
|
|
3796
|
+
): void {
|
|
3797
|
+
box.staticPosition = {
|
|
3798
|
+
from: parent,
|
|
3799
|
+
x: x - parent.x,
|
|
3800
|
+
right: x + width - parent.x,
|
|
3801
|
+
y: y - parent.y,
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
/**
|
|
3806
|
+
* An absolutely positioned child of a flex box or a grid, laid out once the
|
|
3807
|
+
* flow is: against its own containing block, which the box is only where
|
|
3808
|
+
* it is positioned, from where it is as the box's one item — at the start
|
|
3809
|
+
* of a flex box's content box (CSS Flexbox 4.1), of a grid's padding box
|
|
3810
|
+
* (CSS Grid 1, 9.1). Pushed against the box whatever it was, one in a
|
|
3811
|
+
* static flex box was placed from the box's corner, and `fixed` one from
|
|
3812
|
+
* a grid.
|
|
3813
|
+
*/
|
|
3814
|
+
export function positionOutOfFlow(
|
|
3815
|
+
child: Box,
|
|
3816
|
+
container: Box,
|
|
3817
|
+
ctx: LayoutContext,
|
|
3818
|
+
flex: boolean,
|
|
3819
|
+
): void {
|
|
3820
|
+
if (flex) {
|
|
3821
|
+
placeStatic(
|
|
3822
|
+
child,
|
|
3823
|
+
container,
|
|
3824
|
+
container.contentX,
|
|
3825
|
+
container.contentWidth,
|
|
3826
|
+
container.contentY,
|
|
3827
|
+
);
|
|
3828
|
+
} else {
|
|
3829
|
+
placeStatic(
|
|
3830
|
+
child,
|
|
3831
|
+
container,
|
|
3832
|
+
container.x + container.borderLeft,
|
|
3833
|
+
container.width - container.borderLeft - container.borderRight,
|
|
3834
|
+
container.y + container.borderTop,
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
child.staticPosition!.inside = {
|
|
3838
|
+
box: flex ? 'content' : 'padding',
|
|
3839
|
+
...staticAlignment(child, container, flex),
|
|
3840
|
+
};
|
|
3841
|
+
ctx.positioned.push({
|
|
3842
|
+
box: child,
|
|
3843
|
+
containing: containingBlockFor(child) ?? container,
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
/** How far along its free room a flex box's or a grid's one item is set:
|
|
3848
|
+
* 0 at the start, ½ in the middle, 1 at the end, across (from the inline
|
|
3849
|
+
* start) and down. A flex box's main axis by `justify-content`, and the
|
|
3850
|
+
* cross by `align-self`; a grid's by `justify-self` and `align-self`,
|
|
3851
|
+
* each falling back to the container's `-items`. */
|
|
3852
|
+
function staticAlignment(
|
|
3853
|
+
child: Box,
|
|
3854
|
+
container: Box,
|
|
3855
|
+
flex: boolean,
|
|
3856
|
+
): { across: number; down: number } {
|
|
3857
|
+
const own = child.style;
|
|
3858
|
+
const style = container.style;
|
|
3859
|
+
const factor = (align: string): number =>
|
|
3860
|
+
align === 'flex-end' ? 1 : align === 'center' ? 0.5 : 0;
|
|
3861
|
+
const cross = factor(
|
|
3862
|
+
own.alignSelf === AUTO ? style.alignItems : own.alignSelf,
|
|
3863
|
+
);
|
|
3864
|
+
if (!flex) {
|
|
3865
|
+
const justify =
|
|
3866
|
+
own.justifySelf === 'auto' ? style.justifyItems : own.justifySelf;
|
|
3867
|
+
return { across: factor(justify), down: cross };
|
|
3868
|
+
}
|
|
3869
|
+
const justify = style.justifyContent;
|
|
3870
|
+
let main =
|
|
3871
|
+
justify === 'flex-end' || justify === 'end' || justify === 'right'
|
|
3872
|
+
? 1
|
|
3873
|
+
: justify === 'center' ||
|
|
3874
|
+
justify === 'space-around' ||
|
|
3875
|
+
justify === 'space-evenly'
|
|
3876
|
+
? 0.5
|
|
3877
|
+
: 0;
|
|
3878
|
+
if (style.flexDirection.endsWith('-reverse')) main = 1 - main;
|
|
3879
|
+
return style.flexDirection.startsWith('row')
|
|
3880
|
+
? { across: main, down: cross }
|
|
3881
|
+
: { across: cross, down: main };
|
|
3882
|
+
}
|
|
3883
|
+
|
|
727
3884
|
/** The nearest positioned ancestor, or null for the initial containing
|
|
728
3885
|
* block. Walks the box tree rather than the DOM, so an anonymous box in
|
|
729
3886
|
* between is transparent — which is what the spec means by "the nearest
|
|
730
3887
|
* positioned ancestor". */
|
|
731
3888
|
function containingBlockFor(box: Box): Box | null {
|
|
3889
|
+
// a fixed box's is the viewport, whatever is positioned around it — but
|
|
3890
|
+
// a transformed box is one for it too, and one with layout or paint
|
|
3891
|
+
// containment (CSS Containment 2, 3.3)
|
|
3892
|
+
const fixed = box.style.position === 'fixed';
|
|
732
3893
|
let node = box.parent;
|
|
733
3894
|
while (node) {
|
|
734
|
-
if (
|
|
3895
|
+
if (
|
|
3896
|
+
(!fixed && node.style.position !== 'static') ||
|
|
3897
|
+
transformed(node.style) ||
|
|
3898
|
+
contained(node, CONTAIN_LAYOUT | CONTAIN_PAINT) ||
|
|
3899
|
+
node.parent === null
|
|
3900
|
+
) {
|
|
3901
|
+
return node;
|
|
3902
|
+
}
|
|
735
3903
|
node = node.parent;
|
|
736
3904
|
}
|
|
737
3905
|
return null;
|
|
738
3906
|
}
|
|
739
3907
|
|
|
3908
|
+
/** The size containments that hold a box's width: `size` and
|
|
3909
|
+
* `inline-size`. */
|
|
3910
|
+
const CONTAIN_WIDTH = CONTAIN_SIZE | CONTAIN_INLINE_SIZE;
|
|
3911
|
+
|
|
3912
|
+
/**
|
|
3913
|
+
* Whether a box has any of the containments `bits` names, as they apply
|
|
3914
|
+
* to it (CSS Containment 2, 3): none to an inline box that is no atomic
|
|
3915
|
+
* one, and none to a table's inner parts but a cell — nor size
|
|
3916
|
+
* containment to a table or a cell.
|
|
3917
|
+
*/
|
|
3918
|
+
export function contained(box: Box, bits: number): boolean {
|
|
3919
|
+
if (!(box.style.contain & bits)) return false;
|
|
3920
|
+
switch (box.kind) {
|
|
3921
|
+
case 'block':
|
|
3922
|
+
case 'flex':
|
|
3923
|
+
case 'replaced':
|
|
3924
|
+
return true;
|
|
3925
|
+
case 'table':
|
|
3926
|
+
case 'table-cell':
|
|
3927
|
+
return !!(box.style.contain & bits & ~CONTAIN_WIDTH);
|
|
3928
|
+
default:
|
|
3929
|
+
return false;
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
|
|
740
3933
|
/** Move a box and everything under it, keeping the subtree's shape. */
|
|
741
3934
|
export function moveTo(box: Box, x: number, y: number): void {
|
|
3935
|
+
// A position only a probe of an unbounded width comes to — a column after
|
|
3936
|
+
// an infinitely wide one — is no place to move a box to: it stays where
|
|
3937
|
+
// it is, since the probe only wants its size, and the pass after moves it
|
|
3938
|
+
// from somewhere finite (see `placeBlock`).
|
|
3939
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
742
3940
|
translate(box, x - box.x, y - box.y);
|
|
743
3941
|
}
|
|
744
3942
|
|
|
3943
|
+
/** Move what a box holds down, and not the box: a table cell's content
|
|
3944
|
+
* sits where its `vertical-align` puts it, in a box that fills the row. */
|
|
3945
|
+
export function moveContent(box: Box, dy: number): void {
|
|
3946
|
+
translate(box, 0, dy);
|
|
3947
|
+
box.y -= dy;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
745
3950
|
function translate(box: Box, dx: number, dy: number): void {
|
|
746
3951
|
if (!dx && !dy) return;
|
|
747
3952
|
box.x += dx;
|
|
748
3953
|
box.y += dy;
|
|
3954
|
+
// a list item's marker is placed in the same coordinates as its lines
|
|
3955
|
+
if (box.marker) {
|
|
3956
|
+
box.marker.x += dx;
|
|
3957
|
+
box.marker.y += dy;
|
|
3958
|
+
}
|
|
749
3959
|
if (box.lines) {
|
|
750
3960
|
for (const line of box.lines) {
|
|
751
3961
|
line.x += dx;
|
|
@@ -758,6 +3968,7 @@ function translate(box: Box, dx: number, dy: number): void {
|
|
|
758
3968
|
placed.x += dx;
|
|
759
3969
|
placed.y += dy;
|
|
760
3970
|
}
|
|
3971
|
+
for (const edge of line.edges ?? []) edge.x += dx;
|
|
761
3972
|
}
|
|
762
3973
|
}
|
|
763
3974
|
for (const child of box.children) translate(child, dx, dy);
|
|
@@ -784,23 +3995,42 @@ export function resolveEdges(box: Box, containingWidth: number): void {
|
|
|
784
3995
|
style.borderLeftStyle === 'none' || style.borderLeftStyle === 'hidden'
|
|
785
3996
|
? 0
|
|
786
3997
|
: style.borderLeftWidth;
|
|
787
|
-
|
|
788
|
-
box.
|
|
789
|
-
box.
|
|
790
|
-
box.
|
|
3998
|
+
// at least zero: a `calc()` may come to less
|
|
3999
|
+
box.padTop = Math.max(0, edge(style.paddingTop, containingWidth));
|
|
4000
|
+
box.padRight = Math.max(0, edge(style.paddingRight, containingWidth));
|
|
4001
|
+
box.padBottom = Math.max(0, edge(style.paddingBottom, containingWidth));
|
|
4002
|
+
box.padLeft = Math.max(0, edge(style.paddingLeft, containingWidth));
|
|
791
4003
|
box.marginTop = edge(style.marginTop, containingWidth);
|
|
792
4004
|
box.marginRight = edge(style.marginRight, containingWidth);
|
|
793
4005
|
box.marginBottom = edge(style.marginBottom, containingWidth);
|
|
794
4006
|
box.marginLeft = edge(style.marginLeft, containingWidth);
|
|
4007
|
+
box.bottomStrut = null;
|
|
4008
|
+
// A table's parts but its caption have no margins, and its rows and row
|
|
4009
|
+
// groups no padding either (CSS 2.1 8.3, 8.4): a cell set `margin: 50px`
|
|
4010
|
+
// left a gap in its table that no browser draws. The values stay theirs,
|
|
4011
|
+
// for a cell to inherit (`padding: inherit`); they only do nothing here.
|
|
4012
|
+
if (
|
|
4013
|
+
box.kind === 'table-cell' ||
|
|
4014
|
+
box.kind === 'table-row' ||
|
|
4015
|
+
box.kind === 'table-row-group'
|
|
4016
|
+
) {
|
|
4017
|
+
box.marginTop = 0;
|
|
4018
|
+
box.marginRight = 0;
|
|
4019
|
+
box.marginBottom = 0;
|
|
4020
|
+
box.marginLeft = 0;
|
|
4021
|
+
if (box.kind !== 'table-cell') {
|
|
4022
|
+
box.padTop = 0;
|
|
4023
|
+
box.padRight = 0;
|
|
4024
|
+
box.padBottom = 0;
|
|
4025
|
+
box.padLeft = 0;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (box.kind === 'table' || box.kind === 'table-cell') collapseEdges(box);
|
|
795
4029
|
}
|
|
796
4030
|
|
|
797
4031
|
function edge(len: Len, containingWidth: number): number {
|
|
798
4032
|
if (len === AUTO) return 0;
|
|
799
|
-
|
|
800
|
-
return Number.isFinite(containingWidth)
|
|
801
|
-
? (len.pct / 100) * containingWidth
|
|
802
|
-
: 0;
|
|
803
|
-
return len;
|
|
4033
|
+
return resolve(len, containingWidth, 0);
|
|
804
4034
|
}
|
|
805
4035
|
|
|
806
4036
|
/** Whether every in-flow child is inline-level, which is what makes this box
|
|
@@ -816,9 +4046,6 @@ function establishesInlineContext(box: Box): boolean {
|
|
|
816
4046
|
case 'break':
|
|
817
4047
|
sawInline = true;
|
|
818
4048
|
break;
|
|
819
|
-
case 'replaced':
|
|
820
|
-
sawInline = true;
|
|
821
|
-
break;
|
|
822
4049
|
default:
|
|
823
4050
|
if (isInlineLevel(child)) sawInline = true;
|
|
824
4051
|
else return false;
|
|
@@ -834,7 +4061,9 @@ function isInlineLevel(box: Box): boolean {
|
|
|
834
4061
|
d === 'inline' ||
|
|
835
4062
|
d === 'inline-block' ||
|
|
836
4063
|
d === 'inline-flex' ||
|
|
837
|
-
d === 'inline-table'
|
|
4064
|
+
d === 'inline-table' ||
|
|
4065
|
+
// an image told to be a table's part is inline (CSS Display 3, 2.4)
|
|
4066
|
+
(box.kind === 'replaced' && d.startsWith('table-'))
|
|
838
4067
|
);
|
|
839
4068
|
}
|
|
840
4069
|
|
|
@@ -842,8 +4071,13 @@ function isInlineLevel(box: Box): boolean {
|
|
|
842
4071
|
* floats, and does not collapse margins through its edges. */
|
|
843
4072
|
export function establishesBFC(box: Box): boolean {
|
|
844
4073
|
const style = box.style;
|
|
845
|
-
if (style
|
|
846
|
-
|
|
4074
|
+
if (scrolls(style)) return true;
|
|
4075
|
+
if (style.flowRoot) return true;
|
|
4076
|
+
// layout and paint containment make an independent formatting context
|
|
4077
|
+
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT)) return true;
|
|
4078
|
+
// `continue: collapse` makes a block container a formatting context of
|
|
4079
|
+
// its own (CSS Overflow 4, 5.3)
|
|
4080
|
+
if (style.lineClamp !== null) return true;
|
|
847
4081
|
if (style.float !== 'none') return true;
|
|
848
4082
|
if (style.position === 'absolute' || style.position === 'fixed') return true;
|
|
849
4083
|
if (
|
|
@@ -853,24 +4087,142 @@ export function establishesBFC(box: Box): boolean {
|
|
|
853
4087
|
) {
|
|
854
4088
|
return true;
|
|
855
4089
|
}
|
|
856
|
-
if (
|
|
4090
|
+
if (
|
|
4091
|
+
box.kind === 'table-cell' ||
|
|
4092
|
+
box.kind === 'table-caption' ||
|
|
4093
|
+
box.kind === 'table'
|
|
4094
|
+
) {
|
|
4095
|
+
return true;
|
|
4096
|
+
}
|
|
4097
|
+
// The root element establishes the document's formatting context. Here it
|
|
4098
|
+
// is a box below the synthetic initial containing block, so it is named:
|
|
4099
|
+
// without it, <html>'s own margin collapsed with <body>'s first block's.
|
|
4100
|
+
if (box.el?.name === 'html') return true;
|
|
857
4101
|
return box.parent === null;
|
|
858
4102
|
}
|
|
859
4103
|
|
|
860
4104
|
/** What a box's `position: relative` offset moves it by, applied after
|
|
861
4105
|
* layout so it does not affect anything else's position — which is the
|
|
862
|
-
* whole of what makes it *relative
|
|
4106
|
+
* whole of what makes it *relative* — and its translation with it. */
|
|
863
4107
|
export function applyRelativeOffsets(box: Box): void {
|
|
864
4108
|
for (const child of box.children) applyRelativeOffsets(child);
|
|
865
4109
|
const style = box.style;
|
|
4110
|
+
if (transformed(style) && box.kind !== 'inline') {
|
|
4111
|
+
// a transform moves the box it is on, and an inline box is none
|
|
4112
|
+
const [dx, dy] = translationOf(box);
|
|
4113
|
+
if (dx || dy) {
|
|
4114
|
+
translate(box, dx, dy);
|
|
4115
|
+
movedOffLine(box);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
866
4118
|
if (style.position !== 'relative' && style.position !== 'sticky') return;
|
|
4119
|
+
const [dx, dy] = relativeOffset(box);
|
|
4120
|
+
if (!(dx || dy)) return;
|
|
4121
|
+
translate(box, dx, dy);
|
|
4122
|
+
if (box.kind !== 'inline') {
|
|
4123
|
+
movedOffLine(box);
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
offsetInline(box, dx, dy);
|
|
4127
|
+
// the blocks it was broken around move with it (9.2.1.1)
|
|
4128
|
+
const blocks = box.cut ? CUT_BLOCKS.get(box) : undefined;
|
|
4129
|
+
if (blocks) for (const block of blocks) translate(block, dx, dy);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
/** Whether a box is transformed, which is what makes it a containing
|
|
4133
|
+
* block and a layer as a positioned box is (CSS Transforms 1, 2). */
|
|
4134
|
+
export function transformed(style: ComputedStyle): boolean {
|
|
4135
|
+
return style.translate !== null || style.transformTranslate !== null;
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
/** How far `translate` and a `transform` move a box: a percentage is of
|
|
4139
|
+
* its own border box (CSS Transforms 1, 7). */
|
|
4140
|
+
function translationOf(box: Box): [number, number] {
|
|
4141
|
+
let dx = 0;
|
|
4142
|
+
let dy = 0;
|
|
4143
|
+
for (const moved of [box.style.translate, box.style.transformTranslate]) {
|
|
4144
|
+
if (!moved) continue;
|
|
4145
|
+
dx += resolve(moved[0], box.width, 0);
|
|
4146
|
+
dy += resolve(moved[1], box.height, 0);
|
|
4147
|
+
}
|
|
4148
|
+
return [dx, dy];
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
/** How far `position: relative` moves a box. */
|
|
4152
|
+
function relativeOffset(box: Box): [number, number] {
|
|
4153
|
+
const style = box.style;
|
|
867
4154
|
const parentWidth = box.parent ? box.parent.contentWidth : 0;
|
|
868
|
-
const parentHeight = box.parent ? box.parent.contentHeight : 0;
|
|
869
4155
|
const left = resolveOrNull(style.left, parentWidth);
|
|
870
4156
|
const right = resolveOrNull(style.right, parentWidth);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
4157
|
+
// a percentage down is of a height the containing block sets, as a
|
|
4158
|
+
// percentage height is, and `auto` where its content decides it
|
|
4159
|
+
const top = resolveOrNull(style.top, box.percentHeightBase);
|
|
4160
|
+
const bottom = resolveOrNull(style.bottom, box.percentHeightBase);
|
|
4161
|
+
// both set is over-constrained, and the containing block's direction
|
|
4162
|
+
// says which wins: `left` left to right, `right` right to left (9.4.3)
|
|
4163
|
+
const rtl = box.parent?.style.direction === 'rtl';
|
|
4164
|
+
const dx = right !== null && (left === null || rtl) ? -right : (left ?? 0);
|
|
874
4165
|
const dy = top ?? (bottom !== null ? -bottom : 0);
|
|
875
|
-
|
|
4166
|
+
return [dx, dy];
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
/**
|
|
4170
|
+
* Move an inline box's text where `position: relative` puts the box. Its
|
|
4171
|
+
* text is on its block's lines, laid out apart from the text around it
|
|
4172
|
+
* (`collect`), and those fragments are what move; the lines stay where
|
|
4173
|
+
* they are (CSS 2.1 9.4.3). A box inside it has moved its own already, so
|
|
4174
|
+
* a text in both moves by the two together.
|
|
4175
|
+
*/
|
|
4176
|
+
function offsetInline(box: Box, dx: number, dy: number): void {
|
|
4177
|
+
let block = box.parent;
|
|
4178
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
4179
|
+
const lines = block?.lines;
|
|
4180
|
+
if (!lines) return;
|
|
4181
|
+
INLINE_OFFSETS.set(box, { x: dx, y: dy });
|
|
4182
|
+
for (const line of lines) {
|
|
4183
|
+
for (const text of line.texts) {
|
|
4184
|
+
const owner = text.spans.boxAt?.(text.layoutStart);
|
|
4185
|
+
if (!owner || !holds(box, owner)) continue;
|
|
4186
|
+
text.drawX += dx;
|
|
4187
|
+
text.drawY += dy;
|
|
4188
|
+
const was = TEXT_SHIFTS.get(text);
|
|
4189
|
+
TEXT_SHIFTS.set(text, {
|
|
4190
|
+
x: (was?.x ?? 0) + dx,
|
|
4191
|
+
y: (was?.y ?? 0) + dy,
|
|
4192
|
+
});
|
|
4193
|
+
SHIFTED_LINES.add(line);
|
|
4194
|
+
markMovedOff(lines, line);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
/** Mark the line an inline-block or an image a box moved off was placed
|
|
4200
|
+
* on, where it was on one: the line stays, and the box is drawn where it
|
|
4201
|
+
* went (`MOVED_OFF_LINES`). */
|
|
4202
|
+
function movedOffLine(box: Box): void {
|
|
4203
|
+
let block = box.parent;
|
|
4204
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
4205
|
+
const lines = block?.lines;
|
|
4206
|
+
if (!lines) return;
|
|
4207
|
+
for (const line of lines) {
|
|
4208
|
+
for (const placed of line.atomics) {
|
|
4209
|
+
if (placed.box !== box) continue;
|
|
4210
|
+
markMovedOff(lines, line);
|
|
4211
|
+
return;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
function markMovedOff(lines: LineBox[], line: LineBox): void {
|
|
4217
|
+
const moved = MOVED_OFF_LINES.get(lines);
|
|
4218
|
+
if (moved) moved.add(line);
|
|
4219
|
+
else MOVED_OFF_LINES.set(lines, new Set([line]));
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/** Whether `inner` is `outer` or inside it. */
|
|
4223
|
+
function holds(outer: Box, inner: Box): boolean {
|
|
4224
|
+
for (let at: Box | null = inner; at; at = at.parent) {
|
|
4225
|
+
if (at === outer) return true;
|
|
4226
|
+
}
|
|
4227
|
+
return false;
|
|
876
4228
|
}
|