diff --git a/build/generate-docs.ts b/build/generate-docs.ts
index ae897be6d..9450c2e87 100644
--- a/build/generate-docs.ts
+++ b/build/generate-docs.ts
@@ -208,6 +208,8 @@ function typeToMarkdownLink(type: string): string {
case 'paint':
case 'layout':
return ` [${type}](layers.md#${type.toLocaleLowerCase()})`;
+ case 'fontfaces':
+ return ` [${type}](font-faces.md)`;
default:
// top level types have their own file
return ` [${type}](${type}.md)`;
diff --git a/src/reference/v8.json b/src/reference/v8.json
index 0ef5ab0fa..6b99a5fa5 100644
--- a/src/reference/v8.json
+++ b/src/reference/v8.json
@@ -219,8 +219,7 @@
}
},
"font-faces": {
- "type": "array",
- "value": "fontFaces",
+ "type": "fontFaces",
"doc": "The `font-faces` property can be used to specify what font files to use for rendering text. Font faces contain information needed to render complex texts such as [Devanagari](https://en.wikipedia.org/wiki/Devanagari), [Khmer](https://en.wikipedia.org/wiki/Khmer_script) among many others.
Unicode range
The optional `unicode-range` property can be used to only use a particular font file for characters within the specified unicode range(s). Its value should be an array of strings, each indicating a start and end of a unicode range, similar to the [CSS descriptor with the same name](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range). This allows specifying multiple non-consecutive unicode ranges. When not specified, the default value is `U+0-10FFFF`, meaning the font file will be used for all unicode characters.\n\nRefer to the [Unicode Character Code Charts](https://www.unicode.org/charts/) to see ranges for scripts supported by Unicode. To see what unicode code-points are available in a font, use a tool like [FontDrop](https://fontdrop.info/).\n\nFont Resolution
For every name in a symbol layer’s [`text-font`](./layers.md/#text-font) array, characters are matched if they are covered one of the by the font files in the corresponding entry of the `font-faces` map. Any still-unmatched characters then fall back to the [`glyphs`](./glyphs.md) URL if provided.\n\nSupported Fonts
What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.",
"example": {
"Noto Sans Regular": [{
diff --git a/src/validate/validate.ts b/src/validate/validate.ts
index f8192c3aa..01251163c 100644
--- a/src/validate/validate.ts
+++ b/src/validate/validate.ts
@@ -31,6 +31,7 @@ import {ValidationError} from '../error/validation_error';
import {validateProjection} from './validate_projection';
import {validateProjectionDefinition} from './validate_projectiondefinition';
import {validateState} from './validate_state';
+import {validateFontFaces} from './validate_font_faces';
const VALIDATORS = {
'*'() {
@@ -60,7 +61,8 @@ const VALIDATORS = {
'colorArray': validateColorArray,
'variableAnchorOffsetCollection': validateVariableAnchorOffsetCollection,
'sprite': validateSprite,
- 'state': validateState
+ 'state': validateState,
+ 'fontFaces': validateFontFaces
};
/**
diff --git a/src/validate/validate_font_faces.ts b/src/validate/validate_font_faces.ts
new file mode 100644
index 000000000..0bdaf54bb
--- /dev/null
+++ b/src/validate/validate_font_faces.ts
@@ -0,0 +1,80 @@
+import {ValidationError} from '../error/validation_error';
+import {getType} from '../util/get_type';
+import {isObjectLiteral} from '../util/is_object_literal';
+import {validateObject} from './validate_object';
+import {validateString} from './validate_string';
+import v8 from '../reference/v8.json' with {type: 'json'};
+import type {StyleSpecification} from '../types.g';
+
+interface ValidateFontFacesOptions {
+ key: string;
+ value: unknown;
+ styleSpec: typeof v8;
+ style: StyleSpecification;
+ validateSpec: Function;
+}
+
+export function validateFontFaces(options: ValidateFontFacesOptions): ValidationError[] {
+ const key = options.key;
+ const value = options.value;
+ const validateSpec = options.validateSpec;
+ const styleSpec = options.styleSpec;
+ const style = options.style;
+
+ if (!isObjectLiteral(value)) {
+ return [
+ new ValidationError(
+ key,
+ value,
+ `object expected, ${getType(value)} found`
+ ),
+ ];
+ }
+
+ const errors: ValidationError[] = [];
+
+ for (const fontName in value) {
+ const fontValue = value[fontName];
+ const fontValueType = getType(fontValue);
+
+ if (fontValueType === 'string') {
+ // Validate as a string URL
+ errors.push(...validateString({
+ key: `${key}.${fontName}`,
+ value: fontValue,
+ }));
+ } else if (fontValueType === 'array') {
+ // Validate as an array of font face objects
+ const fontFaceSpec = {
+ url: {
+ type: 'string',
+ required: true,
+ },
+ 'unicode-range': {
+ type: 'array',
+ value: 'string',
+ }
+ };
+
+ for (const [i, fontFace] of (fontValue as any[]).entries()) {
+ errors.push(...validateObject({
+ key: `${key}.${fontName}[${i}]`,
+ value: fontFace,
+ valueSpec: fontFaceSpec,
+ styleSpec,
+ style,
+ validateSpec,
+ }));
+ }
+ } else {
+ errors.push(new ValidationError(
+ `${key}.${fontName}`,
+ fontValue,
+ `string or array expected, ${fontValueType} found`
+ ));
+ }
+ }
+
+ return errors;
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-array.input.json b/test/integration/style-spec/tests/font-faces-array.input.json
new file mode 100644
index 000000000..78d577c85
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-array.input.json
@@ -0,0 +1,7 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": []
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-array.output.json b/test/integration/style-spec/tests/font-faces-array.output.json
new file mode 100644
index 000000000..953e169eb
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-array.output.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "font-faces: object expected, array found",
+ "line": 5
+ }
+]
+
diff --git a/test/integration/style-spec/tests/font-faces-boolean.input.json b/test/integration/style-spec/tests/font-faces-boolean.input.json
new file mode 100644
index 000000000..5ec173a78
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-boolean.input.json
@@ -0,0 +1,7 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": true
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-boolean.output.json b/test/integration/style-spec/tests/font-faces-boolean.output.json
new file mode 100644
index 000000000..a06e182d7
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-boolean.output.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "font-faces: object expected, boolean found",
+ "line": 5
+ }
+]
+
diff --git a/test/integration/style-spec/tests/font-faces-number.input.json b/test/integration/style-spec/tests/font-faces-number.input.json
new file mode 100644
index 000000000..fb307ca68
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-number.input.json
@@ -0,0 +1,7 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": 123
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-number.output.json b/test/integration/style-spec/tests/font-faces-number.output.json
new file mode 100644
index 000000000..ac543b295
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-number.output.json
@@ -0,0 +1,7 @@
+[
+ {
+ "message": "font-faces: object expected, number found",
+ "line": 5
+ }
+]
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-array.input.json b/test/integration/style-spec/tests/font-faces-valid-array.input.json
new file mode 100644
index 000000000..3c12c0930
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-array.input.json
@@ -0,0 +1,12 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": {
+ "Noto Sans": [{
+ "url": "https://example.com/font.ttf",
+ "unicode-range": ["U+1780-17FF"]
+ }]
+ }
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-array.output.json b/test/integration/style-spec/tests/font-faces-valid-array.output.json
new file mode 100644
index 000000000..7dd438752
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-array.output.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-empty.input.json b/test/integration/style-spec/tests/font-faces-valid-empty.input.json
new file mode 100644
index 000000000..205d4ca74
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-empty.input.json
@@ -0,0 +1,7 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": {}
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-empty.output.json b/test/integration/style-spec/tests/font-faces-valid-empty.output.json
new file mode 100644
index 000000000..7dd438752
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-empty.output.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-string.input.json b/test/integration/style-spec/tests/font-faces-valid-string.input.json
new file mode 100644
index 000000000..12e6e4c48
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-string.input.json
@@ -0,0 +1,9 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": {
+ "Noto Sans": "https://example.com/font.ttf"
+ }
+}
+
diff --git a/test/integration/style-spec/tests/font-faces-valid-string.output.json b/test/integration/style-spec/tests/font-faces-valid-string.output.json
new file mode 100644
index 000000000..7dd438752
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces-valid-string.output.json
@@ -0,0 +1,2 @@
+[]
+
diff --git a/test/integration/style-spec/tests/font-faces.input.json b/test/integration/style-spec/tests/font-faces.input.json
new file mode 100644
index 000000000..93886ef91
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces.input.json
@@ -0,0 +1,6 @@
+{
+ "version": 8,
+ "sources": {},
+ "layers": [],
+ "font-faces": "invalid string"
+}
diff --git a/test/integration/style-spec/tests/font-faces.output.json b/test/integration/style-spec/tests/font-faces.output.json
new file mode 100644
index 000000000..3d8987482
--- /dev/null
+++ b/test/integration/style-spec/tests/font-faces.output.json
@@ -0,0 +1,6 @@
+[
+ {
+ "message": "font-faces: object expected, string found",
+ "line": 5
+ }
+]