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);
});
});