Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-coerce top-level results and concat arguments to strings #7280

Merged
merged 8 commits into from Sep 18, 2018
2 changes: 1 addition & 1 deletion build/generate-style-code.js
Expand Up @@ -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(', ')}]`;
Expand Down
14 changes: 11 additions & 3 deletions docs/components/expression-metadata.js
Expand Up @@ -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']
Expand Down
33 changes: 16 additions & 17 deletions src/data/bucket/symbol_bucket.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,7 +56,7 @@ export type CollisionArrays = {
};

export type SymbolFeature = {|
text: string | Formatted | void,
text: Formatted | void,
icon: string | void,
index: number,
sourceLayerIndex: number,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

}
}

Expand Down
2 changes: 1 addition & 1 deletion src/style-spec/expression/definitions/coalesce.js
Expand Up @@ -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);
Expand Down
38 changes: 23 additions & 15 deletions src/style-spec/expression/definitions/coercion.js
Expand Up @@ -2,20 +2,23 @@

import assert from 'assert';

import { ColorType, ValueType, NumberType } from '../types';
import { Color, validateRGBA } from '../values';
import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types';
import {Color, toString as valueToString, validateRGBA} from '../values';
import RuntimeError from '../runtime_error';
import Formatted from '../types/formatted';
import FormatExpression from '../definitions/format';

import type { Expression } from '../expression';
import type ParsingContext from '../parsing_context';
import type EvaluationContext from '../evaluation_context';
import type { Value } from '../values';
import type { Type } from '../types';
import { Formatted, FormattedSection } from './formatted';

const types = {
'to-boolean': BooleanType,
'to-color': ColorType,
'to-number': NumberType,
'to-color': ColorType
'to-string': StringType
};

/**
Expand All @@ -41,6 +44,9 @@ class Coercion implements Expression {
const name: string = (args[0]: any);
assert(types[name], name);

if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2)
return context.error(`Expected one argument.`);

const type = types[name];

const parsed = [];
Expand All @@ -54,7 +60,9 @@ class Coercion implements Expression {
}

evaluate(ctx: EvaluationContext) {
if (this.type.kind === 'color') {
if (this.type.kind === 'boolean') {
return Boolean(this.args[0].evaluate(ctx));
} else if (this.type.kind === 'color') {
let input;
let error;
for (const arg of this.args) {
Expand All @@ -77,16 +85,7 @@ class Coercion implements Expression {
}
}
throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
} else if (this.type.kind === 'formatted') {
let input;
for (const arg of this.args) {
input = arg.evaluate(ctx);
if (typeof input === 'string') {
return new Formatted([new FormattedSection(input, null, null)]);
}
}
throw new RuntimeError(`Could not parse formatted text from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
} else {
} else if (this.type.kind === 'number') {
let value = null;
for (const arg of this.args) {
value = arg.evaluate(ctx);
Expand All @@ -96,6 +95,12 @@ class Coercion implements Expression {
return num;
}
throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`);
} else if (this.type.kind === 'formatted') {
// There is no explicit 'to-formatted' but this coercion can be implicitly
// created by properties that expect the 'formatted' type.
return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
} else {
return valueToString(this.args[0].evaluate(ctx));
}
}

Expand All @@ -108,6 +113,9 @@ class Coercion implements Expression {
}

serialize() {
if (this.type.kind === 'formatted') {
return new FormatExpression([{text: this.args[0], scale: null, font: null}]).serialize();
}
const serialized = [`to-${this.type.kind}`];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
Expand Down
63 changes: 2 additions & 61 deletions 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<Intl$Collator>
}

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;
Expand Down
@@ -1,56 +1,21 @@
// @flow

import { NumberType, ValueType, FormattedType, array, StringType } from '../types';

import Formatted, { FormattedSection } from '../types/formatted';
import { toString } from '../values';

import type { Expression } from '../expression';
import type EvaluationContext from '../evaluation_context';
import type ParsingContext from '../parsing_context';
import type { Type } from '../types';

export class FormattedSection {
text: string
scale: number | null
fontStack: string | null

constructor(text: string, scale: number | null, fontStack: string | null) {
this.text = text;
this.scale = scale;
this.fontStack = fontStack;
}
}

export class Formatted {
sections: Array<FormattedSection>

constructor(sections: Array<FormattedSection>) {
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<FormattedSectionExpression>;

Expand Down Expand Up @@ -101,7 +66,7 @@ export class FormatExpression implements Expression {
return new Formatted(
this.sections.map(section =>
new FormattedSection(
section.text.evaluate(ctx) || "",
toString(section.text.evaluate(ctx)),
section.scale ? section.scale.evaluate(ctx) : null,
section.font ? section.font.evaluate(ctx).join(',') : null
)
Expand Down