Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/ttml-adapter/specs/Adapter.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3629,6 +3629,43 @@ describe("Style inheritance", () => {
expect(styles?.["color"]).toBe("red");
expect(styles?.["font-size"]).toBe("12px");
});

it("should allow a region to inherit from a global style via its own style attribute", () => {
const adapter = new TTMLAdapter();
const { data: cues } = parseResult(
adapter,
`
<tt xml:lang="en">
<head>
<styling>
<style xml:id="sShared" tts:origin="10% 10%" tts:extent="30% 20%" />
</styling>
<layout>
<region xml:id="r1" style="sShared" />
</layout>
</head>
<body>
<div region="r1">
<p begin="0s" end="1s">Hello</p>
</div>
</body>
</tt>
`,
);

const cue = cues.find((c) => c.content.trim() === "Hello");
const region = cue?.region;

/**
* tts:origin/tts:extent only apply to "region" (not "p"/"span"), so this
* can only be satisfied by the region container itself picking up its
* own referenced style, not by the (correct, separate) cascade of
* shared properties onto cue content.
*/
expect(region?.getOrigin()).toEqual(["10%", "10%"]);
expect(region?.width).toBe("30%");
expect(region?.height).toBe("20%");
});
});
// #endregion

Expand Down
128 changes: 77 additions & 51 deletions packages/ttml-adapter/src/Parser/Scope/RegionContainerContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import {
createAnimationContainerContext,
readScopeAnimationContext,
} from "./AnimationContainerContext.js";
import { createTemporalActiveContext } from "./TemporalActiveContext.js";
import {
createTemporalActiveContext,
readScopeTemporalActiveContext,
} from "./TemporalActiveContext.js";
import { readScopeErrorContext } from "./ErrorContext.js";

const regionContextSymbol = Symbol("region");

Expand Down Expand Up @@ -178,6 +182,52 @@ function extractNestedStylesChildren(
});
}

/**
* Styles referenced by region must be already defined in the scope chain and in the document.
* Therefore we do not need to extract them and duplicate them. We just need to check them.
*/
function validateOutOfLineStyleIDREFS(idrefs: string | undefined, scope: Scope): string[] {
if (!idrefs?.length) {
return [];
}

const styleContext = readScopeStyleContainerContext(scope);
const errorContext = readScopeErrorContext(scope)!;

if (!styleContext) {
errorContext.report(
new Error(
`Region referenced style(s) '${idrefs}', but no out-of-line styles were defined in this document. Ignored.`,
),
false,
);

return [];
}

const idrefsStyleList = idrefs!.split(/\s+/);
const referencialStyles: string[] = [];

Comment on lines +208 to +210
for (const idref of idrefsStyleList) {
const style = styleContext.getStyleByIDRef(idref);

if (!style) {
errorContext.report(
new Error(
`Region referenced style '${idref}', but no such out-of-line style was defined in this document. Ignored.`,
),
false,
);

continue;
}

referencialStyles.push(idref);
}

return referencialStyles;
}

// ***************************** //
// *** ANIMATIONS EXTRACTION *** //
// ***************************** //
Expand Down Expand Up @@ -235,10 +285,11 @@ function createTTMLRegion(
}) ||
undefined;

const inlineStyles = extractInlineStyles(attributes);
const referencialStyles = validateOutOfLineStyleIDREFS(attributes["style"], sourceScope);
const nestedStyles = extractNestedStylesChildren(children);
const inlineStyles = extractInlineStyles(attributes);

const styleIds = [inlineStyles["xml:id"], nestedStyles["xml:id"]];
const styleIds = [...referencialStyles, nestedStyles["xml:id"], inlineStyles["xml:id"]];

const animations = extractNestedAnimationsChildren(children);

Expand Down Expand Up @@ -349,7 +400,15 @@ function getRegionStylesByScope(scope: Scope): TTMLStyle[] {
return styleContext.styles;
}

const REGION_GEOMETRY_ATTRIBUTES = new Set(["tts:origin", "tts:extent", "tts:position"]);
const REGION_GEOMETRY_CSS_PROPERTIES = new Set([
"x",
"y",
"left",
"top",
"width",
"height",
"position",
]);

export class TTMLRegion implements Region {
public id: string;
Expand Down Expand Up @@ -386,61 +445,28 @@ export class TTMLRegion implements Region {
}
}

function computeRegionVisualStylesByScope(scope: Scope): Record<string, string> {
const styleContext = isolateContext(readScopeStyleContainerContext(scope));
function computeRegionStylesByScope(scope: Scope): Record<string, string> {
const temporalActiveContext = isolateContext(readScopeTemporalActiveContext(scope));

if (!styleContext) {
if (!temporalActiveContext) {
return {};
Comment on lines +448 to 452
}

const { styles } = styleContext;

return styles
.filter((s) => s.kind === "nested")
.concat(styles.filter((s) => s.kind === "inline"))
.reduce<Record<string, string>>((acc, style) => {
const visualStyleAttributes: Record<string, string> = {};

for (const attr in style.styleAttributes) {
if (!REGION_GEOMETRY_ATTRIBUTES.has(attr)) {
visualStyleAttributes[attr] = style.styleAttributes[attr]!;
}
}
return temporalActiveContext.computeStylesForElement("region");
}

const filteredStyle: TTMLStyle = Object.create(style, {
styleAttributes: {
value: visualStyleAttributes,
enumerable: true,
},
});
function computeRegionVisualStylesByScope(scope: Scope): Record<string, string> {
const styles = computeRegionStylesByScope(scope);

return Object.assign(acc, filteredStyle.apply("region"));
}, {});
return Object.fromEntries(
Object.entries(styles).filter(([attr]) => !REGION_GEOMETRY_CSS_PROPERTIES.has(attr)),
);
}

export function computeRegionGeometryStylesByScope(scope: Scope): Record<string, string> {
const styleContext = isolateContext(readScopeStyleContainerContext(scope));

if (!styleContext) {
return {};
}
const styles = computeRegionStylesByScope(scope);

const { styles } = styleContext;

return styles
.filter((s) => s.kind === "nested")
.concat(styles.filter((s) => s.kind === "inline"))
.reduce<Record<string, string>>((acc, style) => {
const filtered = Object.fromEntries(
Object.entries(style.styleAttributes).filter(([attr]) =>
REGION_GEOMETRY_ATTRIBUTES.has(attr),
),
);

const filteredStyle = Object.create(style, {
styleAttributes: { value: filtered, enumerable: true },
});

return Object.assign(acc, filteredStyle.apply("region"));
}, {});
return Object.fromEntries(
Object.entries(styles).filter(([attr]) => REGION_GEOMETRY_CSS_PROPERTIES.has(attr)),
);
}