/** * Copyright 2021 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const enum Comparator { LESS_THAN, LESS_OR_EQUAL, GREATER_THAN, GREATER_OR_EQUAL, } interface SizeQuery { type: ContainerConditionType.SizeQuery; feature: string; comparator: Comparator; threshold: number; } interface ContainerConditionConjunction { type: ContainerConditionType.ContainerConditionConjunction; left: ContainerCondition; right: ContainerCondition; } interface ContainerConditionDisjunction { type: ContainerConditionType.ContainerConditionDisjunction; left: ContainerCondition; right: ContainerCondition; } interface ContainerConditionNegation { type: ContainerConditionType.ContainerConditionNegation; right: ContainerCondition; } enum ContainerConditionType { SizeQuery, ContainerConditionConjunction, ContainerConditionDisjunction, ContainerConditionNegation, } type ContainerCondition = | SizeQuery | ContainerConditionConjunction | ContainerConditionDisjunction | ContainerConditionNegation; interface ContainerQueryDescriptor { name?: string; condition: ContainerCondition; className: string; rules: Rule[]; } function uid(): string { return Array.from({ length: 16 }, () => Math.floor(Math.random() * 256).toString(16) ).join(""); } function translateToLogicalProp(feature: string): string { switch (feature.toLowerCase()) { case "inlinesize": return "inlineSize"; case "blocksize": return "blockSize"; case "width": return "inlineSize"; case "height": return "blockSize"; default: throw Error(`Unknown feature name ${feature} in container query`); } } function isSizeQueryFulfilled( condition: SizeQuery, borderBox: ResizeObserverSize ): boolean { const value = borderBox[translateToLogicalProp(condition.feature)]; switch (condition.comparator) { case Comparator.GREATER_OR_EQUAL: return value >= condition.threshold; case Comparator.GREATER_THAN: return value > condition.threshold; case Comparator.LESS_OR_EQUAL: return value <= condition.threshold; case Comparator.LESS_THAN: return value < condition.threshold; } } function isQueryFullfilled_internal( condition: ContainerCondition, borderBox: ResizeObserverSize ): boolean { switch (condition.type) { case ContainerConditionType.ContainerConditionConjunction: return ( isQueryFullfilled_internal(condition.left, borderBox) && isQueryFullfilled_internal(condition.right, borderBox) ); case ContainerConditionType.ContainerConditionDisjunction: return ( isQueryFullfilled_internal(condition.left, borderBox) || isQueryFullfilled_internal(condition.right, borderBox) ); case ContainerConditionType.ContainerConditionNegation: return !isQueryFullfilled_internal(condition.right, borderBox); case ContainerConditionType.SizeQuery: return isSizeQueryFulfilled(condition, borderBox); default: throw Error("wtf?"); } } function isQueryFullfilled( condition: ContainerCondition, entry: ResizeObserverEntry ): boolean { let borderBox; if ("borderBoxSize" in entry) { // At the time of writing, the array will always be length one in Chrome. // In Firefox, it won’t be an array, but a single object. borderBox = entry.borderBoxSize?.[0] ?? entry.borderBoxSize; } else { // Safari doesn’t have borderBoxSize at all, but only offers `contentRect`, // so we have to do some maths ourselves. const computed = getComputedStyle(entry.target); borderBox = { // FIXME: This will if you are not in tblr writing mode blockSize: entry.contentRect.height, inlineSize: entry.contentRect.width, }; // Cut off the "px" suffix from the computed styles. borderBox.blockSize += parseInt(computed.paddingBlockStart.slice(0, -2)) + parseInt(computed.paddingBlockEnd.slice(0, -2)); borderBox.inlineSize += parseInt(computed.paddingInlineStart.slice(0, -2)) + parseInt(computed.paddingInlineEnd.slice(0, -2)); } return isQueryFullfilled_internal(condition, borderBox); } function findParentContainer(el: Element, name?: string): Element | null { while (el) { el = el.parentElement; if (!containerNames.has(el)) continue; if (name) { const containerName = containerNames.get(el)!; if (!containerName.includes(name)) continue; } return el; } return null; } const containerNames: WeakMap = new WeakMap(); function registerContainer(el: Element, name: string) { containerRO.observe(el); if (!containerNames.has(el)) { containerNames.set(el, []); } containerNames.get(el)!.push(name); } const queries: Array = []; function registerContainerQuery(cqd: ContainerQueryDescriptor) { queries.push(cqd); } const containerRO = new ResizeObserver((entries) => { const changedContainers: Map = new Map( entries.map((entry) => [entry.target, entry]) ); for (const query of queries) { for (const { selector } of query.rules) { const els = document.querySelectorAll(selector); for (const el of els) { const container = findParentContainer(el, query.name); if (!container) continue; if (!changedContainers.has(container)) continue; const entry = changedContainers.get(container); el.classList.toggle( query.className, isQueryFullfilled(query.condition, entry) ); } } } }); interface WatchedSelector { selector: string; name: string; } const watchedContainerSelectors: WatchedSelector[] = []; const containerMO = new MutationObserver((entries) => { for (const entry of entries) { for (const node of entry.removedNodes) { if (!(node instanceof HTMLElement)) continue; containerRO.unobserve(node); } for (const node of entry.addedNodes) { if (!(node instanceof HTMLElement)) continue; for (const watchedContainerSelector of watchedContainerSelectors) { // Check if the node itself is a container, and if so, start watching it. if (node.matches(watchedContainerSelector.selector)) { registerContainer(node, watchedContainerSelector.name); } // If the node was added with children, the children will NOT get their own // MO events, so we need to check the children manually. for (const container of node.querySelectorAll( watchedContainerSelector.selector )) { registerContainer(container, watchedContainerSelector.name); } } } } }); containerMO.observe(document.documentElement, { childList: true, subtree: true, }); interface AdhocParser { sheetSrc: string; index: number; name?: string; } // Loosely inspired by // https://drafts.csswg.org/css-syntax/#parser-diagrams export function transpileStyleSheet(sheetSrc: string, srcUrl?: string): string { const p: AdhocParser = { sheetSrc, index: 0, name: srcUrl, }; while (p.index < p.sheetSrc.length) { eatWhitespace(p); if (p.index >= p.sheetSrc.length) break; if (lookAhead("/*", p)) { while (lookAhead("/*", p)) { eatComment(p); eatWhitespace(p); } continue; } if (lookAhead("@container", p)) { const { query, startIndex, endIndex } = parseContainerQuery(p); const replacement = stringifyContainerQuery(query); replacePart(startIndex, endIndex, replacement, p); registerContainerQuery(query); } else { const rule = parseQualifiedRule(p); if (!rule) continue; handleContainerProps(rule, p); } } // If this sheet has no srcURL (like from a