Skip to content

Commit fe28af5

Browse files
committed
Handle basic @import rules to produce the same output as rework
1 parent 2711f30 commit fe28af5

File tree

2 files changed

+131
-39
lines changed

2 files changed

+131
-39
lines changed

src/index.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@ export interface Stylesheet {
77
parsingErrors: string[];
88
};
99
}
10-
export type Rule = QualifiedRule | AtRule | StyleRule;
10+
export type AtRule = ImportAtRule | AtRuleToken;
11+
export type Rule = QualifiedRule | StyleRule | AtRule;
1112

12-
export interface AtRule {
13+
/**
14+
* An at-rule generated when no specific parsing can be found for an at-rule.
15+
* The CSS level 3 spec describes how to parse at-rules in general when more specific rules
16+
* for e. g. @import, @keyframes etc. are part of separate specs and added as extensions to the CSS parser.
17+
*/
18+
export interface AtRuleToken {
1319
type: "at-rule";
1420
name: string;
1521
prelude: InputToken[];
1622
block: SimpleBlock;
23+
position?: Source;
24+
}
25+
export interface ImportAtRule {
26+
type: "import",
27+
import: string,
28+
position?: Source;
1729
}
1830
export interface QualifiedRule {
1931
type: "qualified-rule";
@@ -635,6 +647,36 @@ const enum ParseState {
635647
Declaration = 2,
636648
}
637649

650+
export class AtRuleParser {
651+
public static identity = new AtRuleParser();
652+
keyword: string;
653+
fromAtRule(rule: AtRuleToken): AtRule {
654+
return rule;
655+
}
656+
}
657+
658+
/**
659+
* https://drafts.csswg.org/css-cascade-3/#at-ruledef-import
660+
* Minimal @import parser to cover rework @import rules parser.
661+
*/
662+
export class ImportParser extends AtRuleParser {
663+
fromAtRule(rule: AtRuleToken): ImportAtRule {
664+
// TODO: Handle blocks, media queries, etc.
665+
const type = "import";
666+
const imp = rule.prelude.map(toString).join("").trim();
667+
const position = rule.position;
668+
return position ? { type, import: imp, position } : { type, import: imp };
669+
}
670+
}
671+
ImportParser.prototype.keyword = "import";
672+
673+
/**
674+
* https://www.w3.org/TR/css-animations-1/#keyframes
675+
*/
676+
export class KeyframesParser extends AtRuleParser {
677+
}
678+
KeyframesParser.prototype.keyword = "keyframes";
679+
638680
/**
639681
* 5. Parsing
640682
* https://www.w3.org/TR/css-syntax-3/#parsing
@@ -649,6 +691,12 @@ export class Parser extends Tokenizer {
649691
protected topLevelFlag: boolean;
650692
protected parseState: ParseState;
651693

694+
private atRuleParsers: { [keyword: string]: AtRuleParser } = {};
695+
696+
public addAtRuleParser(atRuleParser: AtRuleParser) {
697+
this.atRuleParsers[atRuleParser.keyword] = atRuleParser;
698+
}
699+
652700
/**
653701
* 5.3.1. Parse a stylesheet
654702
* https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet
@@ -713,8 +761,7 @@ export class Parser extends Tokenizer {
713761
continue;
714762
}
715763
if (typeof inputToken === "object" && inputToken.type === TokenType.atKeyword) {
716-
// TODO: Better typechecking...
717-
const atRule = this.consumeAnAtRule(inputToken as AtKeywordToken);
764+
const atRule = this.consumeAnAtRule(inputToken);
718765
if (atRule) {
719766
rules.push(atRule);
720767
}
@@ -733,29 +780,35 @@ export class Parser extends Tokenizer {
733780
* https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule
734781
*/
735782
protected consumeAnAtRule(reconsumedInputToken: AtKeywordToken): AtRule {
736-
const atRule: AtRule = {
783+
const start = this.debug && this.start();
784+
const atRuleParser = this.atRuleParsers[reconsumedInputToken.name] || AtRuleParser.identity;
785+
const atRule: AtRuleToken = {
737786
type: "at-rule",
738-
name: reconsumedInputToken.name, // TODO: What if it is not an @whatever?
787+
name: reconsumedInputToken.name,
739788
prelude: [],
740789
block: undefined,
741790
};
742791
let inputToken: InputToken;
743792
while (inputToken = this.consumeAToken()) {
744793
if (inputToken === ";") {
745-
return atRule;
794+
break;
746795
} else if (inputToken === "{") {
747796
atRule.block = this.consumeASimpleBlock(inputToken);
748-
return atRule;
797+
break;
749798
} else if (typeof inputToken === "object" && inputToken.type === TokenType.simpleBlock && inputToken.associatedToken === "{") {
750799
atRule.block = inputToken as SimpleBlock;
751-
return atRule;
800+
break;
752801
}
753802
const component = this.consumeAComponentValue(inputToken);
754803
if (component) {
755804
atRule.prelude.push(component);
756805
}
757806
}
758-
return atRule;
807+
if (this.debug) {
808+
const end = this.end();
809+
atRule.position = { start, end };
810+
}
811+
return atRuleParser.fromAtRule(atRule);
759812
}
760813

761814
/**

test/test.ts

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as fs from "fs";
55

66
import { assert } from "chai";
77
import * as cssParse from "css-parse";
8-
import { Parser, TokenType } from "../src/index";
8+
import { Parser, TokenType, ImportParser, KeyframesParser } from "../src/index";
99

1010
describe("css", () => {
1111
let parser: Parser;
@@ -312,6 +312,10 @@ describe("css", () => {
312312
name: "import",
313313
prelude: [" ", { type: TokenType.url, source: `url(~/app.css)`, url: "~/app.css" }],
314314
block: undefined,
315+
position: {
316+
start: { line: 1, column: 1 },
317+
end: { column: 24, line: 1 }
318+
}
315319
},
316320
{
317321
type: "qualified-rule",
@@ -459,6 +463,10 @@ describe("css", () => {
459463
" ",
460464
],
461465
},
466+
position: {
467+
start: { line: 2, column: 17 },
468+
end: { line: 5, column: 18 }
469+
}
462470
},
463471
{
464472
type: "qualified-rule",
@@ -492,33 +500,64 @@ describe("css", () => {
492500
});
493501
});
494502
});
495-
describe("css stylesheet as rework", () => {
496-
function compare(css: string): void {
497-
const nativescript = parser.parseACSSStylesheet(css);
498-
// console.log(JSON.stringify(nativescript));
499-
// Strip type info and undefined properties.
500-
const rework = JSON.parse(JSON.stringify(cssParse(css)));
501-
assert.deepEqual(nativescript, rework);
502-
}
503-
it("div{color:red}p{color:blue}", () => {
504-
compare("div{color:red}p{color:blue}");
505-
});
506-
it("Button, Label { background: red; }", () => {
507-
compare("Button, Label {\n background: red;\n}\n");
508-
});
509-
it("Label { color: argb(1, 255, 0, 0); }", () => {
510-
compare("Label { color: argb(1, 255, 0, 0); }");
511-
});
512-
it("Div { width: 50%; height: 30px; border-width: 2; }", () => {
513-
compare("Div { width: 50%; height: 30px; border-width: 2; }");
514-
});
515-
it("Div {color:#212121;opacity:.9}", () => {
516-
compare("Div {color:#212121;opacity:.9}");
517-
});
518-
it("core.light.css", () => {
519-
const css = fs.readFileSync("./test/assets/core.light.css").toString();
520-
compare(css);
521-
});
522-
// TODO: Complete implementation of string-ly values for declarations and do extensive testing...
503+
});
504+
505+
describe("css as rework", () => {
506+
let parser: Parser;
507+
before("create parser", () => {
508+
parser = new Parser();
509+
parser.addAtRuleParser(new ImportParser());
510+
parser.addAtRuleParser(new KeyframesParser());
511+
});
512+
after("dispose parser", () => parser = null);
513+
514+
function compare(css: string): void {
515+
const nativescript = parser.parseACSSStylesheet(css);
516+
// console.log(JSON.stringify(nativescript));
517+
// Strip type info and undefined properties.
518+
const rework = JSON.parse(JSON.stringify(cssParse(css)));
519+
// console.log("REWORK AST:\n" + JSON.stringify(rework, null, " "));
520+
// console.log("{N} AST:\n" + JSON.stringify(nativescript, null, " "));
521+
assert.deepEqual(nativescript, rework);
522+
}
523+
it("div{color:red}p{color:blue}", () => {
524+
compare("div{color:red}p{color:blue}");
525+
});
526+
it("Button, Label { background: red; }", () => {
527+
compare("Button, Label {\n background: red;\n}\n");
528+
});
529+
it("Label { color: argb(1, 255, 0, 0); }", () => {
530+
compare("Label { color: argb(1, 255, 0, 0); }");
531+
});
532+
it("Div { width: 50%; height: 30px; border-width: 2; }", () => {
533+
compare("Div { width: 50%; height: 30px; border-width: 2; }");
534+
});
535+
it("Div {color:#212121;opacity:.9}", () => {
536+
compare("Div {color:#212121;opacity:.9}");
537+
});
538+
it("core.light.css", () => {
539+
const css = fs.readFileSync("./test/assets/core.light.css").toString();
540+
compare(css);
541+
});
542+
it.skip("simple keyframe", () => {
543+
const css = `
544+
@keyframes example {
545+
0% { transform: scale(1, 1); }
546+
100% { transform: scale(1, 0); }
547+
}
548+
div {
549+
animation: example 5s linear 2s infinite alternate;
550+
}
551+
`;
552+
compare(css);
553+
});
554+
it("simple import", () => {
555+
const css = `
556+
@import url("mycomponent.css");
557+
@import url("mycomponent-print.css") print;
558+
div { background: red; }
559+
`;
560+
compare(css);
523561
});
524562
});
563+

0 commit comments

Comments
 (0)