Skip to content

Commit b01a41f

Browse files
Add vertical-align in format expression (#900)
* Add vertical-align to v8.json * Add vertical-align to format definition * Add static value validation * Add support for image * update vertical-align description * add changelog * add image section `vertical-align` test * change `baseline` to `bottom` * add `vertical-align` to sdk support table * add links to maplibre native issue * review fixes * Define `vertical-align` as set of possible values instead of `string` * simplify vertical-align options description * update example * link design proposal for more details * add images to docs
1 parent 18a8ddc commit b01a41f

File tree

13 files changed

+183
-30
lines changed

13 files changed

+183
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## main
22

33
### ✨ Features and improvements
4+
- Add `vertical-align` option to `format` expression ([#832](https://github.com/maplibre/maplibre-style-spec/issues/832))
45
- _...Add new stuff here..._
56

67
### 🐞 Bug fixes

src/expression/definitions/format.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ColorType,
88
ResolvedImageType,
99
} from '../types';
10-
import {Formatted, FormattedSection} from '../types/formatted';
10+
import {Formatted, FormattedSection, VERTICAL_ALIGN_OPTIONS, VerticalAlign} from '../types/formatted';
1111
import {valueToString, typeOf} from '../values';
1212

1313
import type {Expression} from '../expression';
@@ -22,6 +22,7 @@ type FormattedSectionExpression = {
2222
scale: Expression | null;
2323
font: Expression | null;
2424
textColor: Expression | null;
25+
verticalAlign: Expression | null;
2526
};
2627

2728
export class FormatExpression implements Expression {
@@ -69,10 +70,21 @@ export class FormatExpression implements Expression {
6970
if (!textColor) return null;
7071
}
7172

73+
let verticalAlign = null;
74+
if (arg['vertical-align']) {
75+
if (typeof arg['vertical-align'] === 'string' && !VERTICAL_ALIGN_OPTIONS.includes(arg['vertical-align'] as VerticalAlign)) {
76+
return context.error(`'vertical-align' must be one of: 'bottom', 'center', 'top' but found '${arg['vertical-align']}' instead.`) as null;
77+
}
78+
79+
verticalAlign = context.parse(arg['vertical-align'], 1, StringType);
80+
if (!verticalAlign) return null;
81+
}
82+
7283
const lastExpression = sections[sections.length - 1];
7384
lastExpression.scale = scale;
7485
lastExpression.font = font;
7586
lastExpression.textColor = textColor;
87+
lastExpression.verticalAlign = verticalAlign;
7688
} else {
7789
const content = context.parse(args[i], 1, ValueType);
7890
if (!content) return null;
@@ -82,7 +94,7 @@ export class FormatExpression implements Expression {
8294
return context.error('Formatted text type must be \'string\', \'value\', \'image\' or \'null\'.') as null;
8395

8496
nextTokenMayBeObject = true;
85-
sections.push({content, scale: null, font: null, textColor: null});
97+
sections.push({content, scale: null, font: null, textColor: null, verticalAlign: null});
8698
}
8799
}
88100

@@ -93,15 +105,23 @@ export class FormatExpression implements Expression {
93105
const evaluateSection = section => {
94106
const evaluatedContent = section.content.evaluate(ctx);
95107
if (typeOf(evaluatedContent) === ResolvedImageType) {
96-
return new FormattedSection('', evaluatedContent, null, null, null);
108+
return new FormattedSection(
109+
'',
110+
evaluatedContent,
111+
null,
112+
null,
113+
null,
114+
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
115+
);
97116
}
98117

99118
return new FormattedSection(
100119
valueToString(evaluatedContent),
101120
null,
102121
section.scale ? section.scale.evaluate(ctx) : null,
103122
section.font ? section.font.evaluate(ctx).join(',') : null,
104-
section.textColor ? section.textColor.evaluate(ctx) : null
123+
section.textColor ? section.textColor.evaluate(ctx) : null,
124+
section.verticalAlign ? section.verticalAlign.evaluate(ctx) : null
105125
);
106126
};
107127

@@ -120,6 +140,9 @@ export class FormatExpression implements Expression {
120140
if (section.textColor) {
121141
fn(section.textColor);
122142
}
143+
if (section.verticalAlign) {
144+
fn(section.verticalAlign);
145+
}
123146
}
124147
}
125148

src/expression/types/formatted.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import type {Color} from '../../expression/types/color';
22
import type {ResolvedImage} from '../types/resolved_image';
33

4+
export const VERTICAL_ALIGN_OPTIONS = ['bottom', 'center', 'top'] as const;
5+
export type VerticalAlign = typeof VERTICAL_ALIGN_OPTIONS[number];
6+
47
export class FormattedSection {
58
text: string;
69
image: ResolvedImage | null;
710
scale: number | null;
811
fontStack: string | null;
912
textColor: Color | null;
13+
verticalAlign: VerticalAlign | null;
1014

11-
constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null) {
15+
constructor(text: string, image: ResolvedImage | null, scale: number | null, fontStack: string | null, textColor: Color | null, verticalAlign: VerticalAlign | null) {
1216
this.text = text;
1317
this.image = image;
1418
this.scale = scale;
1519
this.fontStack = fontStack;
1620
this.textColor = textColor;
21+
this.verticalAlign = verticalAlign;
1722
}
1823
}
1924

@@ -25,7 +30,7 @@ export class Formatted {
2530
}
2631

2732
static fromString(unformatted: string): Formatted {
28-
return new Formatted([new FormattedSection(unformatted, null, null, null, null)]);
33+
return new Formatted([new FormattedSection(unformatted, null, null, null, null, null)]);
2934
}
3035

3136
isEmpty(): boolean {

src/reference/v8.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3226,13 +3226,13 @@
32263226
}
32273227
},
32283228
"format": {
3229-
"doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)",
3229+
"doc": "Returns a `formatted` string for displaying mixed-format text in the `text-field` property. The input may contain a string literal or expression, including an [`'image'`](#image) expression. Strings may be followed by a style override object that supports the following properties:\n\n- `\"text-font\"`: Overrides the font stack specified by the root layout property.\n\n- `\"text-color\"`: Overrides the color specified by the root paint property.\n\n- `\"font-scale\"`: Applies a scaling factor on `text-size` as specified by the root layout property.\n\n- `\"vertical-align\"`: Aligns vertically text section or image in relation to the row it belongs to. Possible values are: \n\t- `\"bottom\"` *default*: align the bottom of this section with the bottom of other sections.\n<img alt=\"Visual representation of bottom alignment\" src=\"https://github.com/user-attachments/assets/0474a2fd-a4b2-417c-9187-7a13a28695bc\"/>\n\t- `\"center\"`: align the center of this section with the center of other sections.\n<img alt=\"Visual representation of center alignment\" src=\"https://github.com/user-attachments/assets/92237455-be6d-4c5d-b8f6-8127effc1950\"/>\n\t- `\"top\"`: align the top of this section with the top of other sections.\n<img alt=\"Visual representation of top alignment\" src=\"https://github.com/user-attachments/assets/45dccb28-d977-4abb-a006-4ea9792b7c53\"/>\n\t- Refer to [the design proposal](https://github.com/maplibre/maplibre-style-spec/issues/832) for more details.\n\n - [Change the case of labels](https://maplibre.org/maplibre-gl-js/docs/examples/change-case-of-labels/)\n\n - [Display and style rich text labels](https://maplibre.org/maplibre-gl-js/docs/examples/display-and-style-rich-text-labels/)",
32303230
"example": {
32313231
"syntax": {
3232-
"method": ["value", "{ \"text-font\": string, \"text-color\": color, \"font-scale\": number }", "..."],
3232+
"method": ["value", "{ \"text-font\": string, \"text-color\": color, \"font-scale\": number, \"vertical-align\": \"bottom\" | \"center\" | \"top\" }", "..."],
32333233
"result": "formatted"
32343234
},
3235-
"value": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6}]
3235+
"value": ["format", ["upcase", ["get", "FacilityName"]], {"font-scale": 0.8}, "\n\n", {}, ["downcase", ["get", "Comments"]], {"font-scale": 0.6, "vertical-align": "center"}]
32363236
},
32373237
"group": "Types",
32383238
"sdk-support": {
@@ -3256,6 +3256,11 @@
32563256
"android": "7.3.0",
32573257
"ios": "4.10.0"
32583258
},
3259+
"vertical-align": {
3260+
"js": "https://github.com/maplibre/maplibre-gl-js/issues/5043",
3261+
"android": "https://github.com/maplibre/maplibre-native/issues/3055",
3262+
"ios": "https://github.com/maplibre/maplibre-native/issues/3055"
3263+
},
32593264
"image": {
32603265
"js": "1.6.0",
32613266
"android": "8.6.0",

test/integration/expression/tests/format/basic/test.json

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
"d",
2121
{
2222
"text-color": "rgb(0, 255, 0)"
23+
},
24+
"e",
25+
{
26+
"vertical-align": "center"
2327
}
2428
],
2529
"inputs": [
@@ -43,21 +47,24 @@
4347
"image": null,
4448
"scale": null,
4549
"fontStack": null,
46-
"textColor": null
50+
"textColor": null,
51+
"verticalAlign": null
4752
},
4853
{
4954
"text": "b",
5055
"image": null,
5156
"scale": 2,
5257
"fontStack": null,
53-
"textColor": null
58+
"textColor": null,
59+
"verticalAlign": null
5460
},
5561
{
5662
"text": "c",
5763
"image": null,
5864
"scale": null,
5965
"fontStack": "a,b",
60-
"textColor": null
66+
"textColor": null,
67+
"verticalAlign": null
6168
},
6269
{
6370
"text": "d",
@@ -69,7 +76,16 @@
6976
"g": 1,
7077
"b": 0,
7178
"a": 1
72-
}
79+
},
80+
"verticalAlign": null
81+
},
82+
{
83+
"text": "e",
84+
"image": null,
85+
"scale": null,
86+
"fontStack": null,
87+
"textColor": null,
88+
"verticalAlign": "center"
7389
}
7490
]
7591
}

