From e078c75cb95bb318e8b7d1f4e8a6f7c7e36ae57e Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Mon, 22 Aug 2022 18:12:57 -0400 Subject: [PATCH 01/11] First attempt at new parsing. --- public/anchor-positioning.css | 2 +- src/parse.ts | 169 ++++++++++++++++++++++++++-------- src/transform.ts | 18 +++- 3 files changed, 147 insertions(+), 42 deletions(-) diff --git a/public/anchor-positioning.css b/public/anchor-positioning.css index 502347f5..e05b43d6 100644 --- a/public/anchor-positioning.css +++ b/public/anchor-positioning.css @@ -10,6 +10,6 @@ #my-floating-positioning { position: absolute; top: anchor(--my-anchor-positioning bottom); - left: anchor(--my-anchor-positioning right); + left: anchor(--my-anchor-positioning right, 50px); background: green; } diff --git a/src/parse.ts b/src/parse.ts index 31075188..8f4e9991 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,63 +1,156 @@ import * as csstree from 'css-tree'; -interface TryBlockMap { - [key: string]: string; +interface DeclarationWithValue extends csstree.Declaration { + value: csstree.Value; } -interface PositionFallbackRulesMap { - [key: string]: TryBlockMap[]; +interface AtRuleRaw extends csstree.Atrule { + prelude: csstree.Raw | null; } -export function isFallbackDeclaration(node: csstree.CssNode) { - return ( - node.type === 'Declaration' && node.property.includes('position-fallback') - ); +export function isDeclaration( + node: csstree.CssNode, +): node is DeclarationWithValue { + return node.type === 'Declaration'; } -export function isFallbackAtRule(node: csstree.CssNode) { - return node.type === 'Atrule' && node.name.includes('position-fallback'); +export function isAnchorNameDeclaration( + node: csstree.CssNode, +): node is DeclarationWithValue { + return node.type === 'Declaration' && node.property === 'anchor-name'; } -function parsePositionFallback(ast: csstree.CssNode) { - const parsedFallbackRules = csstree.findAll( - ast, - isFallbackAtRule, - ) as csstree.Atrule[]; - const fallbacks: PositionFallbackRulesMap = {}; +export function isAnchorFunction( + node: csstree.CssNode | null, +): node is csstree.FunctionNode { + return Boolean(node && node.type === 'Function' && node.name === 'anchor'); +} - if (parsedFallbackRules) { - parsedFallbackRules.forEach((fallback) => { - const fallbackName = (fallback.prelude as csstree.Raw).value; - const fallbackTryBlocks: TryBlockMap[] = []; +export function isFallbackDeclaration( + node: csstree.CssNode, +): node is DeclarationWithValue { + return node.type === 'Declaration' && node.property === 'position-fallback'; +} - const children = (fallback.block?.children || []) as csstree.Atrule[]; - children.forEach((childBlock) => { - const tryBlock: TryBlockMap = {}; +export function isFallbackAtRule(node: csstree.CssNode): node is AtRuleRaw { + return node.type === 'Atrule' && node.name === 'position-fallback'; +} - if (childBlock.block?.children) { - ( - childBlock.block?.children as csstree.List - ).forEach((child) => { - tryBlock[child.property] = (child.value as csstree.Raw).value; - }); - } +export function isTryAtRule(node: csstree.CssNode): node is AtRuleRaw { + return node.type === 'Atrule' && node.name === 'try'; +} - fallbackTryBlocks.push(tryBlock); +function parseAnchorFn(node: csstree.FunctionNode) { + let anchorName: string | undefined, + anchorEdge: string | undefined, + fallbackValue = '', + foundComma = false; + node.children.toArray().forEach((child, idx) => { + if (foundComma) { + fallbackValue = `${fallbackValue}${csstree.generate(child)}`; + return; + } + if (child.type === 'Operator' && child.value === ',') { + foundComma = true; + return; + } + if (child.type === 'Identifier' && child.name) { + switch (idx) { + case 0: + anchorName = child.name; + break; + case 1: + anchorEdge = child.name; + break; + } + } + }); + return { + anchorName, + anchorEdge, + fallbackValue, + }; +} + +export function getDataFromCSS(css: string) { + const anchorNames: any = {}; + const anchorFunctions: any = []; + const fallbackDeclarations: any = {}; + const fallbacks: any = {}; + const ast = parseCSS(css); + csstree.walk(ast, function (node) { + const rule = this.rule?.prelude as csstree.Raw | undefined; + if ( + isAnchorNameDeclaration(node) && + node.value.children.first && + rule?.value + ) { + const name = (node.value.children.first as unknown as csstree.Identifier) + .name; + if (anchorNames[name]) { + anchorNames[name].push(rule.value); + } else { + anchorNames[name] = [rule.value]; + } + } + if (isAnchorFunction(node) && rule?.value && this.declaration) { + const { anchorName, anchorEdge, fallbackValue } = parseAnchorFn(node); + anchorFunctions.push({ + floatingEl: rule.value, + floatingEdge: this.declaration.property, + anchorName, + anchorEdge, + fallbackValue, }); - fallbacks[fallbackName] = fallbackTryBlocks; - }); - } + } + if ( + isFallbackDeclaration(node) && + node.value.children.first && + rule?.value + ) { + const name = (node.value.children.first as unknown as csstree.Identifier) + .name; + if (fallbackDeclarations[name]) { + fallbackDeclarations[name].push(rule.value); + } else { + fallbackDeclarations[name] = [rule.value]; + } + } + if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { + const name = node.prelude.value; + const tryBlocks: any = []; + const atRules = node.block.children.filter(isTryAtRule); + atRules.forEach((atRule) => { + if (atRule.block?.children) { + const tryBlock: any = {}; + const declarations = atRule.block.children.filter(isDeclaration); + declarations.forEach((child) => { + const firstChild = child.value.children + .first as unknown as csstree.CssNode; + if (firstChild && isAnchorFunction(firstChild)) { + tryBlock[child.property] = parseAnchorFn(firstChild); + } else { + tryBlock[child.property] = csstree.generate(child.value); + } + }); - return fallbacks; + tryBlocks.push(tryBlock); + } + }); + fallbacks[name] = tryBlocks; + } + }); + console.log(anchorNames); + console.log(anchorFunctions); + console.log(fallbackDeclarations); + console.log(fallbacks); } -// @@@ This is not currently used anywhere... export function parseCSS(cssText: string) { const ast = csstree.parse(cssText, { parseAtrulePrelude: false, parseRulePrelude: false, - parseValue: false, }); - return parsePositionFallback(ast); + return ast; } diff --git a/src/transform.ts b/src/transform.ts index 7bbd2e84..d51774c5 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,10 +1,15 @@ import * as csstree from 'css-tree'; import { fetchCSS, isStyleLink } from './fetch.js'; -import { isFallbackAtRule, isFallbackDeclaration } from './parse.js'; +import { + getDataFromCSS, + isFallbackAtRule, + isFallbackDeclaration, + parseCSS, +} from './parse.js'; export function removeAnchorCSS(originalCSS: string) { - const ast = csstree.parse(originalCSS); + const ast = parseCSS(originalCSS); csstree.walk(ast, function (node, item, list) { if (list) { // remove position fallback at-rules @@ -24,9 +29,12 @@ export function removeAnchorCSS(originalCSS: string) { } export async function transformCSS() { - const [, linkedCSS] = await fetchCSS(); + const [inlineCSS, linkedCSS] = await fetchCSS(); + // Handle linked stylesheets linkedCSS.forEach((sourceCSS) => { + getDataFromCSS(sourceCSS.css); + const updatedCSS = removeAnchorCSS(sourceCSS.css); const blob = new Blob([updatedCSS], { type: 'text/css' }); const linkTags = document.querySelectorAll('link'); @@ -37,6 +45,10 @@ export async function transformCSS() { }); }); + // Handle inline stylesheets + inlineCSS.forEach((sourceCSS) => { + getDataFromCSS(sourceCSS); + }); const styleTagCSS = document.querySelectorAll('style'); styleTagCSS.forEach((element) => { element.innerHTML = removeAnchorCSS(element.innerHTML); From 170e5c62edcdaaa9b93ba487e987607665cefaea Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 23 Aug 2022 10:15:53 -0400 Subject: [PATCH 02/11] add type defs --- src/parse.ts | 73 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 8f4e9991..0d55df6f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -8,6 +8,33 @@ interface AtRuleRaw extends csstree.Atrule { prelude: csstree.Raw | null; } +interface AnchorNames { + [key: string]: string[]; +} + +interface AnchorFunction { + anchorName: string; + anchorEdge: string; + fallbackValue?: string; +} + +interface AnchorFunctionWithFloating extends AnchorFunction { + floatingEl: string; + floatingEdge: string; +} + +interface TryBlock { + [key: string]: string | AnchorFunction; +} + +interface FallbackNames { + [key: string]: string[]; +} + +interface Fallbacks { + [key: string]: TryBlock[]; +} + export function isDeclaration( node: csstree.CssNode, ): node is DeclarationWithValue { @@ -68,18 +95,19 @@ function parseAnchorFn(node: csstree.FunctionNode) { return { anchorName, anchorEdge, - fallbackValue, + fallbackValue: fallbackValue || undefined, }; } export function getDataFromCSS(css: string) { - const anchorNames: any = {}; - const anchorFunctions: any = []; - const fallbackDeclarations: any = {}; - const fallbacks: any = {}; + const anchorNames: AnchorNames = {}; + const anchorFunctions: AnchorFunctionWithFloating[] = []; + const fallbackDeclarations: FallbackNames = {}; + const fallbacks: Fallbacks = {}; const ast = parseCSS(css); csstree.walk(ast, function (node) { const rule = this.rule?.prelude as csstree.Raw | undefined; + // Parse `anchor-name` declaration if ( isAnchorNameDeclaration(node) && node.value.children.first && @@ -93,16 +121,20 @@ export function getDataFromCSS(css: string) { anchorNames[name] = [rule.value]; } } + // Parse `anchor()` function if (isAnchorFunction(node) && rule?.value && this.declaration) { const { anchorName, anchorEdge, fallbackValue } = parseAnchorFn(node); - anchorFunctions.push({ - floatingEl: rule.value, - floatingEdge: this.declaration.property, - anchorName, - anchorEdge, - fallbackValue, - }); + if (anchorName && anchorEdge) { + anchorFunctions.push({ + floatingEl: rule.value, + floatingEdge: this.declaration.property, + anchorName, + anchorEdge, + fallbackValue, + }); + } } + // Parse `position-fallback` declaration if ( isFallbackDeclaration(node) && node.value.children.first && @@ -116,24 +148,33 @@ export function getDataFromCSS(css: string) { fallbackDeclarations[name] = [rule.value]; } } + // Parse `@position-fallback` rule if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { const name = node.prelude.value; - const tryBlocks: any = []; + const tryBlocks: TryBlock[] = []; const atRules = node.block.children.filter(isTryAtRule); atRules.forEach((atRule) => { if (atRule.block?.children) { - const tryBlock: any = {}; + const tryBlock: TryBlock = {}; const declarations = atRule.block.children.filter(isDeclaration); declarations.forEach((child) => { const firstChild = child.value.children .first as unknown as csstree.CssNode; + // Parse value if it's an `anchor()` fn; otherwise store it raw if (firstChild && isAnchorFunction(firstChild)) { - tryBlock[child.property] = parseAnchorFn(firstChild); + const { anchorName, anchorEdge, fallbackValue } = + parseAnchorFn(firstChild); + if (anchorName && anchorEdge) { + tryBlock[child.property] = { + anchorName, + anchorEdge, + fallbackValue, + }; + } } else { tryBlock[child.property] = csstree.generate(child.value); } }); - tryBlocks.push(tryBlock); } }); From aa9aede23f67a116689203e859489d600368a74c Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 23 Aug 2022 11:56:12 -0400 Subject: [PATCH 03/11] Another WIP attempt at parsing anchor data --- index.html | 9 ++- public/position-fallback.css | 46 +++++++++------ src/parse.ts | 107 ++++++++++++++++++++++++++++------- 3 files changed, 121 insertions(+), 41 deletions(-) diff --git a/index.html b/index.html index 545d41cb..4fc34023 100644 --- a/index.html +++ b/index.html @@ -5,16 +5,15 @@ CSS Anchor Positioning Polyfill Demo - + - - +

