From 00c49da805a78ae7a73a066b9d964a09e99b1e0b Mon Sep 17 00:00:00 2001 From: Thierry Bela Nanga Date: Tue, 28 Jan 2025 21:52:16 -0500 Subject: [PATCH] at-rule container validation #53 --- dist/index-umd-web.js | 351 +++++++++++++++++- dist/index.cjs | 351 +++++++++++++++++- dist/index.d.ts | 10 +- dist/lib/parser/parse.js | 6 +- dist/lib/validation/atrule.js | 4 + src/@types/token.d.ts | 6 - src/lib/parser/parse.ts | 8 +- src/lib/validation/at-rules/container.ts | 451 +++++++++++++++++++++++ src/lib/validation/at-rules/index.ts | 3 +- src/lib/validation/atrule.ts | 6 + test/specs/code/media.js | 22 ++ 11 files changed, 1195 insertions(+), 23 deletions(-) create mode 100644 src/lib/validation/at-rules/container.ts diff --git a/dist/index-umd-web.js b/dist/index-umd-web.js index 0ec090c..315c13b 100644 --- a/dist/index-umd-web.js +++ b/dist/index-umd-web.js @@ -14685,6 +14685,348 @@ const validateAtRuleElse = validateAtRuleWhen; + const validateContainerScrollStateFeature = validateContainerSizeFeature; + function validateAtRuleContainer(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + }; + } + const result = validateAtRuleContainerQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + }; + } + function validateAtRuleContainerQueryList(tokens, atRule) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + let result = null; + let tokenType = null; + for (const queries of splitTokenList(tokens)) { + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + result = null; + const match = []; + let token = null; + tokenType = null; + while (queries.length > 0) { + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + if (queries[0].typ == exports.EnumToken.IdenTokenType) { + match.push(queries.shift()); + consumeWhitespace(queries); + } + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (token.typ != exports.EnumToken.ParensTokenType && (token.typ != exports.EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + if (token.typ == exports.EnumToken.ParensTokenType) { + result = validateContainerSizeFeature(token.chi, atRule); + } + else if (token.val == 'scroll-state') { + result = validateContainerScrollStateFeature(token.chi, atRule); + } + else { + result = validateContainerStyleFeature(token.chi, atRule); + } + if (result.valid == ValidationLevel.Drop) { + return result; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ != exports.EnumToken.MediaFeatureAndTokenType && token.typ != exports.EnumToken.MediaFeatureOrTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = token.typ; + } + if (tokenType != token.typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + function validateContainerStyleFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 1) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + if ([exports.EnumToken.DashedIdenTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ) || + (tokens[0].typ == exports.EnumToken.MediaQueryConditionTokenType && tokens[0].op.typ == exports.EnumToken.ColonTokenType)) { + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + } + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + function validateContainerSizeFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + if (tokens.length == 1) { + const token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateContainerSizeFeature([token.val], atRule); + } + if (token.typ == exports.EnumToken.ParensTokenType) { + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); + } + function validateAtRuleContainerQueryStyleInParams(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + let token = tokens[0]; + let tokenType = null; + let result = null; + while (tokens.length > 0) { + token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (tokens[0].typ != exports.EnumToken.ParensTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + const slices = tokens[0].chi.slice(); + consumeWhitespace(slices); + if (slices.length == 1) { + if ([exports.EnumToken.MediaFeatureNotTokenType, exports.EnumToken.ParensTokenType].includes(slices[0].typ)) { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + else if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + } + else { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = tokens[0].typ; + } + if (tokenType != tokens[0].typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + function validateAtRuleCustomMedia(atRule, options, root) { // media-query-list if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { @@ -14774,6 +15116,9 @@ if (atRule.nam == 'else') { return validateAtRuleElse(atRule); } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } @@ -15383,6 +15728,7 @@ error: '@' + node.nam + ' not allowed here', tokens }; + // console.error({valid, isValid}); if (valid.valid == ValidationLevel.Drop) { errors.push({ action: 'drop', @@ -15651,17 +15997,18 @@ continue; } } - if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports'].includes(value.val))) { + if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == exports.EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == exports.EnumToken.CommentTokenType || value.chi[i].typ == exports.EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { + if ((dashedIdent && value.chi[i].typ == exports.EnumToken.DashedIdenTokenType) || value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { nameIndex = i; } break; diff --git a/dist/index.cjs b/dist/index.cjs index 9f891b0..f6bf712 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -14684,6 +14684,348 @@ function validateAtRuleWhenQueryList(tokenList, atRule) { const validateAtRuleElse = validateAtRuleWhen; +const validateContainerScrollStateFeature = validateContainerSizeFeature; +function validateAtRuleContainer(atRule, options, root) { + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + }; + } + const result = validateAtRuleContainerQueryList(atRule.tokens, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + if (!('chi' in atRule)) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + }; +} +function validateAtRuleContainerQueryList(tokens, atRule) { + if (tokens.length == 0) { + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + let result = null; + let tokenType = null; + for (const queries of splitTokenList(tokens)) { + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + result = null; + const match = []; + let token = null; + tokenType = null; + while (queries.length > 0) { + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + }; + } + if (queries[0].typ == exports.EnumToken.IdenTokenType) { + match.push(queries.shift()); + consumeWhitespace(queries); + } + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (token.typ != exports.EnumToken.ParensTokenType && (token.typ != exports.EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + if (token.typ == exports.EnumToken.ParensTokenType) { + result = validateContainerSizeFeature(token.chi, atRule); + } + else if (token.val == 'scroll-state') { + result = validateContainerScrollStateFeature(token.chi, atRule); + } + else { + result = validateContainerStyleFeature(token.chi, atRule); + } + if (result.valid == ValidationLevel.Drop) { + return result; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + break; + } + token = queries[0]; + if (token.typ != exports.EnumToken.MediaFeatureAndTokenType && token.typ != exports.EnumToken.MediaFeatureOrTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = token.typ; + } + if (tokenType != token.typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + queries.shift(); + consumeWhitespace(queries); + if (queries.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; +} +function validateContainerStyleFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 1) { + if (tokens[0].typ == exports.EnumToken.ParensTokenType) { + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + if ([exports.EnumToken.DashedIdenTokenType, exports.EnumToken.IdenTokenType].includes(tokens[0].typ) || + (tokens[0].typ == exports.EnumToken.MediaQueryConditionTokenType && tokens[0].op.typ == exports.EnumToken.ColonTokenType)) { + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + } + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; +} +function validateContainerSizeFeature(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + if (tokens.length == 1) { + const token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + return validateContainerSizeFeature([token.val], atRule); + } + if (token.typ == exports.EnumToken.ParensTokenType) { + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; + } + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); +} +function validateAtRuleContainerQueryStyleInParams(tokens, atRule) { + tokens = tokens.slice(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + let token = tokens[0]; + let tokenType = null; + let result = null; + while (tokens.length > 0) { + token = tokens[0]; + if (token.typ == exports.EnumToken.MediaFeatureNotTokenType) { + token = token.val; + } + if (tokens[0].typ != exports.EnumToken.ParensTokenType) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + const slices = tokens[0].chi.slice(); + consumeWhitespace(slices); + if (slices.length == 1) { + if ([exports.EnumToken.MediaFeatureNotTokenType, exports.EnumToken.ParensTokenType].includes(slices[0].typ)) { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + else if (![exports.EnumToken.DashedIdenTokenType, exports.EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + result = { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + }; + } + } + else { + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + if (result.valid == ValidationLevel.Drop) { + return result; + } + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + break; + } + if (![exports.EnumToken.MediaFeatureAndTokenType, exports.EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + }; + } + if (tokenType == null) { + tokenType = tokens[0].typ; + } + if (tokenType != tokens[0].typ) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + }; + } + tokens.shift(); + consumeWhitespace(tokens); + if (tokens.length == 0) { + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + }; + } + } + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + }; +} + function validateAtRuleCustomMedia(atRule, options, root) { // media-query-list if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { @@ -14773,6 +15115,9 @@ function validateAtRule(atRule, options, root) { if (atRule.nam == 'else') { return validateAtRuleElse(atRule); } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } @@ -15382,6 +15727,7 @@ async function parseNode(results, context, stats, options, errors, src, map) { error: '@' + node.nam + ' not allowed here', tokens }; + // console.error({valid, isValid}); if (valid.valid == ValidationLevel.Drop) { errors.push({ action: 'drop', @@ -15650,17 +15996,18 @@ function parseAtRulePrelude(tokens, atRule) { continue; } } - if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports'].includes(value.val))) { + if (value.typ == exports.EnumToken.ParensTokenType || (value.typ == exports.EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == exports.EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == exports.EnumToken.CommentTokenType || value.chi[i].typ == exports.EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { + if ((dashedIdent && value.chi[i].typ == exports.EnumToken.DashedIdenTokenType) || value.chi[i].typ == exports.EnumToken.IdenTokenType || value.chi[i].typ == exports.EnumToken.FunctionTokenType || value.chi[i].typ == exports.EnumToken.ColorTokenType) { nameIndex = i; } break; diff --git a/dist/index.d.ts b/dist/index.d.ts index aa0f991..2229944 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -567,21 +567,15 @@ export declare interface MediaFeatureToken extends BaseToken { val: string; } -export declare interface MediaFeatureOnlyToken extends BaseToken { - - typ: EnumToken.MediaFeatureOnlyTokenType, - val: Token; -} - export declare interface MediaFeatureNotToken extends BaseToken { typ: EnumToken.MediaFeatureNotTokenType, val: Token; } -export declare interface MediaFeatureNotToken extends BaseToken { +export declare interface MediaFeatureOnlyToken extends BaseToken { - typ: EnumToken.MediaFeatureNotTokenType, + typ: EnumToken.MediaFeatureOnlyTokenType, val: Token; } diff --git a/dist/lib/parser/parse.js b/dist/lib/parser/parse.js index 25aa865..46d859f 100644 --- a/dist/lib/parser/parse.js +++ b/dist/lib/parser/parse.js @@ -511,6 +511,7 @@ async function parseNode(results, context, stats, options, errors, src, map) { error: '@' + node.nam + ' not allowed here', tokens }; + // console.error({valid, isValid}); if (valid.valid == ValidationLevel.Drop) { errors.push({ action: 'drop', @@ -779,17 +780,18 @@ function parseAtRulePrelude(tokens, atRule) { continue; } } - if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports'].includes(value.val))) { + if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes(value.val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); let i; let nameIndex = -1; let valueIndex = -1; + const dashedIdent = value.typ == EnumToken.FunctionTokenType && value.val == 'style'; for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { continue; } - if (value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { + if ((dashedIdent && value.chi[i].typ == EnumToken.DashedIdenTokenType) || value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { nameIndex = i; } break; diff --git a/dist/lib/validation/atrule.js b/dist/lib/validation/atrule.js index 37e293e..b0cc36d 100644 --- a/dist/lib/validation/atrule.js +++ b/dist/lib/validation/atrule.js @@ -19,6 +19,7 @@ import { validateAtRuleDocument } from './at-rules/document.js'; import { validateAtRuleKeyframes } from './at-rules/keyframes.js'; import { validateAtRuleWhen } from './at-rules/when.js'; import { validateAtRuleElse } from './at-rules/else.js'; +import { validateAtRuleContainer } from './at-rules/container.js'; import { validateAtRuleCustomMedia } from './at-rules/custom-media.js'; function validateAtRule(atRule, options, root) { @@ -69,6 +70,9 @@ function validateAtRule(atRule, options, root) { if (atRule.nam == 'else') { return validateAtRuleElse(atRule); } + if (atRule.nam == 'container') { + return validateAtRuleContainer(atRule); + } if (atRule.nam == 'document') { return validateAtRuleDocument(atRule); } diff --git a/src/@types/token.d.ts b/src/@types/token.d.ts index 0a3cc4a..4a668fd 100644 --- a/src/@types/token.d.ts +++ b/src/@types/token.d.ts @@ -449,12 +449,6 @@ export declare interface MediaFeatureOnlyToken extends BaseToken { val: Token; } -export declare interface MediaFeatureNotToken extends BaseToken { - - typ: EnumToken.MediaFeatureNotTokenType, - val: Token; -} - export declare interface MediaFeatureAndToken extends BaseToken { typ: EnumToken.MediaFeatureAndTokenType; diff --git a/src/lib/parser/parse.ts b/src/lib/parser/parse.ts index f5db8ca..76538d5 100644 --- a/src/lib/parser/parse.ts +++ b/src/lib/parser/parse.ts @@ -768,6 +768,8 @@ async function parseNode(results: TokenizeResult[], context: AstRuleList | AstIn tokens } as ValidationResult; + // console.error({valid, isValid}); + if (valid.valid == ValidationLevel.Drop) { errors.push({ @@ -1148,7 +1150,7 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ } } - if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports'].includes((value).val))) { + if (value.typ == EnumToken.ParensTokenType || (value.typ == EnumToken.FunctionTokenType && ['media', 'supports', 'style', 'scroll-state'].includes((value).val))) { // @todo parse range and declarations // parseDeclaration(parent.chi); @@ -1157,6 +1159,8 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ let nameIndex: number = -1; let valueIndex: number = -1; + const dashedIdent: boolean = value.typ == EnumToken.FunctionTokenType && value.val == 'style'; + for (let i = 0; i < value.chi.length; i++) { if (value.chi[i].typ == EnumToken.CommentTokenType || value.chi[i].typ == EnumToken.WhitespaceTokenType) { @@ -1164,7 +1168,7 @@ export function parseAtRulePrelude(tokens: Token[], atRule: AtRuleToken): Token[ continue; } - if (value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { + if ((dashedIdent && value.chi[i].typ == EnumToken.DashedIdenTokenType) || value.chi[i].typ == EnumToken.IdenTokenType || value.chi[i].typ == EnumToken.FunctionTokenType || value.chi[i].typ == EnumToken.ColorTokenType) { nameIndex = i; } diff --git a/src/lib/validation/at-rules/container.ts b/src/lib/validation/at-rules/container.ts new file mode 100644 index 0000000..00f6380 --- /dev/null +++ b/src/lib/validation/at-rules/container.ts @@ -0,0 +1,451 @@ +import type {AstAtRule, AstNode, MediaFeatureNotToken, Token, ValidationOptions} from "../../../@types"; +import type {ValidationSyntaxResult} from "../../../@types/validation"; +import {EnumToken, ValidationLevel} from "../../ast"; +import {consumeWhitespace, splitTokenList} from "../utils"; + +const validateContainerScrollStateFeature = validateContainerSizeFeature; + +export function validateAtRuleContainer(atRule: AstAtRule, options: ValidationOptions, root?: AstNode): ValidationSyntaxResult { + + // media-query-list + if (!Array.isArray(atRule.tokens) || atRule.tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected supports query list', + tokens: [] + } as ValidationSyntaxResult; + } + + const result: ValidationSyntaxResult = validateAtRuleContainerQueryList(atRule.tokens, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + + if (!('chi' in atRule)) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected at-rule body', + tokens: [] + } as ValidationSyntaxResult; + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens: [] + } as ValidationSyntaxResult; +} + +function validateAtRuleContainerQueryList(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + if (tokens.length == 0) { + + // @ts-ignore + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } as ValidationSyntaxResult; + } + + let result: ValidationSyntaxResult | null = null; + let tokenType: EnumToken | null = null; + + for (const queries of splitTokenList(tokens)) { + + consumeWhitespace(queries); + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } + } + + result = null; + const match: Token[] = []; + let token: Token | null = null; + + tokenType = null; + + while (queries.length > 0) { + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query list', + tokens + } + } + + if (queries[0].typ == EnumToken.IdenTokenType) { + + match.push(queries.shift() as Token); + consumeWhitespace(queries); + } + + if (queries.length == 0) { + + break; + } + + token = queries[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + token = token.val; + } + + if (token.typ != EnumToken.ParensTokenType && (token.typ != EnumToken.FunctionTokenType || !['scroll-state', 'style'].includes(token.val))) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + + if (token.typ == EnumToken.ParensTokenType) { + + result = validateContainerSizeFeature(token.chi, atRule); + } else if (token.val == 'scroll-state') { + + result = validateContainerScrollStateFeature(token.chi, atRule); + } else { + + result = validateContainerStyleFeature(token.chi, atRule); + } + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + + queries.shift(); + consumeWhitespace(queries); + + if (queries.length == 0) { + + break; + } + + token = queries[0]; + + if (token.typ != EnumToken.MediaFeatureAndTokenType && token.typ != EnumToken.MediaFeatureOrTokenType) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + } + } + + if (tokenType == null) { + + tokenType = token.typ; + } + + if (tokenType != token.typ) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + } + } + + queries.shift(); + consumeWhitespace(queries); + + if (queries.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: queries[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } +} + +function validateContainerStyleFeature(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + + consumeWhitespace(tokens); + + if (tokens.length == 1) { + + if (tokens[0].typ == EnumToken.ParensTokenType) { + + return validateContainerStyleFeature(tokens[0].chi, atRule); + } + + if ([EnumToken.DashedIdenTokenType, EnumToken.IdenTokenType].includes(tokens[0].typ) || + ( tokens[0].typ == EnumToken.MediaQueryConditionTokenType &&tokens[0].op.typ == EnumToken.ColonTokenType)) { + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } + } + } + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } +} + +function validateContainerSizeFeature(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + if (tokens.length == 1) { + + const token: Token = tokens[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + return validateContainerSizeFeature([(token as MediaFeatureNotToken).val], atRule); + } + + if (token.typ == EnumToken.ParensTokenType) { + + return validateAtRuleContainerQueryStyleInParams(token.chi, atRule); + } + + if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(tokens[0].typ)) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } + } + + return validateAtRuleContainerQueryStyleInParams(tokens, atRule); +} + +function validateAtRuleContainerQueryStyleInParams(tokens: Token[], atRule: AstAtRule): ValidationSyntaxResult { + + tokens = tokens.slice(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + + let token: Token = tokens[0]; + let tokenType: EnumToken | null = null; + let result: ValidationSyntaxResult | null = null; + + while (tokens.length > 0) { + + token = tokens[0]; + + if (token.typ == EnumToken.MediaFeatureNotTokenType) { + + token = token.val; + } + + if (tokens[0].typ != EnumToken.ParensTokenType) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + + const slices = tokens[0].chi.slice(); + + consumeWhitespace(slices); + + if (slices.length == 1) { + + if ([EnumToken.MediaFeatureNotTokenType, EnumToken.ParensTokenType].includes(slices[0].typ)) { + + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + } else if (![EnumToken.DashedIdenTokenType, EnumToken.MediaQueryConditionTokenType].includes(slices[0].typ)) { + + result = { + valid: ValidationLevel.Drop, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: 'expected container query features', + tokens + } + } + } else { + + result = validateAtRuleContainerQueryStyleInParams(slices, atRule); + + if (result.valid == ValidationLevel.Drop) { + + return result; + } + } + + tokens.shift(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + break; + } + + if (![EnumToken.MediaFeatureAndTokenType, EnumToken.MediaFeatureOrTokenType].includes(tokens[0].typ)) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expecting and/or container query token', + tokens + } + } + + if (tokenType == null) { + + tokenType = tokens[0].typ; + } + + if (tokenType != tokens[0].typ) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'mixing and/or not allowed at the same level', + tokens + } + } + + tokens.shift(); + consumeWhitespace(tokens); + + if (tokens.length == 0) { + + return { + valid: ValidationLevel.Drop, + matches: [], + node: tokens[0], + syntax: '@' + atRule.nam, + error: 'expected container query-in-parens', + tokens + } + } + } + + return { + valid: ValidationLevel.Valid, + matches: [], + node: atRule, + syntax: '@' + atRule.nam, + error: '', + tokens + } +} \ No newline at end of file diff --git a/src/lib/validation/at-rules/index.ts b/src/lib/validation/at-rules/index.ts index 9d8d5a0..2637115 100644 --- a/src/lib/validation/at-rules/index.ts +++ b/src/lib/validation/at-rules/index.ts @@ -10,4 +10,5 @@ export * from './namespace'; export * from './document'; export * from './keyframes'; export * from './when'; -export * from './else'; \ No newline at end of file +export * from './else'; +export * from './container'; \ No newline at end of file diff --git a/src/lib/validation/atrule.ts b/src/lib/validation/atrule.ts index 6d69a3f..fa41ac8 100644 --- a/src/lib/validation/atrule.ts +++ b/src/lib/validation/atrule.ts @@ -4,6 +4,7 @@ import {EnumToken, ValidationLevel} from "../ast"; import {getParsedSyntax, getSyntaxConfig} from "./config"; import {ValidationSyntaxGroupEnum, ValidationToken} from "./parser"; import { + validateAtRuleContainer, validateAtRuleCounterStyle, validateAtRuleDocument, validateAtRuleElse, @@ -95,6 +96,11 @@ export function validateAtRule(atRule: AstAtRule, options: ValidationOptions, ro return validateAtRuleElse(atRule, options, root); } + if (atRule.nam == 'container') { + + return validateAtRuleContainer(atRule, options, root); + } + if (atRule.nam == 'document') { return validateAtRuleDocument(atRule, options, root); diff --git a/test/specs/code/media.js b/test/specs/code/media.js index d73a057..360052f 100644 --- a/test/specs/code/media.js +++ b/test/specs/code/media.js @@ -147,6 +147,28 @@ export function run(describe, expect, transform, parse, render, dirname) { }`)); }); + it('container #9', function () { + + return transform(` + +/* condition list */ +@container card scroll-state(stuck: top) and + style(--themeBackground), + not style(background-color: red), + style(color: green) and style(background-color: transparent), + style(--themeColor: blue) or style(--themeColor: purple){ + h2 { + font-size: 1.5em; + } +} + +`, {beautify: true}).then((result) => expect(result.code).equals(`@container card scroll-state(stuck:top) and style(--themeBackground),not style(background-color:red),style(color:green) and style(background-color:transparent),style(--themeColor:blue) or style(--themeColor:purple) { + h2 { + font-size: 1.5em + } +}`)); + }); + it('custom-media #9', function () { return transform(`