Skip to content

Commit

Permalink
fix: cyclic reference issues in calc parser
Browse files Browse the repository at this point in the history
  • Loading branch information
nmn committed Oct 17, 2024
1 parent 11c0eb3 commit 4b70761
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 83 deletions.
49 changes: 31 additions & 18 deletions packages/style-value-parser/src/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export declare class Parser<T> {
static always<T>(output: T): Parser<T>;
where(predicate: ($$PARAM_0$$: T) => boolean): Parser<T>;
static oneOf<T>(...parsers: ReadonlyArray<Parser<T>>): Parser<T>;
static sequence<T extends ReadonlyArray<Parser<unknown>>>(
static sequence<T extends ConstrainedTuple<Parser<unknown>>>(
...parsers: T
): ParserSequence<T>;
static setOf<T extends ReadonlyArray<Parser<unknown>>>(
static setOf<T extends ConstrainedTuple<Parser<unknown>>>(
...parsers: T
): ParserSet<T>;
static zeroOrMore<T>(parser: Parser<T>): ZeroOrMoreParsers<T>;
Expand Down Expand Up @@ -57,28 +57,41 @@ declare class OneOrMoreParsers<T> extends Parser<ReadonlyArray<T>> {
constructor(parser: Parser<T>);
separatedBy(separator: Parser<unknown>): OneOrMoreParsers<T>;
}

type MapParserTuple<Ps extends ReadonlyArray<Parser<unknown>>> = Ps extends [
Parser<infer X>,
...infer Rest extends Parser<unknown>[],
]
? [X, ...MapParserTuple<Rest>]
: [];

export declare class ParserSequence<
T extends ReadonlyArray<Parser<unknown>>,
> extends Parser<MapParserTuple<T>> {
T extends ConstrainedTuple<Parser<unknown>>,
> extends Parser<ValuesFromParserTuple<T>> {
readonly parsers: T;
constructor(...parsers: T);
constructor(parsers: T);
separatedBy(separator: Parser<unknown>): ParserSequence<T>;
}
declare class ParserSet<
T extends ReadonlyArray<Parser<unknown>>,
> extends Parser<MapParserTuple<T>> {
T extends ConstrainedTuple<Parser<unknown>>,
> extends Parser<ValuesFromParserTuple<T>> {
readonly parsers: T;
constructor(...parsers: T);
constructor(parsers: T);
separatedBy(separator: Parser<unknown>): ParserSet<T>;
}

export type FromParser<P extends Parser<unknown>> =
P extends Parser<infer T> ? T : never;
type ConstrainedTuple<T> =
| $ReadOnly<[T]>
| $ReadOnly<[T, T]>
| $ReadOnly<[T, T, T]>
| $ReadOnly<[T, T, T, T]>
| $ReadOnly<[T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T, T, T]>;

// prettier-ignore
export type FromParser<T extends Parser<any>, Fallback = never> =
| Fallback
| T extends Parser<infer V> ? V : never;

type ValuesFromParserTuple<
T extends ConstrainedTuple<Parser<any>>,
Fallback = never,
> = {
[Key in keyof T]: FromParser<T[Key], Fallback>;
};
106 changes: 64 additions & 42 deletions packages/style-value-parser/src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,16 @@ export class Parser<+T> {
}

// Variadic Generics: ...T,
static sequence<T: $ReadOnlyArray<Parser<mixed>>>(
static sequence<T: ConstrainedTuple<Parser<mixed>>>(
...parsers: T
): ParserSequence<T> {
return new ParserSequence(...parsers);
return new ParserSequence<T>(parsers);
}

static setOf<T: $ReadOnlyArray<Parser<mixed>>>(...parsers: T): ParserSet<T> {
return new ParserSet(...parsers);
static setOf<T: ConstrainedTuple<Parser<mixed>>>(
...parsers: T
): ParserSet<T> {
return new ParserSet<T>(parsers);
}

static zeroOrMore<T>(parser: Parser<T>): ZeroOrMoreParsers<T> {
Expand Down Expand Up @@ -165,7 +167,11 @@ export class Parser<+T> {

static get quotedString(): Parser<string> {
// TODO: Add support for escaped code-points
const doubleQuotes: Parser<string> = Parser.sequence(
const doubleQuotes: Parser<string> = Parser.sequence<
$ReadOnly<
[Parser<string>, Parser<$ReadOnlyArray<string>>, Parser<string>],
>,
>(
Parser.string('"'),
Parser.zeroOrMore(
Parser.oneOf(
Expand All @@ -175,7 +181,11 @@ export class Parser<+T> {
),
),
Parser.string('"'),
).map(([, chars]) => chars.join(''));
).map(
([_openQuote, chars, _closeQuote]: $ReadOnly<
[string, $ReadOnlyArray<string>, string],
>) => chars.join(''),
);
const singleQuotes = Parser.sequence(
Parser.string("'"),
Parser.zeroOrMore(
Expand Down Expand Up @@ -378,17 +388,18 @@ class OneOrMoreParsers<+T> extends Parser<$ReadOnlyArray<T>> {
}
}

export class ParserSequence<+T: $ReadOnlyArray<Parser<mixed>>> extends Parser<
$TupleMap<T, <O>(Parser<O>) => O>,
export class ParserSequence<+T: ConstrainedTuple<Parser<mixed>>> extends Parser<
ValuesFromParserTuple<T>,
> {
+parsers: T;

constructor(...parsers: T) {
super((input): $TupleMap<T, <O>(Parser<O>) => O> | Error => {
constructor(parsers: T) {
super((input: SubString): ValuesFromParserTuple<T> | Error => {
const { startIndex, endIndex } = input;
let failed: null | Error = null;

const output: $TupleMap<T, <O>(Parser<O>) => O | Error> = parsers.map(
// $FlowFixMe[incompatible-type]
const output: ValuesFromParserTuple<T> | Error = parsers.map(
<X>(parser: Parser<X>): X | Error => {
if (failed) {
return Error('already failed');
Expand All @@ -414,34 +425,28 @@ export class ParserSequence<+T: $ReadOnlyArray<Parser<mixed>>> extends Parser<
}

separatedBy(separator: Parser<mixed>): ParserSequence<T> {
// This is UNSAFE. No way to safely map a tuple with Flow.
// $FlowFixMe
const parsers: [...T] = [...this.parsers];

for (let i = 1; i < parsers.length; i++) {
const originalParser = parsers[i];
const modifiedParser = originalParser.prefix(
separator.map(() => undefined),
);
parsers[i] = modifiedParser;
}
// $FlowFixMe[incompatible-type]
const parsers: T = this.parsers.map(
<X>(originalParser: Parser<X>): Parser<X> =>
originalParser.prefix(separator.map(() => undefined)),
);

return new ParserSequence(...parsers);
return new ParserSequence<T>(parsers);
}
}

// Similar to ParserSequence, but the parsers can occur in any order.
class ParserSet<+T: $ReadOnlyArray<Parser<mixed>>> extends Parser<
$TupleMap<T, <O>(Parser<O>) => O>,
class ParserSet<+T: ConstrainedTuple<Parser<mixed>>> extends Parser<
ValuesFromParserTuple<T>,
> {
+parsers: T;

constructor(...parsers: T) {
super((input): $TupleMap<T, <O>(Parser<O>) => O> | Error => {
constructor(parsers: T) {
super((input: SubString): ValuesFromParserTuple<T> | Error => {
const { startIndex, endIndex } = input;
let failed: null | Error = null;

const output = [];
const output: [...ValuesFromParserTuple<T>] = [] as $FlowFixMe;
const indices: Set<number> = new Set();

for (let i = 0; i < parsers.length; i++) {
Expand Down Expand Up @@ -479,26 +484,43 @@ class ParserSet<+T: $ReadOnlyArray<Parser<mixed>>> extends Parser<
return failed;
}

return output as $TupleMap<T, <O>(Parser<O>) => O> | Error;
return output as ValuesFromParserTuple<T>;
});
this.parsers = parsers;
}

separatedBy(separator: Parser<mixed>): ParserSet<T> {
// This is UNSAFE. No way to safely map a tuple with Flow.
// $FlowFixMe
const parsers: [...T] = [...this.parsers];
for (let i = 1; i < parsers.length; i++) {
const originalParser = parsers[i];
const modifiedParser = originalParser.prefix(
separator.map(() => undefined),
);
parsers[i] = modifiedParser;
}
// This is correct, but Flow can't map tuples right.
// $FlowFixMe[incompatible-type]
const parsers: T = this.parsers.map(
<X>(originalParser: Parser<X>): Parser<X> =>
originalParser.prefix(separator.map(() => undefined)),
);

return new ParserSet(...(parsers as T));
return new ParserSet(parsers);
}
}

export type FromParser<P: Parser<mixed>> =
P extends Parser<infer T> ? T : empty;
type ConstrainedTuple<+T> =
| $ReadOnly<[T]>
| $ReadOnly<[T, T]>
| $ReadOnly<[T, T, T]>
| $ReadOnly<[T, T, T, T]>
| $ReadOnly<[T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T, T]>
| $ReadOnly<[T, T, T, T, T, T, T, T, T, T]>;

// prettier-ignore
export type FromParser<+T: Parser<mixed>, Fallback = empty> =
| Fallback
| T extends Parser<infer V> ? V : empty;

type ValuesFromParserTuple<
+T: ConstrainedTuple<Parser<mixed>>,
Fallback = empty,
> = {
[Key in keyof T]: FromParser<T[Key], Fallback>,
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ class CalcSum {
}
static get parse(): Parser<CalcSum> {
return Parser.sequence(
calcValue,
Calc.parse,
Parser.oneOf(Parser.string('+'), Parser.string('-')),
calcValue,
Calc.parse,
)
.separatedBy(Parser.whitespace)
.map(([left, operator, right]) => new CalcSum(left, right, operator));
.map(
([left, operator, right]) =>
new CalcSum(left.value, right.value, operator),
);
}
}
Expand All @@ -66,28 +69,41 @@ class CalcProduct {
}
static get parse(): Parser<CalcProduct> {
return Parser.sequence(
calcValue,
Calc.parse,
Parser.oneOf(Parser.string('*'), Parser.string('/')),
calcValue,
Calc.parse,
)
.separatedBy(Parser.whitespace)
.map(([left, operator, right]) => new CalcProduct(left, right, operator));
.map(
([left, operator, right]) =>
new CalcProduct(left.value, right.value, operator),
);
}
}
const calcValueWithoutParens = Parser.oneOf(
Parser.float,
dimension,
Percentage.parse,
calcConstant,
CalcSum.parse,
CalcProduct.parse,
);
export const calcValue: Parser<CalcValue> = Parser.oneOf(
calcValueWithoutParens,
calcValueWithoutParens
.surroundedBy(Parser.whitespace.optional)
.prefix(Parser.string('('))
.skip(Parser.string(')')),
);
class Calc {
+value: CalcValue;
constructor(value: this['value']) {
this.value = value;
}
toString(): string {
return this.value.toString();
}
static get parse(): Parser<Calc> {
const calcValueWithoutParens = Parser.oneOf(
Parser.float,
dimension,
Percentage.parse,
calcConstant,
CalcSum.parse,
CalcProduct.parse,
);
return Parser.oneOf(
calcValueWithoutParens
.surroundedBy(Parser.whitespace.optional)
.prefix(Parser.string('('))
.skip(Parser.string(')')),
calcValueWithoutParens,
).map((value) => new Calc(value));
}
}
2 changes: 1 addition & 1 deletion packages/style-value-parser/src/css-types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export * from './angle';
export * from './basic-shape';
export * from './blend-mode';
export * from './calc-constant';
export * from './calc-sum';
export * from './calc';
export * from './color';
export * from './common-types';
export * from './custom-ident';
Expand Down

0 comments on commit 4b70761

Please sign in to comment.