CSS Anchor Positioning Polyfill

diff --git a/public/position-fallback.css b/public/position-fallback.css index 37224ad0..d7c7bc42 100644 --- a/public/position-fallback.css +++ b/public/position-fallback.css @@ -1,13 +1,26 @@ -#my-anchor-fallback { - anchor-name: --my-anchor-fallback; - margin-left: 45px; - width: 100px; - height: 30px; +#my-anchor-positioning { + anchor-name: --my-anchor-positioning; background: orange; + margin-left: 100px; + margin-top: 100px; + width: 100px; + height: 100px; +} + +.another { + anchor-name: --my-test-positioning; +} + +.third { + anchor-name: --my-test-positioning; } -#my-floating-fallback { +#my-floating-positioning { position: absolute; + top: anchor(--my-anchor-positioning bottom, 50px); + left: anchor(--my-anchor-positioning right); + bottom: anchor(--my-anchor-positioning top, calc(20% + 20px)); + right: anchor(--nope top); position-fallback: --fallback1; width: 40px; height: 15px; @@ -18,32 +31,33 @@ @position-fallback --fallback1 { @try { /* 1: Position to the right of the anchor. */ - left: anchor(--my-anchor-fallback right); - top: anchor(--my-anchor-fallback top); + left: anchor(--my-anchor-positioning right); + top: anchor(--my-anchor-positioning top); + bottom: anchor(--nope top); } @try { /* 2: Position to the left of the anchor. */ - right: anchor(--my-anchor-fallback left); - top: anchor(--my-anchor-fallback top); + right: anchor(--my-anchor-positioning left); + top: anchor(--my-anchor-positioning top); } @try { /* 3: Position to the bottom of the anchor. */ - left: anchor(--my-anchor-fallback left); - top: anchor(--my-anchor-fallback bottom); + left: anchor(--my-anchor-positioning left); + top: anchor(--my-anchor-positioning bottom); } @try { /* 4: Position to the top of the anchor. */ - left: anchor(--my-anchor-fallback left); - bottom: anchor(--my-anchor-fallback top); + left: anchor(--my-anchor-positioning left); + bottom: anchor(--my-anchor-positioning top); } @try { /* 5: Position to the left with the narrower width. */ - left: anchor(--my-anchor-fallback right); - top: anchor(--my-anchor-fallback top); + left: anchor(--my-anchor-positioning right); + top: anchor(--my-anchor-positioning top); width: 35px; height: 40px; } diff --git a/src/parse.ts b/src/parse.ts index 0d55df6f..8afa75fd 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -9,29 +9,58 @@ interface AtRuleRaw extends csstree.Atrule { } interface AnchorNames { + // `key` is the `anchor-name` value + // `value` is an array of all element selectors with that anchor name [key: string]: string[]; } interface AnchorFunction { + anchorEl?: string[]; anchorName: string; anchorEdge: string; fallbackValue?: string; } -interface AnchorFunctionWithFloating extends AnchorFunction { - floatingEl: string; - floatingEdge: string; +interface AnchorFunctionDeclaration { + // `key` is the property being declared + // `value` is the anchor-positioning data for that property + [key: string]: AnchorFunction | undefined; +} + +interface AnchorFunctionDeclarations { + // `key` is the floating element selector + // `value` is an object with all anchor-function declarations on that element + [key: string]: AnchorFunctionDeclaration; +} + +interface AnchorPosition { + // `key` is the property being declared, or "fallbackPositions" + // `value` is the anchor-positioning data for that property, + // or the list of fallback positions + [key: string]: AnchorFunction | TryBlock[] | undefined; +} + +interface AnchorPositions { + // `key` is the floating element selector + // `value` is an object with all anchor-positioning data for that element + [key: string]: AnchorPosition; } interface TryBlock { - [key: string]: string | AnchorFunction; + // `key` is the property being declared + // `value` is the property value, or parsed anchor-fn data + [key: string]: string | AnchorFunction | undefined; } interface FallbackNames { - [key: string]: string[]; + // `key` is the floating element selector + // `value` is the `position-fallback` value (name) + [key: string]: string; } interface Fallbacks { + // `key` is the `position-fallback` value (name) + // `value` is an array of `@try` block declarations (in order) [key: string]: TryBlock[]; } @@ -101,8 +130,8 @@ function parseAnchorFn(node: csstree.FunctionNode) { export function getDataFromCSS(css: string) { const anchorNames: AnchorNames = {}; - const anchorFunctions: AnchorFunctionWithFloating[] = []; - const fallbackDeclarations: FallbackNames = {}; + const anchorFunctions: AnchorFunctionDeclarations = {}; + const fallbackNames: FallbackNames = {}; const fallbacks: Fallbacks = {}; const ast = parseCSS(css); csstree.walk(ast, function (node) { @@ -125,13 +154,15 @@ export function getDataFromCSS(css: string) { if (isAnchorFunction(node) && rule?.value && this.declaration) { const { anchorName, anchorEdge, fallbackValue } = parseAnchorFn(node); if (anchorName && anchorEdge) { - anchorFunctions.push({ - floatingEl: rule.value, - floatingEdge: this.declaration.property, + const obj = { anchorName, anchorEdge, fallbackValue, - }); + }; + anchorFunctions[rule.value] = { + ...anchorFunctions[rule.value], + [this.declaration.property]: obj, + }; } } // Parse `position-fallback` declaration @@ -142,11 +173,7 @@ export function getDataFromCSS(css: string) { ) { const name = (node.value.children.first as unknown as csstree.Identifier) .name; - if (fallbackDeclarations[name]) { - fallbackDeclarations[name].push(rule.value); - } else { - fallbackDeclarations[name] = [rule.value]; - } + fallbackNames[rule.value] = name; } // Parse `@position-fallback` rule if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { @@ -181,10 +208,50 @@ export function getDataFromCSS(css: string) { fallbacks[name] = tryBlocks; } }); - console.log(anchorNames); - console.log(anchorFunctions); - console.log(fallbackDeclarations); - console.log(fallbacks); + // console.log('anchorNames', anchorNames); + // console.log('anchorFunctions', anchorFunctions); + // console.log('fallbackNames', fallbackNames); + // console.log('fallbacks', fallbacks); + + const validPositions: AnchorPositions = {}; + Object.keys(anchorFunctions).forEach((floatingEl) => { + const fallbackName = fallbackNames[floatingEl]; + const positionFallbacks = fallbackName + ? fallbacks[fallbackName] + : undefined; + if (positionFallbacks) { + positionFallbacks.forEach((tryBlock) => { + Object.keys(tryBlock).forEach((prop) => { + if (typeof tryBlock[prop] === 'object') { + const anchorName = (tryBlock[prop] as AnchorFunction).anchorName; + const anchorEl = anchorNames[anchorName]; + if (anchorEl) { + (tryBlock[prop] as AnchorFunction).anchorEl = anchorEl; + } else { + tryBlock[prop] = undefined; + } + } + }); + }); + } + validPositions[floatingEl] = { positionFallbacks }; + const anchorFns = anchorFunctions[floatingEl]; + Object.keys(anchorFns).forEach((floatingEdge) => { + const anchorObj = anchorFns[floatingEdge]; + if (anchorObj) { + const anchorEl = anchorNames[anchorObj.anchorName]; + let pos: AnchorFunction | undefined; + if (anchorEl) { + pos = { + ...anchorObj, + anchorEl, + }; + } + validPositions[floatingEl][floatingEdge] = pos; + } + }); + }); + console.log(validPositions); } export function parseCSS(cssText: string) { From 86123e1fd1e67115a4f8ada6add5a34d5a1a70bb Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 23 Aug 2022 16:55:00 -0400 Subject: [PATCH 04/11] Handle fallback-values properly --- src/parse.ts | 112 +++++++++++++++++++++------------------------------ 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 8afa75fd..7ce16db3 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -16,15 +16,15 @@ interface AnchorNames { interface AnchorFunction { anchorEl?: string[]; - anchorName: string; - anchorEdge: string; - fallbackValue?: string; + anchorName?: string; + anchorEdge?: string; + fallbackValue: string; } interface AnchorFunctionDeclaration { // `key` is the property being declared // `value` is the anchor-positioning data for that property - [key: string]: AnchorFunction | undefined; + [key: string]: AnchorFunction; } interface AnchorFunctionDeclarations { @@ -34,10 +34,12 @@ interface AnchorFunctionDeclarations { } interface AnchorPosition { - // `key` is the property being declared, or "fallbackPositions" - // `value` is the anchor-positioning data for that property, - // or the list of fallback positions - [key: string]: AnchorFunction | TryBlock[] | undefined; + declarations: { + // `key` is the property being declared + // `value` is the anchor-positioning data for that property + [key: string]: AnchorFunction; + }; + fallbacks?: TryBlock[]; } interface AnchorPositions { @@ -49,7 +51,7 @@ interface AnchorPositions { interface TryBlock { // `key` is the property being declared // `value` is the property value, or parsed anchor-fn data - [key: string]: string | AnchorFunction | undefined; + [key: string]: string | AnchorFunction; } interface FallbackNames { @@ -124,7 +126,7 @@ function parseAnchorFn(node: csstree.FunctionNode) { return { anchorName, anchorEdge, - fallbackValue: fallbackValue || undefined, + fallbackValue: fallbackValue || '0px', }; } @@ -152,18 +154,10 @@ export function getDataFromCSS(css: string) { } // Parse `anchor()` function if (isAnchorFunction(node) && rule?.value && this.declaration) { - const { anchorName, anchorEdge, fallbackValue } = parseAnchorFn(node); - if (anchorName && anchorEdge) { - const obj = { - anchorName, - anchorEdge, - fallbackValue, - }; - anchorFunctions[rule.value] = { - ...anchorFunctions[rule.value], - [this.declaration.property]: obj, - }; - } + anchorFunctions[rule.value] = { + ...anchorFunctions[rule.value], + [this.declaration.property]: parseAnchorFn(node), + }; } // Parse `position-fallback` declaration if ( @@ -179,25 +173,18 @@ export function getDataFromCSS(css: string) { if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { const name = node.prelude.value; const tryBlocks: TryBlock[] = []; - const atRules = node.block.children.filter(isTryAtRule); - atRules.forEach((atRule) => { + const tryAtRules = node.block.children.filter(isTryAtRule); + tryAtRules.forEach((atRule) => { if (atRule.block?.children) { const tryBlock: TryBlock = {}; + // Only declarations are allowed inside a `@try` block const declarations = atRule.block.children.filter(isDeclaration); declarations.forEach((child) => { const firstChild = child.value.children .first as unknown as csstree.CssNode; // Parse value if it's an `anchor()` fn; otherwise store it raw if (firstChild && isAnchorFunction(firstChild)) { - const { anchorName, anchorEdge, fallbackValue } = - parseAnchorFn(firstChild); - if (anchorName && anchorEdge) { - tryBlock[child.property] = { - anchorName, - anchorEdge, - fallbackValue, - }; - } + tryBlock[child.property] = parseAnchorFn(firstChild); } else { tryBlock[child.property] = csstree.generate(child.value); } @@ -205,52 +192,45 @@ export function getDataFromCSS(css: string) { tryBlocks.push(tryBlock); } }); - fallbacks[name] = tryBlocks; + if (tryBlocks.length) { + fallbacks[name] = tryBlocks; + } } }); - // console.log('anchorNames', anchorNames); - // console.log('anchorFunctions', anchorFunctions); - // console.log('fallbackNames', fallbackNames); - // console.log('fallbacks', fallbacks); const validPositions: AnchorPositions = {}; - Object.keys(anchorFunctions).forEach((floatingEl) => { + for (const [floatingEl, anchorFns] of Object.entries(anchorFunctions)) { const fallbackName = fallbackNames[floatingEl]; const positionFallbacks = fallbackName ? fallbacks[fallbackName] : undefined; if (positionFallbacks) { + // Populate `anchorEl` for each fallback `anchor()` fn positionFallbacks.forEach((tryBlock) => { - Object.keys(tryBlock).forEach((prop) => { - if (typeof tryBlock[prop] === 'object') { - const anchorName = (tryBlock[prop] as AnchorFunction).anchorName; - const anchorEl = anchorNames[anchorName]; - if (anchorEl) { - (tryBlock[prop] as AnchorFunction).anchorEl = anchorEl; - } else { - tryBlock[prop] = undefined; - } + for (const [prop, value] of Object.entries(tryBlock)) { + if (typeof value === 'object') { + const anchorName = (value as AnchorFunction).anchorName; + const anchorEl = anchorName ? anchorNames[anchorName] : undefined; + (tryBlock[prop] as AnchorFunction).anchorEl = anchorEl; } - }); + } }); } - validPositions[floatingEl] = { positionFallbacks }; - const anchorFns = anchorFunctions[floatingEl]; - Object.keys(anchorFns).forEach((floatingEdge) => { - const anchorObj = anchorFns[floatingEdge]; - if (anchorObj) { - const anchorEl = anchorNames[anchorObj.anchorName]; - let pos: AnchorFunction | undefined; - if (anchorEl) { - pos = { - ...anchorObj, - anchorEl, - }; - } - validPositions[floatingEl][floatingEdge] = pos; - } - }); - }); + validPositions[floatingEl] = { + fallbacks: positionFallbacks, + declarations: {}, + }; + for (const [floatingEdge, anchorObj] of Object.entries(anchorFns)) { + // Populate `anchorEl` for each `anchor()` fn + const anchorEl = anchorObj.anchorName + ? anchorNames[anchorObj.anchorName] + : undefined; + validPositions[floatingEl].declarations[floatingEdge] = { + ...anchorObj, + anchorEl, + }; + } + } console.log(validPositions); } From 4466d4bd1cfbbe3ff7e04205048a8dc1ce7e8a4a Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 23 Aug 2022 17:23:47 -0400 Subject: [PATCH 05/11] refactor --- index.html | 8 +-- src/parse.ts | 160 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 119 insertions(+), 49 deletions(-) diff --git a/index.html b/index.html index 4fc34023..b309e459 100644 --- a/index.html +++ b/index.html @@ -5,15 +5,15 @@ CSS Anchor Positioning Polyfill Demo - + /> - +

