Skip to content
Merged
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
241 changes: 122 additions & 119 deletions packages/core/src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,138 +77,139 @@ const failValidation = (
);
};

const ATTRIBUTE_VALIDATORS: {
number: AttributeValidator<NumberAttributeSpec>;
string: AttributeValidator<StringAttributeSpec>;
boolean: AttributeValidator<BooleanAttributeSpec>;
tuple: AttributeValidator<TupleAttributeSpec>;
} = {
number: (context, spec, value) => {
if (value.type === "NumberLiteral") {
const numberValue = value as NumberLiteral;
if (spec.validator && !spec.validator(numberValue.value)) {
failValidation(context, "failed validation", value.position);
}
return;
}

if (value.type === "StringLiteral") {
return;
}

failValidation(context, "expects a numeric value", value.position);
},
string: (context, spec, value) => {
if (value.type !== "StringLiteral") {
failValidation(
context,
`expects a string value got '${value.type}'`,
value.position
);
}
const stringValue = value as StringLiteral;
if (spec.validator && !spec.validator(stringValue.value)) {
failValidation(context, "failed validation", value.position);
}
},
boolean: (context, spec, value) => {
if (value.type !== "IdentifierLiteral") {
failValidation(context, "expects 'true' or 'false'", value.position);
}
const booleanValue = value as IdentifierLiteral;
const normalized = booleanValue.name.toLowerCase();
if (normalized !== "true" && normalized !== "false") {
failValidation(context, "expects 'true' or 'false'", value.position);
}
if (spec.validator && !spec.validator(normalized === "true")) {
failValidation(context, "failed validation", value.position);
}
},
tuple: (context, spec, value) => {
if (value.type !== "TupleLiteral") {
failValidation(context, "expects a tuple", value.position);
}
const tupleValue = value as TupleLiteral;
if (tupleValue.values.length !== spec.length) {
failValidation(
context,
`expects a tuple of length ${spec.length}`,
value.position
);
}
const itemTypes = spec.itemTypes;
const collected: Array<number | string> = [];
let shouldRunValidator = true;

for (let index = 0; index < tupleValue.values.length; index++) {
const entry = tupleValue.values[index]!;
const expectedType = itemTypes?.[index] ?? itemTypes?.[0] ?? "number";
class Parser {
private index = 0;
private tokens: Token[];
private readonly keywordCatalog = getSvgKeywords();
private newGenTokens: Token[] = [];
private fellThrough: boolean = false;
private ATTRIBUTE_VALIDATORS: {
number: AttributeValidator<NumberAttributeSpec>;
string: AttributeValidator<StringAttributeSpec>;
boolean: AttributeValidator<BooleanAttributeSpec>;
tuple: AttributeValidator<TupleAttributeSpec>;
}

if (expectedType === "number") {
if (entry.type === "NumberLiteral") {
collected.push((entry as NumberLiteral).value);
continue;
constructor(tokens: Token[]) {
this.tokens = tokens;
this.ATTRIBUTE_VALIDATORS = {
number: (context, spec, value) => {
if (value.type === "NumberLiteral") {
const numberValue = value as NumberLiteral;
if (spec.validator && !spec.validator(numberValue.value)) {
failValidation(context, "failed validation", value.position);
}
return;
}

if (entry.type === "StringLiteral") {
collected.push((entry as StringLiteral).value);
shouldRunValidator = false;
continue;
if (value.type === "StringLiteral" && this.fellThrough) {
return;
}

failValidation(
context,
"expects numeric or string tuple items",
entry.position
);
continue;
}

if (expectedType === "string") {
if (entry.type !== "StringLiteral") {
failValidation(context, "expects string tuple items", entry.position);
failValidation(context, "expects a numeric value", value.position);
},
string: (context, spec, value) => {
if (value.type !== "StringLiteral") {
failValidation(
context,
`expects a string value got '${value.type}'`,
value.position
);
}
collected.push((entry as StringLiteral).value);
continue;
}

if (expectedType === "identifier") {
if (entry.type !== "IdentifierLiteral") {
const stringValue = value as StringLiteral;
if (spec.validator && !spec.validator(stringValue.value)) {
failValidation(context, "failed validation", value.position);
}
},
boolean: (context, spec, value) => {
if (value.type !== "IdentifierLiteral") {
failValidation(context, "expects 'true' or 'false'", value.position);
}
const booleanValue = value as IdentifierLiteral;
const normalized = booleanValue.name.toLowerCase();
if (normalized !== "true" && normalized !== "false") {
failValidation(context, "expects 'true' or 'false'", value.position);
}
if (spec.validator && !spec.validator(normalized === "true")) {
failValidation(context, "failed validation", value.position);
}
},
tuple: (context, spec, value) => {
if (value.type !== "TupleLiteral") {
failValidation(context, "expects a tuple", value.position);
}
const tupleValue = value as TupleLiteral;
if (tupleValue.values.length !== spec.length) {
failValidation(
context,
"expects identifier tuple items",
entry.position
`expects a tuple of length ${spec.length}`,
value.position
);
}
collected.push((entry as IdentifierLiteral).name);
continue;
}
const itemTypes = spec.itemTypes;
const collected: Array<number | string> = [];
let shouldRunValidator = true;

for (let index = 0; index < tupleValue.values.length; index++) {
const entry = tupleValue.values[index]!;
const expectedType = itemTypes?.[index] ?? itemTypes?.[0] ?? "number";

if (expectedType === "number") {
if (entry.type === "NumberLiteral") {
collected.push((entry as NumberLiteral).value);
continue;
}

if (entry.type === "StringLiteral") {
collected.push((entry as StringLiteral).value);
shouldRunValidator = false;
continue;
}

failValidation(
context,
"expects numeric or string tuple items",
entry.position
);
continue;
}

failValidation(
context,
"has unsupported tuple item type",
entry.position
);
}
if (expectedType === "string") {
if (entry.type !== "StringLiteral") {
failValidation(context, "expects string tuple items", entry.position);
}
collected.push((entry as StringLiteral).value);
continue;
}

if (
shouldRunValidator &&
spec.validator &&
!spec.validator(collected as never)
) {
failValidation(context, "failed validation", value.position);
}
},
};
if (expectedType === "identifier") {
if (entry.type !== "IdentifierLiteral") {
failValidation(
context,
"expects identifier tuple items",
entry.position
);
}
collected.push((entry as IdentifierLiteral).name);
continue;
}

class Parser {
private index = 0;
private tokens: Token[];
private readonly keywordCatalog = getSvgKeywords();
private newGenTokens: Token[] = [];
failValidation(
context,
"has unsupported tuple item type",
entry.position
);
}

constructor(tokens: Token[]) {
this.tokens = tokens;
if (
shouldRunValidator &&
spec.validator &&
!spec.validator(collected as never)
) {
failValidation(context, "failed validation", value.position);
}
},
};
}

parse(): [RootNode, SpecialBlock[]] {
Expand Down Expand Up @@ -350,6 +351,7 @@ class Parser {
);
}
this.validateAttributeValue(parentKeyword, nameToken.value, spec, value);
this.fellThrough = false;
seen.add(nameToken.value);
return {
type: "Attribute",
Expand Down Expand Up @@ -591,6 +593,7 @@ class Parser {
private parseAttributeValue(): LiteralValue {
const fall = this.fallThrough();
if (fall) {
this.fellThrough = true;
return fall;
}
const token = this.peek();
Expand Down Expand Up @@ -709,7 +712,7 @@ class Parser {
spec: T,
value: LiteralValue
): void {
const handler = ATTRIBUTE_VALIDATORS[spec.type] as AttributeValidator<T>;
const handler = this.ATTRIBUTE_VALIDATORS[spec.type] as AttributeValidator<T>;
handler({ keyword, attributeName }, spec, value);
}

Expand Down
31 changes: 27 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,39 @@ process.on("uncaughtException", (err) => {

function runExample(): void {
const code = `
svg {
svg {
// Internal coordinate space: viewBox x y w h
box: (0, 0, 200, 200)

// Rendered size on the page
size: (200px, 200px)

// How to map box to size
preserve: (xMidYMid, meet)

circle {
id: "pulse"
at: (100, 100)
r: 40
fill: #fff
fill: "hotpink"

animate {}
animate {
prop: "r"
from: 40px
to: 60px
dur: 2s
repeat: indefinite
}
}


@hover, @active {
#pulse {
cy: 150px
r: 60px
}
}
}

`;
console.log(compile(code));
}
Expand Down
Empty file added test.mirrow
Empty file.