diff --git a/index.d.ts b/index.d.ts index 96ef91a..c80784e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3554,7 +3554,7 @@ type BorderImageOutsetProperty = Globals | TLength | string | number; type BorderImageRepeatProperty = Globals | "repeat" | "round" | "space" | "stretch" | string; -type BorderImageSliceProperty = Globals | "fill" | string | number; +type BorderImageSliceProperty = Globals | string | number; type BorderImageSourceProperty = Globals | "none" | string; @@ -3894,7 +3894,7 @@ type MaskBorderOutsetProperty = Globals | TLength | string | number; type MaskBorderRepeatProperty = Globals | "repeat" | "round" | "space" | "stretch" | string; -type MaskBorderSliceProperty = Globals | "fill" | string | number; +type MaskBorderSliceProperty = Globals | string | number; type MaskBorderSourceProperty = Globals | "none" | string; @@ -4184,7 +4184,7 @@ type BorderBottomProperty = Globals | BrWidth | BrStyle | Colo type BorderColorProperty = Globals | Color | string; -type BorderImageProperty = Globals | "fill" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; +type BorderImageProperty = Globals | "none" | "repeat" | "round" | "space" | "stretch" | string | number; type BorderInlineEndProperty = Globals | BrWidth | BrStyle | NamedColor | DeprecatedSystemColor | "currentcolor" | string; @@ -4232,7 +4232,7 @@ type MarginProperty = Globals | TLength | "auto" | string; type MaskProperty = Globals | MaskLayer | string; -type MaskBorderProperty = Globals | "alpha" | "fill" | "luminance" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; +type MaskBorderProperty = Globals | "alpha" | "luminance" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; type OffsetProperty = Globals | Position | GeometryBox | "auto" | "none" | string; diff --git a/index.js.flow b/index.js.flow index 24be516..54a11b7 100644 --- a/index.js.flow +++ b/index.js.flow @@ -3310,7 +3310,7 @@ type BorderImageOutsetProperty = Globals | TLength | string | number; type BorderImageRepeatProperty = Globals | "repeat" | "round" | "space" | "stretch" | string; -type BorderImageSliceProperty = Globals | "fill" | string | number; +type BorderImageSliceProperty = Globals | string | number; type BorderImageSourceProperty = Globals | "none" | string; @@ -3650,7 +3650,7 @@ type MaskBorderOutsetProperty = Globals | TLength | string | number; type MaskBorderRepeatProperty = Globals | "repeat" | "round" | "space" | "stretch" | string; -type MaskBorderSliceProperty = Globals | "fill" | string | number; +type MaskBorderSliceProperty = Globals | string | number; type MaskBorderSourceProperty = Globals | "none" | string; @@ -3940,7 +3940,7 @@ type BorderBottomProperty = Globals | BrWidth | BrStyle | Colo type BorderColorProperty = Globals | Color | string; -type BorderImageProperty = Globals | "fill" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; +type BorderImageProperty = Globals | "none" | "repeat" | "round" | "space" | "stretch" | string | number; type BorderInlineEndProperty = Globals | BrWidth | BrStyle | NamedColor | DeprecatedSystemColor | "currentcolor" | string; @@ -3988,7 +3988,7 @@ type MarginProperty = Globals | TLength | "auto" | string; type MaskProperty = Globals | MaskLayer | string; -type MaskBorderProperty = Globals | "alpha" | "fill" | "luminance" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; +type MaskBorderProperty = Globals | "alpha" | "luminance" | "none" | "repeat" | "round" | "space" | "stretch" | string | number; type OffsetProperty = Globals | Position | GeometryBox | "auto" | "none" | string; diff --git a/src/compat.ts b/src/compat.ts index eb8c502..2627a3e 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -1,4 +1,4 @@ -import { Combinator, combinatorData, Component, componentData, componentGroupData, Entity, EntityType } from './parser'; +import { Combinator, combinators, Component, componentData, componentGroupData, Entity, EntityType } from './parser'; const importsCache: { [cssPath: string]: MDN.PropertiesCompat | null } = {}; @@ -108,7 +108,7 @@ export function compatSyntax(data: MDN.CompatData, entities: EntityType[]): Enti const alternativeEntities: EntityType[] = [entity]; for (const keyword of alternatives) { - alternativeEntities.push(combinatorData(Combinator.SingleBar), componentData(Component.Keyword, keyword)); + alternativeEntities.push(combinators[Combinator.SingleBar], componentData(Component.Keyword, keyword)); } compatEntities.push(componentGroupData(alternativeEntities)); diff --git a/src/parser.ts b/src/parser.ts index 7e48df1..90ff6e9 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; +export 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,55 @@ 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]); } -export function combinatorData(combinator: Combinator, multiplier: MultiplierType | null = null): ICombinator { - return { - entity: Entity.Combinator, - combinator, - multiplier, - }; +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 isMandatoryMultiplied(multiplier: MultiplierType | null) { + return multiplier !== null && (isCurlyBracetMultiplier(multiplier) && multiplier.min > 1); +} + +export function isOptionallyMultiplied(multiplier: MultiplierType | null) { + return ( + multiplier !== null && + ((isCurlyBracetMultiplier(multiplier) && multiplier.min < multiplier.max && 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; } export function componentData( @@ -198,8 +257,63 @@ 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 sectionEntities = entities.slice( + i > 0 + ? combinatorIndexes[i - 1] + 1 + : // Slice from beginning + 0, + i < combinatorIndexes.length + ? combinatorIndexes[i] + : // 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[combinatorIndexes[i]]); + } + } + + return groupedEntities; +} diff --git a/src/typer.ts b/src/typer.ts index 499555d..914b9ee 100644 --- a/src/typer.ts +++ b/src/typer.ts @@ -3,14 +3,12 @@ import * as cssTypes from 'mdn-data/css/types.json'; import { Combinator, Component, - ComponentType, - Entity, EntityType, - ICombinator, - IFunction, - IMultiplierCurlyBracet, - Multiplier, - MultiplierType, + isCombinator, + isComponent, + isMandatoryEntity, + isMandatoryMultiplied, + isOptionallyMultiplied, } from './parser'; export enum Type { @@ -76,89 +74,84 @@ 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++; + } + } + } + let types: TypeType[] = []; + for (const entity of entities) { if (isComponent(entity)) { - if (shouldIncludeComponent(entity)) { - if (mayBeMultiplied(entity.multiplier)) { + 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) { types = addString(types); + continue; } - - if (!isMultiplied(entity.multiplier)) { - switch (entity.component) { - case Component.Keyword: - if (String(Number(entity.value)) === entity.value) { - types = addNumericLiteral(types, Number(entity.value)); - } else { - types = addStringLiteral(types, entity.value); - } - break; - case Component.DataType: { - const value = entity.value.slice(1, -1); - const property = /'([^']+)'/.exec(value); - if (property) { - const name = property[1]; - types = addPropertyReference(types, name); - } else if (value in basicDataTypes) { - types = addType(types, basicDataTypes[value]); - } else { - types = addDataType(types, value); - } - break; - } - case Component.Group: { - for (const type of typing(entity.entities)) { - types = addType(types, type); - } - } - } + } else { + // In case of `something another-thing?` we want to add string until component combinations is solved + if (mandatoryCombinatorCount > 0 && mandatoryNonCombinatorsCount > 0) { + types = addString(types); + continue; } } - } else if (isCombinator(entity)) { - if (entity.combinator === Combinator.DoubleBar || isMandatoryCombinator(entity)) { + + if (isMandatoryMultiplied(entity.multiplier)) { + // In case of `something{2,3}` we fallback to `string` and stop as it needs to be multiplied + types = addString(types); + continue; + } else if (isOptionallyMultiplied(entity.multiplier)) { + // In case of `something{1,2}` or `something+` we fallback to `string` but moves on + // as it doesn't necessary needs to be multiplied types = addString(types); } - } else if (isFunction(entity)) { - types = addString(types); - } - } - 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; + switch (entity.component) { + case Component.Keyword: + if (String(Number(entity.value)) === entity.value) { + types = addNumericLiteral(types, Number(entity.value)); + } else { + types = addStringLiteral(types, entity.value); } - } 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; + case Component.DataType: { + const value = entity.value.slice(1, -1); + const property = /'([^']+)'/.exec(value); + if (property) { + const name = property[1]; + types = addPropertyReference(types, name); + } else if (value in basicDataTypes) { + types = addType(types, basicDataTypes[value]); + } else { + types = addDataType(types, value); } - } else { break; } + case Component.Group: { + for (const type of typing(entity.entities)) { + types = addType(types, type); + } + } + } + } else if (isCombinator(entity)) { + if (entity.combinator === Combinator.DoubleBar || isMandatoryEntity(entity)) { + types = addString(types); } + } else { + types = addString(types); } - return true; + } + + if (mandatoryNonCombinatorsCount > 1 && mandatoryCombinatorCount > 1) { + return [{ type: Type.String }]; } return types; @@ -297,47 +290,3 @@ export function hasType(originalTypes: TypeType[], type: TypeType): boolean { const testTypes = addType(originalTypes, type); return originalTypes === testTypes; } - -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 | null) { - return multiplier !== null && (isCurlyBracetMultiplier(multiplier) && multiplier.min > 1); -} - -function mayBeMultiplied(multiplier: MultiplierType | null) { - return ( - multiplier !== null && - ((isCurlyBracetMultiplier(multiplier) && 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 < 2) || - entity.multiplier.sign === Multiplier.Asterisk || - entity.multiplier.sign === Multiplier.QuestionMark) - ); -}