From 01d07c087a7e25681a9b31499ef0b9abd42927ee Mon Sep 17 00:00:00 2001 From: Fredrik Nicol Date: Wed, 4 Apr 2018 22:08:32 +0200 Subject: [PATCH 1/5] Add tests for combined keywords --- __tests__/typer.ts | 79 +++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/__tests__/typer.ts b/__tests__/typer.ts index 6321566..f704d7e 100644 --- a/__tests__/typer.ts +++ b/__tests__/typer.ts @@ -3,10 +3,10 @@ import typing, { Type } from '../src/typer'; describe('typing', () => { it('types combinators', () => { - expect(typing(parse('something another-thing'))).toHaveLength(1); - expect(typing(parse('something && another-thing'))).toHaveLength(1); - expect(typing(parse('something || another-thing'))).toHaveLength(3); expect(typing(parse('something | another-thing'))).toHaveLength(2); + expect(typing(parse('something || another-thing'))).toHaveLength(4); + expect(typing(parse('something && another-thing'))).toHaveLength(2); + expect(typing(parse('something another-thing'))).toHaveLength(1); }); it('types components', () => { @@ -26,52 +26,53 @@ describe('typing', () => { }); it('types optional components', () => { - expect(typing(parse('something another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, - ]); - expect(typing(parse('something another-thing? yet-another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, ]); - expect(typing(parse('something? another-thing yet-another-thing? | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing? yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, ]); - expect(typing(parse('something? another-thing? yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing' }, ]); - expect(typing(parse('something? another-thing? yet-another-thing? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.StringLiteral }, - { type: Type.StringLiteral }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing? yet-another-thing'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'yet-another-thing' }, ]); - expect(typing(parse('something another-thing yet-another-thing? | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something? another-thing? yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, + { type: Type.StringLiteral, literal: 'another-thing' }, + { type: Type.StringLiteral, literal: 'yet-another-thing' }, ]); - expect(typing(parse('something another-thing? yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing yet-another-thing?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something another-thing' }, ]); - expect(typing(parse('something? another-thing yet-another-thing | 100'))).toMatchObject([ - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something another-thing? yet-another-thing'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, ]); }); it('types optional group components', () => { - expect(typing(parse('[ something ] [ another-thing ]? | 100'))).toMatchObject([ - { type: Type.StringLiteral }, - { type: Type.String }, - { type: Type.NumericLiteral }, + expect(typing(parse('something [ another-thing | yet-another-thing ]?'))).toMatchObject([ + { type: Type.StringLiteral, literal: 'something another-thing' }, + { type: Type.StringLiteral, literal: 'something yet-another-thing' }, + { type: Type.StringLiteral, literal: 'something' }, ]); }); }); From c19826d7ee2ac7b30c4262e883ff12a7d3d13318 Mon Sep 17 00:00:00 2001 From: Fredrik Nicol Date: Tue, 24 Apr 2018 23:51:42 +0200 Subject: [PATCH 2/5] Fix typo --- src/at-rules.ts | 2 +- src/comment.ts | 4 ++-- src/data.ts | 4 ++-- src/parser.ts | 8 ++++---- src/selectors.ts | 2 +- src/typer.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/at-rules.ts b/src/at-rules.ts index 2543d15..636bcd8 100644 --- a/src/at-rules.ts +++ b/src/at-rules.ts @@ -69,7 +69,7 @@ export let getAtRules = () => { } } - // Cache + // Memoize getAtRules = () => ({ literals, rules, diff --git a/src/comment.ts b/src/comment.ts index 48b05e7..8901dc7 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -123,7 +123,7 @@ function getCompatRows(compatibilityData: MDN.CompatData) { } function supportVersion(supports: MDN.Support | MDN.Support[] | undefined): string[] { - supports = supports ? (Array.isArray(supports) ? supports : [supports]).reverse() : []; + supports = supports ? (Array.isArray(supports) ? supports : [supports]) : []; // Ignore versions hidden under flags supports = supports.filter(({ flags }) => !flags); @@ -251,7 +251,7 @@ function formatL10n(phrase: string) { } if (chunk.startsWith('{{') && chunk.endsWith('}}')) { - warn('Unknown curly bracket block `%s` in i10n', chunk); + warn('Unknown curly braces block `%s` in i10n', chunk); return chunk; } diff --git a/src/data.ts b/src/data.ts index e1ee0a3..d1b3525 100644 --- a/src/data.ts +++ b/src/data.ts @@ -28,7 +28,7 @@ export let getProperties = () => { } } - // Cache + // Memoize getProperties = () => properties; return properties; @@ -55,7 +55,7 @@ export let getSyntaxes = () => { } } - // Cache + // Memoize getSyntaxes = () => syntaxes; return syntaxes; diff --git a/src/parser.ts b/src/parser.ts index 90ff6e9..c9cb1e9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -183,18 +183,18 @@ export function isCombinator(entity: EntityType): entity is ICombinator { return entity.entity === Entity.Combinator; } -export function isCurlyBracetMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { +export function isCurlyBracesMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { return multiplier.sign === Multiplier.CurlyBracet; } export function isMandatoryMultiplied(multiplier: MultiplierType | null) { - return multiplier !== null && (isCurlyBracetMultiplier(multiplier) && multiplier.min > 1); + return multiplier !== null && (isCurlyBracesMultiplier(multiplier) && multiplier.min > 1); } export function isOptionallyMultiplied(multiplier: MultiplierType | null) { return ( multiplier !== null && - ((isCurlyBracetMultiplier(multiplier) && multiplier.min < multiplier.max && multiplier.max > 1) || + ((isCurlyBracesMultiplier(multiplier) && multiplier.min < multiplier.max && multiplier.max > 1) || multiplier.sign === Multiplier.Asterisk || multiplier.sign === Multiplier.PlusSign || multiplier.sign === Multiplier.HashMark || @@ -209,7 +209,7 @@ export function isMandatoryEntity(entity: EntityType) { if (entity.multiplier) { return ( - (isCurlyBracetMultiplier(entity.multiplier) && entity.multiplier.min > 0) || + (isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.min > 0) || entity.multiplier.sign === Multiplier.PlusSign || entity.multiplier.sign === Multiplier.HashMark || entity.multiplier.sign === Multiplier.ExclamationPoint diff --git a/src/selectors.ts b/src/selectors.ts index 9eef1a9..4ed3bd1 100644 --- a/src/selectors.ts +++ b/src/selectors.ts @@ -26,7 +26,7 @@ export let getPseudos = () => { } } } - // Cache + // Memoize getPseudos = () => ({ simple, advanced, diff --git a/src/typer.ts b/src/typer.ts index 07960d5..07535ee 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -73,7 +73,7 @@ let getBasicDataTypes = () => { return dataTypes; }, {}); - // Cache + // Memoize getBasicDataTypes = () => types; return types; From 61541854b260b89f588854781e05ecaa2d4c9159 Mon Sep 17 00:00:00 2001 From: Fredrik Nicol Date: Wed, 25 Apr 2018 13:50:17 +0200 Subject: [PATCH 3/5] WIP --- src/keywords.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ src/typer.ts | 9 ++++++ 2 files changed, 82 insertions(+) create mode 100644 src/keywords.ts diff --git a/src/keywords.ts b/src/keywords.ts new file mode 100644 index 0000000..c8e04d7 --- /dev/null +++ b/src/keywords.ts @@ -0,0 +1,73 @@ +import { isProperty, isSyntax, getSyntax, getPropertySyntax } from './data'; +import parse, { + Component, + EntityType, + ICombinator, + isCombinator, + isComponent, + isCurlyBracesMultiplier, +} from './parser'; + +const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3; + +export function isCombinedKeywordsCandidate(entities: EntityType[]) { + for (const entity of entities) { + if (isCombinator(entity)) { + continue; + } + if (isComponent(entity)) { + switch (entity.component) { + case Component.DataType: { + if (isSyntax(entity.value) && !isCombinedKeywordsCandidate(parse(getSyntax(entity.value)))) { + return false; + } + if (isProperty(entity.value) && !isCombinedKeywordsCandidate(parse(getPropertySyntax(entity.value)))) { + return false; + } + + // Missing or basic data type + return false; + } + case Component.Group: { + if (!isCombinedKeywordsCandidate(entity.entities)) { + return false; + } + break; + } + } + + if ( + entity.multiplier !== null && + // We can work with a small amount. But too many isn't worth it. + !(isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.max < CURLY_BRACES_MULTIPLIER_MAXIMUM) + ) { + return false; + } + + continue; + } + + // This means we have something unknown or too complicated + return false; + } + + return true; +} + +export function precedenceCombinator(entities: EntityType[]) { + let combinator: ICombinator | null = null; + + for (const entity of entities) { + if (isCombinator(entity)) { + if (!combinator) { + combinator = entity; + } + if (combinator !== entity) { + // This should never happen if grouping works as it should. So we just wnt to make sure. + throw new Error('Combinators must be grouped by precedence'); + } + } + } + + return combinator; +} diff --git a/src/typer.ts b/src/typer.ts index 07535ee..bc5a5be 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -11,6 +11,7 @@ import { isMandatoryMultiplied, isOptionallyMultiplied, } from './parser'; +import { isCombinedKeywordsCandidate } from './keywords'; export enum Type { Alias, @@ -80,6 +81,14 @@ let getBasicDataTypes = () => { }; export default function typing(entities: EntityType[]): TypeType[] { + if (isCombinedKeywordsCandidate(entities)) { + return [ + { + type: Type.String, + }, + ]; + } + let mandatoryCombinatorCount = 0; let mandatoryNonCombinatorsCount = 0; for (const entity of entities) { From 2cdbbcf948e8e5c688adda42bbd1bbb9245ff5d7 Mon Sep 17 00:00:00 2001 From: Fredrik Nicol Date: Fri, 27 Apr 2018 16:04:03 +0200 Subject: [PATCH 4/5] WIP --- src/keywords.ts | 24 +++++++++++------------- src/typer.ts | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/keywords.ts b/src/keywords.ts index c8e04d7..a39f29b 100644 --- a/src/keywords.ts +++ b/src/keywords.ts @@ -7,29 +7,30 @@ import parse, { isComponent, isCurlyBracesMultiplier, } from './parser'; +import { TypeType } from './typer'; const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3; -export function isCombinedKeywordsCandidate(entities: EntityType[]) { +export function combineKeywords(entities: EntityType[]) { + const types: TypeType[] = []; + const combinator = precedenceCombinator(entities); + for (const entity of entities) { - if (isCombinator(entity)) { - continue; - } if (isComponent(entity)) { switch (entity.component) { case Component.DataType: { - if (isSyntax(entity.value) && !isCombinedKeywordsCandidate(parse(getSyntax(entity.value)))) { - return false; + if (isSyntax(entity.value)) { + const keywords = combineKeywords(parse(getSyntax(entity.value))); } - if (isProperty(entity.value) && !isCombinedKeywordsCandidate(parse(getPropertySyntax(entity.value)))) { - return false; + if (isProperty(entity.value) && !combineKeywords(parse(getPropertySyntax(entity.value)))) { + return null; } // Missing or basic data type return false; } case Component.Group: { - if (!isCombinedKeywordsCandidate(entity.entities)) { + if (!combineKeywords(entity.entities)) { return false; } break; @@ -41,14 +42,11 @@ export function isCombinedKeywordsCandidate(entities: EntityType[]) { // We can work with a small amount. But too many isn't worth it. !(isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.max < CURLY_BRACES_MULTIPLIER_MAXIMUM) ) { - return false; + return null; } continue; } - - // This means we have something unknown or too complicated - return false; } return true; diff --git a/src/typer.ts b/src/typer.ts index bc5a5be..43efe7d 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -11,7 +11,7 @@ import { isMandatoryMultiplied, isOptionallyMultiplied, } from './parser'; -import { isCombinedKeywordsCandidate } from './keywords'; +import { combineKeywords } from './keywords'; export enum Type { Alias, @@ -81,7 +81,7 @@ let getBasicDataTypes = () => { }; export default function typing(entities: EntityType[]): TypeType[] { - if (isCombinedKeywordsCandidate(entities)) { + if (combineKeywords(entities)) { return [ { type: Type.String, From b885649ae170bec3df95bcb4ff779d5a4148aaca Mon Sep 17 00:00:00 2001 From: Fredrik Nicol Date: Sat, 28 Apr 2018 09:12:47 +0200 Subject: [PATCH 5/5] WIP --- src/keywords.ts | 71 ------------------------------------------------- src/parser.ts | 18 +++++++++++++ src/typer.ts | 68 +++++++++++++++++++++++++++++++++++++++------- 3 files changed, 77 insertions(+), 80 deletions(-) delete mode 100644 src/keywords.ts diff --git a/src/keywords.ts b/src/keywords.ts deleted file mode 100644 index a39f29b..0000000 --- a/src/keywords.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { isProperty, isSyntax, getSyntax, getPropertySyntax } from './data'; -import parse, { - Component, - EntityType, - ICombinator, - isCombinator, - isComponent, - isCurlyBracesMultiplier, -} from './parser'; -import { TypeType } from './typer'; - -const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3; - -export function combineKeywords(entities: EntityType[]) { - const types: TypeType[] = []; - const combinator = precedenceCombinator(entities); - - for (const entity of entities) { - if (isComponent(entity)) { - switch (entity.component) { - case Component.DataType: { - if (isSyntax(entity.value)) { - const keywords = combineKeywords(parse(getSyntax(entity.value))); - } - if (isProperty(entity.value) && !combineKeywords(parse(getPropertySyntax(entity.value)))) { - return null; - } - - // Missing or basic data type - return false; - } - case Component.Group: { - if (!combineKeywords(entity.entities)) { - return false; - } - break; - } - } - - if ( - entity.multiplier !== null && - // We can work with a small amount. But too many isn't worth it. - !(isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.max < CURLY_BRACES_MULTIPLIER_MAXIMUM) - ) { - return null; - } - - continue; - } - } - - return true; -} - -export function precedenceCombinator(entities: EntityType[]) { - let combinator: ICombinator | null = null; - - for (const entity of entities) { - if (isCombinator(entity)) { - if (!combinator) { - combinator = entity; - } - if (combinator !== entity) { - // This should never happen if grouping works as it should. So we just wnt to make sure. - throw new Error('Combinators must be grouped by precedence'); - } - } - } - - return combinator; -} diff --git a/src/parser.ts b/src/parser.ts index c9cb1e9..ac5f31c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -263,6 +263,24 @@ function multiplierData(raw: string[]): MultiplierType | null { } } +export function precedenceCombinator(entities: EntityType[]) { + let combinator: ICombinator | null = null; + + for (const entity of entities) { + if (isCombinator(entity)) { + if (!combinator) { + combinator = entity; + } + if (combinator !== entity) { + // This should never happen if grouping works as it should. So we just wnt to make sure. + throw new Error('Combinators must be grouped by precedence'); + } + } + } + + return combinator; +} + function groupByPrecedence(entities: EntityType[], precedence: number = Combinator.SingleBar): EntityType[] { if (precedence < 0) { // We've reached the lowest precedence possible diff --git a/src/typer.ts b/src/typer.ts index 43efe7d..e54122c 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -1,7 +1,7 @@ import * as cssTypes from 'mdn-data/css/types.json'; -import { isProperty, isSyntax } from './data'; +import { isProperty, isSyntax, getSyntax, getPropertySyntax } from './data'; import { warn } from './logger'; -import { +import parse, { Combinator, Component, EntityType, @@ -10,8 +10,9 @@ import { isMandatoryEntity, isMandatoryMultiplied, isOptionallyMultiplied, + isCurlyBracesMultiplier, + precedenceCombinator, } from './parser'; -import { combineKeywords } from './keywords'; export enum Type { Alias, @@ -50,6 +51,8 @@ export type TypeType = IBasic | IStringLiteral | INumeric export type ResolvedType = TypeType; +const CURLY_BRACES_MULTIPLIER_MAXIMUM = 3; + let getBasicDataTypes = () => { const types = Object.keys(cssTypes).reduce<{ [name: string]: IBasic }>((dataTypes, name) => { switch (name) { @@ -81,12 +84,10 @@ let getBasicDataTypes = () => { }; export default function typing(entities: EntityType[]): TypeType[] { - if (combineKeywords(entities)) { - return [ - { - type: Type.String, - }, - ]; + const strictTypes = strictTyping(entities); + + if (strictTypes !== null) { + return strictTypes; } let mandatoryCombinatorCount = 0; @@ -179,6 +180,55 @@ export default function typing(entities: EntityType[]): TypeType[] { return types; } +export function strictTyping(entities: EntityType[]): TypeType[] | null { + const types: TypeType[] = []; + const combinator = precedenceCombinator(entities); + + for (const entity of entities) { + if (isComponent(entity)) { + switch (entity.component) { + case Component.DataType: { + if (isSyntax(entity.value) || isProperty(entity.value)) { + const strictTypes = strictTyping( + parse(isSyntax(entity.value) ? getSyntax(entity.value) : getPropertySyntax(entity.value)), + ); + + if (strictTypes === null) { + return null; + } + } + + // Missing or basic data type + return null; + } + case Component.Group: { + const strictTypes = strictTyping(entity.entities); + + if (strictTypes === null) { + return null; + } + + // TODO + + break; + } + } + + if ( + entity.multiplier !== null && + // We can work with a small amount. But too many isn't worth it. + !(isCurlyBracesMultiplier(entity.multiplier) && entity.multiplier.max < CURLY_BRACES_MULTIPLIER_MAXIMUM) + ) { + return null; + } + + continue; + } + } + + return types; +} + function addLength(types: Array>): Array> { if (types.every(type => type.type !== Type.Length)) { return [