diff --git a/build/generate-style-code.js b/build/generate-style-code.js index 500afc676db..e47f7408b78 100644 --- a/build/generate-style-code.js +++ b/build/generate-style-code.js @@ -25,7 +25,7 @@ global.flowType = function (property) { case 'color': return `Color`; case 'formatted': - return `string | Formatted`; + return `Formatted`; case 'array': if (property.length) { return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`; diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index d96033e5a2f..b4fc300d788 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -65,14 +65,22 @@ const types = { type: 'object', parameters: ['value', { repeat: [ 'fallback: value' ] }] }], - 'to-number': [{ - type: 'number', - parameters: ['value', { repeat: [ 'fallback: value' ] }] + 'to-boolean': [{ + type: 'boolean', + parameters: ['value'] }], 'to-color': [{ type: 'color', parameters: ['value', { repeat: [ 'fallback: value' ] }] }], + 'to-number': [{ + type: 'number', + parameters: ['value', { repeat: [ 'fallback: value' ] }] + }], + 'to-string': [{ + type: 'string', + parameters: ['value'] + }], at: [{ type: 'ItemType', parameters: ['number', 'array'] diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 17aa10c4d13..5417861e644 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -18,7 +18,7 @@ import Anchor from '../../symbol/anchor'; import { getSizeData } from '../../symbol/symbol_size'; import { register } from '../../util/web_worker_transfer'; import EvaluationParameters from '../../style/evaluation_parameters'; -import {Formatted} from '../../style-spec/expression/definitions/formatted'; +import Formatted from '../../style-spec/expression/types/formatted'; import type { @@ -56,7 +56,7 @@ export type CollisionArrays = { }; export type SymbolFeature = {| - text: string | Formatted | void, + text: Formatted | void, icon: string | void, index: number, sourceLayerIndex: number, @@ -349,10 +349,16 @@ class SymbolBucket implements Bucket { continue; } - let text; + let text: Formatted | void; if (hasText) { - text = layer.getValueAndResolveTokens('text-field', feature); - text = transformText(text, layer, feature); + // Expression evaluation will automatically coerce to Formatted + // but plain string token evaluation skips that pathway so do the + // conversion here. + const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature); + text = transformText(resolvedTokens instanceof Formatted ? + resolvedTokens : + Formatted.fromString(resolvedTokens), + layer, feature); } let icon; @@ -384,20 +390,13 @@ class SymbolBucket implements Bucket { if (text) { const fontStack = textFont.evaluate(feature, {}).join(','); - const stack = stacks[fontStack] = stacks[fontStack] || {}; const textAlongLine = layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point'; - if (text instanceof Formatted) { - for (const section of text.sections) { - const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); - const sectionFont = section.fontStack || fontStack; - const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; - this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode); - } - } else { - const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text); - this.calculateGlyphDependencies(text, stack, textAlongLine, doesAllowVerticalWritingMode); + for (const section of text.sections) { + const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString()); + const sectionFont = section.fontStack || fontStack; + const sectionStack = stacks[sectionFont] = stacks[sectionFont] || {}; + this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode); } - } } diff --git a/src/style-spec/expression/definitions/coalesce.js b/src/style-spec/expression/definitions/coalesce.js index 3f8a8895542..26d879c0707 100644 --- a/src/style-spec/expression/definitions/coalesce.js +++ b/src/style-spec/expression/definitions/coalesce.js @@ -31,7 +31,7 @@ class Coalesce implements Expression { const parsedArgs = []; for (const arg of args.slice(1)) { - const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, {omitTypeAnnotations: true}); + const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, {typeAnnotation: 'omit'}); if (!parsed) return null; outputType = outputType || parsed.type; parsedArgs.push(parsed); diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index e48ea2192b4..03bd1afb923 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -2,20 +2,23 @@ import assert from 'assert'; -import { ColorType, ValueType, NumberType } from '../types'; -import { Color, validateRGBA } from '../values'; +import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types'; +import {Color, toString as valueToString, validateRGBA} from '../values'; import RuntimeError from '../runtime_error'; +import Formatted from '../types/formatted'; +import FormatExpression from '../definitions/format'; import type { Expression } from '../expression'; import type ParsingContext from '../parsing_context'; import type EvaluationContext from '../evaluation_context'; import type { Value } from '../values'; import type { Type } from '../types'; -import { Formatted, FormattedSection } from './formatted'; const types = { + 'to-boolean': BooleanType, + 'to-color': ColorType, 'to-number': NumberType, - 'to-color': ColorType + 'to-string': StringType }; /** @@ -41,6 +44,9 @@ class Coercion implements Expression { const name: string = (args[0]: any); assert(types[name], name); + if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2) + return context.error(`Expected one argument.`); + const type = types[name]; const parsed = []; @@ -54,7 +60,9 @@ class Coercion implements Expression { } evaluate(ctx: EvaluationContext) { - if (this.type.kind === 'color') { + if (this.type.kind === 'boolean') { + return Boolean(this.args[0].evaluate(ctx)); + } else if (this.type.kind === 'color') { let input; let error; for (const arg of this.args) { @@ -77,16 +85,7 @@ class Coercion implements Expression { } } throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`); - } else if (this.type.kind === 'formatted') { - let input; - for (const arg of this.args) { - input = arg.evaluate(ctx); - if (typeof input === 'string') { - return new Formatted([new FormattedSection(input, null, null)]); - } - } - throw new RuntimeError(`Could not parse formatted text from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`); - } else { + } else if (this.type.kind === 'number') { let value = null; for (const arg of this.args) { value = arg.evaluate(ctx); @@ -96,6 +95,12 @@ class Coercion implements Expression { return num; } throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`); + } else if (this.type.kind === 'formatted') { + // There is no explicit 'to-formatted' but this coercion can be implicitly + // created by properties that expect the 'formatted' type. + return Formatted.fromString(valueToString(this.args[0].evaluate(ctx))); + } else { + return valueToString(this.args[0].evaluate(ctx)); } } @@ -108,6 +113,9 @@ class Coercion implements Expression { } serialize() { + if (this.type.kind === 'formatted') { + return new FormatExpression([{text: this.args[0], scale: null, font: null}]).serialize(); + } const serialized = [`to-${this.type.kind}`]; this.eachChild(child => { serialized.push(child.serialize()); }); return serialized; diff --git a/src/style-spec/expression/definitions/collator.js b/src/style-spec/expression/definitions/collator.js index c99f9e3a8df..bc7ae8ad8b8 100644 --- a/src/style-spec/expression/definitions/collator.js +++ b/src/style-spec/expression/definitions/collator.js @@ -1,73 +1,14 @@ // @flow import { StringType, BooleanType, CollatorType } from '../types'; +import Collator from '../types/collator'; import type { Expression } from '../expression'; import type EvaluationContext from '../evaluation_context'; import type ParsingContext from '../parsing_context'; import type { Type } from '../types'; -// Flow type declarations for Intl cribbed from -// https://github.com/facebook/flow/issues/1270 - -declare var Intl: { - Collator: Class -} - -declare class Intl$Collator { - constructor ( - locales?: string | string[], - options?: CollatorOptions - ): Intl$Collator; - - static ( - locales?: string | string[], - options?: CollatorOptions - ): Intl$Collator; - - compare (a: string, b: string): number; - - resolvedOptions(): any; -} - -type CollatorOptions = { - localeMatcher?: 'lookup' | 'best fit', - usage?: 'sort' | 'search', - sensitivity?: 'base' | 'accent' | 'case' | 'variant', - ignorePunctuation?: boolean, - numeric?: boolean, - caseFirst?: 'upper' | 'lower' | 'false' -} - -export class Collator { - locale: string | null; - sensitivity: 'base' | 'accent' | 'case' | 'variant'; - collator: Intl$Collator; - - constructor(caseSensitive: boolean, diacriticSensitive: boolean, locale: string | null) { - if (caseSensitive) - this.sensitivity = diacriticSensitive ? 'variant' : 'case'; - else - this.sensitivity = diacriticSensitive ? 'accent' : 'base'; - - this.locale = locale; - this.collator = new Intl.Collator(this.locale ? this.locale : [], - { sensitivity: this.sensitivity, usage: 'search' }); - } - - compare(lhs: string, rhs: string): number { - return this.collator.compare(lhs, rhs); - } - - resolvedLocale(): string { - // We create a Collator without "usage: search" because we don't want - // the search options encoded in our result (e.g. "en-u-co-search") - return new Intl.Collator(this.locale ? this.locale : []) - .resolvedOptions().locale; - } -} - -export class CollatorExpression implements Expression { +export default class CollatorExpression implements Expression { type: Type; caseSensitive: Expression; diacriticSensitive: Expression; diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/format.js similarity index 76% rename from src/style-spec/expression/definitions/formatted.js rename to src/style-spec/expression/definitions/format.js index d6e517479b1..071c2110b4c 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/format.js @@ -1,56 +1,21 @@ // @flow import { NumberType, ValueType, FormattedType, array, StringType } from '../types'; - +import Formatted, { FormattedSection } from '../types/formatted'; +import { toString } from '../values'; import type { Expression } from '../expression'; import type EvaluationContext from '../evaluation_context'; import type ParsingContext from '../parsing_context'; import type { Type } from '../types'; -export class FormattedSection { - text: string - scale: number | null - fontStack: string | null - - constructor(text: string, scale: number | null, fontStack: string | null) { - this.text = text; - this.scale = scale; - this.fontStack = fontStack; - } -} - -export class Formatted { - sections: Array - - constructor(sections: Array) { - this.sections = sections; - } - - toString(): string { - return this.sections.map(section => section.text).join(''); - } - - serialize() { - const serialized = ["format"]; - for (const section of this.sections) { - serialized.push(section.text); - const fontStack = section.fontStack ? - ["literal", section.fontStack.split(',')] : - null; - serialized.push({ "text-font": fontStack, "font-scale": section.scale }); - } - return serialized; - } -} - type FormattedSectionExpression = { text: Expression, scale: Expression | null; font: Expression | null; } -export class FormatExpression implements Expression { +export default class FormatExpression implements Expression { type: Type; sections: Array; @@ -101,7 +66,7 @@ export class FormatExpression implements Expression { return new Formatted( this.sections.map(section => new FormattedSection( - section.text.evaluate(ctx) || "", + toString(section.text.evaluate(ctx)), section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(',') : null ) diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index f39dc07639d..da07b7bc9bd 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -1,8 +1,20 @@ // @flow -import { NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, ErrorType, CollatorType, array, toString } from '../types'; +import { + type Type, + NumberType, + StringType, + BooleanType, + ColorType, + ObjectType, + ValueType, + ErrorType, + CollatorType, + array, + toString as typeToString +} from '../types'; -import { typeOf, Color, validateRGBA } from '../values'; +import { typeOf, Color, validateRGBA, toString as valueToString } from '../values'; import CompoundExpression from '../compound_expression'; import RuntimeError from '../runtime_error'; import Let from './let'; @@ -24,11 +36,10 @@ import { LessThanOrEqual, GreaterThanOrEqual } from './comparison'; -import { CollatorExpression } from './collator'; -import { Formatted, FormatExpression } from './formatted'; +import CollatorExpression from './collator'; +import FormatExpression from './format'; import Length from './length'; -import type { Type } from '../types'; import type { Varargs } from '../compound_expression'; import type { ExpressionRegistry } from '../expression'; @@ -58,8 +69,10 @@ const expressions: ExpressionRegistry = { 'object': Assertion, 'step': Step, 'string': Assertion, + 'to-boolean': Coercion, 'to-color': Coercion, 'to-number': Coercion, + 'to-string': Coercion, 'var': Var }; @@ -108,29 +121,7 @@ CompoundExpression.register(expressions, { 'typeof': [ StringType, [ValueType], - (ctx, [v]) => toString(typeOf(v.evaluate(ctx))) - ], - 'to-string': [ - StringType, - [ValueType], - (ctx, [v]) => { - v = v.evaluate(ctx); - const type = typeof v; - if (v === null) { - return ''; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - return String(v); - } else if (v instanceof Color || v instanceof Formatted) { - return v.toString(); - } else { - return JSON.stringify(v); - } - } - ], - 'to-boolean': [ - BooleanType, - [ValueType], - (ctx, [v]) => Boolean(v.evaluate(ctx)) + (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx))) ], 'to-rgba': [ array(NumberType, 4), @@ -544,8 +535,8 @@ CompoundExpression.register(expressions, { ], 'concat': [ StringType, - varargs(StringType), - (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('') + varargs(ValueType), + (ctx, args) => args.map(arg => valueToString(arg.evaluate(ctx))).join('') ], 'resolved-locale': [ StringType, diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index 2c8668d0e52..015a4cac93d 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -2,7 +2,7 @@ import assert from 'assert'; import { isValue, typeOf, Color } from '../values'; -import { Formatted } from './formatted'; +import Formatted from '../types/formatted'; import type { Type } from '../types'; import type { Value } from '../values'; diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index d3ef57dd58d..5b9f07c3d86 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -116,7 +116,11 @@ export function isExpression(expression: mixed) { */ export function createExpression(expression: mixed, propertySpec: StylePropertySpecification): Result> { const parser = new ParsingContext(definitions, [], getExpectedType(propertySpec)); - const parsed = parser.parse(expression); + + // For string-valued properties, coerce to string at the top level rather than asserting. + const parsed = parser.parse(expression, undefined, undefined, undefined, + propertySpec.type === 'string' ? {typeAnnotation: 'coerce'} : undefined); + if (!parsed) { assert(parser.errors.length > 0); return error(parser.errors); @@ -347,22 +351,23 @@ function findZoomCurve(expression: Expression): Step | Interpolate | ParsingErro return result; } -import { ColorType, StringType, NumberType, BooleanType, ValueType, array } from './types'; +import { ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, array } from './types'; -function getExpectedType(spec: StylePropertySpecification): Type | null { +function getExpectedType(spec: StylePropertySpecification): Type { const types = { color: ColorType, string: StringType, number: NumberType, enum: StringType, - boolean: BooleanType + boolean: BooleanType, + formatted: FormattedType }; if (spec.type === 'array') { return array(types[spec.value] || ValueType, spec.length); } - return types[spec.type] || null; + return types[spec.type]; } function getDefaultValue(spec: StylePropertySpecification): Value { diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 4a0c2d64ec5..a92e5dd53ba 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -1,7 +1,6 @@ // @flow import Scope from './scope'; - import { checkSubtype } from './types'; import ParsingError from './parsing_error'; import Literal from './definitions/literal'; @@ -9,7 +8,7 @@ import Assertion from './definitions/assertion'; import Coercion from './definitions/coercion'; import EvaluationContext from './evaluation_context'; import CompoundExpression from './compound_expression'; -import { CollatorExpression } from './definitions/collator'; +import CollatorExpression from './definitions/collator'; import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant'; import Var from './definitions/var'; @@ -61,7 +60,7 @@ class ParsingContext { index?: number, expectedType?: ?Type, bindings?: Array<[string, Expression]>, - options: {omitTypeAnnotations?: boolean} = {} + options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'} = {} ): ?Expression { if (index) { return this.concat(index, expectedType, bindings)._parse(expr, options); @@ -69,12 +68,21 @@ class ParsingContext { return this._parse(expr, options); } - _parse(expr: mixed, options: {omitTypeAnnotations?: boolean}): ?Expression { - + _parse(expr: mixed, options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'}): ?Expression { if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') { expr = ['literal', expr]; } + function annotate(parsed, type, typeAnnotation: 'assert' | 'coerce' | 'omit') { + if (typeAnnotation === 'assert') { + return new Assertion(type, [parsed]); + } else if (typeAnnotation === 'coerce') { + return new Coercion(type, [parsed]); + } else { + return parsed; + } + } + if (Array.isArray(expr)) { if (expr.length === 0) { return this.error(`Expected an array with at least one element. If you wanted a literal array, use ["literal", []].`); @@ -95,24 +103,19 @@ class ParsingContext { const expected = this.expectedType; const actual = parsed.type; - // When we expect a number, string, boolean, or array but - // have a Value, we can wrap it in a refining assertion. - // When we expect a Color but have a String or Value, we - // can wrap it in "to-color" coercion. + // When we expect a number, string, boolean, or array but have a value, wrap it in an assertion. + // When we expect a color or formatted string, but have a string or value, wrap it in a coercion. // Otherwise, we do static type-checking. + // + // These behaviors are overridable for: + // * The "coalesce" operator, which needs to omit type annotations. + // * String-valued properties (e.g. `text-field`), where coercion is more convenient than assertion. + // if ((expected.kind === 'string' || expected.kind === 'number' || expected.kind === 'boolean' || expected.kind === 'object' || expected.kind === 'array') && actual.kind === 'value') { - if (!options.omitTypeAnnotations) { - parsed = new Assertion(expected, [parsed]); - } - } else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) { - if (!options.omitTypeAnnotations) { - parsed = new Coercion(expected, [parsed]); - } - } else if (expected.kind === 'formatted' && (actual.kind === 'value' || actual.kind === 'string')) { - if (!options.omitTypeAnnotations) { - parsed = new Coercion(expected, [parsed]); - } - } else if (this.checkSubtype(this.expectedType, parsed.type)) { + parsed = annotate(parsed, expected, options.typeAnnotation || 'assert'); + } else if ((expected.kind === 'color' || expected.kind === 'formatted') && (actual.kind === 'value' || actual.kind === 'string')) { + parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); + } else if (this.checkSubtype(expected, actual)) { return null; } } diff --git a/src/style-spec/expression/types/collator.js b/src/style-spec/expression/types/collator.js new file mode 100644 index 00000000000..40e80ec3214 --- /dev/null +++ b/src/style-spec/expression/types/collator.js @@ -0,0 +1,61 @@ +// @flow + +// Flow type declarations for Intl cribbed from +// https://github.com/facebook/flow/issues/1270 + +declare var Intl: { + Collator: Class +}; + +declare class Intl$Collator { + constructor ( + locales?: string | string[], + options?: CollatorOptions + ): Intl$Collator; + + static ( + locales?: string | string[], + options?: CollatorOptions + ): Intl$Collator; + + compare (a: string, b: string): number; + + resolvedOptions(): any; +} + +type CollatorOptions = { + localeMatcher?: 'lookup' | 'best fit', + usage?: 'sort' | 'search', + sensitivity?: 'base' | 'accent' | 'case' | 'variant', + ignorePunctuation?: boolean, + numeric?: boolean, + caseFirst?: 'upper' | 'lower' | 'false' +} + +export default class Collator { + locale: string | null; + sensitivity: 'base' | 'accent' | 'case' | 'variant'; + collator: Intl$Collator; + + constructor(caseSensitive: boolean, diacriticSensitive: boolean, locale: string | null) { + if (caseSensitive) + this.sensitivity = diacriticSensitive ? 'variant' : 'case'; + else + this.sensitivity = diacriticSensitive ? 'accent' : 'base'; + + this.locale = locale; + this.collator = new Intl.Collator(this.locale ? this.locale : [], + { sensitivity: this.sensitivity, usage: 'search' }); + } + + compare(lhs: string, rhs: string): number { + return this.collator.compare(lhs, rhs); + } + + resolvedLocale(): string { + // We create a Collator without "usage: search" because we don't want + // the search options encoded in our result (e.g. "en-u-co-search") + return new Intl.Collator(this.locale ? this.locale : []) + .resolvedOptions().locale; + } +} diff --git a/src/style-spec/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js new file mode 100644 index 00000000000..8063e80f9f0 --- /dev/null +++ b/src/style-spec/expression/types/formatted.js @@ -0,0 +1,41 @@ +// @flow + +export class FormattedSection { + text: string; + scale: number | null; + fontStack: string | null; + + constructor(text: string, scale: number | null, fontStack: string | null) { + this.text = text; + this.scale = scale; + this.fontStack = fontStack; + } +} + +export default class Formatted { + sections: Array; + + constructor(sections: Array) { + this.sections = sections; + } + + static fromString(unformatted: string): Formatted { + return new Formatted([new FormattedSection(unformatted, null, null)]); + } + + toString(): string { + return this.sections.map(section => section.text).join(''); + } + + serialize() { + const serialized = ["format"]; + for (const section of this.sections) { + serialized.push(section.text); + const fontStack = section.fontStack ? + ["literal", section.fontStack.split(',')] : + null; + serialized.push({ "text-font": fontStack, "font-scale": section.scale }); + } + return serialized; + } +} diff --git a/src/style-spec/expression/values.js b/src/style-spec/expression/values.js index 1ed127c1c8e..3f8b5c874c2 100644 --- a/src/style-spec/expression/values.js +++ b/src/style-spec/expression/values.js @@ -3,8 +3,9 @@ import assert from 'assert'; import Color from '../util/color'; -import { Collator } from './definitions/collator'; -import { NullType, NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, CollatorType, array } from './types'; +import Collator from './types/collator'; +import Formatted from './types/formatted'; +import { NullType, NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, CollatorType, FormattedType, array } from './types'; import type { Type } from './types'; @@ -27,7 +28,7 @@ export function validateRGBA(r: mixed, g: mixed, b: mixed, a?: mixed): ?string { return null; } -export type Value = null | string | boolean | number | Color | Collator | $ReadOnlyArray | { +[string]: Value } +export type Value = null | string | boolean | number | Color | Collator | Formatted | $ReadOnlyArray | { +[string]: Value } export function isValue(mixed: mixed): boolean { if (mixed === null) { @@ -42,6 +43,8 @@ export function isValue(mixed: mixed): boolean { return true; } else if (mixed instanceof Collator) { return true; + } else if (mixed instanceof Formatted) { + return true; } else if (Array.isArray(mixed)) { for (const item of mixed) { if (!isValue(item)) { @@ -74,6 +77,8 @@ export function typeOf(value: Value): Type { return ColorType; } else if (value instanceof Collator) { return CollatorType; + } else if (value instanceof Formatted) { + return FormattedType; } else if (Array.isArray(value)) { const length = value.length; let itemType: ?Type; @@ -97,4 +102,17 @@ export function typeOf(value: Value): Type { } } +export function toString(value: Value) { + const type = typeof value; + if (value === null) { + return ''; + } else if (type === 'string' || type === 'number' || type === 'boolean') { + return String(value); + } else if (value instanceof Color || value instanceof Formatted) { + return value.toString(); + } else { + return JSON.stringify(value); + } +} + export { Color, Collator }; diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index a9de750e326..73bd7fef9f9 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -36,7 +36,9 @@ function convertIdentityFunction(parameters, propertySpec): Array { const get = ['get', parameters.property]; if (parameters.default === undefined) { - return get; + // By default, expressions for string-valued properties get coerced. To preserve + // legacy function semantics, insert an explicit assertion instead. + return propertySpec.type === 'string' ? ['string', get] : get; } else if (propertySpec.type === 'enum') { return [ 'match', @@ -224,7 +226,7 @@ export function convertTokenString(s: string) { const literal = s.slice(pos, re.lastIndex - match[0].length); pos = re.lastIndex; if (literal.length > 0) result.push(literal); - result.push(['to-string', ['get', match[1]]]); + result.push(['get', match[1]]); } if (result.length === 1) { @@ -234,7 +236,7 @@ export function convertTokenString(s: string) { if (pos < s.length) { result.push(s.slice(pos)); } else if (result.length === 2) { - return result[1]; + return ['to-string', result[1]]; } return result; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 40a19398baf..a817aa6c8de 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -3192,7 +3192,7 @@ } }, "concat": { - "doc": "Returns a `string` consisting of the concatenation of the inputs. If any inputs are `formatted`, returns a `formatted` with default formatting options for all unformatted inputs.", + "doc": "Returns a `string` consisting of the concatenation of the inputs. Each input is converted to a string as if by `to-string`.", "group": "String", "sdk-support": { "basic functionality": { diff --git a/src/style/style_layer/background_style_layer_properties.js b/src/style/style_layer/background_style_layer_properties.js index 64644162963..c4c9b36ab37 100644 --- a/src/style/style_layer/background_style_layer_properties.js +++ b/src/style/style_layer/background_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/circle_style_layer_properties.js b/src/style/style_layer/circle_style_layer_properties.js index cfa515be11a..65ea36aeb18 100644 --- a/src/style/style_layer/circle_style_layer_properties.js +++ b/src/style/style_layer/circle_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/fill_extrusion_style_layer_properties.js b/src/style/style_layer/fill_extrusion_style_layer_properties.js index fbfd2a08f5c..c8e27695110 100644 --- a/src/style/style_layer/fill_extrusion_style_layer_properties.js +++ b/src/style/style_layer/fill_extrusion_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/fill_style_layer_properties.js b/src/style/style_layer/fill_style_layer_properties.js index c321dede0d8..d0173c989b3 100644 --- a/src/style/style_layer/fill_style_layer_properties.js +++ b/src/style/style_layer/fill_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/heatmap_style_layer_properties.js b/src/style/style_layer/heatmap_style_layer_properties.js index a2cc3bacdca..76b6079e550 100644 --- a/src/style/style_layer/heatmap_style_layer_properties.js +++ b/src/style/style_layer/heatmap_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/hillshade_style_layer_properties.js b/src/style/style_layer/hillshade_style_layer_properties.js index 0c7b96f850d..39d94286ad8 100644 --- a/src/style/style_layer/hillshade_style_layer_properties.js +++ b/src/style/style_layer/hillshade_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/layer_properties.js.ejs b/src/style/style_layer/layer_properties.js.ejs index a916e899726..573538879dc 100644 --- a/src/style/style_layer/layer_properties.js.ejs +++ b/src/style/style_layer/layer_properties.js.ejs @@ -20,7 +20,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; <% if (layoutProperties.length) { -%> export type LayoutProps = {| diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js index 468b937ecc0..a9fb955ef94 100644 --- a/src/style/style_layer/line_style_layer_properties.js +++ b/src/style/style_layer/line_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type LayoutProps = {| "line-cap": DataConstantProperty<"butt" | "round" | "square">, diff --git a/src/style/style_layer/raster_style_layer_properties.js b/src/style/style_layer/raster_style_layer_properties.js index 97d46906570..b813a73459c 100644 --- a/src/style/style_layer/raster_style_layer_properties.js +++ b/src/style/style_layer/raster_style_layer_properties.js @@ -15,7 +15,7 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type PaintProps = {| diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index adf350a093e..7c77ff40fe7 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -15,13 +15,13 @@ import { import type Color from '../../style-spec/util/color'; -import type {Formatted} from '../../style-spec/expression/definitions/formatted'; +import type Formatted from '../../style-spec/expression/types/formatted'; export type LayoutProps = {| "symbol-placement": DataConstantProperty<"point" | "line" | "line-center">, "symbol-spacing": DataConstantProperty, "symbol-avoid-edges": DataConstantProperty, - "symbol-z-order": DataConstantProperty, + "symbol-z-order": DataConstantProperty<"viewport-y" | "source">, "icon-allow-overlap": DataConstantProperty, "icon-ignore-placement": DataConstantProperty, "icon-optional": DataConstantProperty, @@ -38,7 +38,7 @@ export type LayoutProps = {| "icon-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, "text-pitch-alignment": DataConstantProperty<"map" | "viewport" | "auto">, "text-rotation-alignment": DataConstantProperty<"map" | "viewport" | "auto">, - "text-field": DataDrivenProperty, + "text-field": DataDrivenProperty, "text-font": DataDrivenProperty>, "text-size": DataDrivenProperty, "text-max-width": DataDrivenProperty, diff --git a/src/symbol/mergelines.js b/src/symbol/mergelines.js index 3468cf74f30..ebd0607f20a 100644 --- a/src/symbol/mergelines.js +++ b/src/symbol/mergelines.js @@ -1,7 +1,6 @@ // @flow import type {SymbolFeature} from '../data/bucket/symbol_bucket'; -import {Formatted} from '../style-spec/expression/definitions/formatted'; export default function (features: Array): Array { const leftIndex: {[string]: number} = {}; @@ -42,7 +41,7 @@ export default function (features: Array): Array { for (let k = 0; k < features.length; k++) { const feature = features[k]; const geom = feature.geometry; - const text = feature.text instanceof Formatted ? feature.text.toString() : feature.text; + const text = feature.text ? feature.text.toString() : null; if (!text) { add(k); diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index fc6036fed9d..7aa6d787eec 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -9,7 +9,7 @@ import { plugin as rtlTextPlugin } from '../source/rtl_text_plugin'; import type {StyleGlyph} from '../style/style_glyph'; import type {ImagePosition} from '../render/image_atlas'; -import {Formatted} from '../style-spec/expression/definitions/formatted'; +import Formatted from '../style-spec/expression/types/formatted'; const WritingMode = { horizontal: 1, @@ -53,25 +53,17 @@ class TaggedString { this.sections = []; } - static fromFeature(text: string | Formatted, defaultFontStack: string) { + static fromFeature(text: Formatted, defaultFontStack: string) { const result = new TaggedString(); - if (text instanceof Formatted) { - for (let i = 0; i < text.sections.length; i++) { - const section = text.sections[i]; - result.sections.push({ - scale: section.scale || 1, - fontStack: section.fontStack || defaultFontStack - }); - result.text += section.text; - for (let j = 0; j < section.text.length; j++) { - result.sectionIndex.push(i); - } - } - } else { - result.text = text; - result.sections.push({ scale: 1, fontStack: defaultFontStack }); - for (let i = 0; i < text.length; i++) { - result.sectionIndex.push(0); + for (let i = 0; i < text.sections.length; i++) { + const section = text.sections[i]; + result.sections.push({ + scale: section.scale || 1, + fontStack: section.fontStack || defaultFontStack + }); + result.text += section.text; + for (let j = 0; j < section.text.length; j++) { + result.sectionIndex.push(i); } } return result; @@ -142,7 +134,7 @@ function breakLines(input: TaggedString, lineBreakPoints: Array): Array< return lines; } -function shapeText(text: string | Formatted, +function shapeText(text: Formatted, glyphs: {[string]: {[number]: ?StyleGlyph}}, defaultFontStack: string, maxWidth: number, diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 8aef10f1228..c88cbb5561b 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -17,7 +17,6 @@ import classifyRings from '../util/classify_rings'; import EXTENT from '../data/extent'; import SymbolBucket from '../data/bucket/symbol_bucket'; import EvaluationParameters from '../style/evaluation_parameters'; -import {Formatted} from '../style-spec/expression/definitions/formatted'; import {SIZE_PACK_FACTOR} from './symbol_size'; import type {Shaping, PositionedIcon} from './shaping'; @@ -106,7 +105,7 @@ export function performSymbolLayout(bucket: SymbolBucket, const shapedTextOrientations = {}; const text = feature.text; if (text) { - const unformattedText = text instanceof Formatted ? text.toString() : text; + const unformattedText = text.toString(); const textOffset: [number, number] = (layout.get('text-offset').evaluate(feature, {}).map((t)=> t * oneEm): any); const spacing = layout.get('text-letter-spacing').evaluate(feature, {}) * oneEm; const spacingIfAllowed = allowsLetterSpacing(unformattedText) ? spacing : 0; diff --git a/src/symbol/transform_text.js b/src/symbol/transform_text.js index e7058086c90..0086539cf21 100644 --- a/src/symbol/transform_text.js +++ b/src/symbol/transform_text.js @@ -4,7 +4,7 @@ import { plugin as rtlTextPlugin } from '../source/rtl_text_plugin'; import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer'; import type {Feature} from '../style-spec/expression'; -import {Formatted} from '../style-spec/expression/definitions/formatted'; +import Formatted from '../style-spec/expression/types/formatted'; function transformText(text: string, layer: SymbolStyleLayer, feature: Feature) { const transform = layer.layout.get('text-transform').evaluate(feature, {}); @@ -22,13 +22,9 @@ function transformText(text: string, layer: SymbolStyleLayer, feature: Feature) } -export default function(text: string | Formatted, layer: SymbolStyleLayer, feature: Feature) { - if (text instanceof Formatted) { - text.sections.forEach(section => { - section.text = transformText(section.text, layer, feature); - }); - return text; - } else { - return transformText(text, layer, feature); - } +export default function(text: Formatted, layer: SymbolStyleLayer, feature: Feature): Formatted { + text.sections.forEach(section => { + section.text = transformText(section.text, layer, feature); + }); + return text; } diff --git a/test/integration/expression-tests/coalesce/inference/test.json b/test/integration/expression-tests/coalesce/inference/test.json index b8cd7a1a346..8399bb10035 100644 --- a/test/integration/expression-tests/coalesce/inference/test.json +++ b/test/integration/expression-tests/coalesce/inference/test.json @@ -17,11 +17,9 @@ "outputs": [ "one", "two", - { - "error": "Expected value to be of type string, but found number instead." - }, - {"error": "Expected value to be of type string, but found null instead."} + "5", + "" ], - "serialized": ["string", ["coalesce", ["get", "a"], ["get", "b"]]] + "serialized": ["to-string", ["coalesce", ["get", "a"], ["get", "b"]]] } } diff --git a/test/integration/expression-tests/concat/coercion/test.json b/test/integration/expression-tests/concat/coercion/test.json new file mode 100644 index 00000000000..9b872b8ad84 --- /dev/null +++ b/test/integration/expression-tests/concat/coercion/test.json @@ -0,0 +1,14 @@ +{ + "expression": ["concat", ["get", "a"], ["get", "1"], ["get", "true"]], + "inputs": [[{}, {"properties": {"a": "a", "1": 1, "true": true}}]], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "string" + }, + "outputs": ["a1true"], + "serialized": ["concat", ["get", "a"], ["get", "1"], ["get", "true"]] + } +} diff --git a/test/integration/expression-tests/formatted/basic/test.json b/test/integration/expression-tests/format/basic/test.json similarity index 100% rename from test/integration/expression-tests/formatted/basic/test.json rename to test/integration/expression-tests/format/basic/test.json diff --git a/test/integration/expression-tests/format/coercion/test.json b/test/integration/expression-tests/format/coercion/test.json new file mode 100644 index 00000000000..e7570b146ab --- /dev/null +++ b/test/integration/expression-tests/format/coercion/test.json @@ -0,0 +1,50 @@ +{ + "expression": [ + "format", + ["get", "a"], + {}, + ["get", "1"], + {}, + ["get", "true"], + {} + ], + "inputs": [[{}, {"properties": {"a": "a", "1": 1, "true": true}}]], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + }, + { + "text": "1", + "scale": null, + "fontStack": null + }, + { + "text": "true", + "scale": null, + "fontStack": null + } + ] + } + ], + "serialized": [ + "format", + ["get", "a"], + {}, + ["get", "1"], + {}, + ["get", "true"], + {} + ] + } +} diff --git a/test/integration/expression-tests/format/implicit-assert/test.json b/test/integration/expression-tests/format/implicit-assert/test.json new file mode 100644 index 00000000000..ee26d2f0b43 --- /dev/null +++ b/test/integration/expression-tests/format/implicit-assert/test.json @@ -0,0 +1,14 @@ +{ + "expression": ["number", ["get", "p"]], + "propertySpec": { + "type": "formatted" + }, + "expected": { + "compiled": { + "result": "error", + "errors": [ + {"key": "", "error": "Expected formatted but found number instead."} + ] + } + } +} diff --git a/test/integration/expression-tests/format/implicit-coerce/test.json b/test/integration/expression-tests/format/implicit-coerce/test.json new file mode 100644 index 00000000000..dabce92bb5f --- /dev/null +++ b/test/integration/expression-tests/format/implicit-coerce/test.json @@ -0,0 +1,53 @@ +{ + "expression": ["to-string", ["get", "p"]], + "propertySpec": { + "type": "formatted" + }, + "inputs": [ + [{}, {"properties": {}}], + [{}, {"properties": {"p": 0}}], + [{}, {"properties": {"p": "a"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "0", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + } + ] + } + ], + "serialized": [ + "format", + ["to-string", ["get", "p"]], + {} + ] + } +} diff --git a/test/integration/expression-tests/format/implicit-omit/test.json b/test/integration/expression-tests/format/implicit-omit/test.json new file mode 100644 index 00000000000..44405f7fdd4 --- /dev/null +++ b/test/integration/expression-tests/format/implicit-omit/test.json @@ -0,0 +1,53 @@ +{ + "expression": ["coalesce", ["get", "q"], ["get", "p"]], + "propertySpec": { + "type": "formatted" + }, + "inputs": [ + [{}, {"properties": {}}], + [{}, {"properties": {"p": 0}}], + [{}, {"properties": {"p": "a"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "0", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + } + ] + } + ], + "serialized": [ + "format", + ["coalesce", ["get", "q"], ["get", "p"]], + {} + ] + } +} diff --git a/test/integration/expression-tests/format/implicit/test.json b/test/integration/expression-tests/format/implicit/test.json new file mode 100644 index 00000000000..19485f2d658 --- /dev/null +++ b/test/integration/expression-tests/format/implicit/test.json @@ -0,0 +1,53 @@ +{ + "expression": ["get", "p"], + "propertySpec": { + "type": "formatted" + }, + "inputs": [ + [{}, {"properties": {}}], + [{}, {"properties": {"p": 0}}], + [{}, {"properties": {"p": "a"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "0", + "scale": null, + "fontStack": null + } + ] + }, + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + } + ] + } + ], + "serialized": [ + "format", + ["get", "p"], + {} + ] + } +} diff --git a/test/integration/expression-tests/formatted/to-string/test.json b/test/integration/expression-tests/format/to-string/test.json similarity index 100% rename from test/integration/expression-tests/formatted/to-string/test.json rename to test/integration/expression-tests/format/to-string/test.json diff --git a/test/integration/expression-tests/legacy/interval/tokens-zoom/test.json b/test/integration/expression-tests/legacy/interval/tokens-zoom/test.json index 05e95024253..fed8f4f31bb 100644 --- a/test/integration/expression-tests/legacy/interval/tokens-zoom/test.json +++ b/test/integration/expression-tests/legacy/interval/tokens-zoom/test.json @@ -1,8 +1,8 @@ { - "expression": {"type": "interval", "stops": [[0, "0 {a}"], [1, "1 {b}"]]}, + "expression": {"type": "interval", "stops": [[0, "0 {a}"], [1, "{b}"]]}, "inputs": [ - [{"zoom": 0}, {"properties": {"a": "a", "b": "b"}}], - [{"zoom": 1}, {"properties": {"a": "a", "b": "b"}}], + [{"zoom": 0}, {"properties": {"a": "a", "b": 2}}], + [{"zoom": 1}, {"properties": {"a": "a", "b": 2}}], [{"zoom": 0}, {"properties": {}}] ], "propertySpec": { @@ -18,13 +18,13 @@ "isZoomConstant": false, "type": "string" }, - "outputs": ["0 a", "1 b", "0 "], + "outputs": ["0 a", "2", "0 "], "serialized": [ "step", ["zoom"], - ["concat", "0 ", ["to-string", ["get", "a"]]], + ["concat", "0 ", ["get", "a"]], 1, - ["concat", "1 ", ["to-string", ["get", "b"]]] + ["to-string", ["get", "b"]] ] } } diff --git a/test/integration/expression-tests/to-boolean/2-ary/test.json b/test/integration/expression-tests/to-boolean/2-ary/test.json new file mode 100644 index 00000000000..94de6a23d81 --- /dev/null +++ b/test/integration/expression-tests/to-boolean/2-ary/test.json @@ -0,0 +1,10 @@ +{ + "expression": ["to-boolean", ["get", "x"], ["get", "y"]], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [{"key": "", "error": "Expected one argument."}] + } + } +} diff --git a/test/integration/expression-tests/to-string/2-ary/test.json b/test/integration/expression-tests/to-string/2-ary/test.json new file mode 100644 index 00000000000..4ee8bde793d --- /dev/null +++ b/test/integration/expression-tests/to-string/2-ary/test.json @@ -0,0 +1,10 @@ +{ + "expression": ["to-string", ["get", "x"], ["get", "y"]], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [{"key": "", "error": "Expected one argument."}] + } + } +} diff --git a/test/integration/expression-tests/to-string/implicit/test.json b/test/integration/expression-tests/to-string/implicit/test.json new file mode 100644 index 00000000000..d13d2b807d0 --- /dev/null +++ b/test/integration/expression-tests/to-string/implicit/test.json @@ -0,0 +1,24 @@ +{ + "expression": ["get", "p"], + "propertySpec": { + "type": "string" + }, + "inputs": [ + [{}, {"properties": {}}], + [{}, {"properties": {"p": 0}}], + [{}, {"properties": {"p": "a"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "string" + }, + "outputs": ["", "0", "a"], + "serialized": [ + "to-string", + ["get", "p"] + ] + } +} diff --git a/test/unit/style-spec/migrate.test.js b/test/unit/style-spec/migrate.test.js index fc7c46f896e..100b5003043 100644 --- a/test/unit/style-spec/migrate.test.js +++ b/test/unit/style-spec/migrate.test.js @@ -36,11 +36,11 @@ t('converts token strings to expressions', (t) => { layers: [{ id: '1', type: 'symbol', - layout: {'text-field': 'a{x}', 'icon-image': 'b{y}'} + layout: {'text-field': 'a{x}', 'icon-image': '{y}'} }] }, spec.latest.$version); - t.deepEqual(migrated.layers[0].layout['text-field'], ['concat', 'a', ['to-string', ['get', 'x']]]); - t.deepEqual(migrated.layers[0].layout['icon-image'], ['concat', 'b', ['to-string', ['get', 'y']]]); + t.deepEqual(migrated.layers[0].layout['text-field'], ['concat', 'a', ['get', 'x']]); + t.deepEqual(migrated.layers[0].layout['icon-image'], ['to-string', ['get', 'y']]); t.end(); }); diff --git a/test/unit/symbol/shaping.test.js b/test/unit/symbol/shaping.test.js index 69183efa35c..1e915bc5829 100644 --- a/test/unit/symbol/shaping.test.js +++ b/test/unit/symbol/shaping.test.js @@ -2,6 +2,7 @@ import { test } from 'mapbox-gl-js-test'; import fs from 'fs'; import path from 'path'; import * as shaping from '../../../src/symbol/shaping'; +import Formatted from '../../../src/style-spec/expression/types/formatted'; const WritingMode = shaping.WritingMode; let UPDATE = false; @@ -20,50 +21,50 @@ test('shaping', (t) => { JSON.parse('{}'); - shaped = shaping.shapeText(`hi${String.fromCharCode(0)}`, glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString(`hi${String.fromCharCode(0)}`), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-null.json')))); // Default shaping. - shaped = shaping.shapeText('abcde', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-default.json')))); // Letter spacing. - shaped = shaping.shapeText('abcde', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-spacing.json')))); // Line break. - shaped = shaping.shapeText('abcde abcde', glyphs, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde abcde'), glyphs, fontStack, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-linebreak.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, require('../../expected/text-shaping-linebreak.json')); const expectedNewLine = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'))); - shaped = shaping.shapeText('abcde\nabcde', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newline.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLine); - shaped = shaping.shapeText('abcde\r\nabcde', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde\r\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); t.deepEqual(shaped.positionedGlyphs, expectedNewLine.positionedGlyphs); const expectedNewLinesInMiddle = JSON.parse(fs.readFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'))); - shaped = shaping.shapeText('abcde\n\nabcde', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString('abcde\n\nabcde'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); if (UPDATE) fs.writeFileSync(path.join(__dirname, '/../../expected/text-shaping-newlines-in-middle.json'), JSON.stringify(shaped, null, 2)); t.deepEqual(shaped, expectedNewLinesInMiddle); // Null shaping. - shaped = shaping.shapeText('', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString(''), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); t.equal(false, shaped); - shaped = shaping.shapeText(String.fromCharCode(0), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString(String.fromCharCode(0)), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); t.equal(false, shaped); // https://github.com/mapbox/mapbox-gl-js/issues/3254 - shaped = shaping.shapeText(' foo bar\n', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); - const shaped2 = shaping.shapeText('foo bar', glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(Formatted.fromString(' foo bar\n'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + const shaped2 = shaping.shapeText(Formatted.fromString('foo bar'), glyphs, fontStack, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); t.same(shaped.positionedGlyphs, shaped2.positionedGlyphs); t.end();