feat(css-parser): diagnostics for at rule page
MODIFY
internal/css-parser/parser/declaration.ts
internal/css-parser/parser/declaration.ts
@@ -17,9 +17,22 @@ import {
import {parseAtRule} from "@internal/css-parser/parser/rules";
import {parseComponentValue} from "@internal/css-parser/parser/value";
+interface ParseDeclarations {
+ parser: CSSParser;
+ endingTokenType: keyof Tokens;
+ onAtKeyword?: OnAtKeyword;
+ onAtDeclaration?: OnAtDeclaration;
+ parentAtKeywordToken?: Tokens["AtKeyword"];
+}
+
export function parseDeclarations(
- parser: CSSParser,
- endingTokenType: keyof Tokens,
+ {
+ parser,
+ endingTokenType,
+ onAtKeyword,
+ onAtDeclaration,
+ parentAtKeywordToken,
+ }: ParseDeclarations,
): Array<CSSAtRule | CSSDeclaration> {
const declarations: Array<CSSAtRule | CSSDeclaration> = [];
@@ -32,12 +45,25 @@ export function parseDeclarations(
break;
}
if (matchToken(parser, "AtKeyword")) {
- declarations.push(parseAtRule(parser));
+ const token = parser.getToken() as Tokens["AtKeyword"];
+ if (onAtKeyword) {
+ const allowed = onAtKeyword(token);
+ if (!allowed) {
+ nextToken(parser);
+ continue;
+ }
+ }
+ declarations.push(parseAtRule({parser, onAtDeclaration, onAtKeyword}));
continue;
}
if (matchToken(parser, "Ident")) {
while (!matchEndOfDeclaration(parser, endingTokenType)) {
- const declaration = parseDeclaration(parser, endingTokenType);
+ const declaration = parseDeclaration({
+ parser,
+ endingTokenType,
+ onAtDeclaration,
+ parentAtKeywordToken,
+ });
declaration && declarations.push(declaration);
}
continue;
@@ -54,9 +80,15 @@ export function parseDeclarations(
return declarations;
}
+interface ParseDeclaration {
+ parser: CSSParser;
+ endingTokenType: keyof Tokens;
+ onAtDeclaration?: OnAtDeclaration;
+ parentAtKeywordToken?: Tokens["AtKeyword"];
+}
+
export function parseDeclaration(
- parser: CSSParser,
- endingTokenType: keyof Tokens,
+ {onAtDeclaration, parentAtKeywordToken, endingTokenType, parser}: ParseDeclaration,
): CSSDeclaration | undefined {
while (!matchEndOfDeclaration(parser, endingTokenType)) {
const currentToken = parser.getToken();
@@ -67,6 +99,9 @@ export function parseDeclaration(
});
return undefined;
}
+ if (onAtDeclaration) {
+ onAtDeclaration(currentToken, parentAtKeywordToken);
+ }
let name: string | CSSCustomProperty;
if (isCustomProperty(currentToken.value)) {
name = parser.finishNode(
@@ -129,7 +164,22 @@ export function parseDeclaration(
return undefined;
}
-export function parseDeclarationBlock(parser: CSSParser): CSSBlock | undefined {
+export type OnAtKeyword = (token: Tokens["AtKeyword"]) => boolean;
+export type OnAtDeclaration = (
+ token: Tokens["Ident"],
+ previousAtKeywordToken?: Tokens["AtKeyword"],
+) => boolean;
+
+interface ParseDeclarationBlock {
+ parser: CSSParser;
+ onAtKeyword?: OnAtKeyword;
+ onAtDeclaration?: OnAtDeclaration;
+ parentAtKeywordToken?: Tokens["AtKeyword"];
+}
+
+export function parseDeclarationBlock(
+ {parser, onAtKeyword, onAtDeclaration, parentAtKeywordToken}: ParseDeclarationBlock,
+): CSSBlock | undefined {
const start = parser.getPosition();
const startingToken = parser.getToken();
const startingTokenValue = getBlockStartTokenValue(parser, startingToken);
@@ -142,7 +192,13 @@ export function parseDeclarationBlock(parser: CSSParser): CSSBlock | undefined {
nextToken(parser);
- value = parseDeclarations(parser, endingTokenType);
+ value = parseDeclarations({
+ parser,
+ endingTokenType,
+ onAtKeyword,
+ onAtDeclaration,
+ parentAtKeywordToken,
+ });
return parser.finishNode(
start,

MODIFY
internal/css-parser/parser/font.ts
internal/css-parser/parser/font.ts
@@ -10,7 +10,7 @@ export function parseFontFace(parser: CSSParser): CSSFontFace | undefined {
readToken(parser, "Whitespace");
}
- const block = parseDeclarationBlock(parser);
+ const block = parseDeclarationBlock({parser});
if (block) {
return parser.finishNode(

MODIFY
internal/css-parser/parser/keyframe.ts
internal/css-parser/parser/keyframe.ts
@@ -158,7 +158,10 @@ function parseKeyframeBlocks(parser: CSSParser): CSSKeyframeBlock[] | undefined
}
nextToken(parser);
- const value = parseDeclarations(parser, "RightCurlyBracket");
+ const value = parseDeclarations({
+ parser,
+ endingTokenType: "RightCurlyBracket",
+ });
if (name && value) {
nextToken(parser);

MODIFY
internal/css-parser/parser/page.ts
internal/css-parser/parser/page.ts
@@ -8,9 +8,32 @@ import {
import {matchToken, readToken} from "@internal/css-parser/tokenizer";
import {parseDeclarationBlock} from "@internal/css-parser/parser/declaration";
import {descriptions} from "@internal/diagnostics";
+import {
+ ALLOWED_PAGE_MARGIN_PROPERTIES,
+ ALLOWED_PAGE_PROPERTIES,
+} from "@internal/css-parser/utils";
const VALID_PSEUDO_PAGE = new Set(["left", "right", "first", "blank"]);
+const ALLOWED_AT_RULES = [
+ "top-left-corner",
+ "top-left",
+ "top-center",
+ "top-right",
+ "top-right-corner",
+ "bottom-left-corner",
+ "bottom-left",
+ "bottom-center",
+ "bottom-right",
+ "bottom-right-corner",
+ "left-top",
+ "left-middle",
+ "left-bottom",
+ "right-top",
+ "right-middle",
+ "right-bottom",
+];
+
function parsePseudoPage(parser: CSSParser): CSSPseudoPage | undefined {
const possibleIdent = parser.getToken();
if (
@@ -84,7 +107,55 @@ export function parseAtPage(parser: CSSParser): CSSAtPage | undefined {
const token = parser.getToken();
if (token.type === "LeftCurlyBracket") {
// in case @page doesn't have any prelude
- const block = parseDeclarationBlock(parser);
+ const block = parseDeclarationBlock({
+ parser,
+ /**
+ * This callback is used to check the at-rule keywords inside @page
+ * are correct
+ *
+ * https://www.w3.org/TR/css-page-3/#syntax-page-selector
+ * @param atKeywordToken
+ */
+ onAtKeyword: (atKeywordToken) => {
+ return ALLOWED_AT_RULES.includes(atKeywordToken.value);
+ },
+ /**
+ * This Function checks whether the properties inside @page and other valid at rules (@top-left, @top-center, etc)
+ * are correct. Only a certain number of properties are accepted.
+ *
+ * https://www.w3.org/TR/css-page-3/#syntax-page-selector
+ * @param declaration
+ * @param previousAtKeywordToken
+ */
+ onAtDeclaration: function(declaration, previousAtKeywordToken) {
+ if (
+ previousAtKeywordToken &&
+ ALLOWED_AT_RULES.includes(previousAtKeywordToken.value)
+ ) {
+ if (!ALLOWED_PAGE_MARGIN_PROPERTIES.includes(declaration.value)) {
+ parser.unexpectedDiagnostic({
+ description: descriptions.CSS_PARSER.AT_PAGE_AT_RULE_INVALID_DECLARATION(
+ previousAtKeywordToken.value,
+ declaration.value,
+ ALLOWED_PAGE_MARGIN_PROPERTIES,
+ ),
+ token: declaration,
+ });
+ return false;
+ }
+ } else if (!ALLOWED_PAGE_PROPERTIES.includes(declaration.value)) {
+ parser.unexpectedDiagnostic({
+ description: descriptions.CSS_PARSER.AT_PAGE_INVALID_DECLARATION(
+ declaration.value,
+ ALLOWED_PAGE_PROPERTIES,
+ ),
+ token: declaration,
+ });
+ return false;
+ }
+ return true;
+ },
+ });
if (block) {
return parser.finishNode(
start,
@@ -136,7 +207,7 @@ export function parseAtPage(parser: CSSParser): CSSAtPage | undefined {
while (matchToken(parser, "Whitespace")) {
readToken(parser, "Whitespace");
}
- const block = parseDeclarationBlock(parser);
+ const block = parseDeclarationBlock({parser});
if (block) {
return parser.finishNode(
start,

MODIFY
internal/css-parser/parser/rules.ts
internal/css-parser/parser/rules.ts
@@ -4,7 +4,11 @@ import {matchToken, nextToken, readToken} from "@internal/css-parser/tokenizer";
import {parseSelectors} from "@internal/css-parser/parser/selectors";
import {descriptions} from "@internal/diagnostics";
import {parseKeyframe} from "@internal/css-parser/parser/keyframe";
-import {parseDeclarationBlock} from "@internal/css-parser/parser/declaration";
+import {
+ OnAtDeclaration,
+ OnAtKeyword,
+ parseDeclarationBlock,
+} from "@internal/css-parser/parser/declaration";
import {parseComponentValue} from "@internal/css-parser/parser/value";
import {parseMediaList} from "@internal/css-parser/parser/media";
import {parseAtSupports} from "@internal/css-parser/parser/supports";
@@ -39,7 +43,7 @@ export function parseRules(
}
if (matchToken(parser, "AtKeyword")) {
- rules.push(parseAtRule(parser));
+ rules.push(parseAtRule({parser}));
continue;
}
@@ -62,7 +66,7 @@ function parseRule(parser: CSSParser): CSSRule | undefined {
{
type: "CSSRule",
prelude,
- block: parseDeclarationBlock(parser),
+ block: parseDeclarationBlock({parser}),
},
);
}
@@ -75,7 +79,15 @@ function parseRule(parser: CSSParser): CSSRule | undefined {
return undefined;
}
-export function parseAtRule(parser: CSSParser): CSSAtRule {
+interface ParseAtRule {
+ parser: CSSParser;
+ onAtKeyword?: OnAtKeyword;
+ onAtDeclaration?: OnAtDeclaration;
+}
+
+export function parseAtRule(
+ {parser, onAtDeclaration, onAtKeyword}: ParseAtRule,
+): CSSAtRule {
const start = parser.getPosition();
const previousToken = parser.getToken() as Tokens["AtKeyword"];
const token = parser.expectToken("AtKeyword");
@@ -121,7 +133,12 @@ export function parseAtRule(parser: CSSParser): CSSAtRule {
}
}
if (matchToken(parser, "LeftCurlyBracket")) {
- block = parseDeclarationBlock(parser);
+ block = parseDeclarationBlock({
+ parser,
+ parentAtKeywordToken: previousToken,
+ onAtDeclaration,
+ onAtKeyword,
+ });
break;
}
const parsedValue = parseComponentValue(parser);

MODIFY
internal/css-parser/parser/supports.ts
internal/css-parser/parser/supports.ts
@@ -48,7 +48,10 @@ function parseAtSupportsInParens(
}
return undefined;
}
- const declaration = parseDeclaration(parser, "RightParen");
+ const declaration = parseDeclaration({
+ parser,
+ endingTokenType: "RightParen",
+ });
if (declaration) {
const feature = parser.finishNode(
start,

RENAME
internal/css-parser/test-fixtures/page/input.css
internal/css-parser/test-fixtures/at-page/input.css
RENAME
internal/css-parser/test-fixtures/page/input.test.md
internal/css-parser/test-fixtures/at-page/input.test.md
**DO NOT MODIFY**. This file has been autogenerated. Run `rome test internal/css-parser/index.test.ts --update-snapshots` to update.
-## `page`
+## `at-page`
```javascript
CSSRoot {
@@ -23,17 +23,17 @@ CSSRoot {
value: [
CSSIdentifier {
value: "none"
- loc: SourceLocation page/input.css 2:24-2:28
+ loc: SourceLocation at-page/input.css 2:24-2:28
}
]
important: false
- loc: SourceLocation page/input.css 2:15-2:29
+ loc: SourceLocation at-page/input.css 2:15-2:29
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 2:13-2:30
+ loc: SourceLocation at-page/input.css 2:13-2:30
}
- loc: SourceLocation page/input.css 2:1-2:30
+ loc: SourceLocation at-page/input.css 2:1-2:30
}
CSSDeclaration {
name: "margin-left"
@@ -41,15 +41,15 @@ CSSRoot {
CSSDimension {
value: 4
unit: "cm"
- loc: SourceLocation page/input.css 3:14-3:17
+ loc: SourceLocation at-page/input.css 3:14-3:17
}
]
important: false
- loc: SourceLocation page/input.css 3:1-3:17
+ loc: SourceLocation at-page/input.css 3:1-3:17
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 1:19-4:1
+ loc: SourceLocation at-page/input.css 1:19-4:1
}
prelude: CSSPageSelectorList {
value: [
@@ -57,16 +57,16 @@ CSSRoot {
ident: "MyPage"
pseudo: CSSPseudoPage {
value: "blank"
- loc: SourceLocation page/input.css 1:13-1:18
+ loc: SourceLocation at-page/input.css 1:13-1:18
}
- loc: SourceLocation page/input.css 1:6-1:18
+ loc: SourceLocation at-page/input.css 1:6-1:18
}
]
- loc: SourceLocation page/input.css 1:5-1:19
+ loc: SourceLocation at-page/input.css 1:5-1:19
}
- loc: SourceLocation page/input.css 1:5-4:1
+ loc: SourceLocation at-page/input.css 1:5-4:1
}
- loc: SourceLocation page/input.css 1:0-4:1
+ loc: SourceLocation at-page/input.css 1:0-4:1
}
CSSAtRule {
name: "page"
@@ -84,17 +84,17 @@ CSSRoot {
value: [
CSSIdentifier {
value: "none"
- loc: SourceLocation page/input.css 7:24-7:28
+ loc: SourceLocation at-page/input.css 7:24-7:28
}
]
important: false
- loc: SourceLocation page/input.css 7:15-7:29
+ loc: SourceLocation at-page/input.css 7:15-7:29
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 7:13-7:30
+ loc: SourceLocation at-page/input.css 7:13-7:30
}
- loc: SourceLocation page/input.css 7:1-7:30
+ loc: SourceLocation at-page/input.css 7:1-7:30
}
CSSDeclaration {
name: "margin-left"
@@ -102,31 +102,31 @@ CSSRoot {
CSSDimension {
value: 4
unit: "cm"
- loc: SourceLocation page/input.css 8:14-8:17
+ loc: SourceLocation at-page/input.css 8:14-8:17
}
]
important: false
- loc: SourceLocation page/input.css 8:1-8:17
+ loc: SourceLocation at-page/input.css 8:1-8:17
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 6:13-9:1
+ loc: SourceLocation at-page/input.css 6:13-9:1
}
prelude: CSSPageSelectorList {
value: [
CSSPageSelector {
pseudo: CSSPseudoPage {
value: "blank"
- loc: SourceLocation page/input.css 6:7-6:12
+ loc: SourceLocation at-page/input.css 6:7-6:12
}
- loc: SourceLocation page/input.css 6:6-6:12
+ loc: SourceLocation at-page/input.css 6:6-6:12
}
]
- loc: SourceLocation page/input.css 6:5-6:13
+ loc: SourceLocation at-page/input.css 6:5-6:13
}
- loc: SourceLocation page/input.css 6:5-9:1
+ loc: SourceLocation at-page/input.css 6:5-9:1
}
- loc: SourceLocation page/input.css 6:0-9:1
+ loc: SourceLocation at-page/input.css 6:0-9:1
}
CSSAtRule {
name: "page"
@@ -144,17 +144,17 @@ CSSRoot {
value: [
CSSIdentifier {
value: "none"
- loc: SourceLocation page/input.css 12:24-12:28
+ loc: SourceLocation at-page/input.css 12:24-12:28
}
]
important: false
- loc: SourceLocation page/input.css 12:15-12:29
+ loc: SourceLocation at-page/input.css 12:15-12:29
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 12:13-12:30
+ loc: SourceLocation at-page/input.css 12:13-12:30
}
- loc: SourceLocation page/input.css 12:1-12:30
+ loc: SourceLocation at-page/input.css 12:1-12:30
}
CSSDeclaration {
name: "margin-left"
@@ -162,25 +162,25 @@ CSSRoot {
CSSDimension {
value: 4
unit: "cm"
- loc: SourceLocation page/input.css 13:14-13:17
+ loc: SourceLocation at-page/input.css 13:14-13:17
}
]
important: false
- loc: SourceLocation page/input.css 13:1-13:17
+ loc: SourceLocation at-page/input.css 13:1-13:17
}
]
startingTokenValue: "{"
- loc: SourceLocation page/input.css 11:6-14:1
+ loc: SourceLocation at-page/input.css 11:6-14:1
}
- loc: SourceLocation page/input.css 11:5-14:1
+ loc: SourceLocation at-page/input.css 11:5-14:1
}
- loc: SourceLocation page/input.css 11:0-14:1
+ loc: SourceLocation at-page/input.css 11:0-14:1
}
]
comments: []
corrupt: false
diagnostics: []
- path: RelativePath<page/input.css>
- loc: SourceLocation page/input.css 1:0-14:1
+ path: RelativePath<at-page/input.css>
+ loc: SourceLocation at-page/input.css 1:0-14:1
}
```

ADD
internal/css-parser/test-fixtures/invalid/at-page/page-margin-properties/input.css
+@page {
+ @top-center {
+ display: none;
+ }
+}
+

ADD
internal/css-parser/test-fixtures/invalid/at-page/page-margin-properties/input.test.md
+# `index.test.ts`
+
+**DO NOT MODIFY**. This file has been autogenerated. Run `rome test internal/css-parser/index.test.ts --update-snapshots` to update.
+
+## `invalid > at-page > page-margin-properties`
+
+```javascript
+CSSRoot {
+ body: [
+ CSSAtRule {
+ name: "page"
+ prelude: []
+ block: CSSAtPage {
+ block: CSSBlock {
+ value: [
+ CSSAtRule {
+ name: "top-center"
+ prelude: []
+ block: CSSBlock {
+ value: [
+ CSSDeclaration {
+ name: "display"
+ value: [
+ CSSIdentifier {
+ value: "none"
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 3:11-3:15
+ }
+ ]
+ important: false
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 3:2-3:15
+ }
+ ]
+ startingTokenValue: "{"
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 2:13-4:2
+ }
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 2:1-4:2
+ }
+ ]
+ startingTokenValue: "{"
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 1:6-5:1
+ }
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 1:5-5:1
+ }
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 1:0-5:1
+ }
+ ]
+ comments: []
+ corrupt: false
+ diagnostics: [
+ {
+ origins: [{entity: "ParserCore<css>"}]
+ description: {
+ advice: [
+ log {category: "info", text: [RAW_MARKUP {value: "Did you mean <emphasis>"}, "padding", RAW_MARKUP {value: "</emphasis>?"}]}
+ diff {diff: [[0, "dis"], [1, "p"], [0, "l"], [1, "a"], [0, "y"], [2, "dding"]], language: "unknown"}
+ log {
+ category: "info"
+ text: RAW_MARKUP {value: "Or one of these?"}
+ }
+ list {
+ list: [
+ "direction"
+ "visibility"
+ "padding-top"
+ "white-space"
+ "padding-left"
+ "word-spacing"
+ "unicode-bidi"
+ "padding-right"
+ "padding-bottom"
+ "letter-spacing"
+ "background-color"
+ "background-image"
+ "background-repeat"
+ "background-attachment"
+ "background-position"
+ "background"
+ "border-top-width"
+ "border-right-width"
+ "border-bottom-width"
+ "border-left-width"
+ "border-width"
+ "border-top-color"
+ "border-right-color"
+ "border-bottom-color"
+ "border-left-color"
+ "border-color"
+ "border-top-style"
+ "border-right-style"
+ "border-bottom-style"
+ "border-left-style"
+ "border-short-style"
+ "border-top"
+ "border-right"
+ "border-bottom"
+ "border-left"
+ "border"
+ "counter-reset"
+ "counter-increment"
+ "font-family"
+ "font-size"
+ "font-style"
+ "font-variant"
+ "font-weight"
+ "font"
+ "height"
+ "min-height"
+ "max-height"
+ "margin-top"
+ "margin-right"
+ "margin-bottom"
+ "margin-left"
+ "margin"
+ "outline-width"
+ "outline-style"
+ "outline-color"
+ "outline"
+ "text-align"
+ "text-decoration"
+ "text-indent"
+ "text-transform"
+ "width"
+ "min-width"
+ "max-width"
+ "content"
+ "color"
+ "line-height"
+ "quotes"
+ "vertical-align"
+ "z-index"
+ ]
+ truncate: true
+ }
+ ]
+ category: ["parse"]
+ categoryValue: "css"
+ message: [
+ RAW_MARKUP {value: "The at-rule <emphasis>@"}
+ "top-center"
+ RAW_MARKUP {value: "</emphasis> accepts only a certain number of properties and <emphasis>"}
+ "display"
+ RAW_MARKUP {value: "</emphasis> is not one of them."}
+ ]
+ }
+ location: {
+ language: "css"
+ path: RelativePath<invalid/at-page/page-margin-properties/input.css>
+ end: Position 3:9
+ start: Position 3:2
+ }
+ }
+ ]
+ path: RelativePath<invalid/at-page/page-margin-properties/input.css>
+ loc: SourceLocation invalid/at-page/page-margin-properties/input.css 1:0-5:1
+}
+```

ADD
internal/css-parser/test-fixtures/invalid/at-page/page-properties/input.css
+@page {
+ display: none;
+}

ADD
internal/css-parser/test-fixtures/invalid/at-page/page-properties/input.test.md
+# `index.test.ts`
+
+**DO NOT MODIFY**. This file has been autogenerated. Run `rome test internal/css-parser/index.test.ts --update-snapshots` to update.
+
+## `invalid > at-page > page-properties`
+
+```javascript
+CSSRoot {
+ body: [
+ CSSAtRule {
+ name: "page"
+ prelude: []
+ block: CSSAtPage {
+ block: CSSBlock {
+ value: [
+ CSSDeclaration {
+ name: "display"
+ value: [
+ CSSIdentifier {
+ value: "none"
+ loc: SourceLocation invalid/at-page/page-properties/input.css 2:10-2:14
+ }
+ ]
+ important: false
+ loc: SourceLocation invalid/at-page/page-properties/input.css 2:1-2:14
+ }
+ ]
+ startingTokenValue: "{"
+ loc: SourceLocation invalid/at-page/page-properties/input.css 1:6-3:1
+ }
+ loc: SourceLocation invalid/at-page/page-properties/input.css 1:5-3:1
+ }
+ loc: SourceLocation invalid/at-page/page-properties/input.css 1:0-3:1
+ }
+ ]
+ comments: []
+ corrupt: false
+ diagnostics: [
+ {
+ origins: [{entity: "ParserCore<css>"}]
+ description: {
+ advice: [
+ log {category: "info", text: [RAW_MARKUP {value: "Did you mean <emphasis>"}, "padding", RAW_MARKUP {value: "</emphasis>?"}]}
+ diff {diff: [[0, "dis"], [1, "p"], [0, "l"], [1, "a"], [0, "y"], [2, "dding"]], language: "unknown"}
+ log {
+ category: "info"
+ text: RAW_MARKUP {value: "Or one of these?"}
+ }
+ list {
+ list: [
+ "direction"
+ "visibility"
+ "padding-top"
+ "white-space"
+ "padding-left"
+ "word-spacing"
+ "padding-right"
+ "padding-bottom"
+ "letter-spacing"
+ "background-color"
+ "background-image"
+ "background-repeat"
+ "background-attachment"
+ "background-position"
+ "background"
+ "border-top-width"
+ "border-right-width"
+ "border-bottom-width"
+ "border-left-width"
+ "border-width"
+ "border-top-color"
+ "border-right-color"
+ "border-bottom-color"
+ "border-left-color"
+ "border-color"
+ "border-top-style"
+ "border-right-style"
+ "border-bottom-style"
+ "border-left-style"
+ "border-short-style"
+ "border-top"
+ "border-right"
+ "border-bottom"
+ "border-left"
+ "border"
+ "counter-reset"
+ "counter-increment"
+ "font-family"
+ "font-size"
+ "font-style"
+ "font-variant"
+ "font-weight"
+ "font"
+ "height"
+ "min-height"
+ "max-height"
+ "margin-top"
+ "margin-right"
+ "margin-bottom"
+ "margin-left"
+ "margin"
+ "outline-width"
+ "outline-style"
+ "outline-color"
+ "outline"
+ "text-align"
+ "text-decoration"
+ "text-indent"
+ "text-transform"
+ "width"
+ "min-width"
+ "max-width"
+ "color"
+ "quotes"
+ ]
+ truncate: true
+ }
+ ]
+ category: ["parse"]
+ categoryValue: "css"
+ message: [
+ RAW_MARKUP {value: "The at-rule <emphasis>@page</emphasis> accepts only a certain number of properties and <emphasis>"}
+ "display"
+ RAW_MARKUP {value: "</emphasis> is not one of them."}
+ ]
+ }
+ location: {
+ language: "css"
+ path: RelativePath<invalid/at-page/page-properties/input.css>
+ end: Position 2:8
+ start: Position 2:1
+ }
+ }
+ ]
+ path: RelativePath<invalid/at-page/page-properties/input.css>
+ loc: SourceLocation invalid/at-page/page-properties/input.css 1:0-3:1
+}
+```

MODIFY
internal/css-parser/utils.ts
internal/css-parser/utils.ts
@@ -183,3 +183,115 @@ export type AND = typeof AND;
export type OR = typeof OR;
export const CONDITIONS = [NOT, AND, OR];
+
+const BACKGROUND_PROPERTIES = [
+ "background-color",
+ "background-image",
+ "background-repeat",
+ "background-attachment",
+ "background-position",
+ "background",
+];
+const BORDER_PROPERTIES = [
+ "border-top-width",
+ "border-right-width",
+ "border-bottom-width",
+ "border-left-width",
+ "border-width",
+ "border-top-color",
+ "border-right-color",
+ "border-bottom-color",
+ "border-left-color",
+ "border-color",
+ "border-top-style",
+ "border-right-style",
+ "border-bottom-style",
+ "border-left-style",
+ "border-short-style",
+ "border-top",
+ "border-right",
+ "border-bottom",
+ "border-left",
+ "border",
+];
+const COUNTER_PROPERTIES = ["counter-reset", "counter-increment"];
+const FONT_PROPERTIES = [
+ "font-family",
+ "font-size",
+ "font-style",
+ "font-variant",
+ "font-weight",
+ "font",
+];
+const HEIGTH_PROPERTIES = ["height", "min-height", "max-height"];
+const MARGIN_PROPERTIES = [
+ "margin-top",
+ "margin-right",
+ "margin-bottom",
+ "margin-left",
+ "margin",
+];
+const OUTLINE_PROPERTIES = [
+ "outline-width",
+ "outline-style",
+ "outline-color",
+ "outline",
+];
+const PADDING_PROPERTIES = [
+ "padding-top",
+ "padding-right",
+ "padding-bottom",
+ "padding-left",
+ "padding",
+];
+const TEXT_PROPERTIES = [
+ "letter-spacing",
+ "text-align",
+ "text-decoration",
+ "text-indent",
+ "text-transform",
+ "white-space",
+ "word-spacing",
+];
+const WIDTH_PROPERTIES = ["width", "min-width", "max-width"];
+
+// https://www.w3.org/TR/css-page-3/#page-property-list
+export const ALLOWED_PAGE_PROPERTIES: string[] = [
+ ...BACKGROUND_PROPERTIES,
+ ...BORDER_PROPERTIES,
+ ...COUNTER_PROPERTIES,
+ ...FONT_PROPERTIES,
+ ...HEIGTH_PROPERTIES,
+ ...MARGIN_PROPERTIES,
+ ...OUTLINE_PROPERTIES,
+ ...PADDING_PROPERTIES,
+ ...TEXT_PROPERTIES,
+ ...WIDTH_PROPERTIES,
+ "direction",
+ "color",
+ "quotes",
+ "visibility",
+];
+
+// https://www.w3.org/TR/css-page-3/#margin-property-list
+export const ALLOWED_PAGE_MARGIN_PROPERTIES: string[] = [
+ ...BACKGROUND_PROPERTIES,
+ ...BORDER_PROPERTIES,
+ ...COUNTER_PROPERTIES,
+ ...FONT_PROPERTIES,
+ ...HEIGTH_PROPERTIES,
+ ...MARGIN_PROPERTIES,
+ ...OUTLINE_PROPERTIES,
+ ...PADDING_PROPERTIES,
+ ...TEXT_PROPERTIES,
+ ...WIDTH_PROPERTIES,
+ "direction",
+ "unicode-bidi",
+ "content",
+ "color",
+ "line-height",
+ "quotes",
+ "vertical-align",
+ "visibility",
+ "z-index",
+];

MODIFY
internal/diagnostics/descriptions/parsers/cssParser.ts
internal/diagnostics/descriptions/parsers/cssParser.ts
@@ -180,4 +180,26 @@ export const cssParser = createDiagnosticsCategory({
AT_PAGE_INVALID_PSEUDO_PAGE: {
message: markup`Pseudo page can accept only <emphasis>left, right, first or blank</emphasis> and there can't be spaces in between`,
},
+
+ AT_PAGE_INVALID_DECLARATION: (wrongIdent, validProperties) => ({
+ message: markup`The at-rule <emphasis>@page</emphasis> accepts only a certain number of properties and <emphasis>${wrongIdent}</emphasis> is not one of them.`,
+ advice: buildSuggestionAdvice(
+ wrongIdent,
+ validProperties,
+ {minRating: 0, ignoreCase: false},
+ ),
+ }),
+
+ AT_PAGE_AT_RULE_INVALID_DECLARATION: (
+ currentAtRule,
+ wrongIdent,
+ validProperties,
+ ) => ({
+ message: markup`The at-rule <emphasis>@${currentAtRule}</emphasis> accepts only a certain number of properties and <emphasis>${wrongIdent}</emphasis> is not one of them.`,
+ advice: buildSuggestionAdvice(
+ wrongIdent,
+ validProperties,
+ {minRating: 0, ignoreCase: false},
+ ),
+ }),
});

feat(css-parser): diagnostics for at rule page (#1427)