test/integration/expression/tests/format/coercion/test.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,24 @@
4444
"image": null,
4545
"scale": null,
4646
"fontStack": null,
47-
"textColor": null
47+
"textColor": null,
48+
"verticalAlign": null
4849
},
4950
{
5051
"text": "1",
5152
"image": null,
5253
"scale": null,
5354
"fontStack": null,
54-
"textColor": null
55+
"textColor": null,
56+
"verticalAlign": null
5557
},
5658
{
5759
"text": "true",
5860
"image": null,
5961
"scale": null,
6062
"fontStack": null,
61-
"textColor": null
63+
"textColor": null,
64+
"verticalAlign": null
6265
}
6366
]
6467
}

test/integration/expression/tests/format/data-driven-font/test.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"image": null,
4343
"scale": 1.5,
4444
"fontStack": null,
45-
"textColor": null
45+
"textColor": null,
46+
"verticalAlign": null
4647
}
4748
]
4849
},
@@ -53,7 +54,8 @@
5354
"image": null,
5455
"scale": 0.5,
5556
"fontStack": null,
56-
"textColor": null
57+
"textColor": null,
58+
"verticalAlign": null
5759
}
5860
]
5961
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"expression": [
3+
"format",
4+
"a",
5+
{
6+
"vertical-align": [
7+
"get",
8+
"vertical-align"
9+
]
10+
}
11+
],
12+
"inputs": [
13+
[
14+
{},
15+
{
16+
"properties": {
17+
"vertical-align": "center"
18+
}
19+
}
20+
],
21+
[
22+
{},
23+
{
24+
"properties": {
25+
"vertical-align": "top"
26+
}
27+
}
28+
]
29+
],
30+
"expected": {
31+
"compiled": {
32+
"result": "success",
33+
"isFeatureConstant": false,
34+
"isZoomConstant": true,
35+
"type": "formatted"
36+
},
37+
"outputs": [
38+
{
39+
"sections": [
40+
{
41+
"text": "a",
42+
"image": null,
43+
"scale": null,
44+
"fontStack": null,
45+
"textColor": null,
46+
"verticalAlign": "center"
47+
}
48+
]
49+
},
50+
{
51+
"sections": [
52+
{
53+
"text": "a",
54+
"image": null,
55+
"scale": null,
56+
"fontStack": null,
57+
"textColor": null,
58+
"verticalAlign": "top"
59+
}
60+
]
61+
}
62+
]
63+
}
64+
}

