UNPKG

@mapbox/mapbox-gl-style-spec

Version:

a specification for mapbox gl styles

143 lines (120 loc) 5.04 kB
import {ValueType} from '../types'; import {Color, typeOf, toString as valueToString} from '../values'; import Formatted from '../types/formatted'; import ResolvedImage from '../types/resolved_image'; import Literal from './literal'; import type {Type} from '../types'; import type {Expression, SerializedExpression} from '../expression'; import type ParsingContext from '../parsing_context'; import type EvaluationContext from '../evaluation_context'; // eslint-disable-next-line @typescript-eslint/no-explicit-any function coerceValue(type: string, value: any): any { switch (type) { case 'string': return valueToString(value); case 'number': return +value; case 'boolean': return !!value; case 'color': return Color.parse(value); case 'formatted': { return Formatted.fromString(valueToString(value)); } case 'resolvedImage': { return ResolvedImage.build(valueToString(value)); } } return value; } function clampToAllowedNumber(value: number, min?: number, max?: number, step?: number): number { if (step !== undefined) { value = step * Math.round(value / step); } if (min !== undefined && value < min) { value = min; } if (max !== undefined && value > max) { value = max; } return value; } class Config implements Expression { type: Type; key: string; scope: string | null | undefined; constructor(type: Type, key: string, scope?: string) { this.type = type; this.key = key; this.scope = scope; } static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Config | null | void { let type = context.expectedType; if (type === null || type === undefined) { type = ValueType; } if (args.length < 2 || args.length > 3) { return context.error(`Invalid number of arguments for 'config' expression.`); } const configKey = context.parse(args[1], 1); if (!(configKey instanceof Literal)) { return context.error(`Key name of 'config' expression must be a string literal.`); } if (args.length >= 3) { const configScope = context.parse(args[2], 2); if (!(configScope instanceof Literal)) { return context.error(`Scope of 'config' expression must be a string literal.`); } return new Config(type, valueToString(configKey.value), valueToString(configScope.value)); } return new Config(type, valueToString(configKey.value)); } // eslint-disable-next-line @typescript-eslint/no-explicit-any evaluate(ctx: EvaluationContext): any { const FQIDSeparator = '\u001F'; const configKey = [this.key, this.scope, ctx.scope].filter(Boolean).join(FQIDSeparator); const config = ctx.getConfig(configKey); if (!config) return null; const {type, value, values, minValue, maxValue, stepValue} = config; const defaultValue = config.default.evaluate(ctx); let result = defaultValue; if (value) { // temporarily override scope to parent to evaluate config expressions passed from the parent const originalScope = ctx.scope; ctx.scope = (originalScope || '').split(FQIDSeparator).slice(1).join(FQIDSeparator); result = value.evaluate(ctx); ctx.scope = originalScope; } if (type) { result = coerceValue(type, result); } if (result !== undefined && (minValue !== undefined || maxValue !== undefined || stepValue !== undefined)) { if (typeof result === 'number') { result = clampToAllowedNumber(result, minValue, maxValue, stepValue); } else if (Array.isArray(result)) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return result = result.map((item) => (typeof item === 'number' ? clampToAllowedNumber(item, minValue, maxValue, stepValue) : item)); } } if (value !== undefined && result !== undefined && values && !values.includes(result)) { // The result is not among the allowed values. Instead, use the default value from the option. result = defaultValue; if (type) { result = coerceValue(type, result); } } // @ts-expect-error - TS2367 - This comparison appears to be unintentional because the types 'string' and 'Type' have no overlap. if ((type && type !== this.type) || (result !== undefined && typeOf(result) !== this.type)) { result = coerceValue(this.type.kind, result); } return result; } eachChild() { } outputDefined(): boolean { return false; } serialize(): SerializedExpression { const res = ["config", this.key]; if (this.scope) { res.concat(this.key); } return res; } } export default Config;