Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`;
Expand Down
3 changes: 1 addition & 2 deletions src/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.<h2>Unicode range</h2>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<h2>Font Resolution</h2>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<h2>Supported Fonts</h2>What type of fonts are supported is implementation-defined. Unsupported fonts are ignored.",
"example": {
"Noto Sans Regular": [{
Expand Down
4 changes: 3 additions & 1 deletion src/validate/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
'*'() {
Expand Down Expand Up @@ -60,7 +61,8 @@ const VALIDATORS = {
'colorArray': validateColorArray,
'variableAnchorOffsetCollection': validateVariableAnchorOffsetCollection,
'sprite': validateSprite,
'state': validateState
'state': validateState,
'fontFaces': validateFontFaces
};

/**
Expand Down
80 changes: 80 additions & 0 deletions src/validate/validate_font_faces.ts
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +8 to +14
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the general Function type is not type-safe. Consider using a more specific function signature or importing the correct type from the codebase, similar to how other validators handle this parameter.

Suggested change
interface ValidateFontFacesOptions {
key: string;
value: unknown;
styleSpec: typeof v8;
style: StyleSpecification;
validateSpec: Function;
import type {ValidationError} from '../error/validation_error';
interface ValidateFontFacesOptions {
key: string;
value: unknown;
styleSpec: typeof v8;
style: StyleSpecification;
validateSpec: (options: { key: string; value: unknown; styleSpec: typeof v8; style: StyleSpecification; }) => ValidationError[];

Copilot uses AI. Check for mistakes.
}

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()) {
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using any[] cast bypasses type safety. Since fontValueType === 'array' was already checked on line 46, consider using Array<unknown> instead of any[] for better type safety.

Suggested change
for (const [i, fontFace] of (fontValue as any[]).entries()) {
for (const [i, fontFace] of (fontValue as Array<unknown>).entries()) {

Copilot uses AI. Check for mistakes.
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;
}

7 changes: 7 additions & 0 deletions test/integration/style-spec/tests/font-faces-array.input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": []
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "font-faces: object expected, array found",
"line": 5
}
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": true
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "font-faces: object expected, boolean found",
"line": 5
}
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": 123
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "font-faces: object expected, number found",
"line": 5
}
]

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": {
"Noto Sans": [{
"url": "https://example.com/font.ttf",
"unicode-range": ["U+1780-17FF"]
}]
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[]

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[]

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": {
"Noto Sans": "https://example.com/font.ttf"
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[]

6 changes: 6 additions & 0 deletions test/integration/style-spec/tests/font-faces.input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 8,
"sources": {},
"layers": [],
"font-faces": "invalid string"
}
6 changes: 6 additions & 0 deletions test/integration/style-spec/tests/font-faces.output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"message": "font-faces: object expected, string found",
"line": 5
}
]