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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/core/ui/core/properties/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export { unsetValue } from './property-shared';
const cssPropertyNames: string[] = [];
const cssShorthandConverters = new Map<string, (value: string) => [any, any][]>();
const cssShorthandLonghands = new Map<string, string[]>();
// Bumped per shorthand registration, so rules parsed before it can still expand it.
let cssShorthandsVersion = 0;
const HAS_OWN = Object.prototype.hasOwnProperty;
const symbolPropertyMap = {};
const cssSymbolPropertyMap = {};
Expand Down Expand Up @@ -155,6 +157,16 @@ export function _pendingCssShorthandSubstitution(cssName: string, value: string)
return declarations;
}

/** Changes whenever a shorthand is registered. */
export function _getCssShorthandsVersion(): number {
return cssShorthandsVersion;
}

/** Whether `cssName` is a registered shorthand. */
export function _isCssShorthand(cssName: string): boolean {
return cssShorthandConverters.has(cssName);
}

/**
* Expand a shorthand declaration into its longhand declarations, or `undefined`
* when the property is not a shorthand, the value still has to be evaluated per
Expand Down Expand Up @@ -1390,6 +1402,7 @@ export class ShorthandProperty<T extends Style, P> implements ShorthandProperty<
this.cssName = `css:${options.cssName}`;
this.cssLocalName = `${options.cssName}`;
cssShorthandConverters.set(options.cssName, options.converter as (value: string) => [any, any][]);
cssShorthandsVersion++;

const converter = options.converter;

Expand Down
57 changes: 57 additions & 0 deletions packages/core/ui/styling/css-selector.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { parse } from '../../css/reworkcss.js';
import { Screen } from '../../platform';
import { createSelector, RuleSet, StyleSheetSelectorScope, SelectorTier, fromAstNode, Node, Changes, matchSelectorCandidates } from './css-selector';
import { _populateRules } from './style-scope';
import { CssProperty, ShorthandProperty } from '../core/properties';

describe('css-selector', () => {
it('button[attr]', () => {
Expand Down Expand Up @@ -447,6 +448,62 @@ describe('css-selector', () => {
expect(new Set(rule.declarations.map((d) => d.value)).size).toBe(1);
});

describe('shorthands registered after parsing', () => {
// Plugins usually load after the app CSS has been parsed.
function registerLateShorthand(prefix: string) {
const start = new CssProperty<any, string>({ name: `${prefix}Start`, cssName: `${prefix}-start` });
const end = new CssProperty<any, string>({ name: `${prefix}End`, cssName: `${prefix}-end` });
new ShorthandProperty<any, string>({
name: `${prefix}Sides`,
cssName: `${prefix}-sides`,
getter: () => '',
converter: (value) => {
const [a, b = a] = String(value).split(' ');
return [
[start, a],
[end, b],
];
},
});
}

it('expands them in place on first use, once', () => {
const rule = createOne(`button { color: red; lateone-sides: 1 2; lateone-end: 5; }`);
expect(rule.declarations.map((d) => d.property)).toEqual(['color', 'lateone-sides', 'lateone-end']);

registerLateShorthand('lateone');
rule.expandLateShorthands();
expect(rule.declarations).toEqual([
{ property: 'color', value: 'red' },
{ property: 'lateone-start', value: '1' },
{ property: 'lateone-end', value: '2' },
{ property: 'lateone-end', value: '5' },
]);

const expanded = rule.declarations;
rule.expandLateShorthands();
expect(rule.declarations).toBe(expanded);
});

it('keeps a variable value pending per longhand', () => {
const rule = createOne(`button { latetwo-sides: var(--s); }`);
registerLateShorthand('latetwo');
rule.expandLateShorthands();

expect(rule.declarations.map((d) => d.property)).toEqual(['latetwo-start', 'latetwo-end']);
expect(rule.declarations.map((d) => String(d.value))).toEqual(['var(--s)', 'var(--s)']);
});

it('leaves rules without them untouched', () => {
const rule = createOne(`button { margin: 4; color: red; }`);
const declarations = rule.declarations;
registerLateShorthand('latethree');
rule.expandLateShorthands();

expect(rule.declarations).toBe(declarations);
});
});

it('strips the unsupported !important flag while parsing', () => {
const rule = createOne(`button { color: red !important; }`);
expect(rule.declarations).toEqual([{ property: 'color', value: 'red' }]);
Expand Down
37 changes: 35 additions & 2 deletions packages/core/ui/styling/css-selector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { parse as convertToCSSWhatSelector, Selector as CSSWhatSelector, DataType as CSSWhatDataType } from 'css-what';
import '../../globals';
import { _expandCssShorthand, _pendingCssShorthandSubstitution, isCssVariable } from '../core/properties';
import { _expandCssShorthand, _getCssShorthandsVersion, _isCssShorthand, _pendingCssShorthandSubstitution, isCssVariable } from '../core/properties';
import { isNullOrUndefined } from '../../utils/types';
import { cleanupImportantFlags } from './css-utils';

Expand Down Expand Up @@ -919,11 +919,42 @@ export class RuleSet {
public tag?: string | number;
public scopedTag?: string;

private shorthandsVersion = _getCssShorthandsVersion();

constructor(selectors: SelectorCore[], declarations: Declaration[]) {
this.selectors = selectors;
this.declarations = declarations;
this.selectors.forEach((sel) => (sel.ruleset = this));
}

/** Expands, once, shorthands registered after this rule was parsed (e.g. by a plugin). */
public expandLateShorthands(): void {
const version = _getCssShorthandsVersion();
if (this.shorthandsVersion === version) {
return;
}
this.shorthandsVersion = version;

const declarations = this.declarations;
let expanded: Declaration[];
for (let i = 0, length = declarations.length; i < length; i++) {
const declaration = declarations[i];
if (!expanded && typeof declaration.value === 'string' && _isCssShorthand(declaration.property)) {
expanded = declarations.slice(0, i);
}
if (!expanded) {
continue;
}
if (typeof declaration.value === 'string') {
pushDeclaration(expanded, declaration.property, declaration.value);
} else {
expanded.push(declaration);
}
}
if (expanded) {
this.declarations = expanded;
}
}
public toString(): string {
let desc = `${this.selectors.join(', ')} {${this.declarations.map((d, i) => `${i === 0 ? ' ' : ''}${d.property}: ${d.value}`).join('; ')} }`;
if (this.mediaQueryString) {
Expand Down Expand Up @@ -954,8 +985,10 @@ export function fromAstNode(astRule: ReworkCSS.Rule): RuleSet {

function appendDeclaration(declarations: Declaration[], decl: ReworkCSS.Declaration): void {
const property = isCssVariable(decl.property) ? decl.property : decl.property.toLowerCase();
const value = cleanupImportantFlags(decl.value, decl.property);
pushDeclaration(declarations, property, cleanupImportantFlags(decl.value, decl.property));
}

function pushDeclaration(declarations: Declaration[], property: string, value: string): void {
// The cascade is defined on longhands: a shorthand declares each of its
// longhands in its place, so it is expanded here to keep source order meaningful.
const expanded = _expandCssShorthand(property, value);
Expand Down
9 changes: 8 additions & 1 deletion packages/core/ui/styling/style-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,14 @@ export class CssState {
}

const newPropertyValues = new view.style.PropertyBag();
matchingSelectors.forEach((selector) => selector.ruleset.declarations.forEach((declaration) => (newPropertyValues[declaration.property] = declaration.value)));
for (let i = 0, length = matchingSelectors.length; i < length; i++) {
const ruleset = matchingSelectors[i].ruleset;
ruleset.expandLateShorthands();
const declarations = ruleset.declarations;
for (let j = 0, count = declarations.length; j < count; j++) {
newPropertyValues[declarations[j].property] = declarations[j].value;
}
}

const oldProperties = this._appliedPropertyValues;
// A local write is the only thing that can drop a value the cascade already
Expand Down
Loading