@@ -23,6 +23,7 @@ const { makeCacheable } = require("../util/identifier");
2323 */
2424const {
2525 ABSOLUTE_UNIT_SCALE,
26+ ANIMATION_KEYWORD_SLOTS,
2627 BOX_FAMILY_PREFIX,
2728 BOX_LONGHANDS,
2829 CALC_CONSTANTS,
@@ -32,6 +33,7 @@ const {
3233 CSS_WIDE_KEYWORDS,
3334 CUSTOM_IDENT_LIST_PROPERTIES,
3435 DROPPABLE_WHEN_EMPTY_AT_RULES,
36+ EASING_KEYWORDS,
3537 ENCODED_ALREADY,
3638 ENGINE_TRANSFER_DIFFERS,
3739 FAMILY_LONGHANDS,
@@ -65,6 +67,7 @@ const {
6567 SUPPORT_BROWSERS,
6668 THROUGH_MATRIX,
6769 THROUGH_TRANSFER,
70+ TRANSITION_BEHAVIORS,
6871 UNSHARED_LONGHAND_KEYWORDS,
6972 foldAdd,
7073 foldDivide,
@@ -12123,7 +12126,19 @@ const _foldInto = (path, items, texts, at, from) => {
1212312126 if (shorthandColon <= 0 || longhandColon <= 0) return false;
1212412127 const property = _printedProperty(shorthandText, shorthandColon);
1212512128 const sides = BOX_LONGHANDS.get(property) || PAIR_LONGHANDS.get(property);
12126- if (sides === undefined) return false;
12129+ if (sides === undefined) {
12130+ const folded = _foldIntoSlots(
12131+ property,
12132+ _printedValue(shorthandText, shorthandColon),
12133+ _printedProperty(longhandText, longhandColon),
12134+ _printedValue(longhandText, longhandColon)
12135+ );
12136+ if (folded === null) return false;
12137+ // Never longer: the slot's value is written once, its name no more.
12138+ const important = path.important(items[at]) ? _IMPORTANT : "";
12139+ texts[at] = `${property}:${folded}${important};`;
12140+ return true;
12141+ }
1212712142 const slot = sides.indexOf(_printedProperty(longhandText, longhandColon));
1212812143 if (slot === -1) return false;
1212912144 const shorthandValue = _printedValue(shorthandText, shorthandColon);
@@ -12169,6 +12184,221 @@ const _foldInto = (path, items, texts, at, from) => {
1216912184 return true;
1217012185};
1217112186
12187+ // A `<time>`, which a `transition` or an `animation` reads as its duration
12188+ // first and its delay second.
12189+ const _TIME_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(?:s|ms)$/i;
12190+ const _ZERO_TIME_RE = /^[+-]?(?:0+\.?0*|\.0+)(?:s|ms)$/i;
12191+ // An easing function a layer may hold, the keywords aside.
12192+ const _EASING_CALL_RE = /^(?:cubic-bezier|steps|linear)\(/i;
12193+
12194+ /**
12195+ * A longhand's value written into the shorthand of its own family standing
12196+ * before it, or null where the shorthand's slots cannot be read apart.
12197+ * @param {string} property the shorthand's printed property name
12198+ * @param {string} shorthandValue its printed value
12199+ * @param {string} longhand the longhand's printed property name
12200+ * @param {string} longhandValue its printed value
12201+ * @returns {string | null} the shorthand's new value, or null
12202+ */
12203+ const _foldIntoSlots = (property, shorthandValue, longhand, longhandValue) => {
12204+ // A vendor spelling folds only into the shorthand of the same vendor.
12205+ const prefix = /^-[a-z]+-/.exec(property);
12206+ const base = prefix === null ? property : property.slice(prefix[0].length);
12207+ if (prefix !== null && !longhand.startsWith(prefix[0])) return null;
12208+ const slot = prefix === null ? longhand : longhand.slice(prefix[0].length);
12209+ // A substitution may stand for any slot, and a comma list is more than one layer.
12210+ if (_hasSubstitution(shorthandValue) || _hasSubstitution(longhandValue)) {
12211+ return null;
12212+ }
12213+ if (
12214+ _valueItems(shorthandValue).length !== 1 ||
12215+ _valueItems(longhandValue).length !== 1
12216+ ) {
12217+ return null;
12218+ }
12219+ const components = _splitPrintedComponents(shorthandValue);
12220+ const value = _splitPrintedComponents(longhandValue);
12221+ if (value.length !== 1 || components.length === 0) return null;
12222+ for (const one of [...components, value[0]]) {
12223+ if (CSS_WIDE_KEYWORDS.has(toLowerCaseIfNeeded(one))) return null;
12224+ }
12225+ if (base === "flex") {
12226+ // IE10's `-ms-flex` reads omitted factors the 2012 draft's way.
12227+ if (prefix !== null && prefix[0] === "-ms-") return null;
12228+ const factors = _flexFactors(components);
12229+ const at = ["flex-grow", "flex-shrink", "flex-basis"].indexOf(slot);
12230+ if (factors === null || at === -1) return null;
12231+ if (_BARE_NUMBER_RE.test(value[0]) !== (at !== 2)) return null;
12232+ factors[at] = value[0];
12233+ return _joinSlots(_shortestFlexValue(factors) || factors);
12234+ }
12235+ if (base === "transition") {
12236+ return _foldTransitionSlot(components, slot, value[0]);
12237+ }
12238+ if (base === "animation") {
12239+ return _foldAnimationSlot(components, slot, value[0]);
12240+ }
12241+ const longhands = FAMILY_LONGHANDS.get(base);
12242+ // `text-wrap` was one keyword before it had longhands, and an engine reading
12243+ // it so drops the whole of a value with two.
12244+ if (
12245+ longhands === undefined ||
12246+ base === "text-wrap" ||
12247+ !_shorthandSlotsReadable(base)
12248+ ) {
12249+ return null;
12250+ }
12251+ const filled = _familySlots(longhands, components);
12252+ const taken = _familySlots(longhands, value);
12253+ if (filled === null || taken === null || !taken.has(slot)) return null;
12254+ filled.set(slot, value[0]);
12255+ const order = longhands.filter((name) => filled.has(name));
12256+ return _familyValue(
12257+ base,
12258+ order,
12259+ order.map((name) => /** @type {string} */ (filled.get(name)))
12260+ );
12261+ };
12262+
12263+ /**
12264+ * A `transition` longhand written into a one-layer `transition`, its slots
12265+ * then written in the order the printer writes an authored one.
12266+ * @param {string[]} components the layer's components
12267+ * @param {string} slot the longhand's unprefixed name
12268+ * @param {string} value its one component
12269+ * @returns {string | null} the layer's new value, or null
12270+ */
12271+ const _foldTransitionSlot = (components, slot, value) => {
12272+ const names = [];
12273+ const times = [];
12274+ const easings = [];
12275+ const behaviors = [];
12276+ for (const one of components) {
12277+ const lower = toLowerCaseIfNeeded(one);
12278+ if (_TIME_RE.test(one)) {
12279+ times.push(one);
12280+ } else if (EASING_KEYWORDS.has(lower) || _EASING_CALL_RE.test(one)) {
12281+ easings.push(one);
12282+ } else if (TRANSITION_BEHAVIORS.has(lower)) {
12283+ behaviors.push(one);
12284+ } else {
12285+ names.push(one);
12286+ }
12287+ }
12288+ // An easing keyword is a property name too, so which slot it fills is the
12289+ // engine's to say where no other name stands beside it.
12290+ if (names.length > 1 || times.length > 2 || easings.length > 1) return null;
12291+ if (behaviors.length > 1) return null;
12292+ const lower = toLowerCaseIfNeeded(value);
12293+ if (slot === "transition-duration" || slot === "transition-delay") {
12294+ if (!_TIME_RE.test(value)) return null;
12295+ if (slot === "transition-duration") {
12296+ times[0] = value;
12297+ } else {
12298+ // A delay is the second time, so the first has to be written.
12299+ if (times.length === 0) return null;
12300+ times[1] = value;
12301+ }
12302+ } else if (slot === "transition-timing-function") {
12303+ if (!EASING_KEYWORDS.has(lower) && !_EASING_CALL_RE.test(value)) {
12304+ return null;
12305+ }
12306+ easings[0] = value;
12307+ } else if (slot === "transition-behavior") {
12308+ if (!TRANSITION_BEHAVIORS.has(lower)) return null;
12309+ behaviors[0] = value;
12310+ } else if (slot === "transition-property") {
12311+ // A name that is a keyword of another slot reads as that slot.
12312+ if (
12313+ _TIME_RE.test(value) ||
12314+ EASING_KEYWORDS.has(lower) ||
12315+ TRANSITION_BEHAVIORS.has(lower) ||
12316+ lower === "none"
12317+ ) {
12318+ return null;
12319+ }
12320+ names[0] = value;
12321+ } else {
12322+ return null;
12323+ }
12324+ // What a slot holding its own initial says, it says unwritten too.
12325+ if (times.length === 2 && _ZERO_TIME_RE.test(times[1])) times.pop();
12326+ if (times.length === 1 && _ZERO_TIME_RE.test(times[0])) times.pop();
12327+ if (easings.length === 1 && equalsLowerCase(easings[0], "ease")) {
12328+ easings.pop();
12329+ }
12330+ if (behaviors.length === 1 && equalsLowerCase(behaviors[0], "normal")) {
12331+ behaviors.pop();
12332+ }
12333+ const rest = [...times, ...easings, ...behaviors];
12334+ const named =
12335+ names.length === 1 && equalsLowerCase(names[0], "all") && rest.length > 0
12336+ ? []
12337+ : names;
12338+ const out = [...named, ...rest];
12339+ return out.length === 0 ? "all" : _joinSlots(out);
12340+ };
12341+
12342+ /**
12343+ * An `animation` longhand written into a one-layer `animation` whose every
12344+ * component reads as one slot alone. The new component goes last, where an
12345+ * empty slot takes it; one already filled is replaced where it stands.
12346+ * @param {string[]} components the layer's components
12347+ * @param {string} slot the longhand's unprefixed name
12348+ * @param {string} value its one component
12349+ * @returns {string | null} the layer's new value, or null
12350+ */
12351+ const _foldAnimationSlot = (components, slot, value) => {
12352+ const lower = toLowerCaseIfNeeded(value);
12353+ /** @type {(one: string, lowered: string) => boolean} */
12354+ let fills;
12355+ if (slot === "animation-duration" || slot === "animation-delay") {
12356+ if (!_TIME_RE.test(value)) return null;
12357+ fills = (one) => _TIME_RE.test(one);
12358+ } else if (slot === "animation-iteration-count") {
12359+ if (lower !== "infinite" && !_BARE_NUMBER_RE.test(value)) return null;
12360+ fills = (one, lowered) =>
12361+ lowered === "infinite" || _BARE_NUMBER_RE.test(one);
12362+ } else {
12363+ const keywords = ANIMATION_KEYWORD_SLOTS.get(slot);
12364+ if (keywords === undefined) return null;
12365+ // The easing slot also takes a call, which no other slot does.
12366+ const calls = slot === "animation-timing-function";
12367+ if (!keywords.includes(lower) && !(calls && _EASING_CALL_RE.test(value))) {
12368+ return null;
12369+ }
12370+ fills = (one, lowered) =>
12371+ keywords.includes(lowered) || (calls && _EASING_CALL_RE.test(one));
12372+ }
12373+ // `none` and `normal` are each a name and a keyword slot's too, so a layer
12374+ // holding one is read by its order, which a fold cannot keep.
12375+ const out = [];
12376+ const at = [];
12377+ for (const one of components) {
12378+ const lowered = toLowerCaseIfNeeded(one);
12379+ if (lowered === "none" || lowered === "normal") return null;
12380+ if (fills(one, lowered)) at.push(out.length);
12381+ out.push(one);
12382+ }
12383+ if (lower === "none" || lower === "normal") return null;
12384+ if (slot === "animation-delay") {
12385+ // A delay is the second time; with no duration the first would be it.
12386+ if (at.length === 0) return null;
12387+ if (at.length === 2) out[at[1]] = value;
12388+ else out.push(value);
12389+ } else if (slot === "animation-duration") {
12390+ if (at.length === 0) out.push(value);
12391+ else out[at[0]] = value;
12392+ } else if (at.length === 0) {
12393+ out.push(value);
12394+ } else if (at.length === 1) {
12395+ out[at[0]] = value;
12396+ } else {
12397+ return null;
12398+ }
12399+ return _joinSlots(out);
12400+ };
12401+
1217212402// A number with no unit and no percent, which `flex` reads as a factor.
1217312403const _BARE_NUMBER_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
1217412404
@@ -12198,6 +12428,58 @@ const _shortestFlexValue = (components) => {
1219812428 : null;
1219912429};
1220012430
12431+ /**
12432+ * An authored `flex` value of one to three components read into its three
12433+ * factors in grammar order (CSS Flexbox 1 §7.1.1): a bare number is a factor
12434+ * unless two precede it, an omitted shrink is `1` and basis `0%`, and a keyword
12435+ * the factors it names.
12436+ * @param {string[]} components the value's components, as written
12437+ * @returns {string[] | null} grow, shrink and basis, or null where it is no such value
12438+ */
12439+ const _flexFactors = (components) => {
12440+ if (components.length === 1) {
12441+ const lower = toLowerCaseIfNeeded(components[0]);
12442+ for (const [factors, keyword] of FLEX_KEYWORDS) {
12443+ if (keyword === lower) return factors.split(" ");
12444+ }
12445+ }
12446+ const factors = [];
12447+ let basis = null;
12448+ for (const component of components) {
12449+ if (_BARE_NUMBER_RE.test(component) && factors.length < 2) {
12450+ // The two factors are adjacent: a basis between them parts no pair.
12451+ if (factors.length === 1 && basis !== null && components[0] !== basis) {
12452+ return null;
12453+ }
12454+ factors.push(component);
12455+ } else {
12456+ if (basis !== null) return null;
12457+ basis = component;
12458+ }
12459+ }
12460+ if (factors.length === 0) return null;
12461+ return [
12462+ factors[0],
12463+ factors.length === 2 ? factors[1] : "1",
12464+ basis === null ? "0%" : basis
12465+ ];
12466+ };
12467+
12468+ /**
12469+ * The shortest spelling of an authored `flex` value in whatever order and count
12470+ * its grammar allows, or `null` when what was written is already that short.
12471+ * @param {string[]} components the value's components, as written
12472+ * @returns {string[] | null} the fragments to join, or `null` to keep them as they are
12473+ */
12474+ const _shortestAuthoredFlex = (components) => {
12475+ const factors = _flexFactors(components);
12476+ if (factors === null) return null;
12477+ const shortest = _shortestFlexValue(factors) || factors;
12478+ return shortest.join(" ").length < components.join(" ").length
12479+ ? shortest
12480+ : null;
12481+ };
12482+
1220112483/**
1220212484 * Split on the whitespace at the top of one value, a nested call's own spaces
1220312485 * staying inside it.
@@ -16653,6 +16935,9 @@ module.exports._setRuleEntry = _setRuleEntry;
1665316935module.exports._setHoistedRuns = _setHoistedRuns;
1665416936module.exports._shortestColor = _shortestColor;
1665516937module.exports._shortestFlexValue = _shortestFlexValue;
16938+ module.exports._shortestAuthoredFlex = _shortestAuthoredFlex;
16939+ module.exports._joinSlots = _joinSlots;
16940+ module.exports._splitPrintedComponents = _splitPrintedComponents;
1665616941module.exports._shorthandSlotsReadable = _shorthandSlotsReadable;
1665716942module.exports._splitSelectorList = _splitSelectorList;
1665816943module.exports._splitTopLevelArguments = _splitTopLevelArguments;
0 commit comments