Skip to content

Commit

Permalink
Merge pull request #1444 from vivliostyle/feat/page-float-update
Browse files Browse the repository at this point in the history
feat: Support extended CSS Page float values such as `float: top right`
  • Loading branch information
MurakamiShinyu authored Jan 17, 2025
2 parents 7d8a921 + 3075654 commit 5b094aa
Show file tree
Hide file tree
Showing 19 changed files with 1,597 additions and 62 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/vivliostyle/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ border-image-outset = [NUM | LENGTH]{1,4};
border-image-repeat = [ stretch | repeat | round | space ]{1,2};
bottom = APLENGTH;
caption-side = top | bottom;
clear = none | left | right | top | bottom | both | all | same;
clear = none | left | right | top | bottom | inline-start | inline-end | block-start | block-end | both | all | same;
clip = rect(ALENGTH{4}) | rect(SPACE(ALENGTH{4})) | auto;
color = COLOR;
LIST_STYLE_TYPE = IDENT;
Expand Down Expand Up @@ -510,7 +510,7 @@ crop-offset = auto | LENGTH;
/* CSS Page Floats */
float-reference = inline | column | region | page;
float = block-start | block-end | inline-start | inline-end | snap-block | snap-inline | left | right | top | bottom | none | footnote;
float = none | footnote | [ block-start || block-end || inline-start || inline-end || snap-block || snap-inline || left || right || top || bottom ];
float-min-wrap-block = PPLENGTH;
/* CSS Ruby */
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/vivliostyle/css-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
import * as Css from "./css";
import * as CssTokenizer from "./css-tokenizer";
import * as Logging from "./logging";
import * as Base from "./base";
import { ValidationTxt } from "./assets";
import { TokenType } from "./css-tokenizer";

