diff --git a/__tests__/parser.ts b/__tests__/parser.ts index 5bcea5c..aa58cc1 100644 --- a/__tests__/parser.ts +++ b/__tests__/parser.ts @@ -9,7 +9,7 @@ describe('parsing', () => { }); it('parses components', () => { - expect(parse('something [ something ]')).toMatchObject([ + expect(parse('something [ something ]')).toMatchObject([ { component: Component.Keyword }, {}, { component: Component.DataType }, @@ -113,5 +113,55 @@ describe('parsing', () => { ], }, ]); + + expect(parse('something && something something || something')).toMatchObject([ + { + component: Component.Group, + entities: [ + {}, + { combinator: Combinator.DoubleAmpersand }, + { + component: Component.Group, + entities: [{}, { combinator: Combinator.Juxtaposition }, {}], + }, + ], + }, + { combinator: Combinator.DoubleBar }, + {}, + ]); + + expect(parse('something || something something && something')).toMatchObject([ + {}, + { combinator: Combinator.DoubleBar }, + { + component: Component.Group, + entities: [ + { + component: Component.Group, + entities: [{}, { combinator: Combinator.Juxtaposition }, {}], + }, + { combinator: Combinator.DoubleAmpersand }, + {}, + ], + }, + ]); + + expect(parse('something | something something something')).toMatchObject([ + {}, + { combinator: Combinator.SingleBar }, + { + component: Component.Group, + entities: [{}, { combinator: Combinator.Juxtaposition }, {}, { combinator: Combinator.Juxtaposition }, {}], + }, + ]); + + expect(parse('something something something | something')).toMatchObject([ + { + component: Component.Group, + entities: [{}, { combinator: Combinator.Juxtaposition }, {}, { combinator: Combinator.Juxtaposition }, {}], + }, + { combinator: Combinator.SingleBar }, + {}, + ]); }); }); diff --git a/src/parser.ts b/src/parser.ts index 9c1a882..5ce53b0 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -11,15 +11,16 @@ export enum Component { Group, } +// Higher number is higher precedence export enum Combinator { /** Components are mandatory and should appear in that order */ - Juxtaposition, + Juxtaposition = 0, /** Components are mandatory but may appear in any order */ - DoubleAmpersand, + DoubleAmpersand = 1, /** At least one of the components must be present, and they may appear in any order */ - DoubleBar, + DoubleBar = 2, /** Exactly one of the components must be present */ - SingleBar, + SingleBar = 3, } export enum Multiplier { @@ -72,7 +73,6 @@ export type ComponentType = INonGroupData | IGroupData; export interface ICombinator { entity: Entity.Combinator; - multiplier: MultiplierType | null; combinator: Combinator; } @@ -81,7 +81,7 @@ export interface IFunction { multiplier: MultiplierType | null; } -interface IUnknown { +export interface IUnknown { entity: Entity.Unknown; multiplier: MultiplierType | null; } @@ -92,10 +92,29 @@ const REGEX_ENTITY = /(?:^|\s)((?:[\w]+\([^\)]*\))|[^\s*+?#!{]+)([*+?#!]|{(\d+), const REGEX_DATA_TYPE = /^(<[^>]+>)/g; const REGEX_KEYWORD = /^([\w-]+)/g; +const combinators: { [key: number]: ICombinator } = { + [Combinator.Juxtaposition]: { + entity: Entity.Combinator, + combinator: Combinator.Juxtaposition, + }, + [Combinator.DoubleAmpersand]: { + entity: Entity.Combinator, + combinator: Combinator.DoubleAmpersand, + }, + [Combinator.DoubleBar]: { + entity: Entity.Combinator, + combinator: Combinator.DoubleBar, + }, + [Combinator.SingleBar]: { + entity: Entity.Combinator, + combinator: Combinator.SingleBar, + }, +}; + export default function parse(syntax: string): EntityType[] { const levels: EntityType[][] = [[]]; - const deepestLevel = () => levels[levels.length - 1]; let previousMatchWasComponent = false; + let entityMatch: RegExpExecArray | null; while ((entityMatch = REGEX_ENTITY.exec(syntax))) { const [, value, ...rawMultiplier] = entityMatch; @@ -104,27 +123,27 @@ export default function parse(syntax: string): EntityType[] { previousMatchWasComponent = false; continue; } else if (value.indexOf('&&') === 0) { - deepestLevel().push(combinatorData(Combinator.DoubleAmpersand, multiplierData(rawMultiplier))); + deepestLevel().push(combinators[Combinator.DoubleAmpersand]); previousMatchWasComponent = false; continue; } else if (value.indexOf('||') === 0) { - deepestLevel().push(combinatorData(Combinator.DoubleBar, multiplierData(rawMultiplier))); + deepestLevel().push(combinators[Combinator.DoubleBar]); previousMatchWasComponent = false; continue; } else if (value.indexOf('|') === 0) { - deepestLevel().push(combinatorData(Combinator.SingleBar, multiplierData(rawMultiplier))); + deepestLevel().push(combinators[Combinator.SingleBar]); previousMatchWasComponent = false; continue; } else if (value.indexOf(']') === 0) { const definitions = levels.pop(); if (definitions) { - deepestLevel().push(componentGroupData(definitions, multiplierData(rawMultiplier))); + deepestLevel().push(componentGroupData(groupByPrecedence(definitions), multiplierData(rawMultiplier))); } previousMatchWasComponent = true; continue; } else { - if (previousMatchWasComponent === true) { - deepestLevel().push(combinatorData(Combinator.Juxtaposition)); + if (previousMatchWasComponent) { + deepestLevel().push(combinators[Combinator.Juxtaposition]); } if (value.indexOf('[') === 0) { @@ -149,15 +168,54 @@ export default function parse(syntax: string): EntityType[] { deepestLevel().push({ entity: Entity.Unknown, multiplier: multiplierData(rawMultiplier) }); } - return levels[0]; + function deepestLevel() { + return levels[levels.length - 1]; + } + + return groupByPrecedence(levels[0]); } -function combinatorData(combinator: Combinator, multiplier: MultiplierType | null = null): ICombinator { - return { - entity: Entity.Combinator, - combinator, - multiplier, - }; +export function isFunction(entity: EntityType): entity is IFunction { + return entity.entity === Entity.Function; +} + +export function isComponent(entity: EntityType): entity is ComponentType { + return entity.entity === Entity.Component; +} + +export function isCombinator(entity: EntityType): entity is ICombinator { + return entity.entity === Entity.Combinator; +} + +export function isCurlyBracetMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { + return multiplier.sign === Multiplier.CurlyBracet; +} + +export function isMultiplied(multiplier: MultiplierType) { + return ( + (isCurlyBracetMultiplier(multiplier) && (multiplier.min > 1 || multiplier.max > 1)) || + multiplier.sign === Multiplier.Asterisk || + multiplier.sign === Multiplier.PlusSign || + multiplier.sign === Multiplier.HashMark || + multiplier.sign === Multiplier.ExclamationPoint + ); +} + +export function isMandatoryEntity(entity: EntityType) { + if (isCombinator(entity)) { + return entity === combinators[Combinator.DoubleAmpersand] || entity === combinators[Combinator.Juxtaposition]; + } + + if (entity.multiplier) { + return ( + (isCurlyBracetMultiplier(entity.multiplier) && entity.multiplier.min === 0) || + entity.multiplier.sign === Multiplier.PlusSign || + entity.multiplier.sign === Multiplier.HashMark || + entity.multiplier.sign === Multiplier.ExclamationPoint + ); + } + + return true; } function componentData( @@ -173,7 +231,7 @@ function componentData( }; } -function componentGroupData(entities: EntityType[], multiplier: MultiplierType | null): ComponentType { +function componentGroupData(entities: EntityType[], multiplier: MultiplierType | null = null): ComponentType { return { entity: Entity.Component, component: Component.Group, @@ -198,8 +256,65 @@ function multiplierData(raw: string[]): MultiplierType | null { case '!': return { sign: Multiplier.ExclamationPoint }; case '{': - return { sign: Multiplier.CurlyBracet, min: +raw[1], max: +raw[2] }; + return { sign: Multiplier.CurlyBracet, min: Number(raw[1]), max: Number(raw[2]) }; default: return null; } } + +function groupByPrecedence(entities: EntityType[], precedence: number = Combinator.SingleBar): EntityType[] { + if (precedence < 0) { + // We've reached the lowest precedence possible + return entities; + } + + const combinator = combinators[precedence]; + const combinatorIndexes: number[] = []; + + // Search for indexes where the combinator is used + for (let i = entities.indexOf(combinator); i > -1; i = entities.indexOf(combinator, i + 1)) { + combinatorIndexes.push(i); + } + + const nextPrecedence = precedence - 1; + + if (combinatorIndexes.length === 0) { + return groupByPrecedence(entities, nextPrecedence); + } + + const groupedEntities: EntityType[] = []; + + // Yes, what you see is correct: it's index of indexes + for ( + let i = 0; + // Add one loop to finnish up the last entities + i < combinatorIndexes.length + 1; + i++ + ) { + const currentIndex = combinatorIndexes[i]; + + const sectionEntities = entities.slice( + i > 0 + ? combinatorIndexes[i - 1] + 1 + : // Slice from beginning + 0, + i < combinatorIndexes.length + ? currentIndex + : // Slice to end + entities.length, + ); + + // Only group if there's more than one entity in between + if (sectionEntities.length > 1) { + groupedEntities.push(componentGroupData(groupByPrecedence(sectionEntities, nextPrecedence))); + } else { + groupedEntities.push(...sectionEntities); + } + + if (i < combinatorIndexes.length) { + groupedEntities.push(entities[currentIndex]); + } + } + + return groupedEntities; +} diff --git a/src/typer.ts b/src/typer.ts index 48f1807..ceccdcc 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -4,14 +4,11 @@ import * as cssTypes from 'mdn-data/css/types.json'; import parse, { Combinator, Component, - ComponentType, - Entity, EntityType, - ICombinator, - IFunction, - IMultiplierCurlyBracet, - Multiplier, - MultiplierType, + isCombinator, + isComponent, + isMandatoryEntity, + isMultiplied, } from './parser'; export enum Type { @@ -83,64 +80,93 @@ const basicDataTypes = [...Object.keys(cssTypes), 'hex-color'].reduce<{ }, {}); export default function typing(entities: EntityType[]): TypeType[] { + let mandatoryCombinatorCount = 0; + let mandatoryNonCombinatorsCount = 0; + for (const entity of entities) { + if (isMandatoryEntity(entity)) { + if (isCombinator(entity)) { + mandatoryCombinatorCount++; + } else { + mandatoryNonCombinatorsCount++; + } + } + } + const types: TypeType[] = []; let hasLength = false; let hasString = false; let hasNumber = false; + for (const entity of entities) { if (isComponent(entity)) { - if (shouldIncludeComponent(entity)) { - switch (entity.component) { - case Component.Keyword: - if (String(Number(entity.value)) === entity.value) { - addNumericLiteral(Number(entity.value)); - } else { - addStringLiteral(entity.value); - } - break; - case Component.DataType: { - const value = entity.value.slice(1, -1); - const property = /'([^']+)'/.exec(value); - if (property) { - const name = property[1]; - if (name in properties) { - if (entity.multiplier && isMultiplied(entity.multiplier)) { - addString(); - } + if (isMandatoryEntity(entity)) { + // In case of `something another-thing` we want to fall back to string until component combinations is solved + if (mandatoryCombinatorCount > 0 && mandatoryNonCombinatorsCount > 1) { + addString(); + continue; + } + } else { + // In case of `something another-thing?` we want to add string until component combinations is solved + if (mandatoryCombinatorCount > 0 && mandatoryNonCombinatorsCount > 0) { + addString(); + continue; + } + } - for (const type of typing(parse(properties[name].syntax))) { - add(type); - } - } else { + switch (entity.component) { + case Component.Keyword: + if (String(Number(entity.value)) === entity.value) { + addNumericLiteral(Number(entity.value)); + } else { + addStringLiteral(entity.value); + } + break; + case Component.DataType: { + const value = entity.value.slice(1, -1); + const property = /'([^']+)'/.exec(value); + if (property) { + const name = property[1]; + if (name in properties) { + if (entity.multiplier && isMultiplied(entity.multiplier)) { addString(); } - } else if (value in basicDataTypes) { - add(basicDataTypes[value]); + + for (const type of typing(parse(properties[name].syntax))) { + add(type); + } } else { - addDataType(value); - } - break; - } - case Component.Group: { - if (entity.multiplier && isMultiplied(entity.multiplier)) { addString(); } + } else if (value in basicDataTypes) { + add(basicDataTypes[value]); + } else { + addDataType(value); + } + break; + } + case Component.Group: { + if (entity.multiplier && isMultiplied(entity.multiplier)) { + addString(); + } - for (const type of typing(entity.entities)) { - add(type); - } + for (const type of typing(entity.entities)) { + add(type); } } } } else if (isCombinator(entity)) { - if (entity.combinator === Combinator.DoubleBar || isMandatoryCombinator(entity)) { + if (entity.combinator === Combinator.DoubleBar || isMandatoryEntity(entity)) { addString(); } - } else if (isFunction(entity)) { + } else { addString(); } } + if (mandatoryNonCombinatorsCount > 1 && mandatoryCombinatorCount > 1) { + return [{ type: Type.String }]; + } + function addLength() { if (!hasLength) { types.push({ @@ -224,82 +250,5 @@ export default function typing(entities: EntityType[]): TypeType[] { } } - function previousEntity(currentEntity: EntityType) { - return entities[entities.indexOf(currentEntity) - 1]; - } - - function nextEntity(currentEntity: EntityType) { - return entities[entities.indexOf(currentEntity) + 1]; - } - - function shouldIncludeComponent(component: ComponentType) { - for (let i = entities.indexOf(component) - 1; i >= 0; i--) { - const entity = entities[i]; - if (entity && entity.entity === Entity.Combinator) { - if (isMandatoryCombinator(entity)) { - const previous = previousEntity(entity); - if (previous && !isOptionalEntity(previous)) { - return false; - } - } else { - break; - } - } - } - for (let i = entities.indexOf(component) + 1; i < entities.length; i++) { - const entity = entities[i]; - if (entity && entity.entity === Entity.Combinator) { - if (isMandatoryCombinator(entity)) { - const next = nextEntity(entity); - if (next && !isOptionalEntity(next)) { - return false; - } - } else { - break; - } - } - } - return true; - } - return types; } - -function isFunction(entity: EntityType): entity is IFunction { - return entity.entity === Entity.Function; -} - -function isComponent(entity: EntityType): entity is ComponentType { - return entity.entity === Entity.Component; -} - -function isCombinator(entity: EntityType): entity is ICombinator { - return entity.entity === Entity.Combinator; -} - -function isCurlyBracetMultiplier(multiplier: MultiplierType): multiplier is IMultiplierCurlyBracet { - return multiplier.sign === Multiplier.CurlyBracet; -} - -function isMultiplied(multiplier: MultiplierType) { - return ( - (isCurlyBracetMultiplier(multiplier) && (multiplier.min > 1 || multiplier.max > 1)) || - multiplier.sign === Multiplier.Asterisk || - multiplier.sign === Multiplier.PlusSign || - multiplier.sign === Multiplier.HashMark || - multiplier.sign === Multiplier.ExclamationPoint - ); -} - -function isMandatoryCombinator({ combinator }: ICombinator) { - return combinator === Combinator.DoubleAmpersand || combinator === Combinator.Juxtaposition; -} - -function isOptionalEntity(entity: EntityType) { - return ( - entity.multiplier && - ((isCurlyBracetMultiplier(entity.multiplier) && entity.multiplier.min > 0) || - entity.multiplier.sign === Multiplier.Asterisk || - entity.multiplier.sign === Multiplier.QuestionMark) - ); -}