diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index 40928cf52b7..2c217674b0f 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -134,6 +134,13 @@ const types = { collator: [{ type: 'collator', parameters: [ '{ "case-sensitive": boolean, "diacritic-sensitive": boolean, "locale": string }' ] + }], + formatted: [{ + type: 'formatted', + parameters: [ + 'string', + '{ "font-scale": number, "text-font": array }' + ] }] }; diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/formatted.js index 0b8dd9d242a..881aa35309a 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/formatted.js @@ -95,7 +95,8 @@ export class FormattedExpression implements Expression { } possibleOutputs() { - // TODO: think about right answer here + // Technically the combinatoric set of all children + // Usually, this.text will be undefined anyway return [undefined]; } diff --git a/src/style-spec/expression/parsing_context.js b/src/style-spec/expression/parsing_context.js index 44a425f136d..5e4d1a51e21 100644 --- a/src/style-spec/expression/parsing_context.js +++ b/src/style-spec/expression/parsing_context.js @@ -114,7 +114,6 @@ class ParsingContext { parsed = new Coercion(expected, [parsed]); } } else if (expected.kind === 'formatted' && (actual.kind === 'value' || actual.kind === 'string')) { - // TODO: this isn't working yet if (!options.omitTypeAnnotations) { parsed = new Coercion(expected, [parsed]); } diff --git a/src/symbol/quads.js b/src/symbol/quads.js index 90b1353d29d..a5ae86fd15a 100644 --- a/src/symbol/quads.js +++ b/src/symbol/quads.js @@ -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.fontStack][positionedGlyph.glyph]; + const glyphPositions = positions[positionedGlyph.fontStack]; + const glyph = glyphPositions && glyphPositions[positionedGlyph.glyph]; if (!glyph) continue; const rect = glyph.rect; diff --git a/src/symbol/shaping.js b/src/symbol/shaping.js index 6c8b61acd90..30022a3a737 100644 --- a/src/symbol/shaping.js +++ b/src/symbol/shaping.js @@ -26,7 +26,7 @@ export type PositionedGlyph = { y: number, vertical: boolean, scale: number, - fontStack: string // TODO: either don't store per glyph or reduce to a number? + fontStack: string }; // A collection of positioned glyphs and some metadata @@ -53,14 +53,14 @@ class TaggedString { this.sections = []; } - static fromFeature(text: string | Formatted, rootFontStack: string) { + 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 || rootFontStack + fontStack: section.fontStack || defaultFontStack }); result.text += section.text; for (let j = 0; j < section.text.length; j++) { @@ -69,7 +69,7 @@ class TaggedString { } } else { result.text = text; - result.sections.push({ scale: 1, fontStack: rootFontStack }); + result.sections.push({ scale: 1, fontStack: defaultFontStack }); for (let i = 0; i < text.length; i++) { result.sectionIndex.push(0); } @@ -144,7 +144,7 @@ function breakLines(input: TaggedString, lineBreakPoints: Array): Array< function shapeText(text: string | Formatted, glyphs: {[string]: {[number]: ?StyleGlyph}}, - rootFontStack: string, + defaultFontStack: string, maxWidth: number, lineHeight: number, textAnchor: SymbolAnchor, @@ -153,7 +153,7 @@ function shapeText(text: string | Formatted, translate: [number, number], verticalHeight: number, writingMode: 1 | 2): Shaping | false { - const logicalInput = TaggedString.fromFeature(text, rootFontStack); + const logicalInput = TaggedString.fromFeature(text, defaultFontStack); if (writingMode === WritingMode.vertical) { logicalInput.verticalizePunctuation(); @@ -212,6 +212,7 @@ function shapeText(text: string | Formatted, if (!positionedGlyphs.length) return false; + shaping.text = shaping.text.toString(); return shaping; } @@ -252,7 +253,8 @@ function determineAverageLineWidth(logicalInput: TaggedString, for (let index = 0; index < logicalInput.length(); index++) { const section = logicalInput.getSection(index); - const glyph = glyphMap[section.fontStack][logicalInput.getCharCode(index)]; + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[logicalInput.getCharCode(index)]; if (!glyph) continue; totalWidth += glyph.metrics.advance * section.scale + spacing; @@ -361,7 +363,8 @@ function determineLineBreaks(logicalInput: TaggedString, for (let i = 0; i < logicalInput.length(); i++) { const section = logicalInput.getSection(i); const codePoint = logicalInput.getCharCode(i); - const glyph = glyphMap[section.fontStack][codePoint]; + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; if (glyph && !whitespace[codePoint]) currentX += glyph.metrics.advance * section.scale + spacing; @@ -465,7 +468,8 @@ function shapeLines(shaping: Shaping, // at 24 points, we can calculate how much it will move when // we scale up or down. const baselineOffset = (lineMaxScale - section.scale) * 24; - const glyph = glyphMap[section.fontStack][codePoint]; + const positions = glyphMap[section.fontStack]; + const glyph = positions && positions[codePoint]; if (!glyph) continue; @@ -512,7 +516,8 @@ function justifyLine(positionedGlyphs: Array, return; const lastPositionedGlyph = positionedGlyphs[end]; - const glyph = glyphMap[lastPositionedGlyph.fontStack][lastPositionedGlyph.glyph]; + const positions = glyphMap[lastPositionedGlyph.fontStack]; + const glyph = positions && positions[lastPositionedGlyph.glyph]; if (glyph) { const lastAdvance = glyph.metrics.advance * lastPositionedGlyph.scale; const lineIndent = (positionedGlyphs[end].x + lastAdvance) * justify; 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/unit/symbol/shaping.test.js b/test/unit/symbol/shaping.test.js index 23f59fbc9ce..69183efa35c 100644 --- a/test/unit/symbol/shaping.test.js +++ b/test/unit/symbol/shaping.test.js @@ -11,60 +11,59 @@ if (typeof process !== 'undefined' && process.env !== undefined) { test('shaping', (t) => { 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();