Expand Down Expand Up @@ -2099,6 +2099,8 @@ export class ValidatorSet {
if (CSS.supports(name, value.toString())) {
// Browser supports this property
receiver.simpleProperty(origName, value, important);
} else if (prefix && !Base.knownPrefixes.includes(`-${prefix}-`)) {
// Ignore properties with unknown prefix to avoid unnecessary warnings
} else {
receiver.unknownProperty(origName, value);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/vivliostyle/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ export const ident: { [key: string]: Ident } = {
same: getName("same"),
scale: getName("scale"),
snap_block: getName("snap-block"),
snap_inline: getName("snap-inline"),
solid: getName("solid"),
spread: getName("spread"),
_static: getName("static"),
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/vivliostyle/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ export function isRunning(position: Css.Val): boolean {
export function getComputedDisplayValue(
display: Css.Ident,
position: Css.Ident,
float: Css.Ident,
float: Css.Val,
isRoot: boolean,
): { display: Css.Ident; position: Css.Ident; float: Css.Ident } {
): { display: Css.Ident; position: Css.Ident; float: Css.Val } {
if (display === Css.ident.none) {
// no need to convert values when 'display' is 'none'
} else if (isAbsolutelyPositioned(position)) {
Expand All @@ -107,7 +107,7 @@ export function getComputedDisplayValue(
export function isBlock(
display: Css.Ident,
position: Css.Ident,
float: Css.Ident,
float: Css.Val,
isRoot: boolean,
): boolean {
return (
Expand Down Expand Up @@ -149,7 +149,7 @@ export function isRubyInternalDisplay(display: Css.Ident | string): boolean {
export function establishesBFC(
display: Css.Ident,
position: Css.Ident,
float: Css.Ident,
float: Css.Val,
overflow: Css.Ident,
writingMode?: Css.Ident,
parentWritingMode?: Css.Ident,
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/vivliostyle/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2663,7 +2663,14 @@ export class Column extends VtreeImpl.Container implements Layout.Column {
this,
);
}
switch (clear) {
const clearLR = /^(top|bottom|(block|inline)-(start|end))$/.test(clear)
? PageFloats.resolveInlineFloatDirection(
clear,
nodeContext.vertical,
nodeContext.direction,
)
: clear;
switch (clearLR) {
case "left":
clearEdge = dir * Math.max(clearEdge * dir, this.leftFloatEdge * dir);
break;
Expand Down
190 changes: 143 additions & 47 deletions packages/core/src/vivliostyle/page-floats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,14 +805,65 @@ export class PageFloatLayoutContext
return CssLogicalUtil.toPhysical(side, writingMode, direction);
}

private toLogicalFloatSides(floatSide: string): string[] {
const sides = floatSide.split(" ");

// Convert to logical sides and remove duplicates
const logicalSides: string[] = [];
for (const side of sides) {
const logicalSide = this.toLogical(side);
if (!logicalSides.includes(logicalSide)) {
logicalSides.push(logicalSide);
}
}

// Convert "block-start block-end" to "snap-block" and
// "inline-start inline-end" to "snap-inline".
// More precisely, when multiple "*block*" values are found
// convert the first one to "snap-block" and remove the rest,
// and when multiple "*inline*" values are found convert the
// first one to "snap-inline" and remove the rest.
const logicalFloatSides: string[] = [];
let foundSnapBlock = false;
let foundSnapInline = false;
for (let i = 0; i < logicalSides.length; i++) {
const side = logicalSides[i];
if (side.includes("block")) {
if (!foundSnapBlock) {
// Convert to "snap-block" if another block side is found
if (logicalSides.slice(i + 1).some((s) => s.includes("block"))) {
logicalFloatSides.push("snap-block");
foundSnapBlock = true;
} else {
logicalFloatSides.push(side);
}
}
} else if (side.includes("inline")) {
if (!foundSnapInline) {
// Convert to "snap-inline" if another inline side is found
if (logicalSides.slice(i + 1).some((s) => s.includes("inline"))) {
logicalFloatSides.push("snap-inline");
foundSnapInline = true;
} else {
logicalFloatSides.push(side);
}
}
}
}

return logicalFloatSides;
}

removeEndFloatFragments(floatSide: string) {
const logicalFloatSide = this.toLogical(floatSide);
const logicalFloatSide = this.toLogicalFloatSides(floatSide)[0];
if (logicalFloatSide === "block-end" || logicalFloatSide === "inline-end") {
let i = 0;
while (i < this.floatFragments.length) {
const fragment = this.floatFragments[i];
const logicalFloatSide2 = this.toLogical(fragment.floatSide);
if (logicalFloatSide2 === logicalFloatSide) {
const fragmentFloatSide = this.toLogicalFloatSides(
fragment.floatSide,
)[0];
if (fragmentFloatSide === logicalFloatSide) {
this.removePageFloatFragment(fragment);
} else {
i++;
Expand All @@ -827,20 +878,25 @@ export class PageFloatLayoutContext
this.getParent(floatReference).stashEndFloatFragments(float);
return;
}
const logicalFloatSide = this.toLogical(float.floatSide);
const logicalFloatSide = this.toLogicalFloatSides(float.floatSide)[0];
if (
logicalFloatSide === "block-end" ||
logicalFloatSide === "snap-block" ||
logicalFloatSide === "inline-end"
logicalFloatSide === "inline-end" ||
logicalFloatSide === "snap-inline"
) {
let i = 0;
while (i < this.floatFragments.length) {
const fragment = this.floatFragments[i];
const fragmentFloatSide = this.toLogical(fragment.floatSide);
const fragmentFloatSide = this.toLogicalFloatSides(
fragment.floatSide,
)[0];
if (
(fragmentFloatSide === logicalFloatSide ||
(logicalFloatSide === "snap-block" &&
fragmentFloatSide === "block-end")) &&
fragmentFloatSide === "block-end") ||
(logicalFloatSide === "snap-inline" &&
fragmentFloatSide === "inline-end")) &&
fragment.shouldBeStashedBefore(float)
) {
this.stashedFloatFragments.push(fragment);
Expand Down Expand Up @@ -999,7 +1055,7 @@ export class PageFloatLayoutContext
if (condition && !condition(f, this)) {
return l;
}
const logicalFloatSide = this.toLogical(f.floatSide);
const logicalFloatSide = this.toLogicalFloatSides(f.floatSide)[0];
const area = f.area;
const floatMinWrapBlock = f.continuations[0].float.floatMinWrapBlock;
let top = l.top;
Expand Down Expand Up @@ -1112,13 +1168,17 @@ export class PageFloatLayoutContext
condition,
);
}
let logicalFloatSide = this.toLogical(floatSide);
if (logicalFloatSide === "snap-block") {
const logicalFloatSides = this.toLogicalFloatSides(floatSide);
if (logicalFloatSides[0] === "snap-block") {
if (!condition["block-start"] && !condition["block-end"]) {
return null;
}
} else if (logicalFloatSides[0] === "snap-inline") {
if (!condition["inline-start"] && !condition["inline-end"]) {
return null;
}
} else {
if (!condition[logicalFloatSide]) {
if (!condition[logicalFloatSides[0]]) {
return null;
}
}
Expand Down Expand Up @@ -1194,9 +1254,10 @@ export class PageFloatLayoutContext
)
: new GeometryUtil.Rect(inlineStart, blockStart, inlineEnd, blockEnd);
if (
logicalFloatSide === "block-start" ||
logicalFloatSide === "snap-block" ||
logicalFloatSide === "inline-start"
logicalFloatSides[0] === "block-start" ||
logicalFloatSides[0] === "snap-block" ||
logicalFloatSides[0] === "inline-start" ||
logicalFloatSides[0] === "snap-inline"
) {
if (
!limitBlockStartEndValueWithOpenRect(
Expand All @@ -1208,9 +1269,10 @@ export class PageFloatLayoutContext
}
}
if (
logicalFloatSide === "block-end" ||
logicalFloatSide === "snap-block" ||
logicalFloatSide === "inline-end"
logicalFloatSides[0] === "block-end" ||
logicalFloatSides[0] === "snap-block" ||
logicalFloatSides[0] === "inline-end" ||
logicalFloatSides[0] === "snap-inline"
) {
if (
!limitBlockStartEndValueWithOpenRect(
Expand All @@ -1232,10 +1294,10 @@ export class PageFloatLayoutContext
blockSize = area.computedBlockSize;
outerBlockSize = blockSize + area.getInsetBefore() + area.getInsetAfter();
const availableBlockSize = (blockEnd - blockStart) * area.getBoxDir();
if (logicalFloatSide === "snap-block") {
if (logicalFloatSides[0] === "snap-block") {
if (anchorEdge === null) {
// Deferred from previous container
logicalFloatSide = "block-start";
logicalFloatSides[0] = "block-start";
} else {
const containerRect = this.container.getPaddingRect();
const fromStart =
Expand All @@ -1248,14 +1310,28 @@ export class PageFloatLayoutContext
anchorEdge -
outerBlockSize);
if (fromStart <= fromEnd) {
logicalFloatSide = "block-start";
logicalFloatSides[0] = "block-start";
} else {
logicalFloatSide = "block-end";
logicalFloatSides[0] = "block-end";
}
}
if (!condition[logicalFloatSide]) {
if (!condition[logicalFloatSides[0]]) {
if (condition["block-end"]) {
logicalFloatSide = "block-end";
logicalFloatSides[0] = "block-end";
} else {
return null;
}
}
} else if (logicalFloatSides[0] === "snap-inline") {
if (anchorEdge === null) {
// Deferred from previous container
logicalFloatSides[0] = "inline-start";
} else {
// FIXME: snap-inline should be resolved based on the anchor's inline position
if (condition["inline-start"]) {
logicalFloatSides[0] = "inline-start";
} else if (condition["inline-end"]) {
logicalFloatSides[0] = "inline-end";
} else {
return null;
}
Expand All @@ -1265,9 +1341,12 @@ export class PageFloatLayoutContext
return null;
}
if (
logicalFloatSide === "inline-start" ||
logicalFloatSide === "inline-end"
logicalFloatSides[0] === "inline-start" ||
logicalFloatSides[0] === "inline-end" ||
logicalFloatSides[1]
) {
// If the page float is "inline-start" or "inline-end" or has two sides
// (e.g. "block-start inline-end"), the inline size is determined by the content size.
inlineSize = Sizing.getSize(area.clientLayout, area.element, [
Sizing.Size.FIT_CONTENT_INLINE_SIZE,
])[Sizing.Size.FIT_CONTENT_INLINE_SIZE];
Expand All @@ -1287,28 +1366,45 @@ export class PageFloatLayoutContext
blockEnd -= blockOffset;
inlineStart -= inlineOffset;
inlineEnd -= inlineOffset;
switch (logicalFloatSide) {
case "inline-start":
case "block-start":
case "snap-block":
area.setInlinePosition(inlineStart, inlineSize);
area.setBlockPosition(blockStart, blockSize);
break;
case "inline-end":
case "block-end":
area.setInlinePosition(
inlineEnd - outerInlineSize * area.getInlineDir(),
inlineSize,
);
area.setBlockPosition(
blockEnd - outerBlockSize * area.getBoxDir(),
blockSize,
);
break;
default:
throw new Error(`unknown float direction: ${floatSide}`);

if (
logicalFloatSides.some(
(s) => s === "inline-start" || s === "snap-inline",
) ||
(logicalFloatSides.length === 1 &&
(logicalFloatSides[0] === "block-start" ||
logicalFloatSides[0] === "snap-block"))
) {
area.setInlinePosition(inlineStart, inlineSize);
} else if (
logicalFloatSides.some((s) => s === "inline-end") ||
(logicalFloatSides.length === 1 && logicalFloatSides[0] === "block-end")
) {
area.setInlinePosition(
inlineEnd - outerInlineSize * area.getInlineDir(),
inlineSize,
);
}
if (
logicalFloatSides.some(
(s) => s === "block-start" || s === "snap-block",
) ||
(logicalFloatSides.length === 1 &&
(logicalFloatSides[0] === "inline-start" ||
logicalFloatSides[0] === "snap-inline"))
) {
area.setBlockPosition(blockStart, blockSize);
} else if (
logicalFloatSides.some((s) => s === "block-end") ||
(logicalFloatSides.length === 1 && logicalFloatSides[0] === "inline-end")
) {
area.setBlockPosition(
blockEnd - outerBlockSize * area.getBoxDir(),
blockSize,
);
}
return logicalFloatSide;

return logicalFloatSides.join(" ");
}

getFloatFragmentExclusions(): GeometryUtil.Shape[] {
Expand Down Expand Up @@ -1428,7 +1524,7 @@ export class PageFloatLayoutContext
if (!clearSide) {
return result;
}
const logicalFloatSide = this.toLogical(floatSide);
const logicalFloatSide = this.toLogicalFloatSides(floatSide)[0];
const logicalClearSide = this.toLogical(clearSide);
let logicalSides: string[];
if (logicalClearSide === "all") {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/vivliostyle/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export type ResolveFormattingContextHook = (
p2: boolean,
p3: Css.Ident,
p4: Css.Ident,
p5: Css.Ident,
p5: Css.Val,
p6: boolean,
) => Vtree.FormattingContext;

Expand Down
Loading

0 comments on commit 5b094aa

Please sign in to comment.