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\n

Font 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\n

Supported 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 + } +]