From f42af5b4ff8dfdb84e990fd91ab1f51828fbcb89 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 30 Apr 2018 09:24:47 -0700 Subject: [PATCH 1/8] Auto-convert concat arguments to strings --- .../expression/definitions/index.js | 39 +++++++++---------- src/style-spec/expression/values.js | 14 +++++++ src/style-spec/reference/v8.json | 2 +- .../concat/coercion/test.json | 14 +++++++ 4 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 test/integration/expression-tests/concat/coercion/test.json diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index f39dc07639d..0ee11e55c60 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'; @@ -25,10 +37,9 @@ import { GreaterThanOrEqual } from './comparison'; import { CollatorExpression } from './collator'; -import { Formatted, FormatExpression } from './formatted'; +import { FormatExpression } from './formatted'; import Length from './length'; -import type { Type } from '../types'; import type { Varargs } from '../compound_expression'; import type { ExpressionRegistry } from '../expression'; @@ -108,24 +119,12 @@ CompoundExpression.register(expressions, { 'typeof': [ StringType, [ValueType], - (ctx, [v]) => toString(typeOf(v.evaluate(ctx))) + (ctx, [v]) => typeToString(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); - } - } + (ctx, [v]) => valueToString(v.evaluate(ctx)) ], 'to-boolean': [ BooleanType, @@ -544,8 +543,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/values.js b/src/style-spec/expression/values.js index 1ed127c1c8e..80e1d99dffa 100644 --- a/src/style-spec/expression/values.js +++ b/src/style-spec/expression/values.js @@ -4,6 +4,7 @@ import assert from 'assert'; import Color from '../util/color'; import { Collator } from './definitions/collator'; +import { Formatted } from './definitions/formatted'; import { NullType, NumberType, StringType, BooleanType, ColorType, ObjectType, ValueType, CollatorType, array } from './types'; import type { Type } from './types'; @@ -97,4 +98,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/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/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"]] + } +} From 4256281a16c74656414495b4aac56779c7cb6327 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 13 Sep 2018 13:23:40 -0700 Subject: [PATCH 2/8] For string-valued properties, do coercion rather than assertion --- docs/components/expression-metadata.js | 14 +++++-- .../expression/definitions/coalesce.js | 2 +- .../expression/definitions/coercion.js | 19 ++++++--- .../expression/definitions/index.js | 12 +----- src/style-spec/expression/index.js | 6 ++- src/style-spec/expression/parsing_context.js | 42 ++++++++++--------- src/style-spec/function/convert.js | 4 +- .../coalesce/inference/test.json | 8 ++-- .../to-boolean/2-ary/test.json | 10 +++++ .../to-string/2-ary/test.json | 10 +++++ .../to-string/implicit/test.json | 24 +++++++++++ 11 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 test/integration/expression-tests/to-boolean/2-ary/test.json create mode 100644 test/integration/expression-tests/to-string/2-ary/test.json create mode 100644 test/integration/expression-tests/to-string/implicit/test.json 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/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..58f062551b5 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -2,8 +2,8 @@ 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 type { Expression } from '../expression'; @@ -14,8 +14,10 @@ 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 +43,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 +59,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) { @@ -86,7 +93,7 @@ class Coercion implements Expression { } } 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 +103,8 @@ class Coercion implements Expression { return num; } throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`); + } else { + return valueToString(this.args[0].evaluate(ctx)); } } diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 0ee11e55c60..107249f40ce 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -69,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 }; @@ -121,16 +123,6 @@ CompoundExpression.register(expressions, { [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx))) ], - 'to-string': [ - StringType, - [ValueType], - (ctx, [v]) => valueToString(v.evaluate(ctx)) - ], - 'to-boolean': [ - BooleanType, - [ValueType], - (ctx, [v]) => Boolean(v.evaluate(ctx)) - ], 'to-rgba': [ array(NumberType, 4), [ColorType], diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index d3ef57dd58d..69781b3d5f7 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); diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 4a0c2d64ec5..d35777c77bd 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -61,7 +61,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 +69,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 +104,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/function/convert.js b/src/style-spec/function/convert.js index a9de750e326..b200b40cc7b 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', 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/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"] + ] + } +} From 1ff116c6f661825a4df00c28d08304fe451078aa Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 13 Sep 2018 13:49:29 -0700 Subject: [PATCH 3/8] Simplify how token strings are converted No longer need to-string when using concat. However, it is still needed if the token string consists of a single token. --- src/style-spec/function/convert.js | 4 ++-- .../legacy/interval/tokens-zoom/test.json | 12 ++++++------ test/unit/style-spec/migrate.test.js | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index b200b40cc7b..73bd7fef9f9 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -226,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) { @@ -236,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/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/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(); }); From 3b480cdfd5f0c44e4cc06b0aeb160c4512953ff3 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 13 Sep 2018 16:40:38 -0700 Subject: [PATCH 4/8] Auto-convert format arguments to strings --- .../expression/definitions/formatted.js | 4 +- .../{formatted => format}/basic/test.json | 0 .../format/coercion/test.json | 50 +++++++++++++++++++ .../{formatted => format}/to-string/test.json | 0 4 files changed, 52 insertions(+), 2 deletions(-) rename test/integration/expression-tests/{formatted => format}/basic/test.json (100%) create mode 100644 test/integration/expression-tests/format/coercion/test.json rename test/integration/expression-tests/{formatted => format}/to-string/test.json (100%) diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/formatted.js index d6e517479b1..ef4a74b86c3 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/formatted.js @@ -1,7 +1,7 @@ // @flow import { NumberType, ValueType, FormattedType, array, StringType } from '../types'; - +import { toString } from '../values'; import type { Expression } from '../expression'; import type EvaluationContext from '../evaluation_context'; @@ -101,7 +101,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/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/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 From 85fce0183a9d2423c17c1289b1bc6f485db45982 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 13 Sep 2018 16:47:02 -0700 Subject: [PATCH 5/8] Fix implicit conversions for "formatted" type --- .../expression/definitions/coercion.js | 10 ---- src/style-spec/expression/index.js | 9 ++-- src/style-spec/expression/parsing_context.js | 8 ++- .../format/implicit/test.json | 53 +++++++++++++++++++ 4 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 test/integration/expression-tests/format/implicit/test.json diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index 58f062551b5..17ba74b043f 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -11,7 +11,6 @@ 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, @@ -84,15 +83,6 @@ 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 if (this.type.kind === 'number') { let value = null; for (const arg of this.args) { diff --git a/src/style-spec/expression/index.js b/src/style-spec/expression/index.js index 69781b3d5f7..5b9f07c3d86 100644 --- a/src/style-spec/expression/index.js +++ b/src/style-spec/expression/index.js @@ -351,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 d35777c77bd..646c2a39da2 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -1,7 +1,7 @@ // @flow +import assert from 'assert'; import Scope from './scope'; - import { checkSubtype } from './types'; import ParsingError from './parsing_error'; import Literal from './definitions/literal'; @@ -16,6 +16,7 @@ import Var from './definitions/var'; import type {Expression, ExpressionRegistry} from './expression'; import type {Type} from './types'; +import {FormatExpression} from './definitions/formatted'; /** * State associated parsing at a given point in an expression tree. @@ -114,8 +115,11 @@ class ParsingContext { // if ((expected.kind === 'string' || expected.kind === 'number' || expected.kind === 'boolean' || expected.kind === 'object' || expected.kind === 'array') && actual.kind === 'value') { parsed = annotate(parsed, expected, options.typeAnnotation || 'assert'); - } else if ((expected.kind === 'color' || expected.kind === 'formatted') && (actual.kind === 'value' || actual.kind === 'string')) { + } else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) { parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); + } else if (expected.kind === 'formatted' && actual.kind !== 'formatted') { + assert(!options.typeAnnotation); + parsed = new FormatExpression([{text: parsed, scale: null, font: null}]); } else if (this.checkSubtype(expected, actual)) { return null; } 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"], + {} + ] + } +} From 11b6a199c95be29024d3e9c1ac2ce712ad608110 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 13 Sep 2018 17:06:50 -0700 Subject: [PATCH 6/8] Split Collator/Formatted types into separate files This avoids a troublesome circular import. --- src/data/bucket/symbol_bucket.js | 2 +- .../expression/definitions/collator.js | 63 +------------------ .../definitions/{formatted.js => format.js} | 39 +----------- .../expression/definitions/index.js | 4 +- .../expression/definitions/literal.js | 2 +- src/style-spec/expression/parsing_context.js | 4 +- src/style-spec/expression/types/collator.js | 61 ++++++++++++++++++ src/style-spec/expression/types/formatted.js | 37 +++++++++++ src/style-spec/expression/values.js | 12 ++-- .../background_style_layer_properties.js | 2 +- .../circle_style_layer_properties.js | 2 +- .../fill_extrusion_style_layer_properties.js | 2 +- .../fill_style_layer_properties.js | 2 +- .../heatmap_style_layer_properties.js | 2 +- .../hillshade_style_layer_properties.js | 2 +- src/style/style_layer/layer_properties.js.ejs | 2 +- .../line_style_layer_properties.js | 2 +- .../raster_style_layer_properties.js | 2 +- .../symbol_style_layer_properties.js | 4 +- src/symbol/mergelines.js | 2 +- src/symbol/shaping.js | 2 +- src/symbol/symbol_layout.js | 2 +- src/symbol/transform_text.js | 2 +- 23 files changed, 131 insertions(+), 123 deletions(-) rename src/style-spec/expression/definitions/{formatted.js => format.js} (77%) create mode 100644 src/style-spec/expression/types/collator.js create mode 100644 src/style-spec/expression/types/formatted.js diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 17aa10c4d13..3945e7f6f26 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 { 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 77% rename from src/style-spec/expression/definitions/formatted.js rename to src/style-spec/expression/definitions/format.js index ef4a74b86c3..071c2110b4c 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/format.js @@ -1,6 +1,7 @@ // @flow import { NumberType, ValueType, FormattedType, array, StringType } from '../types'; +import Formatted, { FormattedSection } from '../types/formatted'; import { toString } from '../values'; import type { Expression } from '../expression'; @@ -8,49 +9,13 @@ 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; diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index 107249f40ce..da07b7bc9bd 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -36,8 +36,8 @@ import { LessThanOrEqual, GreaterThanOrEqual } from './comparison'; -import { CollatorExpression } from './collator'; -import { FormatExpression } from './formatted'; +import CollatorExpression from './collator'; +import FormatExpression from './format'; import Length from './length'; import type { Varargs } from '../compound_expression'; 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/parsing_context.js b/src/style-spec/expression/parsing_context.js index 646c2a39da2..b12de8a1002 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -9,14 +9,14 @@ 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 FormatExpression from './definitions/format'; import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant'; import Var from './definitions/var'; import type {Expression, ExpressionRegistry} from './expression'; import type {Type} from './types'; -import {FormatExpression} from './definitions/formatted'; /** * State associated parsing at a given point in an expression tree. 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..7637e2717cf --- /dev/null +++ b/src/style-spec/expression/types/formatted.js @@ -0,0 +1,37 @@ +// @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; + } + + 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 80e1d99dffa..3f8b5c874c2 100644 --- a/src/style-spec/expression/values.js +++ b/src/style-spec/expression/values.js @@ -3,9 +3,9 @@ import assert from 'assert'; import Color from '../util/color'; -import { Collator } from './definitions/collator'; -import { Formatted } from './definitions/formatted'; -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'; @@ -28,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) { @@ -43,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)) { @@ -75,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; 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..9a1c4a64109 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, diff --git a/src/symbol/mergelines.js b/src/symbol/mergelines.js index 3468cf74f30..a3bde56367f 100644 --- a/src/symbol/mergelines.js +++ b/src/symbol/mergelines.js @@ -1,7 +1,7 @@ // @flow import type {SymbolFeature} from '../data/bucket/symbol_bucket'; -import {Formatted} from '../style-spec/expression/definitions/formatted'; +import Formatted from '../style-spec/expression/types/formatted'; export default function (features: Array): Array { const leftIndex: {[string]: number} = {}; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index fc6036fed9d..b1e7e1f26aa 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, diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 8aef10f1228..91f10722cab 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -17,7 +17,7 @@ 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 Formatted from '../style-spec/expression/types/formatted'; import {SIZE_PACK_FACTOR} from './symbol_size'; import type {Shaping, PositionedIcon} from './shaping'; diff --git a/src/symbol/transform_text.js b/src/symbol/transform_text.js index e7058086c90..b2904d9fcc9 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, {}); From b0dffd2e3f937db158236b9c0615278b11c7849c Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Mon, 17 Sep 2018 13:47:21 -0700 Subject: [PATCH 7/8] Complete transition to coercing 'text-field' to 'formatted' at evaluation time. - Add coercion after call to 'getValueAndResolveTokens`, since the non-expression pathway here skips the coercion logic in parsing_context - Remove 'string | Formatted' typing and 'text instanceof Formatted' checks - Add Coercion support for 'Formatted', along with dedicated serialization - Use Coercion when parsing expected.kind === 'formatted' instead of directly creating a FormatExpression. This is necessary to accommodate expressions such as 'coalesce' that introduce a typeAnnotation. --- build/generate-style-code.js | 2 +- src/data/bucket/symbol_bucket.js | 31 +++++++++---------- .../expression/definitions/coercion.js | 9 ++++++ src/style-spec/expression/parsing_context.js | 7 +---- src/style-spec/expression/types/formatted.js | 4 +++ .../symbol_style_layer_properties.js | 2 +- src/symbol/mergelines.js | 3 +- src/symbol/shaping.js | 30 +++++++----------- src/symbol/symbol_layout.js | 3 +- src/symbol/transform_text.js | 14 +++------ test/unit/symbol/shaping.test.js | 23 +++++++------- 11 files changed, 61 insertions(+), 67 deletions(-) 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/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 3945e7f6f26..5417861e644 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -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/coercion.js b/src/style-spec/expression/definitions/coercion.js index 17ba74b043f..03bd1afb923 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -5,6 +5,8 @@ import assert from 'assert'; 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'; @@ -93,6 +95,10 @@ 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)); } @@ -107,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/parsing_context.js b/src/style-spec/expression/parsing_context.js index b12de8a1002..a92e5dd53ba 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -1,6 +1,5 @@ // @flow -import assert from 'assert'; import Scope from './scope'; import { checkSubtype } from './types'; import ParsingError from './parsing_error'; @@ -10,7 +9,6 @@ import Coercion from './definitions/coercion'; import EvaluationContext from './evaluation_context'; import CompoundExpression from './compound_expression'; import CollatorExpression from './definitions/collator'; -import FormatExpression from './definitions/format'; import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant'; import Var from './definitions/var'; @@ -115,11 +113,8 @@ class ParsingContext { // if ((expected.kind === 'string' || expected.kind === 'number' || expected.kind === 'boolean' || expected.kind === 'object' || expected.kind === 'array') && actual.kind === 'value') { parsed = annotate(parsed, expected, options.typeAnnotation || 'assert'); - } else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) { + } else if ((expected.kind === 'color' || expected.kind === 'formatted') && (actual.kind === 'value' || actual.kind === 'string')) { parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); - } else if (expected.kind === 'formatted' && actual.kind !== 'formatted') { - assert(!options.typeAnnotation); - parsed = new FormatExpression([{text: parsed, scale: null, font: null}]); } else if (this.checkSubtype(expected, actual)) { return null; } diff --git a/src/style-spec/expression/types/formatted.js b/src/style-spec/expression/types/formatted.js index 7637e2717cf..8063e80f9f0 100644 --- a/src/style-spec/expression/types/formatted.js +++ b/src/style-spec/expression/types/formatted.js @@ -19,6 +19,10 @@ export default class Formatted { 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(''); } diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index 9a1c4a64109..7c77ff40fe7 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -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 a3bde56367f..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/types/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 b1e7e1f26aa..7aa6d787eec 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -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 91f10722cab..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/types/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 b2904d9fcc9..0086539cf21 100644 --- a/src/symbol/transform_text.js +++ b/src/symbol/transform_text.js @@ -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/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(); From 2474692acc3180e7a3d6af91eb716154434ca000 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Mon, 17 Sep 2018 14:57:08 -0700 Subject: [PATCH 8/8] Add expression test cases for implicit format type annotations. --- .../format/implicit-assert/test.json | 14 +++++ .../format/implicit-coerce/test.json | 53 +++++++++++++++++++ .../format/implicit-omit/test.json | 53 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 test/integration/expression-tests/format/implicit-assert/test.json create mode 100644 test/integration/expression-tests/format/implicit-coerce/test.json create mode 100644 test/integration/expression-tests/format/implicit-omit/test.json 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"]], + {} + ] + } +}