test/integration/expression/tests/format/image-sections/test.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
[
99
"image",
1010
"beach-11"
11-
]
11+
],
12+
{
13+
"vertical-align": "center"
14+
}
1215
],
1316
"inputs": [
1417
[
@@ -38,7 +41,8 @@
3841
},
3942
"scale": null,
4043
"fontStack": null,
41-
"textColor": null
44+
"textColor": null,
45+
"verticalAlign": null
4246
},
4347
{
4448
"text": "",
@@ -48,7 +52,8 @@
4852
},
4953
"scale": null,
5054
"fontStack": null,
51-
"textColor": null
55+
"textColor": null,
56+
"verticalAlign": "center"
5257
}
5358
]
5459
}

test/integration/expression/tests/format/implicit-coerce/test.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"image": null,
5757
"scale": null,
5858
"fontStack": null,
59-
"textColor": null
59+
"textColor": null,
60+
"verticalAlign": null
6061
}
6162
]
6263
},
@@ -67,7 +68,8 @@
6768
"image": null,
6869
"scale": null,
6970
"fontStack": null,
70-
"textColor": null
71+
"textColor": null,
72+
"verticalAlign": null
7173
}
7274
]
7375
},
@@ -78,7 +80,8 @@
7880
"image": null,
7981
"scale": null,
8082
"fontStack": null,
81-
"textColor": null
83+
"textColor": null,
84+
"verticalAlign": null
8285
}
8386
]
8487
}

0 commit comments

Comments
 (0)