@maplibre/maplibre-gl-style-spec
Version:
a specification for maplibre styles
155 lines (132 loc) • 5.69 kB
text/typescript
import {
NumberType,
ValueType,
FormattedType,
array,
StringType,
ColorType,
ResolvedImageType,
} from '../types';
import {Formatted, FormattedSection, VERTICAL_ALIGN_OPTIONS, VerticalAlign} from '../types/formatted';
import {valueToString, typeOf} from '../values';
import type {Expression} from '../expression';
import type {EvaluationContext} from '../evaluation_context';
import type {ParsingContext} from '../parsing_context';
import type {Type} from '../types';
type FormattedSectionExpression = {
// Content of a section may be Image expression or other
// type of expression that is coercable to 'string'.
content: Expression;
scale: Expression | null;
font: Expression | null;
textColor: Expression | null;
verticalAlign: Expression | null;
};
export class FormatExpression implements Expression {
type: Type;
sections: Array<FormattedSectionExpression>;
constructor(sections: Array<FormattedSectionExpression>) {
this.type = FormattedType;
this.sections = sections;
}
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
if (args.length < 2) {
return context.error('Expected at least one argument.') as null;
}
const firstArg = args[1];
if (!Array.isArray(firstArg) && typeof firstArg === 'object') {
return context.error('First argument must be an image or text section.') as null;
}
const sections: Array<FormattedSectionExpression> = [];
let nextTokenMayBeObject = false;
for (let i = 1; i <= args.length - 1; ++i) {
const arg = (args[i] as any);
if (nextTokenMayBeObject && typeof arg === 'object' && !Array.isArray(arg)) {
nextTokenMayBeObject = false;
let scale = null;
if (arg['font-scale']) {
scale = context.parse(arg['font-scale'], 1, NumberType);
if (!scale) return null;
}
let font = null;
if (arg['text-font']) {
font = context.parse(arg['text-font'], 1, array(StringType));
if (!font) return null;
}
let textColor = null;
if (arg['text-color']) {
textColor = context.parse(arg['text-color'], 1, ColorType);
if (!textColor) return null;
}
let verticalAlign = null;
if (arg['vertical-align']) {
if (typeof arg['vertical-align'] === 'string' && !VERTICAL_ALIGN_OPTIONS.includes(arg['vertical-align'] as VerticalAlign)) {
return context.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg['vertical-align']}' instead.`) as null;
}
verticalAlign = context.parse(arg['vertical-align'], 1, StringType);
if (!verticalAlign) return null;
}
const lastExpression = sections[sections.length - 1];
lastExpression.scale = scale;
lastExpression.font = font;
lastExpression.textColor = textColor;
lastExpression.verticalAlign = verticalAlign;
} else {
const content = context.parse(args[i], 1, ValueType);
if (!content) return null;
const kind = content.type.kind;
if (kind !== 'string' && kind !== 'value' && kind !== 'null' && kind !== 'resolvedImage')
return context.error('Formatted text type must be \'string\', \'value\', \'image\' or \'null\'.') as null;
nextTokenMayBeObject = true;
sections.push({content, scale: null, font: null, textColor: null, verticalAlign: null});
}
}
return new FormatExpression(sections);
}
evaluate(ctx: EvaluationContext) {
const evaluateSection = section => {
const evaluatedContent = section.content.evaluate(ctx);
if (typeOf(evaluatedContent) === ResolvedImageType) {
return new FormattedSection(
'',
evaluatedContent,
null,
null,
null,
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
);
}
return new FormattedSection(
valueToString(evaluatedContent),
null,
section.scale ? section.scale.evaluate(ctx) : null,
section.font ? section.font.evaluate(ctx).join(',') : null,
section.textColor ? section.textColor.evaluate(ctx) : null,
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
);
};
return new Formatted(this.sections.map(evaluateSection));
}
eachChild(fn: (_: Expression) => void) {
for (const section of this.sections) {
fn(section.content);
if (section.scale) {
fn(section.scale);
}
if (section.font) {
fn(section.font);
}
if (section.textColor) {
fn(section.textColor);
}
if (section.verticalAlign) {
fn(section.verticalAlign);
}
}
}
outputDefined() {
// Technically the combinatoric set of all children
// Usually, this.text will be undefined anyway
return false;
}
}