CSS Anchor Positioning Polyfill

diff --git a/src/parse.ts b/src/parse.ts index 7ce16db3..40c071b2 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -130,6 +130,69 @@ function parseAnchorFn(node: csstree.FunctionNode) { }; } +function getAnchorNameData(node: csstree.CssNode, rule?: csstree.Raw) { + if ( + isAnchorNameDeclaration(node) && + node.value.children.first && + rule?.value + ) { + const name = (node.value.children.first as unknown as csstree.Identifier) + .name; + return { name, selector: rule.value }; + } + return {}; +} + +function getAnchorFunctionData( + node: csstree.CssNode, + declaration: csstree.Declaration | null, + rule?: csstree.Raw, +) { + if (isAnchorFunction(node) && rule?.value && declaration) { + return { [declaration.property]: parseAnchorFn(node) }; + } +} + +function getPositionFallbackDeclaration( + node: csstree.CssNode, + rule?: csstree.Raw, +) { + if (isFallbackDeclaration(node) && node.value.children.first && rule?.value) { + const name = (node.value.children.first as unknown as csstree.Identifier) + .name; + return { name, selector: rule.value }; + } + return {}; +} + +function getPositionFallbackRules(node: csstree.CssNode) { + if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { + const name = node.prelude.value; + const tryBlocks: TryBlock[] = []; + const tryAtRules = node.block.children.filter(isTryAtRule); + tryAtRules.forEach((atRule) => { + if (atRule.block?.children) { + const tryBlock: TryBlock = {}; + // Only declarations are allowed inside a `@try` block + const declarations = atRule.block.children.filter(isDeclaration); + declarations.forEach((child) => { + const firstChild = child.value.children + .first as unknown as csstree.CssNode; + // Parse value if it's an `anchor()` fn; otherwise store it raw + if (firstChild && isAnchorFunction(firstChild)) { + tryBlock[child.property] = parseAnchorFn(firstChild); + } else { + tryBlock[child.property] = csstree.generate(child.value); + } + }); + tryBlocks.push(tryBlock); + } + }); + return { name, fallbacks: tryBlocks }; + } + return {}; +} + export function getDataFromCSS(css: string) { const anchorNames: AnchorNames = {}; const anchorFunctions: AnchorFunctionDeclarations = {}; @@ -138,66 +201,45 @@ export function getDataFromCSS(css: string) { const ast = parseCSS(css); csstree.walk(ast, function (node) { const rule = this.rule?.prelude as csstree.Raw | undefined; + // Parse `anchor-name` declaration - if ( - isAnchorNameDeclaration(node) && - node.value.children.first && - rule?.value - ) { - const name = (node.value.children.first as unknown as csstree.Identifier) - .name; - if (anchorNames[name]) { - anchorNames[name].push(rule.value); + const { name: anchorName, selector: anchorSelector } = getAnchorNameData( + node, + rule, + ); + if (anchorName && anchorSelector) { + if (anchorNames[anchorName]) { + anchorNames[anchorName].push(anchorSelector); } else { - anchorNames[name] = [rule.value]; + anchorNames[anchorName] = [anchorSelector]; } } + // Parse `anchor()` function - if (isAnchorFunction(node) && rule?.value && this.declaration) { + const anchorFnData = getAnchorFunctionData(node, this.declaration, rule); + if (anchorFnData && rule?.value) { anchorFunctions[rule.value] = { ...anchorFunctions[rule.value], - [this.declaration.property]: parseAnchorFn(node), + ...anchorFnData, }; } + // Parse `position-fallback` declaration - if ( - isFallbackDeclaration(node) && - node.value.children.first && - rule?.value - ) { - const name = (node.value.children.first as unknown as csstree.Identifier) - .name; - fallbackNames[rule.value] = name; + const { name: fbName, selector: fbSelector } = + getPositionFallbackDeclaration(node, rule); + if (fbName && fbSelector) { + fallbackNames[fbSelector] = fbName; } + // Parse `@position-fallback` rule - if (isFallbackAtRule(node) && node.prelude?.value && node.block?.children) { - const name = node.prelude.value; - const tryBlocks: TryBlock[] = []; - const tryAtRules = node.block.children.filter(isTryAtRule); - tryAtRules.forEach((atRule) => { - if (atRule.block?.children) { - const tryBlock: TryBlock = {}; - // Only declarations are allowed inside a `@try` block - const declarations = atRule.block.children.filter(isDeclaration); - declarations.forEach((child) => { - const firstChild = child.value.children - .first as unknown as csstree.CssNode; - // Parse value if it's an `anchor()` fn; otherwise store it raw - if (firstChild && isAnchorFunction(firstChild)) { - tryBlock[child.property] = parseAnchorFn(firstChild); - } else { - tryBlock[child.property] = csstree.generate(child.value); - } - }); - tryBlocks.push(tryBlock); - } - }); - if (tryBlocks.length) { - fallbacks[name] = tryBlocks; - } + const { name: fbRuleName, fallbacks: fbTryBlocks } = + getPositionFallbackRules(node); + if (fbRuleName && fbTryBlocks.length) { + fallbacks[fbRuleName] = fbTryBlocks; } }); + // Merge data together under floating-element selector key const validPositions: AnchorPositions = {}; for (const [floatingEl, anchorFns] of Object.entries(anchorFunctions)) { const fallbackName = fallbackNames[floatingEl]; @@ -231,7 +273,35 @@ export function getDataFromCSS(css: string) { }; } } + console.log(validPositions); + + /* Example data shape: + { + '#my-floating-element': { + declarations: { + top: { + anchorName: '--my-anchor', + anchorEl: '#my-target-anchor-element', + anchorEdge: 'bottom', + fallbackValue: '50px', + }, + }, + fallbacks: [ + { + top: { + anchorName: '--my-anchor', + anchorEl: '#my-target-anchor-element', + anchorEdge: 'top', + fallbackValue: '0px', + }, + width: '35px', + }, + ], + }, + } + */ + return validPositions; } export function parseCSS(cssText: string) { From ded836d09cf1d4365063fc572d9b6e484314a56c Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 23 Aug 2022 17:31:24 -0400 Subject: [PATCH 06/11] adjust for testing --- index.html | 9 ++--- public/parsing-test.css | 64 ++++++++++++++++++++++++++++++++++++ public/position-fallback.css | 46 +++++++++----------------- 3 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 public/parsing-test.css diff --git a/index.html b/index.html index b309e459..958eeb0d 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,8 @@ CSS Anchor Positioning Polyfill Demo - + +

CSS Anchor Positioning Polyfill

@@ -49,7 +50,7 @@

Anchor Positioning via anchor() (stylesheet)

Anchor
Floating
- + import polyfill from '/src/index.ts'; diff --git a/public/parsing-test.css b/public/parsing-test.css new file mode 100644 index 00000000..d7c7bc42 --- /dev/null +++ b/public/parsing-test.css @@ -0,0 +1,64 @@ +#my-anchor-positioning { + anchor-name: --my-anchor-positioning; + background: orange; + margin-left: 100px; + margin-top: 100px; + width: 100px; + height: 100px; +} + +.another { + anchor-name: --my-test-positioning; +} + +.third { + anchor-name: --my-test-positioning; +} + +#my-floating-positioning { + position: absolute; + top: anchor(--my-anchor-positioning bottom, 50px); + left: anchor(--my-anchor-positioning right); + bottom: anchor(--my-anchor-positioning top, calc(20% + 20px)); + right: anchor(--nope top); + position-fallback: --fallback1; + width: 40px; + height: 15px; + margin: 5px; + background: green; +} + +@position-fallback --fallback1 { + @try { + /* 1: Position to the right of the anchor. */ + left: anchor(--my-anchor-positioning right); + top: anchor(--my-anchor-positioning top); + bottom: anchor(--nope top); + } + + @try { + /* 2: Position to the left of the anchor. */ + right: anchor(--my-anchor-positioning left); + top: anchor(--my-anchor-positioning top); + } + + @try { + /* 3: Position to the bottom of the anchor. */ + left: anchor(--my-anchor-positioning left); + top: anchor(--my-anchor-positioning bottom); + } + + @try { + /* 4: Position to the top of the anchor. */ + left: anchor(--my-anchor-positioning left); + bottom: anchor(--my-anchor-positioning top); + } + + @try { + /* 5: Position to the left with the narrower width. */ + left: anchor(--my-anchor-positioning right); + top: anchor(--my-anchor-positioning top); + width: 35px; + height: 40px; + } +} diff --git a/public/position-fallback.css b/public/position-fallback.css index d7c7bc42..37224ad0 100644 --- a/public/position-fallback.css +++ b/public/position-fallback.css @@ -1,26 +1,13 @@ -#my-anchor-positioning { - anchor-name: --my-anchor-positioning; - background: orange; - margin-left: 100px; - margin-top: 100px; +#my-anchor-fallback { + anchor-name: --my-anchor-fallback; + margin-left: 45px; width: 100px; - height: 100px; -} - -.another { - anchor-name: --my-test-positioning; -} - -.third { - anchor-name: --my-test-positioning; + height: 30px; + background: orange; } -#my-floating-positioning { +#my-floating-fallback { position: absolute; - top: anchor(--my-anchor-positioning bottom, 50px); - left: anchor(--my-anchor-positioning right); - bottom: anchor(--my-anchor-positioning top, calc(20% + 20px)); - right: anchor(--nope top); position-fallback: --fallback1; width: 40px; height: 15px; @@ -31,33 +18,32 @@ @position-fallback --fallback1 { @try { /* 1: Position to the right of the anchor. */ - left: anchor(--my-anchor-positioning right); - top: anchor(--my-anchor-positioning top); - bottom: anchor(--nope top); + left: anchor(--my-anchor-fallback right); + top: anchor(--my-anchor-fallback top); } @try { /* 2: Position to the left of the anchor. */ - right: anchor(--my-anchor-positioning left); - top: anchor(--my-anchor-positioning top); + right: anchor(--my-anchor-fallback left); + top: anchor(--my-anchor-fallback top); } @try { /* 3: Position to the bottom of the anchor. */ - left: anchor(--my-anchor-positioning left); - top: anchor(--my-anchor-positioning bottom); + left: anchor(--my-anchor-fallback left); + top: anchor(--my-anchor-fallback bottom); } @try { /* 4: Position to the top of the anchor. */ - left: anchor(--my-anchor-positioning left); - bottom: anchor(--my-anchor-positioning top); + left: anchor(--my-anchor-fallback left); + bottom: anchor(--my-anchor-fallback top); } @try { /* 5: Position to the left with the narrower width. */ - left: anchor(--my-anchor-positioning right); - top: anchor(--my-anchor-positioning top); + left: anchor(--my-anchor-fallback right); + top: anchor(--my-anchor-fallback top); width: 35px; height: 40px; } From 148b1722ea427fa9d551a98bd23f845eedccf856 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Wed, 24 Aug 2022 17:49:23 -0400 Subject: [PATCH 07/11] Add comments about overriding data --- src/parse.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/parse.ts b/src/parse.ts index 40c071b2..6bbaa09d 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -218,6 +218,11 @@ export function getDataFromCSS(css: string) { // Parse `anchor()` function const anchorFnData = getAnchorFunctionData(node, this.declaration, rule); if (anchorFnData && rule?.value) { + // This will override earlier declarations + // with the same exact rule selector + // *and* the same exact declaration property: + // (e.g. multiple `top: anchor(...)` declarations + // for the same `.foo {...}` selector) anchorFunctions[rule.value] = { ...anchorFunctions[rule.value], ...anchorFnData, @@ -228,6 +233,10 @@ export function getDataFromCSS(css: string) { const { name: fbName, selector: fbSelector } = getPositionFallbackDeclaration(node, rule); if (fbName && fbSelector) { + // This will override earlier `position-fallback` declarations + // with the same rule selector: + // (e.g. multiple `position-fallback:` declarations + // for the same `.foo {...}` selector) fallbackNames[fbSelector] = fbName; } @@ -235,6 +244,10 @@ export function getDataFromCSS(css: string) { const { name: fbRuleName, fallbacks: fbTryBlocks } = getPositionFallbackRules(node); if (fbRuleName && fbTryBlocks.length) { + // This will override earlier `@position-fallback` lists + // with the same name: + // (e.g. multiple `@position-fallback --my-fallback {...}` uses + // with the same `--my-fallback` name) fallbacks[fbRuleName] = fbTryBlocks; } }); From 5362d07b9c53d1c5058b811dec65bbdfe15447ac Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Wed, 24 Aug 2022 18:35:59 -0400 Subject: [PATCH 08/11] Concatenate CSS before parsing --- index.html | 9 +++--- public/parsing-test.css | 64 ---------------------------------------- src/fetch.ts | 65 +++++++++++++++++++++-------------------- src/transform.ts | 33 +++++++++++---------- 4 files changed, 55 insertions(+), 116 deletions(-) delete mode 100644 public/parsing-test.css diff --git a/index.html b/index.html index 958eeb0d..b309e459 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,7 @@ CSS Anchor Positioning Polyfill Demo - - +

CSS Anchor Positioning Polyfill

@@ -50,7 +49,7 @@

Anchor Positioning via anchor() (stylesheet)

Anchor
Floating
- +
Floating
import polyfill from '/src/index.ts'; diff --git a/public/parsing-test.css b/public/parsing-test.css deleted file mode 100644 index d7c7bc42..00000000 --- a/public/parsing-test.css +++ /dev/null @@ -1,64 +0,0 @@ -#my-anchor-positioning { - anchor-name: --my-anchor-positioning; - background: orange; - margin-left: 100px; - margin-top: 100px; - width: 100px; - height: 100px; -} - -.another { - anchor-name: --my-test-positioning; -} - -.third { - anchor-name: --my-test-positioning; -} - -#my-floating-positioning { - position: absolute; - top: anchor(--my-anchor-positioning bottom, 50px); - left: anchor(--my-anchor-positioning right); - bottom: anchor(--my-anchor-positioning top, calc(20% + 20px)); - right: anchor(--nope top); - position-fallback: --fallback1; - width: 40px; - height: 15px; - margin: 5px; - background: green; -} - -@position-fallback --fallback1 { - @try { - /* 1: Position to the right of the anchor. */ - left: anchor(--my-anchor-positioning right); - top: anchor(--my-anchor-positioning top); - bottom: anchor(--nope top); - } - - @try { - /* 2: Position to the left of the anchor. */ - right: anchor(--my-anchor-positioning left); - top: anchor(--my-anchor-positioning top); - } - - @try { - /* 3: Position to the bottom of the anchor. */ - left: anchor(--my-anchor-positioning left); - top: anchor(--my-anchor-positioning bottom); - } - - @try { - /* 4: Position to the top of the anchor. */ - left: anchor(--my-anchor-positioning left); - bottom: anchor(--my-anchor-positioning top); - } - - @try { - /* 5: Position to the left with the narrower width. */ - left: anchor(--my-anchor-positioning right); - top: anchor(--my-anchor-positioning top); - width: 35px; - height: 40px; - } -} diff --git a/src/fetch.ts b/src/fetch.ts index 70740ff3..615ee92d 100644 --- a/src/fetch.ts +++ b/src/fetch.ts @@ -1,5 +1,5 @@ -interface LinkedCSS { - source: string; +interface StyleData { + source: 'style' | string; css: string; } @@ -9,43 +9,44 @@ export function isStyleLink(link: HTMLLinkElement) { ); } -async function handleLinkedStylesheets(): Promise { - const linkElements = document.querySelectorAll('link'); - const CSSlinks: URL[] = []; - - linkElements.forEach((link) => { - const srcUrl = new URL(link.href, document.baseURI); - if (srcUrl.origin !== location.origin) { - return; - } - if (isStyleLink(link)) { - CSSlinks.push(srcUrl); - } - }); +function getStylesheetUrl(link: HTMLLinkElement): URL | undefined { + const srcUrl = new URL(link.href, document.baseURI); + if (isStyleLink(link) && srcUrl.origin === location.origin) { + return srcUrl; + } +} - const linkedCSS = await Promise.all( - CSSlinks.map(async (link) => { +async function fetchLinkedStylesheets( + sources: (string | URL)[], +): Promise { + return Promise.all( + sources.map(async (src) => { + if (typeof src === 'string') { + return { source: 'style', css: src }; + } // fetch css and push into array of strings - const response = await fetch(link.toString()); + const response = await fetch(src.toString()); const css = await response.text(); - return { source: link.toString(), css }; + return { source: src.toString(), css }; }), ); - - return linkedCSS; } -function handleInlineStyles() { - const styleElements = document.querySelectorAll('style'); - const inlineCSS: string[] = []; - styleElements.forEach((el) => inlineCSS.push(el.innerHTML)); +export async function fetchCSS(): Promise { + const elements = document.querySelectorAll('link, style'); + const sources: (string | URL)[] = []; - return inlineCSS; -} - -export async function fetchCSS(): Promise<[string[], LinkedCSS[]]> { - const linkedCSS = await handleLinkedStylesheets(); - const inlineCSS = handleInlineStyles(); + elements.forEach((el) => { + if (el.tagName.toLowerCase() === 'link') { + const url = getStylesheetUrl(el as HTMLLinkElement); + if (url) { + sources.push(url); + } + } + if (el.tagName.toLowerCase() === 'style') { + sources.push(el.innerHTML); + } + }); - return [inlineCSS, linkedCSS]; + return await fetchLinkedStylesheets(sources); } diff --git a/src/transform.ts b/src/transform.ts index d51774c5..43b4b07e 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -29,28 +29,31 @@ export function removeAnchorCSS(originalCSS: string) { } export async function transformCSS() { - const [inlineCSS, linkedCSS] = await fetchCSS(); + const styleData = await fetchCSS(); + const allCSS: string[] = []; // Handle linked stylesheets - linkedCSS.forEach((sourceCSS) => { - getDataFromCSS(sourceCSS.css); - - const updatedCSS = removeAnchorCSS(sourceCSS.css); - const blob = new Blob([updatedCSS], { type: 'text/css' }); - const linkTags = document.querySelectorAll('link'); - linkTags.forEach((link) => { - if (isStyleLink(link) && sourceCSS.source.includes(link.href)) { - link.href = URL.createObjectURL(blob); - } - }); + styleData.forEach(({ source, css }) => { + allCSS.push(css); + + if (source !== 'style') { + const updatedCSS = removeAnchorCSS(css); + const blob = new Blob([updatedCSS], { type: 'text/css' }); + const linkTags = document.querySelectorAll('link'); + linkTags.forEach((link) => { + if (isStyleLink(link) && source.includes(link.href)) { + link.href = URL.createObjectURL(blob); + } + }); + } }); // Handle inline stylesheets - inlineCSS.forEach((sourceCSS) => { - getDataFromCSS(sourceCSS); - }); const styleTagCSS = document.querySelectorAll('style'); styleTagCSS.forEach((element) => { element.innerHTML = removeAnchorCSS(element.innerHTML); }); + + // Get data from concatenated styles + getDataFromCSS(allCSS.join('\n')); } From b0f81301f64b4b200d33bbd1af40641c4966a059 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Thu, 25 Aug 2022 12:05:01 -0400 Subject: [PATCH 09/11] update comment --- src/parse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 6bbaa09d..0168f903 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -295,7 +295,7 @@ export function getDataFromCSS(css: string) { declarations: { top: { anchorName: '--my-anchor', - anchorEl: '#my-target-anchor-element', + anchorEl: ['#my-target-anchor-element'], anchorEdge: 'bottom', fallbackValue: '50px', }, @@ -304,7 +304,7 @@ export function getDataFromCSS(css: string) { { top: { anchorName: '--my-anchor', - anchorEl: '#my-target-anchor-element', + anchorEl: ['#my-target-anchor-element'], anchorEdge: 'top', fallbackValue: '0px', }, From 9115a017f4edbc1824944861e3d02d85535c3b1b Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 30 Aug 2022 15:38:21 -0400 Subject: [PATCH 10/11] Fix missing `position-fallback` declarations. --- src/parse.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/parse.ts b/src/parse.ts index 0168f903..efd788f0 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -34,7 +34,7 @@ interface AnchorFunctionDeclarations { } interface AnchorPosition { - declarations: { + declarations?: { // `key` is the property being declared // `value` is the anchor-positioning data for that property [key: string]: AnchorFunction; @@ -254,8 +254,9 @@ export function getDataFromCSS(css: string) { // Merge data together under floating-element selector key const validPositions: AnchorPositions = {}; - for (const [floatingEl, anchorFns] of Object.entries(anchorFunctions)) { - const fallbackName = fallbackNames[floatingEl]; + + // Store any `position-fallback` declarations + for (const [floatingEl, fallbackName] of Object.entries(fallbackNames)) { const positionFallbacks = fallbackName ? fallbacks[fallbackName] : undefined; @@ -270,19 +271,28 @@ export function getDataFromCSS(css: string) { } } }); + validPositions[floatingEl] = { + fallbacks: positionFallbacks, + }; } - validPositions[floatingEl] = { - fallbacks: positionFallbacks, - declarations: {}, - }; + } + + // Store any `anchor()` fns + for (const [floatingEl, anchorFns] of Object.entries(anchorFunctions)) { for (const [floatingEdge, anchorObj] of Object.entries(anchorFns)) { // Populate `anchorEl` for each `anchor()` fn const anchorEl = anchorObj.anchorName ? anchorNames[anchorObj.anchorName] : undefined; - validPositions[floatingEl].declarations[floatingEdge] = { - ...anchorObj, - anchorEl, + validPositions[floatingEl] = { + ...validPositions[floatingEl], + declarations: { + ...validPositions[floatingEl]?.declarations, + [floatingEdge]: { + ...anchorObj, + anchorEl, + }, + }, }; } } From c18869fce98148bf44790cb3add9d7994091938f Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 30 Aug 2022 16:30:32 -0400 Subject: [PATCH 11/11] tests --- public/position-fallback.css | 3 +- src/parse.ts | 57 ++++++---- src/transform.ts | 30 +++--- tests/unit/fetch.test.ts | 12 +-- tests/unit/parse.test.ts | 204 ++++++++++++++++++++++++++++++----- 5 files changed, 235 insertions(+), 71 deletions(-) diff --git a/public/position-fallback.css b/public/position-fallback.css index 37224ad0..234726b2 100644 --- a/public/position-fallback.css +++ b/public/position-fallback.css @@ -9,6 +9,7 @@ #my-floating-fallback { position: absolute; position-fallback: --fallback1; + left: anchor(--my-anchor-fallback left); width: 40px; height: 15px; margin: 5px; @@ -18,7 +19,7 @@ @position-fallback --fallback1 { @try { /* 1: Position to the right of the anchor. */ - left: anchor(--my-anchor-fallback right); + left: anchor(--my-anchor-fallback right, 10px); top: anchor(--my-anchor-fallback top); } diff --git a/src/parse.ts b/src/parse.ts index efd788f0..8e4586e5 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -98,6 +98,18 @@ export function isTryAtRule(node: csstree.CssNode): node is AtRuleRaw { return node.type === 'Atrule' && node.name === 'try'; } +export function isIdentifier( + node: csstree.CssNode, +): node is csstree.Identifier { + return Boolean(node.type === 'Identifier' && node.name); +} + +export function isPercentage( + node: csstree.CssNode, +): node is csstree.Percentage { + return Boolean(node.type === 'Percentage' && node.value); +} + function parseAnchorFn(node: csstree.FunctionNode) { let anchorName: string | undefined, anchorEdge: string | undefined, @@ -112,15 +124,19 @@ function parseAnchorFn(node: csstree.FunctionNode) { foundComma = true; return; } - if (child.type === 'Identifier' && child.name) { - switch (idx) { - case 0: + switch (idx) { + case 0: + if (isIdentifier(child)) { anchorName = child.name; - break; - case 1: + } + break; + case 1: + if (isIdentifier(child)) { anchorEdge = child.name; - break; - } + } else if (isPercentage(child)) { + anchorEdge = `${child.value}%`; + } + break; } }); return { @@ -193,12 +209,22 @@ function getPositionFallbackRules(node: csstree.CssNode) { return {}; } -export function getDataFromCSS(css: string) { +export function getAST(cssText: string) { + const ast = csstree.parse(cssText, { + parseAtrulePrelude: false, + parseRulePrelude: false, + parseCustomProperty: true, + }); + + return ast; +} + +export function parseCSS(css: string) { const anchorNames: AnchorNames = {}; const anchorFunctions: AnchorFunctionDeclarations = {}; const fallbackNames: FallbackNames = {}; const fallbacks: Fallbacks = {}; - const ast = parseCSS(css); + const ast = getAST(css); csstree.walk(ast, function (node) { const rule = this.rule?.prelude as csstree.Raw | undefined; @@ -257,9 +283,7 @@ export function getDataFromCSS(css: string) { // Store any `position-fallback` declarations for (const [floatingEl, fallbackName] of Object.entries(fallbackNames)) { - const positionFallbacks = fallbackName - ? fallbacks[fallbackName] - : undefined; + const positionFallbacks = fallbacks[fallbackName]; if (positionFallbacks) { // Populate `anchorEl` for each fallback `anchor()` fn positionFallbacks.forEach((tryBlock) => { @@ -326,12 +350,3 @@ export function getDataFromCSS(css: string) { */ return validPositions; } - -export function parseCSS(cssText: string) { - const ast = csstree.parse(cssText, { - parseAtrulePrelude: false, - parseRulePrelude: false, - }); - - return ast; -} diff --git a/src/transform.ts b/src/transform.ts index 43b4b07e..8d17cb73 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -2,27 +2,27 @@ import * as csstree from 'css-tree'; import { fetchCSS, isStyleLink } from './fetch.js'; import { - getDataFromCSS, + getAST, isFallbackAtRule, isFallbackDeclaration, parseCSS, } from './parse.js'; export function removeAnchorCSS(originalCSS: string) { - const ast = parseCSS(originalCSS); + const ast = getAST(originalCSS); csstree.walk(ast, function (node, item, list) { if (list) { - // remove position fallback at-rules - // e.g. `@position-fallback --button-popup {...}` - if (isFallbackAtRule(node)) { - list.remove(item); - } - // remove position fallback declaration // e.g. `position-fallback: --button-popup;` if (isFallbackDeclaration(node)) { list.remove(item); } + + // remove position fallback at-rules + // e.g. `@position-fallback --button-popup {...}` + if (isFallbackAtRule(node)) { + list.remove(item); + } } }); return csstree.generate(ast); @@ -32,6 +32,12 @@ export async function transformCSS() { const styleData = await fetchCSS(); const allCSS: string[] = []; + // Handle inline stylesheets + const styleTagCSS = document.querySelectorAll('style'); + styleTagCSS.forEach((element) => { + element.innerHTML = removeAnchorCSS(element.innerHTML); + }); + // Handle linked stylesheets styleData.forEach(({ source, css }) => { allCSS.push(css); @@ -48,12 +54,6 @@ export async function transformCSS() { } }); - // Handle inline stylesheets - const styleTagCSS = document.querySelectorAll('style'); - styleTagCSS.forEach((element) => { - element.innerHTML = removeAnchorCSS(element.innerHTML); - }); - // Get data from concatenated styles - getDataFromCSS(allCSS.join('\n')); + parseCSS(allCSS.join('\n')); } diff --git a/tests/unit/fetch.test.ts b/tests/unit/fetch.test.ts index fad3beb8..9539d91f 100644 --- a/tests/unit/fetch.test.ts +++ b/tests/unit/fetch.test.ts @@ -23,12 +23,12 @@ describe('fetch stylesheet', () => { it('fetches CSS', async () => { const css = getSampleCSS('anchor-positioning'); fetchMock.getOnce('end:sample.css', css); - const [inlineCSS, linkedCSS] = await fetchCSS(); + const styleData = await fetchCSS(); - expect(inlineCSS).toHaveLength(1); - expect(inlineCSS[0].trim()).toBe('p { color: red; }'); - expect(linkedCSS).toHaveLength(1); - expect(linkedCSS[0].source).toBe(`${location.origin}/sample.css`); - expect(linkedCSS[0].css).toEqual(css); + expect(styleData).toHaveLength(2); + expect(styleData[0].source).toBe(`${location.origin}/sample.css`); + expect(styleData[0].css).toEqual(css); + expect(styleData[1].source).toBe('style'); + expect(styleData[1].css.trim()).toBe('p { color: red; }'); }); }); diff --git a/tests/unit/parse.test.ts b/tests/unit/parse.test.ts index b648517c..2d857370 100644 --- a/tests/unit/parse.test.ts +++ b/tests/unit/parse.test.ts @@ -2,41 +2,189 @@ import { parseCSS } from '../../src/parse.js'; import { getSampleCSS, sampleBaseCSS } from './../helpers.js'; describe('parseCSS', () => { - it('parses and returns @position-fallback strategy', () => { - const css = getSampleCSS('position-fallback'); - const result = parseCSS(css); + it('handles missing `@position-fallback` at-rule or `anchor()` fn', () => { + const result = parseCSS(sampleBaseCSS); - expect(result).toEqual({ - '--fallback1': [ - { - top: 'anchor(--my-anchor-fallback top)', - left: 'anchor(--my-anchor-fallback right)', - }, - { - right: 'anchor(--my-anchor-fallback left)', - top: 'anchor(--my-anchor-fallback top)', + expect(result).toEqual({}); + }); + + it('parses `anchor()` function (math)', () => { + const css = getSampleCSS('anchor'); + const result = parseCSS(css); + const expected = { + '#my-floating': { + declarations: { + '--center': { + anchorName: '--my-anchor', + anchorEl: ['#my-anchor'], + anchorEdge: '50%', + fallbackValue: '0px', + }, }, - { - left: 'anchor(--my-anchor-fallback left)', - top: 'anchor(--my-anchor-fallback bottom)', + }, + }; + + expect(result).toEqual(expected); + }); + + it('parses `anchor()` function (positioning)', () => { + const css = getSampleCSS('anchor-positioning'); + const result = parseCSS(css); + const expected = { + '#my-floating-positioning': { + declarations: { + top: { + anchorName: '--my-anchor-positioning', + anchorEl: ['#my-anchor-positioning'], + anchorEdge: 'bottom', + fallbackValue: '0px', + }, + left: { + anchorName: '--my-anchor-positioning', + anchorEl: ['#my-anchor-positioning'], + anchorEdge: 'right', + fallbackValue: '50px', + }, }, - { - left: 'anchor(--my-anchor-fallback left)', - bottom: 'anchor(--my-anchor-fallback top)', + }, + }; + + expect(result).toEqual(expected); + }); + + it('parses `@position-fallback` strategy', () => { + const css = getSampleCSS('position-fallback'); + const result = parseCSS(css); + const expected = { + '#my-floating-fallback': { + declarations: { + left: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'left', + fallbackValue: '0px', + }, }, - { - left: 'anchor(--my-anchor-fallback right)', - top: 'anchor(--my-anchor-fallback top)', - width: '35px', - height: '40px', + fallbacks: [ + { + left: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'right', + fallbackValue: '10px', + }, + top: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'top', + fallbackValue: '0px', + }, + }, + { + right: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'left', + fallbackValue: '0px', + }, + top: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'top', + fallbackValue: '0px', + }, + }, + { + left: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'left', + fallbackValue: '0px', + }, + top: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'bottom', + fallbackValue: '0px', + }, + }, + { + left: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'left', + fallbackValue: '0px', + }, + bottom: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'top', + fallbackValue: '0px', + }, + }, + { + left: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'right', + fallbackValue: '0px', + }, + top: { + anchorName: '--my-anchor-fallback', + anchorEl: ['#my-anchor-fallback'], + anchorEdge: 'top', + fallbackValue: '0px', + }, + width: '35px', + height: '40px', + }, + ], + }, + }; + + expect(result).toEqual(expected); + }); + + it('handles duplicate anchor-names', () => { + const css = ` + #a1 { + anchor-name: --my-anchor; + } + #a2 { + anchor-name: --my-anchor; + } + #f1 { + position: absolute; + top: anchor(--my-anchor bottom); + } + `; + const result = parseCSS(css); + const expected = { + '#f1': { + declarations: { + top: { + anchorName: '--my-anchor', + anchorEl: ['#a1', '#a2'], + anchorEdge: 'bottom', + fallbackValue: '0px', + }, }, - ], - }); + }, + }; + + expect(result).toEqual(expected); }); - it('does not find @position-fallback at-rule or anchor() function', () => { - const result = parseCSS(sampleBaseCSS); + it('handles invalid/missing `position-fallback`', () => { + const css = ` + #floating { + position: absolute; + position-fallback: --fallback; + } + `; + const result = parseCSS(css); + const expected = {}; - expect(result).toEqual({}); + expect(result).toEqual(expected); }); });