From f11e6c7898537d368e7fc77df35dea668bb636c6 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Wed, 1 Aug 2018 12:39:25 -0700 Subject: [PATCH 1/5] Introduce "format" expression. Returns "formatted" type with formatting annotations applied to subsections. "text-field" now accepts formatted text, allowing symbols to use multiple fonts within the same label. --- build/generate-flow-typed-style-spec.js | 2 + build/generate-style-code.js | 6 +- docs/components/expression-metadata.js | 8 + docs/pages/style-spec.js | 16 ++ src/data/bucket/symbol_bucket.js | 35 ++- src/source/rtl_text_plugin.js | 2 + src/source/worker.js | 4 +- .../expression/definitions/formatted.js | 145 +++++++++++ .../expression/definitions/index.js | 4 +- .../expression/definitions/literal.js | 4 + src/style-spec/expression/types.js | 6 +- src/style-spec/reference/v8.json | 15 +- src/style-spec/types.js | 5 +- src/style-spec/validate/validate.js | 10 +- src/style-spec/validate/validate_formatted.js | 11 + .../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 | 3 +- src/symbol/quads.js | 16 +- src/symbol/shaping.js | 226 ++++++++++++++---- src/symbol/symbol_layout.js | 19 +- src/symbol/transform_text.js | 16 +- test/expected/text-shaping-default.json | 22 +- test/expected/text-shaping-linebreak.json | 40 +++- test/expected/text-shaping-newline.json | 40 +++- .../text-shaping-newlines-in-middle.json | 40 +++- test/expected/text-shaping-null.json | 10 +- test/expected/text-shaping-spacing.json | 22 +- .../formatted/basic/test.json | 80 +++++++ .../formatted/to-string/test.json | 42 ++++ .../text-field/formatted-line/expected.png | Bin 0 -> 30757 bytes .../text-field/formatted-line/style.json | 56 +++++ .../text-field/formatted/expected.png | Bin 0 -> 4867 bytes .../text-field/formatted/style.json | 53 ++++ test/unit/style-spec/spec.test.js | 3 +- test/unit/symbol/shaping.test.js | 27 +-- 44 files changed, 862 insertions(+), 148 deletions(-) create mode 100644 src/style-spec/expression/definitions/formatted.js create mode 100644 src/style-spec/validate/validate_formatted.js create mode 100644 test/integration/expression-tests/formatted/basic/test.json create mode 100644 test/integration/expression-tests/formatted/to-string/test.json create mode 100644 test/integration/render-tests/text-field/formatted-line/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-line/style.json create mode 100644 test/integration/render-tests/text-field/formatted/expected.png create mode 100644 test/integration/render-tests/text-field/formatted/style.json diff --git a/build/generate-flow-typed-style-spec.js b/build/generate-flow-typed-style-spec.js index f704771114a..59569593ff8 100644 --- a/build/generate-flow-typed-style-spec.js +++ b/build/generate-flow-typed-style-spec.js @@ -118,6 +118,8 @@ fs.writeFileSync('src/style-spec/types.js', `// @flow export type ColorSpecification = string; +export type FormattedSpecification = string; + export type FilterSpecification = | ['has', string] | ['!has', string] diff --git a/build/generate-style-code.js b/build/generate-style-code.js index 8fa49c1bb21..70e7b852b2b 100644 --- a/build/generate-style-code.js +++ b/build/generate-style-code.js @@ -5,6 +5,7 @@ const fs = require('fs'); const ejs = require('ejs'); const spec = require('../src/style-spec/reference/v8'); const Color = require('../src/style-spec/util/color'); +const {Formatted} = require('../src/style-spec/expression/definitions/formatted'); global.camelize = function (str) { return str.replace(/(?:^|-)(.)/g, function (_, x) { @@ -24,6 +25,8 @@ global.flowType = function (property) { return Object.keys(property.values).map(JSON.stringify).join(' | '); case 'color': return `Color`; + case 'formatted': + return `string | Formatted`; case 'array': if (property.length) { return `[${new Array(property.length).fill(flowType({type: property.value})).join(', ')}]`; @@ -61,6 +64,8 @@ global.runtimeType = function (property) { return 'StringType'; case 'color': return `ColorType`; + case 'formatted': + return `FormattedType`; case 'array': if (property.length) { return `array(${runtimeType({type: property.value})}, ${property.length})`; @@ -131,4 +136,3 @@ const layers = Object.keys(spec.layer.type.values).map((type) => { for (const layer of layers) { fs.writeFileSync(`src/style/style_layer/${layer.type.replace('-', '_')}_style_layer_properties.js`, propertiesJs(layer)) } - diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index 40928cf52b7..15c8f55741d 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -134,6 +134,14 @@ const types = { collator: [{ type: 'collator', parameters: [ '{ "case-sensitive": boolean, "diacritic-sensitive": boolean, "locale": string }' ] + }], + format: [{ + type: 'formatted', + parameters: [ + 'input_1: string, options_1: { "font-scale": number, "text-font": array }', + '...', + 'input_n: string, options_n: { "font-scale": number, "text-font": array }' + ] }] }; diff --git a/docs/pages/style-spec.js b/docs/pages/style-spec.js index a55e6a056dc..efc2532c968 100644 --- a/docs/pages/style-spec.js +++ b/docs/pages/style-spec.js @@ -159,6 +159,9 @@ const navigation = [ { "title": "String" }, + { + "title": "Formatted" + }, { "title": "Boolean" }, @@ -948,6 +951,19 @@ export default class extends React.Component {

Especially of note is the support for hsl, which can be easier to reason about than rgb().

+
+ +

Formatted

+

The formatted type represents a string broken into sections annotated with separate formatting options.

+ {highlightJSON(` + { + "text-field": ["format", + "foo", { "font-scale": 1.2 }, + "bar", { "font-scale": 0.8 } + ] + }`)} +
+

String

diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index 8d1cd96ec18..6f85864a84b 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -18,6 +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 type { Bucket, @@ -53,7 +54,7 @@ export type CollisionArrays = { }; export type SymbolFeature = {| - text: string | void, + text: string | Formatted | void, icon: string | void, index: number, sourceLayerIndex: number, @@ -328,6 +329,18 @@ class SymbolBucket implements Bucket { this.lineVertexArray = new SymbolLineVertexArray(); } + calculateGlyphDependencies(text: string, stack: {[number]: boolean}, textAlongLine: boolean, doesAllowVerticalWritingMode: boolean) { + for (let i = 0; i < text.length; i++) { + stack[text.charCodeAt(i)] = true; + if (textAlongLine && doesAllowVerticalWritingMode) { + const verticalChar = verticalizedCharacterMap[text.charAt(i)]; + if (verticalChar) { + stack[verticalChar.charCodeAt(0)] = true; + } + } + } + } + populate(features: Array, options: PopulateParameters) { const layer = this.layers[0]; const layout = layer.layout; @@ -336,7 +349,7 @@ class SymbolBucket implements Bucket { const textField = layout.get('text-field'); const iconImage = layout.get('icon-image'); const hasText = - (textField.value.kind !== 'constant' || textField.value.value.length > 0) && + (textField.value.kind !== 'constant' || textField.value.value.toString().length > 0) && (textFont.value.kind !== 'constant' || textFont.value.value.length > 0); const hasIcon = iconImage.value.kind !== 'constant' || iconImage.value.value && iconImage.value.value.length > 0; @@ -392,16 +405,18 @@ class SymbolBucket implements Bucket { 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'; - const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text); - for (let i = 0; i < text.length; i++) { - stack[text.charCodeAt(i)] = true; - if (textAlongLine && doesAllowVerticalWritingMode) { - const verticalChar = verticalizedCharacterMap[text.charAt(i)]; - if (verticalChar) { - stack[verticalChar.charCodeAt(0)] = true; - } + 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); } + } } diff --git a/src/source/rtl_text_plugin.js b/src/source/rtl_text_plugin.js index 25b2602ee17..db2c711ca4f 100644 --- a/src/source/rtl_text_plugin.js +++ b/src/source/rtl_text_plugin.js @@ -54,10 +54,12 @@ export const setRTLTextPlugin = function(url: string, callback: ErrorCallback) { export const plugin: { applyArabicShaping: ?Function, processBidirectionalText: ?(string, Array) => Array, + processStyledBidirectionalText: ?(string, Array, Array) => Array<[string, Array]>, isLoaded: () => boolean } = { applyArabicShaping: null, processBidirectionalText: null, + processStyledBidirectionalText: null, isLoaded: function() { return foregroundLoadComplete || // Foreground: loaded if the completion callback returned successfully plugin.applyArabicShaping != null; // Background: loaded if the plugin functions have been compiled diff --git a/src/source/worker.js b/src/source/worker.js index 0d73371edc1..98fcb3a7419 100644 --- a/src/source/worker.js +++ b/src/source/worker.js @@ -55,12 +55,13 @@ export default class Worker { this.workerSourceTypes[name] = WorkerSource; }; - this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function}) => { + this.self.registerRTLTextPlugin = (rtlTextPlugin: {applyArabicShaping: Function, processBidirectionalText: Function, processStyledBidirectionalText?: Function}) => { if (globalRTLTextPlugin.isLoaded()) { throw new Error('RTL text plugin already registered.'); } globalRTLTextPlugin['applyArabicShaping'] = rtlTextPlugin.applyArabicShaping; globalRTLTextPlugin['processBidirectionalText'] = rtlTextPlugin.processBidirectionalText; + globalRTLTextPlugin['processStyledBidirectionalText'] = rtlTextPlugin.processStyledBidirectionalText; }; } @@ -197,4 +198,3 @@ if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) { new Worker(self); } - diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/formatted.js new file mode 100644 index 00000000000..b9f5238e51e --- /dev/null +++ b/src/style-spec/expression/definitions/formatted.js @@ -0,0 +1,145 @@ +// @flow + +import { NumberType, ValueType, FormattedType } from '../types'; + + +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 FormattedExpression implements Expression { + type: Type; + sections: Array; + + constructor(sections: Array) { + this.type = FormattedType; + this.sections = sections; + } + + static parse(args: Array, context: ParsingContext): ?Expression { + if (args.length < 3) { + return context.error(`Expected at least two arguments.`); + } + + if ((args.length - 1) % 2 !== 0) { + return context.error(`Expected an even number of arguments.`); + } + + const sections: Array = []; + for (let i = 1; i < args.length - 1; i += 2) { + const text = context.parse(args[i], 1, ValueType); + if (!text) return null; + const kind = text.type.kind; + if (kind !== 'string' && kind !== 'value' && kind !== 'null') + return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); + + const options = (args[i + 1]: any); + if (typeof options !== "object" || Array.isArray(options)) + return context.error(`Format options argument must be an object.`); + + let scale = null; + if (options['font-scale']) { + scale = context.parse(options['font-scale'], 1, NumberType); + if (!scale) return null; + } + + let font = null; + if (options['text-font']) { + font = context.parse(options['text-font'], 1, ValueType); // Require array of strings? + if (!font) return null; + } + sections.push({text, scale, font}); + } + + return new FormattedExpression(sections); + } + + evaluate(ctx: EvaluationContext) { + return new Formatted( + this.sections.map(section => + new FormattedSection( + section.text.evaluate(ctx) || "", + section.scale ? section.scale.evaluate(ctx) : null, + section.font ? section.font.evaluate(ctx).join(',') : null + ) + ) + ); + } + + eachChild(fn: (Expression) => void) { + for (const section of this.sections) { + fn(section.text); + if (section.scale) { + fn(section.scale); + } + if (section.font) { + fn(section.font); + } + } + } + + possibleOutputs() { + // Technically the combinatoric set of all children + // Usually, this.text will be undefined anyway + return [undefined]; + } + + serialize() { + const serialized = ["format"]; + for (const section of this.sections) { + serialized.push(section.text.serialize()); + const options = {}; + if (section.scale) { + options['font-scale'] = section.scale.serialize(); + } + if (section.font) { + options['text-font'] = section.font.serialize(); + } + serialized.push(options); + } + return serialized; + } +} diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index b0221082afc..c36001e97db 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -26,6 +26,7 @@ import { GreaterThanOrEqual } from './comparison'; import { CollatorExpression } from './collator'; +import { Formatted, FormattedExpression } from './formatted'; import Length from './length'; import type { Type } from '../types'; @@ -46,6 +47,7 @@ const expressions: ExpressionRegistry = { 'case': Case, 'coalesce': Coalesce, 'collator': CollatorExpression, + 'format': FormattedExpression, 'interpolate': Interpolate, 'length': Length, 'let': Let, @@ -117,7 +119,7 @@ CompoundExpression.register(expressions, { return ''; } else if (type === 'string' || type === 'number' || type === 'boolean') { return String(v); - } else if (v instanceof Color) { + } else if (v instanceof Color || v instanceof Formatted) { return v.toString(); } else { return JSON.stringify(v); diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index d1800400afc..d686aa6e036 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -2,6 +2,7 @@ import assert from 'assert'; import { isValue, typeOf, Color } from '../values'; +import { Formatted } from './formatted'; import type { Type } from '../types'; import type { Value } from '../values'; @@ -60,6 +61,9 @@ class Literal implements Expression { // couldn't actually generate with a "literal" expression, // so we have to implement an equivalent serialization here return ["rgba"].concat(this.value.toArray()); + } else if (this.value instanceof Formatted) { + // Same as Color + return this.value.serialize(); } else { assert(this.value === null || typeof this.value === 'string' || diff --git a/src/style-spec/expression/types.js b/src/style-spec/expression/types.js index 33324df1636..5a6cd4c2d8d 100644 --- a/src/style-spec/expression/types.js +++ b/src/style-spec/expression/types.js @@ -9,6 +9,7 @@ export type ObjectTypeT = { kind: 'object' }; export type ValueTypeT = { kind: 'value' }; export type ErrorTypeT = { kind: 'error' }; export type CollatorTypeT = { kind: 'collator' }; +export type FormattedTypeT = { kind: 'formatted' }; export type Type = NullTypeT | @@ -20,7 +21,8 @@ export type Type = ValueTypeT | ArrayType | // eslint-disable-line no-use-before-define ErrorTypeT | - CollatorTypeT + CollatorTypeT | + FormattedTypeT export type ArrayType = { kind: 'array', @@ -37,6 +39,7 @@ export const ObjectType = { kind: 'object' }; export const ValueType = { kind: 'value' }; export const ErrorType = { kind: 'error' }; export const CollatorType = { kind: 'collator' }; +export const FormattedType = { kind: 'formatted' }; export function array(itemType: Type, N: ?number): ArrayType { return { @@ -63,6 +66,7 @@ const valueMemberTypes = [ StringType, BooleanType, ColorType, + FormattedType, ObjectType, array(ValueType) ]; diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index fd4c24b768c..93750fe70d4 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -1505,10 +1505,10 @@ "property-type": "data-constant" }, "text-field": { - "type": "string", + "type": "formatted", "default": "", "tokens": true, - "doc": "Value to use for a text label.", + "doc": "Value to use for a text label. If a plain `string` is provided, it will be treated as a `formatted` with default/inherited formatting options.", "sdk-support": { "basic functionality": { "js": "0.10.0", @@ -2505,6 +2505,15 @@ } } }, + "format": { + "doc": "Returns `formatted` text containing annotations for use in mixed-format `text-field` entries. If set, the `text-font` argument overrides the font specified by the root layout properties. If set, the `font-scale` argument specifies a scaling factor relative to the `text-size` specified in the root layout properties.", + "group": "Types", + "sdk-support": { + "basic functionality": { + "js": "0.48.0" + } + } + }, "to-string": { "doc": "Converts the input value to a string. If the input is `null`, the result is `\"\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.", "group": "Types", @@ -3099,7 +3108,7 @@ } }, "concat": { - "doc": "Returns a string consisting of the concatenation of the inputs.", + "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.", "group": "String", "sdk-support": { "basic functionality": { diff --git a/src/style-spec/types.js b/src/style-spec/types.js index 143288020a2..2db91885160 100644 --- a/src/style-spec/types.js +++ b/src/style-spec/types.js @@ -4,6 +4,8 @@ export type ColorSpecification = string; +export type FormattedSpecification = string; + export type FilterSpecification = | ['has', string] | ['!has', string] @@ -225,7 +227,7 @@ export type SymbolLayerSpecification = {| "icon-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, "text-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, "text-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, - "text-field"?: DataDrivenPropertyValueSpecification, + "text-field"?: DataDrivenPropertyValueSpecification, "text-font"?: DataDrivenPropertyValueSpecification>, "text-size"?: DataDrivenPropertyValueSpecification, "text-max-width"?: DataDrivenPropertyValueSpecification, @@ -405,4 +407,3 @@ export type LayerSpecification = | RasterLayerSpecification | HillshadeLayerSpecification | BackgroundLayerSpecification; - diff --git a/src/style-spec/validate/validate.js b/src/style-spec/validate/validate.js index 231d9ff8686..5caa8d75800 100644 --- a/src/style-spec/validate/validate.js +++ b/src/style-spec/validate/validate.js @@ -18,6 +18,7 @@ import validateLayer from './validate_layer'; import validateSource from './validate_source'; import validateLight from './validate_light'; import validateString from './validate_string'; +import validateFormatted from './validate_formatted'; const VALIDATORS = { '*': function() { @@ -35,7 +36,8 @@ const VALIDATORS = { 'object': validateObject, 'source': validateSource, 'light': validateLight, - 'string': validateString + 'string': validateString, + 'formatted': validateFormatted }; @@ -64,8 +66,12 @@ export default function validate(options) { return VALIDATORS[valueSpec.type](options); } else { - return validateObject(extend({}, options, { + const valid = validateObject(extend({}, options, { valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec })); + if (!valid) { + console.log("not valid"); + } + return valid; } } diff --git a/src/style-spec/validate/validate_formatted.js b/src/style-spec/validate/validate_formatted.js new file mode 100644 index 00000000000..61ed402df3d --- /dev/null +++ b/src/style-spec/validate/validate_formatted.js @@ -0,0 +1,11 @@ +// @flow +import validateExpression from './validate_expression'; +import validateString from './validate_string'; + +export default function validateFormatted(options: any) { + if (validateString(options).length === 0) { + return []; + } + + return validateExpression(options); +} diff --git a/src/style/style_layer/background_style_layer_properties.js b/src/style/style_layer/background_style_layer_properties.js index 5b1cbacbefd..182cd821891 100644 --- a/src/style/style_layer/background_style_layer_properties.js +++ b/src/style/style_layer/background_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "background-color": DataConstantProperty, diff --git a/src/style/style_layer/circle_style_layer_properties.js b/src/style/style_layer/circle_style_layer_properties.js index 3e9176f6947..9321d797397 100644 --- a/src/style/style_layer/circle_style_layer_properties.js +++ b/src/style/style_layer/circle_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "circle-radius": DataDrivenProperty, 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 ac0966a83be..78a29cdcf18 100644 --- a/src/style/style_layer/fill_extrusion_style_layer_properties.js +++ b/src/style/style_layer/fill_extrusion_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "fill-extrusion-opacity": DataConstantProperty, diff --git a/src/style/style_layer/fill_style_layer_properties.js b/src/style/style_layer/fill_style_layer_properties.js index 9851f734508..a62dd756e1c 100644 --- a/src/style/style_layer/fill_style_layer_properties.js +++ b/src/style/style_layer/fill_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "fill-antialias": DataConstantProperty, diff --git a/src/style/style_layer/heatmap_style_layer_properties.js b/src/style/style_layer/heatmap_style_layer_properties.js index dd53a9ee9a0..d425156c001 100644 --- a/src/style/style_layer/heatmap_style_layer_properties.js +++ b/src/style/style_layer/heatmap_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "heatmap-radius": DataDrivenProperty, diff --git a/src/style/style_layer/hillshade_style_layer_properties.js b/src/style/style_layer/hillshade_style_layer_properties.js index f16f9b51fd6..3f2e5c2ecdd 100644 --- a/src/style/style_layer/hillshade_style_layer_properties.js +++ b/src/style/style_layer/hillshade_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "hillshade-illumination-direction": DataConstantProperty, diff --git a/src/style/style_layer/layer_properties.js.ejs b/src/style/style_layer/layer_properties.js.ejs index d9740285990..7bf3c2bf567 100644 --- a/src/style/style_layer/layer_properties.js.ejs +++ b/src/style/style_layer/layer_properties.js.ejs @@ -19,6 +19,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + <% if (layoutProperties.length) { -%> export type LayoutProps = {| <% for (const property of layoutProperties) { -%> diff --git a/src/style/style_layer/line_style_layer_properties.js b/src/style/style_layer/line_style_layer_properties.js index bef2f2ed44a..442d67d04b3 100644 --- a/src/style/style_layer/line_style_layer_properties.js +++ b/src/style/style_layer/line_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type LayoutProps = {| "line-cap": DataConstantProperty<"butt" | "round" | "square">, "line-join": DataDrivenProperty<"bevel" | "round" | "miter">, diff --git a/src/style/style_layer/raster_style_layer_properties.js b/src/style/style_layer/raster_style_layer_properties.js index e0193d510b2..4c0e28bb8f3 100644 --- a/src/style/style_layer/raster_style_layer_properties.js +++ b/src/style/style_layer/raster_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type PaintProps = {| "raster-opacity": DataConstantProperty, diff --git a/src/style/style_layer/symbol_style_layer_properties.js b/src/style/style_layer/symbol_style_layer_properties.js index ed22587f068..1d06c35d6bd 100644 --- a/src/style/style_layer/symbol_style_layer_properties.js +++ b/src/style/style_layer/symbol_style_layer_properties.js @@ -14,6 +14,8 @@ import { import type Color from '../../style-spec/util/color'; +import type {Formatted} from '../../style-spec/expression/definitions/formatted'; + export type LayoutProps = {| "symbol-placement": DataConstantProperty<"point" | "line" | "line-center">, "symbol-spacing": DataConstantProperty, @@ -34,7 +36,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 0c9c46a9cb4..f8785fdcbdf 100644 --- a/src/symbol/mergelines.js +++ b/src/symbol/mergelines.js @@ -1,6 +1,7 @@ // @flow import type {SymbolFeature} from '../data/bucket/symbol_bucket'; +import {Formatted} from '../style-spec/expression/definitions/formatted'; export default function (features: Array) { const leftIndex: {[string]: number} = {}; @@ -41,7 +42,7 @@ export default function (features: Array) { for (let k = 0; k < features.length; k++) { const feature = features[k]; const geom = feature.geometry; - const text = feature.text; + const text = feature.text instanceof Formatted ? feature.text.toString() : feature.text; if (!text) { add(k); diff --git a/src/symbol/quads.js b/src/symbol/quads.js index e0edec15abd..a5ae86fd15a 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -119,7 +119,7 @@ export function getGlyphQuads(anchor: Anchor, layer: SymbolStyleLayer, alongLine: boolean, feature: Feature, - positions: {[number]: GlyphPosition}): Array { + positions: {[string]: {[number]: GlyphPosition}}): Array { const oneEm = 24; const textRotate = layer.layout.get('text-rotate').evaluate(feature, {}) * Math.PI / 180; @@ -131,7 +131,8 @@ export function getGlyphQuads(anchor: Anchor, for (let k = 0; k < positionedGlyphs.length; k++) { const positionedGlyph = positionedGlyphs[k]; - const glyph = positions[positionedGlyph.glyph]; + const glyphPositions = positions[positionedGlyph.fontStack]; + const glyph = glyphPositions && glyphPositions[positionedGlyph.glyph]; if (!glyph) continue; const rect = glyph.rect; @@ -141,7 +142,7 @@ export function getGlyphQuads(anchor: Anchor, const glyphPadding = 1.0; const rectBuffer = GLYPH_PBF_BORDER + glyphPadding; - const halfAdvance = glyph.metrics.advance / 2; + const halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2; const glyphOffset = alongLine ? [positionedGlyph.x + halfAdvance, positionedGlyph.y] : @@ -151,11 +152,10 @@ export function getGlyphQuads(anchor: Anchor, [0, 0] : [positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1]]; - - const x1 = glyph.metrics.left - rectBuffer - halfAdvance + builtInOffset[0]; - const y1 = -glyph.metrics.top - rectBuffer + builtInOffset[1]; - const x2 = x1 + rect.w; - const y2 = y1 + rect.h; + const x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0]; + const y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1]; + const x2 = x1 + rect.w * positionedGlyph.scale; + const y2 = y1 + rect.h * positionedGlyph.scale; const tl = new Point(x1, y1); const tr = new Point(x2, y1); diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index d6667185749..30022a3a737 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -9,6 +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'; const WritingMode = { horizontal: 1, @@ -23,7 +24,9 @@ export type PositionedGlyph = { glyph: number, x: number, y: number, - vertical: boolean + vertical: boolean, + scale: number, + fontStack: string }; // A collection of positioned glyphs and some metadata @@ -39,22 +42,109 @@ export type Shaping = { type SymbolAnchor = 'center' | 'left' | 'right' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; type TextJustify = 'left' | 'center' | 'right'; -function breakLines(text: string, lineBreakPoints: Array) { +class TaggedString { + text: string; + sectionIndex: Array + sections: Array<{ scale: number, fontStack: string }> + + constructor() { + this.text = ""; + this.sectionIndex = []; + this.sections = []; + } + + static fromFeature(text: string | 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); + } + } + return result; + } + + length(): number { + return this.text.length; + } + + getSection(index: number): { scale: number, fontStack: string } { + return this.sections[this.sectionIndex[index]]; + } + + getCharCode(index: number): number { + return this.text.charCodeAt(index); + } + + verticalizePunctuation() { + this.text = verticalizePunctuation(this.text); + } + + trim() { + let beginningWhitespace = 0; + for (let i = 0; + i < this.text.length && whitespace[this.text.charCodeAt(i)]; + i++) { + beginningWhitespace++; + } + let trailingWhitespace = this.text.length; + for (let i = this.text.length - 1; + i >= 0 && i >= beginningWhitespace && whitespace[this.text.charCodeAt(i)]; + i--) { + trailingWhitespace--; + } + this.text = this.text.substring(beginningWhitespace, trailingWhitespace); + this.sectionIndex = this.sectionIndex.slice(beginningWhitespace, trailingWhitespace); + } + + substring(start: number, end: number): TaggedString { + const substring = new TaggedString(); + substring.text = this.text.substring(start, end); + substring.sectionIndex = this.sectionIndex.slice(start, end); + substring.sections = this.sections; + return substring; + } + + toString(): string { + return this.text; + } + + getMaxScale() { + return this.sectionIndex.reduce((max, index) => Math.max(max, this.sections[index].scale), 0); + } +} + +function breakLines(input: TaggedString, lineBreakPoints: Array): Array { const lines = []; + const text = input.text; let start = 0; for (const lineBreak of lineBreakPoints) { - lines.push(text.substring(start, lineBreak)); + lines.push(input.substring(start, lineBreak)); start = lineBreak; } if (start < text.length) { - lines.push(text.substring(start, text.length)); + lines.push(input.substring(start, text.length)); } return lines; } -function shapeText(text: string, - glyphs: {[number]: ?StyleGlyph}, +function shapeText(text: string | Formatted, + glyphs: {[string]: {[number]: ?StyleGlyph}}, + defaultFontStack: string, maxWidth: number, lineHeight: number, textAnchor: SymbolAnchor, @@ -63,9 +153,10 @@ function shapeText(text: string, translate: [number, number], verticalHeight: number, writingMode: 1 | 2): Shaping | false { - let logicalInput = text.trim(); + const logicalInput = TaggedString.fromFeature(text, defaultFontStack); + if (writingMode === WritingMode.vertical) { - logicalInput = verticalizePunctuation(logicalInput); + logicalInput.verticalizePunctuation(); } const positionedGlyphs = []; @@ -79,11 +170,39 @@ function shapeText(text: string, writingMode }; - let lines: Array; - - const {processBidirectionalText} = rtlTextPlugin; - if (processBidirectionalText) { - lines = processBidirectionalText(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)); + let lines: Array; + + const {processBidirectionalText, processStyledBidirectionalText} = rtlTextPlugin; + if (processBidirectionalText && logicalInput.sections.length === 1) { + // Bidi doesn't have to be style-aware + lines = []; + const untaggedLines = + processBidirectionalText(logicalInput.toString(), + determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)); + for (const line of untaggedLines) { + const taggedLine = new TaggedString(); + taggedLine.text = line; + taggedLine.sections = logicalInput.sections; + for (let i = 0; i < line.length; i++) { + taggedLine.sectionIndex.push(0); + } + lines.push(taggedLine); + } + } else if (processStyledBidirectionalText) { + // Need version of mapbox-gl-rtl-text with style support for combining RTL text + // with formatting + lines = []; + const processedLines = + processStyledBidirectionalText(logicalInput.text, + logicalInput.sectionIndex, + determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)); + for (const line of processedLines) { + const taggedLine = new TaggedString(); + taggedLine.text = line[0]; + taggedLine.sectionIndex = line[1]; + taggedLine.sections = logicalInput.sections; + lines.push(taggedLine); + } } else { lines = breakLines(logicalInput, determineLineBreaks(logicalInput, spacing, maxWidth, glyphs)); } @@ -93,6 +212,7 @@ function shapeText(text: string, if (!positionedGlyphs.length) return false; + shaping.text = shaping.text.toString(); return shaping; } @@ -125,17 +245,19 @@ const breakable: {[number]: boolean} = { // See https://github.com/mapbox/mapbox-gl-js/issues/3658 }; -function determineAverageLineWidth(logicalInput: string, +function determineAverageLineWidth(logicalInput: TaggedString, spacing: number, maxWidth: number, - glyphs: {[number]: ?StyleGlyph}) { + glyphMap: {[string]: {[number]: ?StyleGlyph}}) { let totalWidth = 0; - for (let index = 0; index < logicalInput.length; index++) { - const glyph = glyphs[logicalInput.charCodeAt(index)]; + for (let index = 0; index < logicalInput.length(); index++) { + const section = logicalInput.getSection(index); + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[logicalInput.getCharCode(index)]; if (!glyph) continue; - totalWidth += glyph.metrics.advance + spacing; + totalWidth += glyph.metrics.advance * section.scale + spacing; } const lineCount = Math.max(1, Math.ceil(totalWidth / maxWidth)); @@ -223,10 +345,10 @@ function leastBadBreaks(lastLineBreak: ?Break): Array { return leastBadBreaks(lastLineBreak.priorBreak).concat(lastLineBreak.index); } -function determineLineBreaks(logicalInput: string, +function determineLineBreaks(logicalInput: TaggedString, spacing: number, maxWidth: number, - glyphs: {[number]: ?StyleGlyph}): Array { + glyphMap: {[string]: {[number]: ?StyleGlyph}}): Array { if (!maxWidth) return []; @@ -234,20 +356,22 @@ function determineLineBreaks(logicalInput: string, return []; const potentialLineBreaks = []; - const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs); + const targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap); let currentX = 0; - for (let i = 0; i < logicalInput.length; i++) { - const codePoint = logicalInput.charCodeAt(i); - const glyph = glyphs[codePoint]; + for (let i = 0; i < logicalInput.length(); i++) { + const section = logicalInput.getSection(i); + const codePoint = logicalInput.getCharCode(i); + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; if (glyph && !whitespace[codePoint]) - currentX += glyph.metrics.advance + spacing; + currentX += glyph.metrics.advance * section.scale + spacing; // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. - if ((i < logicalInput.length - 1) && + if ((i < logicalInput.length() - 1) && (breakable[codePoint] || charAllowsIdeographicBreaking(codePoint))) { @@ -257,14 +381,14 @@ function determineLineBreaks(logicalInput: string, currentX, targetWidth, potentialLineBreaks, - calculatePenalty(codePoint, logicalInput.charCodeAt(i + 1)), + calculatePenalty(codePoint, logicalInput.getCharCode(i + 1)), false)); } } return leastBadBreaks( evaluateBreak( - logicalInput.length, + logicalInput.length(), currentX, targetWidth, potentialLineBreaks, @@ -305,8 +429,8 @@ function getAnchorAlignment(anchor: SymbolAnchor) { } function shapeLines(shaping: Shaping, - glyphs: {[number]: ?StyleGlyph}, - lines: Array, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, + lines: Array, lineHeight: number, textAnchor: SymbolAnchor, textJustify: TextJustify, @@ -326,27 +450,35 @@ function shapeLines(shaping: Shaping, textJustify === 'right' ? 1 : textJustify === 'left' ? 0 : 0.5; - for (let line of lines) { - line = line.trim(); + for (const line of lines) { + line.trim(); + + const lineMaxScale = line.getMaxScale(); - if (!line.length) { + if (!line.length()) { y += lineHeight; // Still need a line feed after empty line continue; } const lineStartIndex = positionedGlyphs.length; - for (let i = 0; i < line.length; i++) { - const codePoint = line.charCodeAt(i); - const glyph = glyphs[codePoint]; + for (let i = 0; i < line.length(); i++) { + const section = line.getSection(i); + const codePoint = line.getCharCode(i); + // We don't know the baseline, but since we're laying out + // at 24 points, we can calculate how much it will move when + // we scale up or down. + const baselineOffset = (lineMaxScale - section.scale) * 24; + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; if (!glyph) continue; if (!charHasUprightVerticalOrientation(codePoint) || writingMode === WritingMode.horizontal) { - positionedGlyphs.push({glyph: codePoint, x, y, vertical: false}); - x += glyph.metrics.advance + spacing; + positionedGlyphs.push({glyph: codePoint, x, y: y + baselineOffset, vertical: false, scale: section.scale, fontStack: section.fontStack}); + x += glyph.metrics.advance * section.scale + spacing; } else { - positionedGlyphs.push({glyph: codePoint, x, y: 0, vertical: true}); - x += verticalHeight + spacing; + positionedGlyphs.push({glyph: codePoint, x, y: baselineOffset, vertical: true, scale: section.scale, fontStack: section.fontStack}); + x += verticalHeight * section.scale + spacing; } } @@ -355,18 +487,18 @@ function shapeLines(shaping: Shaping, const lineLength = x - spacing; maxLineLength = Math.max(lineLength, maxLineLength); - justifyLine(positionedGlyphs, glyphs, lineStartIndex, positionedGlyphs.length - 1, justify); + justifyLine(positionedGlyphs, glyphMap, lineStartIndex, positionedGlyphs.length - 1, justify); } x = 0; - y += lineHeight; + y += lineHeight * lineMaxScale; } const {horizontalAlign, verticalAlign} = getAnchorAlignment(textAnchor); align(positionedGlyphs, justify, horizontalAlign, verticalAlign, maxLineLength, lineHeight, lines.length); // Calculate the bounding box - const height = lines.length * lineHeight; + const height = y - yOffset; shaping.top += -verticalAlign * height; shaping.bottom = shaping.top + height; @@ -376,16 +508,18 @@ function shapeLines(shaping: Shaping, // justify right = 1, left = 0, center = 0.5 function justifyLine(positionedGlyphs: Array, - glyphs: {[number]: ?StyleGlyph}, + glyphMap: {[string]: {[number]: ?StyleGlyph}}, start: number, end: number, justify: 1 | 0 | 0.5) { if (!justify) return; - const glyph = glyphs[positionedGlyphs[end].glyph]; + const lastPositionedGlyph = positionedGlyphs[end]; + const positions = glyphMap[lastPositionedGlyph.fontStack]; + const glyph = positions && positions[lastPositionedGlyph.glyph]; if (glyph) { - const lastAdvance = glyph.metrics.advance; + const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale; const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; for (let j = start; j <= end; j++) { diff --git a/src/symbol/symbol_layout.js b/src/symbol/symbol_layout.js index 730972c0c9f..4f07e5fd793 100644 --- a/src/symbol/symbol_layout.js +++ b/src/symbol/symbol_layout.js @@ -18,6 +18,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 type {Shaping, PositionedIcon} from './shaping'; import type {CollisionBoxArray} from '../data/array_types'; @@ -100,24 +101,24 @@ export function performSymbolLayout(bucket: SymbolBucket, for (const feature of bucket.features) { const fontstack = layout.get('text-font').evaluate(feature, {}).join(','); - const glyphs = glyphMap[fontstack] || {}; - const glyphPositionMap = glyphPositions[fontstack] || {}; + const glyphPositionMap = glyphPositions; const shapedTextOrientations = {}; const text = feature.text; if (text) { + const unformattedText = text instanceof Formatted ? text.toString() : text; 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(text) ? spacing : 0; + const spacingIfAllowed = allowsLetterSpacing(unformattedText) ? spacing : 0; const textAnchor = layout.get('text-anchor').evaluate(feature, {}); const textJustify = layout.get('text-justify').evaluate(feature, {}); const maxWidth = layout.get('symbol-placement') === 'point' ? layout.get('text-max-width').evaluate(feature, {}) * oneEm : 0; - shapedTextOrientations.horizontal = shapeText(text, glyphs, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.horizontal); - if (allowsVerticalWritingMode(text) && textAlongLine && keepUpright) { - shapedTextOrientations.vertical = shapeText(text, glyphs, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.vertical); + shapedTextOrientations.horizontal = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.horizontal); + if (allowsVerticalWritingMode(unformattedText) && textAlongLine && keepUpright) { + shapedTextOrientations.vertical = shapeText(text, glyphMap, fontstack, maxWidth, lineHeight, textAnchor, textJustify, spacingIfAllowed, textOffset, oneEm, WritingMode.vertical); } } @@ -164,7 +165,7 @@ function addFeature(bucket: SymbolBucket, feature: SymbolFeature, shapedTextOrientations: any, shapedIcon: PositionedIcon | void, - glyphPositionMap: {[number]: GlyphPosition}, + glyphPositionMap: {[string]: {[number]: GlyphPosition}}, sizes: Sizes) { const layoutTextSize = sizes.layoutTextSize.evaluate(feature, {}); const layoutIconSize = sizes.layoutIconSize.evaluate(feature, {}); @@ -278,7 +279,7 @@ function addTextVertices(bucket: SymbolBucket, lineArray: {lineStartIndex: number, lineLength: number}, writingMode: number, placedTextSymbolIndices: Array, - glyphPositionMap: {[number]: GlyphPosition}, + glyphPositionMap: {[string]: {[number]: GlyphPosition}}, sizes: Sizes) { const glyphQuads = getGlyphQuads(anchor, shapedText, layer, textAlongLine, feature, glyphPositionMap); @@ -341,7 +342,7 @@ function addSymbol(bucket: SymbolBucket, iconAlongLine: boolean, iconOffset: [number, number], feature: SymbolFeature, - glyphPositionMap: {[number]: GlyphPosition}, + glyphPositionMap: {[string]: {[number]: GlyphPosition}}, sizes: Sizes) { const lineArray = bucket.addToLineVertexArray(anchor, line); diff --git a/src/symbol/transform_text.js b/src/symbol/transform_text.js index 39f80c6e5e6..45040543618 100644 --- a/src/symbol/transform_text.js +++ b/src/symbol/transform_text.js @@ -4,8 +4,9 @@ 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'; -export default function(text: string, layer: SymbolStyleLayer, feature: Feature) { +function transformText(text: string, layer: SymbolStyleLayer, feature: Feature) { const transform = layer.layout.get('text-transform').evaluate(feature, {}); if (transform === 'uppercase') { text = text.toLocaleUpperCase(); @@ -19,3 +20,16 @@ export default function(text: string, layer: SymbolStyleLayer, feature: Feature) return text; } + + +export default function(text: string | Formatted, layer: SymbolStyleLayer, feature: Feature) { + if (text instanceof Formatted) { + // OK to transform in place? + text.sections.forEach(section => { + section.text = transformText(section.text, layer, feature); + }); + return text; + } else { + return transformText(text, layer, feature); + } +} diff --git a/test/expected/text-shaping-default.json b/test/expected/text-shaping-default.json index 34324492e0f..4ba26796ccf 100644 --- a/test/expected/text-shaping-default.json +++ b/test/expected/text-shaping-default.json @@ -4,31 +4,41 @@ "glyph": 97, "x": -32.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "abcde", @@ -37,4 +47,4 @@ "left": -32.5, "right": 32.5, "writingMode": 1 -} \ No newline at end of file +} diff --git a/test/expected/text-shaping-linebreak.json b/test/expected/text-shaping-linebreak.json index 1948fcecab1..2be83dac03a 100644 --- a/test/expected/text-shaping-linebreak.json +++ b/test/expected/text-shaping-linebreak.json @@ -4,61 +4,81 @@ "glyph": 97, "x": -32.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 97, "x": -32.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "abcde abcde", diff --git a/test/expected/text-shaping-newline.json b/test/expected/text-shaping-newline.json index 583c5018b1f..977e3a5f42e 100644 --- a/test/expected/text-shaping-newline.json +++ b/test/expected/text-shaping-newline.json @@ -4,61 +4,81 @@ "glyph": 97, "x": -32.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -29, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 97, "x": -32.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -5, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "abcde\nabcde", diff --git a/test/expected/text-shaping-newlines-in-middle.json b/test/expected/text-shaping-newlines-in-middle.json index a11952a84a6..8a22ba5c720 100644 --- a/test/expected/text-shaping-newlines-in-middle.json +++ b/test/expected/text-shaping-newlines-in-middle.json @@ -4,61 +4,81 @@ "glyph": 97, "x": -32.5, "y": -41, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": -41, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -41, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": -41, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": -41, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 97, "x": -32.5, "y": 7, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -19.5, "y": 7, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": 7, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 5.5, "y": 7, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 19.5, "y": 7, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "abcde\n\nabcde", diff --git a/test/expected/text-shaping-null.json b/test/expected/text-shaping-null.json index 65c9cb911aa..242ef1c7a9d 100644 --- a/test/expected/text-shaping-null.json +++ b/test/expected/text-shaping-null.json @@ -4,13 +4,17 @@ "glyph": 104, "x": -10, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 105, "x": 4, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "hi\u0000", @@ -19,4 +23,4 @@ "left": -10, "right": 10, "writingMode": 1 -} \ No newline at end of file +} diff --git a/test/expected/text-shaping-spacing.json b/test/expected/text-shaping-spacing.json index b9b712bf563..5e8872e0f6f 100644 --- a/test/expected/text-shaping-spacing.json +++ b/test/expected/text-shaping-spacing.json @@ -4,31 +4,41 @@ "glyph": 97, "x": -38.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 98, "x": -22.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 99, "x": -5.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 100, "x": 8.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" }, { "glyph": 101, "x": 25.5, "y": -17, - "vertical": false + "vertical": false, + "scale": 1, + "fontStack": "Test" } ], "text": "abcde", @@ -37,4 +47,4 @@ "left": -38.5, "right": 38.5, "writingMode": 1 -} \ No newline at end of file +} diff --git a/test/integration/expression-tests/formatted/basic/test.json b/test/integration/expression-tests/formatted/basic/test.json new file mode 100644 index 00000000000..eef30eb57f4 --- /dev/null +++ b/test/integration/expression-tests/formatted/basic/test.json @@ -0,0 +1,80 @@ +{ + "expression": [ + "format", + "a", + {}, + "b", + { + "font-scale": 2 + }, + "c", + { + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } + ], + "inputs": [ + [ + {}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "formatted" + }, + "outputs": [ + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + }, + { + "text": "b", + "scale": 2, + "fontStack": null + }, + { + "text": "c", + "scale": null, + "fontStack": "a,b" + } + ] + } + ], + "serialized": [ + "format", + "a", + { + "font-scale": null, + "text-font": null + }, + "b", + { + "font-scale": 2, + "text-font": null + }, + "c", + { + "font-scale": null, + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } + ] + } +} diff --git a/test/integration/expression-tests/formatted/to-string/test.json b/test/integration/expression-tests/formatted/to-string/test.json new file mode 100644 index 00000000000..2c505712fbd --- /dev/null +++ b/test/integration/expression-tests/formatted/to-string/test.json @@ -0,0 +1,42 @@ +{ + "expression": [ + "to-string", + [ + "format", + "a", + {}, + "b", + { + "font-scale": 2 + }, + "c", + { + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } + ] + ], + "inputs": [ + [ + {}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "string" + }, + "outputs": [ + "abc" + ], + "serialized": "abc" + } +} diff --git a/test/integration/render-tests/text-field/formatted-line/expected.png b/test/integration/render-tests/text-field/formatted-line/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a5c6a684a0854f0216264415251fbbedd0ed865a GIT binary patch literal 30757 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 G=lH*nY$4D9 literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/text-field/formatted-line/style.json b/test/integration/render-tests/text-field/formatted-line/style.json new file mode 100644 index 00000000000..cd8e99a5f1e --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-line/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "literal", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "text-allow-overlap": true, + "text-ignore-placement": true, + "text-field": ["format", + ["get", "name"], { "font-scale": 1.2 }, + " - ", {}, + ["get", "class"], { "font-scale": 0.8, "text-font": ["literal", [ "NotoCJK" ]] } + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 10 + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted/expected.png b/test/integration/render-tests/text-field/formatted/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..08f856cb509f84717eb04e0d68dde1fafcd0902d GIT binary patch literal 4867 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 Xr#bW*!Mx^IOY88#*ofXR%;o { const oneEm = 24; - const name = 'Test'; - const stacks = { + const fontStack = 'Test'; + const glyphs = { 'Test': JSON.parse(fs.readFileSync(path.join(__dirname, '/../../fixtures/fontstack-glyphs.json'))) }; - const glyphs = stacks[name]; let shaped; JSON.parse('{}'); - shaped = shaping.shapeText(`hi${String.fromCharCode(0)}`, glyphs, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(`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, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 15 * oneEm, oneEm, 'center', 'center', 0.125 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 4 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 15 * oneEm, oneEm, 'center', 'center', 0, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('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, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText('', 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, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + shaped = shaping.shapeText(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, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); - const shaped2 = shaping.shapeText('foo bar', glyphs, 15 * oneEm, oneEm, 'center', 'center', 0 * oneEm, [0, 0], oneEm, WritingMode.horizontal); + 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); t.same(shaped.positionedGlyphs, shaped2.positionedGlyphs); t.end(); From c88b4dd8b44bde3eccb3608f246ac28fb332d9cd Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Wed, 18 Jul 2018 10:55:54 -0700 Subject: [PATCH 2/5] Support implicit argument coercions for compound expressions with multiple overloads. Support string->formatted coercion (currently unused). --- .../expression/compound_expression.js | 53 ++++++++++++------- .../expression/definitions/coercion.js | 10 ++++ src/style-spec/expression/parsing_context.js | 4 ++ 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/style-spec/expression/compound_expression.js b/src/style-spec/expression/compound_expression.js index e5417f1c6bf..d9dd2b7223d 100644 --- a/src/style-spec/expression/compound_expression.js +++ b/src/style-spec/expression/compound_expression.js @@ -67,22 +67,6 @@ class CompoundExpression implements Expression { signature.length === args.length - 1 // correct param count )); - // First parse all the args - const parsedArgs: Array = []; - for (let i = 1; i < args.length; i++) { - const arg = args[i]; - let expected; - if (overloads.length === 1) { - const params = overloads[0][0]; - expected = Array.isArray(params) ? - params[i - 1] : - params.type; - } - const parsed = context.parse(arg, 1 + parsedArgs.length, expected); - if (!parsed) return null; - parsedArgs.push(parsed); - } - let signatureContext: ParsingContext = (null: any); for (const [params, evaluate] of overloads) { @@ -90,6 +74,29 @@ class CompoundExpression implements Expression { // we eventually succeed, we haven't polluted `context.errors`. signatureContext = new ParsingContext(context.registry, context.path, null, context.scope); + // First parse all the args, potentially coercing to the + // types expected by this overload. + const parsedArgs: Array = []; + let argParseFailed = false; + for (let i = 1; i < args.length; i++) { + const arg = args[i]; + const expectedType = Array.isArray(params) ? + params[i - 1] : + params.type; + + const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType); + if (!parsed) { + argParseFailed = true; + break; + } + parsedArgs.push(parsed); + } + if (argParseFailed) { + // Couldn't coerce args of this overload to expected type, move + // on to next one. + continue; + } + if (Array.isArray(params)) { if (params.length !== parsedArgs.length) { signatureContext.error(`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`); @@ -117,10 +124,16 @@ class CompoundExpression implements Expression { const signatures = expected .map(([params]) => stringifySignature(params)) .join(' | '); - const actualTypes = parsedArgs - .map(arg => toString(arg.type)) - .join(', '); - context.error(`Expected arguments of type ${signatures}, but found (${actualTypes}) instead.`); + + const actualTypes = []; + // For error message, re-parse arguments without trying to + // apply any coercions + for (let i = 1; i < args.length; i++) { + const parsed = context.parse(args[i], 1 + actualTypes.length); + if (!parsed) return null; + actualTypes.push(toString(parsed.type)); + } + context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`); } return null; diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index ba50f63aee4..59fcbef4e2b 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -10,6 +10,7 @@ import type { Expression } from '../expression'; import type ParsingContext from '../parsing_context'; import type EvaluationContext from '../evaluation_context'; import type { Type } from '../types'; +import { Formatted, FormattedSection } from './formatted'; const types = { 'to-number': NumberType, @@ -73,6 +74,15 @@ 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 { let value = null; for (const arg of this.args) { diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 1ce14898323..5e4d1a51e21 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -113,6 +113,10 @@ class ParsingContext { 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)) { return null; } From c742e35f5d6e8097723076b9cd869702ffdfbffc Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Fri, 27 Jul 2018 13:44:11 -0700 Subject: [PATCH 3/5] Update to mapbox-gl-rtl-text 0.2.0 for formatted text support. Add test using new formatted RTL functionality. --- debug/csp.html | 2 +- docs/pages/example/mapbox-gl-rtl-text.html | 2 +- package.json | 2 +- src/index.js | 2 +- .../text-field/formatted-arabic/expected.png | Bin 0 -> 5873 bytes .../text-field/formatted-arabic/style.json | 61 ++++++++++++++++++ test/suite_implementation.js | 1 + yarn.lock | 6 +- 8 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 test/integration/render-tests/text-field/formatted-arabic/expected.png create mode 100644 test/integration/render-tests/text-field/formatted-arabic/style.json diff --git a/debug/csp.html b/debug/csp.html index 6f13f833396..3cb47672d54 100644 --- a/debug/csp.html +++ b/debug/csp.html @@ -27,7 +27,7 @@ hash: true }); -mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.2/mapbox-gl-rtl-text.js'); +mapboxgl.setRTLTextPlugin('https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.0/mapbox-gl-rtl-text.js'); diff --git a/docs/pages/example/mapbox-gl-rtl-text.html b/docs/pages/example/mapbox-gl-rtl-text.html index 8e99ed3afe5..7e9a18dd222 100644 --- a/docs/pages/example/mapbox-gl-rtl-text.html +++ b/docs/pages/example/mapbox-gl-rtl-text.html @@ -1,7 +1,7 @@