#!/usr/bin/env node (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('fs')) : typeof define === 'function' && define.amd ? define(['fs'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.fs)); })(this, (function (require$$0) { 'use strict'; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function hasKey(obj, keys) { var o = obj; keys.slice(0, -1).forEach(function (key) { o = o[key] || {}; }); var key = keys[keys.length - 1]; return key in o; } function isNumber(x) { if (typeof x === 'number') { return true; } if ((/^0x[0-9a-f]+$/i).test(x)) { return true; } return (/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/).test(x); } function isConstructorOrProto(obj, key) { return (key === 'constructor' && typeof obj[key] === 'function') || key === '__proto__'; } var minimist = function (args, opts) { if (!opts) { opts = {}; } var flags = { bools: {}, strings: {}, unknownFn: null, }; if (typeof opts.unknown === 'function') { flags.unknownFn = opts.unknown; } if (typeof opts.boolean === 'boolean' && opts.boolean) { flags.allBools = true; } else { [].concat(opts.boolean).filter(Boolean).forEach(function (key) { flags.bools[key] = true; }); } var aliases = {}; function aliasIsBoolean(key) { return aliases[key].some(function (x) { return flags.bools[x]; }); } Object.keys(opts.alias || {}).forEach(function (key) { aliases[key] = [].concat(opts.alias[key]); aliases[key].forEach(function (x) { aliases[x] = [key].concat(aliases[key].filter(function (y) { return x !== y; })); }); }); [].concat(opts.string).filter(Boolean).forEach(function (key) { flags.strings[key] = true; if (aliases[key]) { [].concat(aliases[key]).forEach(function (k) { flags.strings[k] = true; }); } }); var defaults = opts.default || {}; var argv = { _: [] }; function argDefined(key, arg) { return (flags.allBools && (/^--[^=]+$/).test(arg)) || flags.strings[key] || flags.bools[key] || aliases[key]; } function setKey(obj, keys, value) { var o = obj; for (var i = 0; i < keys.length - 1; i++) { var key = keys[i]; if (isConstructorOrProto(o, key)) { return; } if (o[key] === undefined) { o[key] = {}; } if ( o[key] === Object.prototype || o[key] === Number.prototype || o[key] === String.prototype ) { o[key] = {}; } if (o[key] === Array.prototype) { o[key] = []; } o = o[key]; } var lastKey = keys[keys.length - 1]; if (isConstructorOrProto(o, lastKey)) { return; } if ( o === Object.prototype || o === Number.prototype || o === String.prototype ) { o = {}; } if (o === Array.prototype) { o = []; } if (o[lastKey] === undefined || flags.bools[lastKey] || typeof o[lastKey] === 'boolean') { o[lastKey] = value; } else if (Array.isArray(o[lastKey])) { o[lastKey].push(value); } else { o[lastKey] = [o[lastKey], value]; } } function setArg(key, val, arg) { if (arg && flags.unknownFn && !argDefined(key, arg)) { if (flags.unknownFn(arg) === false) { return; } } var value = !flags.strings[key] && isNumber(val) ? Number(val) : val; setKey(argv, key.split('.'), value); (aliases[key] || []).forEach(function (x) { setKey(argv, x.split('.'), value); }); } Object.keys(flags.bools).forEach(function (key) { setArg(key, defaults[key] === undefined ? false : defaults[key]); }); var notFlags = []; if (args.indexOf('--') !== -1) { notFlags = args.slice(args.indexOf('--') + 1); args = args.slice(0, args.indexOf('--')); } for (var i = 0; i < args.length; i++) { var arg = args[i]; var key; var next; if ((/^--.+=/).test(arg)) { // Using [\s\S] instead of . because js doesn't support the // 'dotall' regex modifier. See: // http://stackoverflow.com/a/1068308/13216 var m = arg.match(/^--([^=]+)=([\s\S]*)$/); key = m[1]; var value = m[2]; if (flags.bools[key]) { value = value !== 'false'; } setArg(key, value, arg); } else if ((/^--no-.+/).test(arg)) { key = arg.match(/^--no-(.+)/)[1]; setArg(key, false, arg); } else if ((/^--.+/).test(arg)) { key = arg.match(/^--(.+)/)[1]; next = args[i + 1]; if ( next !== undefined && !(/^(-|--)[^-]/).test(next) && !flags.bools[key] && !flags.allBools && (aliases[key] ? !aliasIsBoolean(key) : true) ) { setArg(key, next, arg); i += 1; } else if ((/^(true|false)$/).test(next)) { setArg(key, next === 'true', arg); i += 1; } else { setArg(key, flags.strings[key] ? '' : true, arg); } } else if ((/^-[^-]+/).test(arg)) { var letters = arg.slice(1, -1).split(''); var broken = false; for (var j = 0; j < letters.length; j++) { next = arg.slice(j + 2); if (next === '-') { setArg(letters[j], next, arg); continue; } if ((/[A-Za-z]/).test(letters[j]) && next[0] === '=') { setArg(letters[j], next.slice(1), arg); broken = true; break; } if ( (/[A-Za-z]/).test(letters[j]) && (/-?\d+(\.\d*)?(e-?\d+)?$/).test(next) ) { setArg(letters[j], next, arg); broken = true; break; } if (letters[j + 1] && letters[j + 1].match(/\W/)) { setArg(letters[j], arg.slice(j + 2), arg); broken = true; break; } else { setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg); } } key = arg.slice(-1)[0]; if (!broken && key !== '-') { if ( args[i + 1] && !(/^(-|--)[^-]/).test(args[i + 1]) && !flags.bools[key] && (aliases[key] ? !aliasIsBoolean(key) : true) ) { setArg(key, args[i + 1], arg); i += 1; } else if (args[i + 1] && (/^(true|false)$/).test(args[i + 1])) { setArg(key, args[i + 1] === 'true', arg); i += 1; } else { setArg(key, flags.strings[key] ? '' : true, arg); } } } else { if (!flags.unknownFn || flags.unknownFn(arg) !== false) { argv._.push(flags.strings._ || !isNumber(arg) ? arg : Number(arg)); } if (opts.stopEarly) { argv._.push.apply(argv._, args.slice(i + 1)); break; } } } Object.keys(defaults).forEach(function (k) { if (!hasKey(argv, k.split('.'))) { setKey(argv, k.split('.'), defaults[k]); (aliases[k] || []).forEach(function (x) { setKey(argv, x.split('.'), defaults[k]); }); } }); if (opts['--']) { argv['--'] = notFlags.slice(); } else { notFlags.forEach(function (k) { argv._.push(k); }); } return argv; }; var minimist$1 = /*@__PURE__*/getDefaultExportFromCjs(minimist); var rw = {}; var dash = {}; var decode$2 = function(options) { if (options) { if (typeof options === "string") return encoding(options); if (options.encoding !== null) return encoding(options.encoding); } return identity(); }; function identity() { var chunks = []; return { push: function(chunk) { chunks.push(chunk); }, value: function() { return Buffer.concat(chunks); } }; } function encoding(encoding) { var chunks = []; return { push: function(chunk) { chunks.push(chunk); }, value: function() { return Buffer.concat(chunks).toString(encoding); } }; } var fs$3 = require$$0, decode$1 = decode$2; var readFile = function(path, options, callback) { if (arguments.length < 3) callback = options, options = null; switch (path) { case "/dev/stdin": return readStream(process.stdin, options, callback); } fs$3.stat(path, function(error, stat) { if (error) return callback(error); if (stat.isFile()) return fs$3.readFile(path, options, callback); readStream(fs$3.createReadStream(path, options ? {flags: options.flag || "r"} : {}), options, callback); // N.B. flag / flags }); }; function readStream(stream, options, callback) { var decoder = decode$1(options); stream.on("error", callback); stream.on("data", function(d) { decoder.push(d); }); stream.on("end", function() { callback(null, decoder.value()); }); } var fs$2 = require$$0, decode = decode$2; var readFileSync = function(filename, options) { if (fs$2.statSync(filename).isFile()) { return fs$2.readFileSync(filename, options); } else { var fd = fs$2.openSync(filename, options && options.flag || "r"), decoder = decode(options); while (true) { // eslint-disable-line no-constant-condition try { var buffer = new Buffer(bufferSize), bytesRead = fs$2.readSync(fd, buffer, 0, bufferSize); } catch (e) { if (e.code === "EOF") break; fs$2.closeSync(fd); throw e; } if (bytesRead === 0) break; decoder.push(buffer.slice(0, bytesRead)); } fs$2.closeSync(fd); return decoder.value(); } }; var bufferSize = 1 << 16; var encode$2 = function(data, options) { return typeof data === "string" ? new Buffer(data, typeof options === "string" ? options : options && options.encoding !== null ? options.encoding : "utf8") : data; }; var fs$1 = require$$0, encode$1 = encode$2; var writeFile = function(path, data, options, callback) { if (arguments.length < 4) callback = options, options = null; switch (path) { case "/dev/stdout": return writeStream(process.stdout, "write", data, options, callback); case "/dev/stderr": return writeStream(process.stderr, "write", data, options, callback); } fs$1.stat(path, function(error, stat) { if (error && error.code !== "ENOENT") return callback(error); if (stat && stat.isFile()) return fs$1.writeFile(path, data, options, callback); writeStream(fs$1.createWriteStream(path, options ? {flags: options.flag || "w"} : {}), "end", data, options, callback); // N.B. flag / flags }); }; function writeStream(stream, send, data, options, callback) { stream.on("error", function(error) { callback(error.code === "EPIPE" ? null : error); }); // ignore broken pipe, e.g., | head stream[send](encode$1(data, options), function(error) { callback(error && error.code === "EPIPE" ? null : error); }); } var fs = require$$0, encode = encode$2; var writeFileSync = function(filename, data, options) { var stat; try { stat = fs.statSync(filename); } catch (error) { if (error.code !== "ENOENT") throw error; } if (!stat || stat.isFile()) { fs.writeFileSync(filename, data, options); } else { var fd = fs.openSync(filename, options && options.flag || "w"), bytesWritten = 0, bytesTotal = (data = encode(data, options)).length; while (bytesWritten < bytesTotal) { try { bytesWritten += fs.writeSync(fd, data, bytesWritten, bytesTotal - bytesWritten, null); } catch (error) { if (error.code === "EPIPE") break; // ignore broken pipe, e.g., | head fs.closeSync(fd); throw error; } } fs.closeSync(fd); } }; var slice = Array.prototype.slice; function dashify(method, file) { return function(path) { var argv = arguments; if (path == "-") (argv = slice.call(argv)).splice(0, 1, file); return method.apply(null, argv); }; } dash.readFile = dashify(readFile, "/dev/stdin"); dash.readFileSync = dashify(readFileSync, "/dev/stdin"); dash.writeFile = dashify(writeFile, "/dev/stdout"); dash.writeFileSync = dashify(writeFileSync, "/dev/stdout"); rw.dash = dash; rw.readFile = readFile; rw.readFileSync = readFileSync; rw.writeFile = writeFile; rw.writeFileSync = writeFileSync; // Note: Do not inherit from Error. It breaks when transpiling to ES5. class ValidationError { constructor(key, value, message, identifier) { this.message = (key ? `${key}: ` : '') + message; if (identifier) this.identifier = identifier; if (value !== null && value !== undefined && value.__line__) { this.line = value.__line__; } } } function validateConstants(options) { const key = options.key; const constants = options.value; if (constants) { return [new ValidationError(key, constants, 'constants have been deprecated as of v8')]; } else { return []; } } function extendBy(output, ...inputs) { for (const input of inputs) { for (const k in input) { output[k] = input[k]; } } return output; } // Turn jsonlint-lines-primitives objects into primitive objects function unbundle(value) { if (value instanceof Number || value instanceof String || value instanceof Boolean) { return value.valueOf(); } else { return value; } } function deepUnbundle(value) { if (Array.isArray(value)) { return value.map(deepUnbundle); } else if (value instanceof Object && !(value instanceof Number || value instanceof String || value instanceof Boolean)) { const unbundledValue = {}; for (const key in value) { unbundledValue[key] = deepUnbundle(value[key]); } return unbundledValue; } return unbundle(value); } class ExpressionParsingError extends Error { constructor(key, message) { super(message); this.message = message; this.key = key; } } /** * Tracks `let` bindings during expression parsing. * @private */ class Scope { constructor(parent, bindings = []) { this.parent = parent; this.bindings = {}; for (const [name, expression] of bindings) { this.bindings[name] = expression; } } concat(bindings) { return new Scope(this, bindings); } get(name) { if (this.bindings[name]) { return this.bindings[name]; } if (this.parent) { return this.parent.get(name); } throw new Error(`${name} not found in scope.`); } has(name) { if (this.bindings[name]) return true; return this.parent ? this.parent.has(name) : false; } } const NullType = { kind: 'null' }; const NumberType = { kind: 'number' }; const StringType = { kind: 'string' }; const BooleanType = { kind: 'boolean' }; const ColorType = { kind: 'color' }; const ObjectType = { kind: 'object' }; const ValueType = { kind: 'value' }; const ErrorType = { kind: 'error' }; const CollatorType = { kind: 'collator' }; const FormattedType = { kind: 'formatted' }; const PaddingType = { kind: 'padding' }; const ResolvedImageType = { kind: 'resolvedImage' }; const VariableAnchorOffsetCollectionType = { kind: 'variableAnchorOffsetCollection' }; function array$1(itemType, N) { return { kind: 'array', itemType, N }; } function toString$1(type) { if (type.kind === 'array') { const itemType = toString$1(type.itemType); return typeof type.N === 'number' ? `array<${itemType}, ${type.N}>` : type.itemType.kind === 'value' ? 'array' : `array<${itemType}>`; } else { return type.kind; } } const valueMemberTypes = [ NullType, NumberType, StringType, BooleanType, ColorType, FormattedType, ObjectType, array$1(ValueType), PaddingType, ResolvedImageType, VariableAnchorOffsetCollectionType ]; /** * Returns null if `t` is a subtype of `expected`; otherwise returns an * error message. * @private */ function checkSubtype(expected, t) { if (t.kind === 'error') { // Error is a subtype of every type return null; } else if (expected.kind === 'array') { if (t.kind === 'array' && ((t.N === 0 && t.itemType.kind === 'value') || !checkSubtype(expected.itemType, t.itemType)) && (typeof expected.N !== 'number' || expected.N === t.N)) { return null; } } else if (expected.kind === t.kind) { return null; } else if (expected.kind === 'value') { for (const memberType of valueMemberTypes) { if (!checkSubtype(memberType, t)) { return null; } } } return `Expected ${toString$1(expected)} but found ${toString$1(t)} instead.`; } function isValidType(provided, allowedTypes) { return allowedTypes.some(t => t.kind === provided.kind); } function isValidNativeType(provided, allowedTypes) { return allowedTypes.some(t => { if (t === 'null') { return provided === null; } else if (t === 'array') { return Array.isArray(provided); } else if (t === 'object') { return provided && !Array.isArray(provided) && typeof provided === 'object'; } else { return t === typeof provided; } }); } /** * Verify whether the specified type is of the same type as the specified sample. * * @param provided Type to verify * @param sample Sample type to reference * @returns `true` if both objects are of the same type, `false` otherwise * @example basic types * if (verifyType(outputType, ValueType)) { * // type narrowed to: * outputType.kind; // 'value' * } * @example array types * if (verifyType(outputType, array(NumberType))) { * // type narrowed to: * outputType.kind; // 'array' * outputType.itemType; // NumberTypeT * outputType.itemType.kind; // 'number' * } */ function verifyType(provided, sample) { if (provided.kind === 'array' && sample.kind === 'array') { return provided.itemType.kind === sample.itemType.kind && typeof provided.N === 'number'; } return provided.kind === sample.kind; } // See https://observablehq.com/@mbostock/lab-and-rgb const Xn = 0.96422, Yn = 1, Zn = 0.82521, t0 = 4 / 29, t1 = 6 / 29, t2 = 3 * t1 * t1, t3 = t1 * t1 * t1, deg2rad = Math.PI / 180, rad2deg = 180 / Math.PI; function constrainAngle(angle) { angle = angle % 360; if (angle < 0) { angle += 360; } return angle; } function rgbToLab([r, g, b, alpha]) { r = rgb2xyz(r); g = rgb2xyz(g); b = rgb2xyz(b); let x, z; const y = xyz2lab((0.2225045 * r + 0.7168786 * g + 0.0606169 * b) / Yn); if (r === g && g === b) { x = z = y; } else { x = xyz2lab((0.4360747 * r + 0.3850649 * g + 0.1430804 * b) / Xn); z = xyz2lab((0.0139322 * r + 0.0971045 * g + 0.7141733 * b) / Zn); } const l = 116 * y - 16; return [(l < 0) ? 0 : l, 500 * (x - y), 200 * (y - z), alpha]; } function rgb2xyz(x) { return (x <= 0.04045) ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); } function xyz2lab(t) { return (t > t3) ? Math.pow(t, 1 / 3) : t / t2 + t0; } function labToRgb([l, a, b, alpha]) { let y = (l + 16) / 116, x = isNaN(a) ? y : y + a / 500, z = isNaN(b) ? y : y - b / 200; y = Yn * lab2xyz(y); x = Xn * lab2xyz(x); z = Zn * lab2xyz(z); return [ xyz2rgb(3.1338561 * x - 1.6168667 * y - 0.4906146 * z), // D50 -> sRGB xyz2rgb(-0.9787684 * x + 1.9161415 * y + 0.0334540 * z), xyz2rgb(0.0719453 * x - 0.2289914 * y + 1.4052427 * z), alpha, ]; } function xyz2rgb(x) { x = (x <= 0.00304) ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055; return (x < 0) ? 0 : (x > 1) ? 1 : x; // clip to 0..1 range } function lab2xyz(t) { return (t > t1) ? t * t * t : t2 * (t - t0); } function rgbToHcl(rgbColor) { const [l, a, b, alpha] = rgbToLab(rgbColor); const c = Math.sqrt(a * a + b * b); const h = Math.round(c * 10000) ? constrainAngle(Math.atan2(b, a) * rad2deg) : NaN; return [h, c, l, alpha]; } function hclToRgb([h, c, l, alpha]) { h = isNaN(h) ? 0 : h * deg2rad; return labToRgb([l, Math.cos(h) * c, Math.sin(h) * c, alpha]); } // https://drafts.csswg.org/css-color-4/#hsl-to-rgb function hslToRgb([h, s, l, alpha]) { h = constrainAngle(h); s /= 100; l /= 100; function f(n) { const k = (n + h / 30) % 12; const a = s * Math.min(l, 1 - l); return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); } return [f(0), f(8), f(4), alpha]; } /** * CSS color parser compliant with CSS Color 4 Specification. * Supports: named colors, `transparent` keyword, all rgb hex notations, * rgb(), rgba(), hsl() and hsla() functions. * Does not round the parsed values to integers from the range 0..255. * * Syntax: * * = | * = | * * rgb() = rgb( {3} [ / ]? ) | rgb( {3} [ / ]? ) * rgb() = rgb( #{3} , ? ) | rgb( #{3} , ? ) * * hsl() = hsl( [ / ]? ) * hsl() = hsl( , , , ? ) * * Caveats: * - - with optional `deg` suffix; `grad`, `rad`, `turn` are not supported * - `none` keyword is not supported * - comments inside rgb()/hsl() are not supported * - legacy color syntax rgba() is supported with an identical grammar and behavior to rgb() * - legacy color syntax hsla() is supported with an identical grammar and behavior to hsl() * * @param input CSS color string to parse. * @returns Color in sRGB color space, with `red`, `green`, `blue` * and `alpha` channels normalized to the range 0..1, * or `undefined` if the input is not a valid color string. */ function parseCssColor(input) { input = input.toLowerCase().trim(); if (input === 'transparent') { return [0, 0, 0, 0]; } // 'white', 'black', 'blue' const namedColorsMatch = namedColors[input]; if (namedColorsMatch) { const [r, g, b] = namedColorsMatch; return [r / 255, g / 255, b / 255, 1]; } // #f0c, #f0cf, #ff00cc, #ff00ccff if (input.startsWith('#')) { const hexRegexp = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/; if (hexRegexp.test(input)) { const step = input.length < 6 ? 1 : 2; let i = 1; return [ parseHex(input.slice(i, i += step)), parseHex(input.slice(i, i += step)), parseHex(input.slice(i, i += step)), parseHex(input.slice(i, i + step) || 'ff'), ]; } } // rgb(128 0 0), rgb(50% 0% 0%), rgba(255,0,255,0.6), rgb(255 0 255 / 60%), rgb(100% 0% 100% /.6) if (input.startsWith('rgb')) { const rgbRegExp = /^rgba?\(\s*([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s+|\s*(,)\s*)([\de.+-]+)(%)?(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/; const rgbMatch = input.match(rgbRegExp); if (rgbMatch) { const [_, // eslint-disable-line @typescript-eslint/no-unused-vars r, // rp, // % (optional) f1, // , (optional) g, // gp, // % (optional) f2, // , (optional) b, // bp, // % (optional) f3, // ,|/ (optional) a, // (optional) ap, // % (optional) ] = rgbMatch; const argFormat = [f1 || ' ', f2 || ' ', f3].join(''); if (argFormat === ' ' || argFormat === ' /' || argFormat === ',,' || argFormat === ',,,') { const valFormat = [rp, gp, bp].join(''); const maxValue = (valFormat === '%%%') ? 100 : (valFormat === '') ? 255 : 0; if (maxValue) { const rgba = [ clamp(+r / maxValue, 0, 1), clamp(+g / maxValue, 0, 1), clamp(+b / maxValue, 0, 1), a ? parseAlpha(+a, ap) : 1, ]; if (validateNumbers(rgba)) { return rgba; } // invalid numbers } // values must be all numbers or all percentages } return; // comma optional syntax requires no commas at all } } // hsl(120 50% 80%), hsla(120deg,50%,80%,.9), hsl(12e1 50% 80% / 90%) const hslRegExp = /^hsla?\(\s*([\de.+-]+)(?:deg)?(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s+|\s*(,)\s*)([\de.+-]+)%(?:\s*([,\/])\s*([\de.+-]+)(%)?)?\s*\)$/; const hslMatch = input.match(hslRegExp); if (hslMatch) { const [_, // eslint-disable-line @typescript-eslint/no-unused-vars h, // f1, // , (optional) s, // f2, // , (optional) l, // f3, // ,|/ (optional) a, // (optional) ap, // % (optional) ] = hslMatch; const argFormat = [f1 || ' ', f2 || ' ', f3].join(''); if (argFormat === ' ' || argFormat === ' /' || argFormat === ',,' || argFormat === ',,,') { const hsla = [ +h, clamp(+s, 0, 100), clamp(+l, 0, 100), a ? parseAlpha(+a, ap) : 1, ]; if (validateNumbers(hsla)) { return hslToRgb(hsla); } // invalid numbers } // comma optional syntax requires no commas at all } } function parseHex(hex) { return parseInt(hex.padEnd(2, hex), 16) / 255; } function parseAlpha(a, asPercentage) { return clamp(asPercentage ? (a / 100) : a, 0, 1); } function clamp(n, min, max) { return Math.min(Math.max(min, n), max); } /** * The regular expression for numeric values is not super specific, and it may * happen that it will accept a value that is not a valid number. In order to * detect and eliminate such values this function exists. * * @param array Array of uncertain numbers. * @returns `true` if the specified array contains only valid numbers, `false` otherwise. */ function validateNumbers(array) { return !array.some(Number.isNaN); } /** * To generate: * - visit {@link https://www.w3.org/TR/css-color-4/#named-colors} * - run in the console: * @example * copy(`{\n${[...document.querySelector('.named-color-table tbody').children].map((tr) => `${tr.cells[2].textContent.trim()}: [${tr.cells[4].textContent.trim().split(/\s+/).join(', ')}],`).join('\n')}\n}`); */ const namedColors = { aliceblue: [240, 248, 255], antiquewhite: [250, 235, 215], aqua: [0, 255, 255], aquamarine: [127, 255, 212], azure: [240, 255, 255], beige: [245, 245, 220], bisque: [255, 228, 196], black: [0, 0, 0], blanchedalmond: [255, 235, 205], blue: [0, 0, 255], blueviolet: [138, 43, 226], brown: [165, 42, 42], burlywood: [222, 184, 135], cadetblue: [95, 158, 160], chartreuse: [127, 255, 0], chocolate: [210, 105, 30], coral: [255, 127, 80], cornflowerblue: [100, 149, 237], cornsilk: [255, 248, 220], crimson: [220, 20, 60], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgoldenrod: [184, 134, 11], darkgray: [169, 169, 169], darkgreen: [0, 100, 0], darkgrey: [169, 169, 169], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkseagreen: [143, 188, 143], darkslateblue: [72, 61, 139], darkslategray: [47, 79, 79], darkslategrey: [47, 79, 79], darkturquoise: [0, 206, 209], darkviolet: [148, 0, 211], deeppink: [255, 20, 147], deepskyblue: [0, 191, 255], dimgray: [105, 105, 105], dimgrey: [105, 105, 105], dodgerblue: [30, 144, 255], firebrick: [178, 34, 34], floralwhite: [255, 250, 240], forestgreen: [34, 139, 34], fuchsia: [255, 0, 255], gainsboro: [220, 220, 220], ghostwhite: [248, 248, 255], gold: [255, 215, 0], goldenrod: [218, 165, 32], gray: [128, 128, 128], green: [0, 128, 0], greenyellow: [173, 255, 47], grey: [128, 128, 128], honeydew: [240, 255, 240], hotpink: [255, 105, 180], indianred: [205, 92, 92], indigo: [75, 0, 130], ivory: [255, 255, 240], khaki: [240, 230, 140], lavender: [230, 230, 250], lavenderblush: [255, 240, 245], lawngreen: [124, 252, 0], lemonchiffon: [255, 250, 205], lightblue: [173, 216, 230], lightcoral: [240, 128, 128], lightcyan: [224, 255, 255], lightgoldenrodyellow: [250, 250, 210], lightgray: [211, 211, 211], lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193], lightsalmon: [255, 160, 122], lightseagreen: [32, 178, 170], lightskyblue: [135, 206, 250], lightslategray: [119, 136, 153], lightslategrey: [119, 136, 153], lightsteelblue: [176, 196, 222], lightyellow: [255, 255, 224], lime: [0, 255, 0], limegreen: [50, 205, 50], linen: [250, 240, 230], magenta: [255, 0, 255], maroon: [128, 0, 0], mediumaquamarine: [102, 205, 170], mediumblue: [0, 0, 205], mediumorchid: [186, 85, 211], mediumpurple: [147, 112, 219], mediumseagreen: [60, 179, 113], mediumslateblue: [123, 104, 238], mediumspringgreen: [0, 250, 154], mediumturquoise: [72, 209, 204], mediumvioletred: [199, 21, 133], midnightblue: [25, 25, 112], mintcream: [245, 255, 250], mistyrose: [255, 228, 225], moccasin: [255, 228, 181], navajowhite: [255, 222, 173], navy: [0, 0, 128], oldlace: [253, 245, 230], olive: [128, 128, 0], olivedrab: [107, 142, 35], orange: [255, 165, 0], orangered: [255, 69, 0], orchid: [218, 112, 214], palegoldenrod: [238, 232, 170], palegreen: [152, 251, 152], paleturquoise: [175, 238, 238], palevioletred: [219, 112, 147], papayawhip: [255, 239, 213], peachpuff: [255, 218, 185], peru: [205, 133, 63], pink: [255, 192, 203], plum: [221, 160, 221], powderblue: [176, 224, 230], purple: [128, 0, 128], rebeccapurple: [102, 51, 153], red: [255, 0, 0], rosybrown: [188, 143, 143], royalblue: [65, 105, 225], saddlebrown: [139, 69, 19], salmon: [250, 128, 114], sandybrown: [244, 164, 96], seagreen: [46, 139, 87], seashell: [255, 245, 238], sienna: [160, 82, 45], silver: [192, 192, 192], skyblue: [135, 206, 235], slateblue: [106, 90, 205], slategray: [112, 128, 144], slategrey: [112, 128, 144], snow: [255, 250, 250], springgreen: [0, 255, 127], steelblue: [70, 130, 180], tan: [210, 180, 140], teal: [0, 128, 128], thistle: [216, 191, 216], tomato: [255, 99, 71], turquoise: [64, 224, 208], violet: [238, 130, 238], wheat: [245, 222, 179], white: [255, 255, 255], whitesmoke: [245, 245, 245], yellow: [255, 255, 0], yellowgreen: [154, 205, 50], }; /** * Color representation used by WebGL. * Defined in sRGB color space and pre-blended with alpha. * @private */ class Color { /** * @param r Red component premultiplied by `alpha` 0..1 * @param g Green component premultiplied by `alpha` 0..1 * @param b Blue component premultiplied by `alpha` 0..1 * @param [alpha=1] Alpha component 0..1 * @param [premultiplied=true] Whether the `r`, `g` and `b` values have already * been multiplied by alpha. If `true` nothing happens if `false` then they will * be multiplied automatically. */ constructor(r, g, b, alpha = 1, premultiplied = true) { this.r = r; this.g = g; this.b = b; this.a = alpha; if (!premultiplied) { this.r *= alpha; this.g *= alpha; this.b *= alpha; if (!alpha) { // alpha = 0 erases completely rgb channels. This behavior is not desirable // if this particular color is later used in color interpolation. // Because of that, a reference to original color is saved. this.overwriteGetter('rgb', [r, g, b, alpha]); } } } /** * Parses CSS color strings and converts colors to sRGB color space if needed. * Officially supported color formats: * - keyword, e.g. 'aquamarine' or 'steelblue' * - hex (with 3, 4, 6 or 8 digits), e.g. '#f0f' or '#e9bebea9' * - rgb and rgba, e.g. 'rgb(0,240,120)' or 'rgba(0%,94%,47%,0.1)' or 'rgb(0 240 120 / .3)' * - hsl and hsla, e.g. 'hsl(0,0%,83%)' or 'hsla(0,0%,83%,.5)' or 'hsl(0 0% 83% / 20%)' * * @param input CSS color string to parse. * @returns A `Color` instance, or `undefined` if the input is not a valid color string. */ static parse(input) { // in zoom-and-property function input could be an instance of Color class if (input instanceof Color) { return input; } if (typeof input !== 'string') { return; } const rgba = parseCssColor(input); if (rgba) { return new Color(...rgba, false); } } /** * Used in color interpolation and by 'to-rgba' expression. * * @returns Gien color, with reversed alpha blending, in sRGB color space. */ get rgb() { const { r, g, b, a } = this; const f = a || Infinity; // reverse alpha blending factor return this.overwriteGetter('rgb', [r / f, g / f, b / f, a]); } /** * Used in color interpolation. * * @returns Gien color, with reversed alpha blending, in HCL color space. */ get hcl() { return this.overwriteGetter('hcl', rgbToHcl(this.rgb)); } /** * Used in color interpolation. * * @returns Gien color, with reversed alpha blending, in LAB color space. */ get lab() { return this.overwriteGetter('lab', rgbToLab(this.rgb)); } /** * Lazy getter pattern. When getter is called for the first time lazy value * is calculated and then overwrites getter function in given object instance. * * @example: * const redColor = Color.parse('red'); * let x = redColor.hcl; // this will invoke `get hcl()`, which will calculate * // the value of red in HCL space and invoke this `overwriteGetter` function * // which in turn will set a field with a key 'hcl' in the `redColor` object. * // In other words it will override `get hcl()` from its `Color` prototype * // with its own property: hcl = [calculated red value in hcl]. * let y = redColor.hcl; // next call will no longer invoke getter but simply * // return the previously calculated value * x === y; // true - `x` is exactly the same object as `y` * * @param getterKey Getter key * @param lazyValue Lazily calculated value to be memoized by current instance * @private */ overwriteGetter(getterKey, lazyValue) { Object.defineProperty(this, getterKey, { value: lazyValue }); return lazyValue; } /** * Used by 'to-string' expression. * * @returns Serialized color in format `rgba(r,g,b,a)` * where r,g,b are numbers within 0..255 and alpha is number within 1..0 * * @example * var purple = new Color.parse('purple'); * purple.toString; // = "rgba(128,0,128,1)" * var translucentGreen = new Color.parse('rgba(26, 207, 26, .73)'); * translucentGreen.toString(); // = "rgba(26,207,26,0.73)" */ toString() { const [r, g, b, a] = this.rgb; return `rgba(${[r, g, b].map(n => Math.round(n * 255)).join(',')},${a})`; } } Color.black = new Color(0, 0, 0, 1); Color.white = new Color(1, 1, 1, 1); Color.transparent = new Color(0, 0, 0, 0); Color.red = new Color(1, 0, 0, 1); // Flow type declarations for Intl cribbed from // https://github.com/facebook/flow/issues/1270 class Collator { constructor(caseSensitive, diacriticSensitive, locale) { if (caseSensitive) this.sensitivity = diacriticSensitive ? 'variant' : 'case'; else this.sensitivity = diacriticSensitive ? 'accent' : 'base'; this.locale = locale; this.collator = new Intl.Collator(this.locale ? this.locale : [], { sensitivity: this.sensitivity, usage: 'search' }); } compare(lhs, rhs) { return this.collator.compare(lhs, rhs); } resolvedLocale() { // We create a Collator without "usage: search" because we don't want // the search options encoded in our result (e.g. "en-u-co-search") return new Intl.Collator(this.locale ? this.locale : []) .resolvedOptions().locale; } } class FormattedSection { constructor(text, image, scale, fontStack, textColor) { this.text = text; this.image = image; this.scale = scale; this.fontStack = fontStack; this.textColor = textColor; } } class Formatted { constructor(sections) { this.sections = sections; } static fromString(unformatted) { return new Formatted([new FormattedSection(unformatted, null, null, null, null)]); } isEmpty() { if (this.sections.length === 0) return true; return !this.sections.some(section => section.text.length !== 0 || (section.image && section.image.name.length !== 0)); } static factory(text) { if (text instanceof Formatted) { return text; } else { return Formatted.fromString(text); } } toString() { if (this.sections.length === 0) return ''; return this.sections.map(section => section.text).join(''); } } /** * A set of four numbers representing padding around a box. Create instances from * bare arrays or numeric values using the static method `Padding.parse`. * @private */ class Padding { constructor(values) { this.values = values.slice(); } /** * Numeric padding values * @param input A padding value * @returns A `Padding` instance, or `undefined` if the input is not a valid padding value. */ static parse(input) { if (input instanceof Padding) { return input; } // Backwards compatibility: bare number is treated the same as array with single value. // Padding applies to all four sides. if (typeof input === 'number') { return new Padding([input, input, input, input]); } if (!Array.isArray(input)) { return undefined; } if (input.length < 1 || input.length > 4) { return undefined; } for (const val of input) { if (typeof val !== 'number') { return undefined; } } // Expand shortcut properties into explicit 4-sided values switch (input.length) { case 1: input = [input[0], input[0], input[0], input[0]]; break; case 2: input = [input[0], input[1], input[0], input[1]]; break; case 3: input = [input[0], input[1], input[2], input[1]]; break; } return new Padding(input); } toString() { return JSON.stringify(this.values); } } /** Set of valid anchor positions, as a set for validation */ const anchors = new Set(['center', 'left', 'right', 'top', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right']); /** * Utility class to assist managing values for text-variable-anchor-offset property. Create instances from * bare arrays using the static method `VariableAnchorOffsetCollection.parse`. * @private */ class VariableAnchorOffsetCollection { constructor(values) { this.values = values.slice(); } static parse(input) { if (input instanceof VariableAnchorOffsetCollection) { return input; } if (!Array.isArray(input) || input.length < 1 || input.length % 2 !== 0) { return undefined; } for (let i = 0; i < input.length; i += 2) { // Elements in even positions should be anchor positions; Elements in odd positions should be offset values const anchorValue = input[i]; const offsetValue = input[i + 1]; if (typeof anchorValue !== 'string' || !anchors.has(anchorValue)) { return undefined; } if (!Array.isArray(offsetValue) || offsetValue.length !== 2 || typeof offsetValue[0] !== 'number' || typeof offsetValue[1] !== 'number') { return undefined; } } return new VariableAnchorOffsetCollection(input); } toString() { return JSON.stringify(this.values); } } class ResolvedImage { constructor(options) { this.name = options.name; this.available = options.available; } toString() { return this.name; } static fromString(name) { if (!name) return null; // treat empty values as no image return new ResolvedImage({ name, available: false }); } } function validateRGBA(r, g, b, a) { if (!(typeof r === 'number' && r >= 0 && r <= 255 && typeof g === 'number' && g >= 0 && g <= 255 && typeof b === 'number' && b >= 0 && b <= 255)) { const value = typeof a === 'number' ? [r, g, b, a] : [r, g, b]; return `Invalid rgba value [${value.join(', ')}]: 'r', 'g', and 'b' must be between 0 and 255.`; } if (!(typeof a === 'undefined' || (typeof a === 'number' && a >= 0 && a <= 1))) { return `Invalid rgba value [${[r, g, b, a].join(', ')}]: 'a' must be between 0 and 1.`; } return null; } function isValue(mixed) { if (mixed === null || typeof mixed === 'string' || typeof mixed === 'boolean' || typeof mixed === 'number' || mixed instanceof Color || mixed instanceof Collator || mixed instanceof Formatted || mixed instanceof Padding || mixed instanceof VariableAnchorOffsetCollection || mixed instanceof ResolvedImage) { return true; } else if (Array.isArray(mixed)) { for (const item of mixed) { if (!isValue(item)) { return false; } } return true; } else if (typeof mixed === 'object') { for (const key in mixed) { if (!isValue(mixed[key])) { return false; } } return true; } else { return false; } } function typeOf(value) { if (value === null) { return NullType; } else if (typeof value === 'string') { return StringType; } else if (typeof value === 'boolean') { return BooleanType; } else if (typeof value === 'number') { return NumberType; } else if (value instanceof Color) { return ColorType; } else if (value instanceof Collator) { return CollatorType; } else if (value instanceof Formatted) { return FormattedType; } else if (value instanceof Padding) { return PaddingType; } else if (value instanceof VariableAnchorOffsetCollection) { return VariableAnchorOffsetCollectionType; } else if (value instanceof ResolvedImage) { return ResolvedImageType; } else if (Array.isArray(value)) { const length = value.length; let itemType; for (const item of value) { const t = typeOf(item); if (!itemType) { itemType = t; } else if (itemType === t) { continue; } else { itemType = ValueType; break; } } return array$1(itemType || ValueType, length); } else { return ObjectType; } } function toString(value) { const type = typeof value; if (value === null) { return ''; } else if (type === 'string' || type === 'number' || type === 'boolean') { return String(value); } else if (value instanceof Color || value instanceof Formatted || value instanceof Padding || value instanceof VariableAnchorOffsetCollection || value instanceof ResolvedImage) { return value.toString(); } else { return JSON.stringify(value); } } class Literal { constructor(type, value) { this.type = type; this.value = value; } static parse(args, context) { if (args.length !== 2) return context.error(`'literal' expression requires exactly one argument, but found ${args.length - 1} instead.`); if (!isValue(args[1])) return context.error('invalid value'); const value = args[1]; let type = typeOf(value); // special case: infer the item type if possible for zero-length arrays const expected = context.expectedType; if (type.kind === 'array' && type.N === 0 && expected && expected.kind === 'array' && (typeof expected.N !== 'number' || expected.N === 0)) { type = expected; } return new Literal(type, value); } evaluate() { return this.value; } eachChild() { } outputDefined() { return true; } } class RuntimeError { constructor(message) { this.name = 'ExpressionEvaluationError'; this.message = message; } toJSON() { return this.message; } } const types$1 = { string: StringType, number: NumberType, boolean: BooleanType, object: ObjectType }; class Assertion { constructor(type, args) { this.type = type; this.args = args; } static parse(args, context) { if (args.length < 2) return context.error('Expected at least one argument.'); let i = 1; let type; const name = args[0]; if (name === 'array') { let itemType; if (args.length > 2) { const type = args[1]; if (typeof type !== 'string' || !(type in types$1) || type === 'object') return context.error('The item type argument of "array" must be one of string, number, boolean', 1); itemType = types$1[type]; i++; } else { itemType = ValueType; } let N; if (args.length > 3) { if (args[2] !== null && (typeof args[2] !== 'number' || args[2] < 0 || args[2] !== Math.floor(args[2]))) { return context.error('The length argument to "array" must be a positive integer literal', 2); } N = args[2]; i++; } type = array$1(itemType, N); } else { if (!types$1[name]) throw new Error(`Types doesn't contain name = ${name}`); type = types$1[name]; } const parsed = []; for (; i < args.length; i++) { const input = context.parse(args[i], i, ValueType); if (!input) return null; parsed.push(input); } return new Assertion(type, parsed); } evaluate(ctx) { for (let i = 0; i < this.args.length; i++) { const value = this.args[i].evaluate(ctx); const error = checkSubtype(this.type, typeOf(value)); if (!error) { return value; } else if (i === this.args.length - 1) { throw new RuntimeError(`Expected value to be of type ${toString$1(this.type)}, but found ${toString$1(typeOf(value))} instead.`); } } throw new Error(); } eachChild(fn) { this.args.forEach(fn); } outputDefined() { return this.args.every(arg => arg.outputDefined()); } } const types = { 'to-boolean': BooleanType, 'to-color': ColorType, 'to-number': NumberType, 'to-string': StringType }; /** * Special form for error-coalescing coercion expressions "to-number", * "to-color". Since these coercions can fail at runtime, they accept multiple * arguments, only evaluating one at a time until one succeeds. * * @private */ class Coercion { constructor(type, args) { this.type = type; this.args = args; } static parse(args, context) { if (args.length < 2) return context.error('Expected at least one argument.'); const name = args[0]; if (!types[name]) throw new Error(`Can't parse ${name} as it is not part of the known types`); if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2) return context.error('Expected one argument.'); const type = types[name]; const parsed = []; for (let i = 1; i < args.length; i++) { const input = context.parse(args[i], i, ValueType); if (!input) return null; parsed.push(input); } return new Coercion(type, parsed); } evaluate(ctx) { switch (this.type.kind) { case 'boolean': return Boolean(this.args[0].evaluate(ctx)); case 'color': { let input; let error; for (const arg of this.args) { input = arg.evaluate(ctx); error = null; if (input instanceof Color) { return input; } else if (typeof input === 'string') { const c = ctx.parseColor(input); if (c) return c; } else if (Array.isArray(input)) { if (input.length < 3 || input.length > 4) { error = `Invalid rbga value ${JSON.stringify(input)}: expected an array containing either three or four numeric values.`; } else { error = validateRGBA(input[0], input[1], input[2], input[3]); } if (!error) { return new Color(input[0] / 255, input[1] / 255, input[2] / 255, input[3]); } } } throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`); } case 'padding': { let input; for (const arg of this.args) { input = arg.evaluate(ctx); const pad = Padding.parse(input); if (pad) { return pad; } } throw new RuntimeError(`Could not parse padding from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`); } case 'variableAnchorOffsetCollection': { let input; for (const arg of this.args) { input = arg.evaluate(ctx); const coll = VariableAnchorOffsetCollection.parse(input); if (coll) { return coll; } } throw new RuntimeError(`Could not parse variableAnchorOffsetCollection from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`); } case 'number': { let value = null; for (const arg of this.args) { value = arg.evaluate(ctx); if (value === null) return 0; const num = Number(value); if (isNaN(num)) continue; return num; } throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`); } case 'formatted': // There is no explicit 'to-formatted' but this coercion can be implicitly // created by properties that expect the 'formatted' type. return Formatted.fromString(toString(this.args[0].evaluate(ctx))); case 'resolvedImage': return ResolvedImage.fromString(toString(this.args[0].evaluate(ctx))); default: return toString(this.args[0].evaluate(ctx)); } } eachChild(fn) { this.args.forEach(fn); } outputDefined() { return this.args.every(arg => arg.outputDefined()); } } const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon']; class EvaluationContext { constructor() { this.globals = null; this.feature = null; this.featureState = null; this.formattedSection = null; this._parseColorCache = {}; this.availableImages = null; this.canonical = null; } id() { return this.feature && 'id' in this.feature ? this.feature.id : null; } geometryType() { return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null; } geometry() { return this.feature && 'geometry' in this.feature ? this.feature.geometry : null; } canonicalID() { return this.canonical; } properties() { return this.feature && this.feature.properties || {}; } parseColor(input) { let cached = this._parseColorCache[input]; if (!cached) { cached = this._parseColorCache[input] = Color.parse(input); } return cached; } } /** * State associated parsing at a given point in an expression tree. * @private */ class ParsingContext { constructor(registry, isConstantFunc, path = [], expectedType, scope = new Scope(), errors = []) { this.registry = registry; this.path = path; this.key = path.map(part => `[${part}]`).join(''); this.scope = scope; this.errors = errors; this.expectedType = expectedType; this._isConstant = isConstantFunc; } /** * @param expr the JSON expression to parse * @param index the optional argument index if this expression is an argument of a parent expression that's being parsed * @param options * @param options.omitTypeAnnotations set true to omit inferred type annotations. Caller beware: with this option set, the parsed expression's type will NOT satisfy `expectedType` if it would normally be wrapped in an inferred annotation. * @private */ parse(expr, index, expectedType, bindings, options = {}) { if (index) { return this.concat(index, expectedType, bindings)._parse(expr, options); } return this._parse(expr, options); } _parse(expr, options) { if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') { expr = ['literal', expr]; } function annotate(parsed, type, typeAnnotation) { if (typeAnnotation === 'assert') { return new Assertion(type, [parsed]); } else if (typeAnnotation === 'coerce') { return new Coercion(type, [parsed]); } else { return parsed; } } if (Array.isArray(expr)) { if (expr.length === 0) { return this.error('Expected an array with at least one element. If you wanted a literal array, use ["literal", []].'); } const op = expr[0]; if (typeof op !== 'string') { this.error(`Expression name must be a string, but found ${typeof op} instead. If you wanted a literal array, use ["literal", [...]].`, 0); return null; } const Expr = this.registry[op]; if (Expr) { let parsed = Expr.parse(expr, this); if (!parsed) return null; if (this.expectedType) { const expected = this.expectedType; const actual = parsed.type; // When we expect a number, string, boolean, or array but have a value, wrap it in an assertion. // When we expect a color or formatted string, but have a string or value, wrap it in a coercion. // Otherwise, we do static type-checking. // // These behaviors are overridable for: // * The "coalesce" operator, which needs to omit type annotations. // * String-valued properties (e.g. `text-field`), where coercion is more convenient than assertion. // if ((expected.kind === 'string' || expected.kind === 'number' || expected.kind === 'boolean' || expected.kind === 'object' || expected.kind === 'array') && actual.kind === 'value') { parsed = annotate(parsed, expected, options.typeAnnotation || 'assert'); } else if ((expected.kind === 'color' || expected.kind === 'formatted' || expected.kind === 'resolvedImage') && (actual.kind === 'value' || actual.kind === 'string')) { parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); } else if (expected.kind === 'padding' && (actual.kind === 'value' || actual.kind === 'number' || actual.kind === 'array')) { parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); } else if (expected.kind === 'variableAnchorOffsetCollection' && (actual.kind === 'value' || actual.kind === 'array')) { parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce'); } else if (this.checkSubtype(expected, actual)) { return null; } } // If an expression's arguments are all literals, we can evaluate // it immediately and replace it with a literal value in the // parsed/compiled result. Expressions that expect an image should // not be resolved here so we can later get the available images. if (!(parsed instanceof Literal) && (parsed.type.kind !== 'resolvedImage') && this._isConstant(parsed)) { const ec = new EvaluationContext(); try { parsed = new Literal(parsed.type, parsed.evaluate(ec)); } catch (e) { this.error(e.message); return null; } } return parsed; } return this.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0); } else if (typeof expr === 'undefined') { return this.error('\'undefined\' value invalid. Use null instead.'); } else if (typeof expr === 'object') { return this.error('Bare objects invalid. Use ["literal", {...}] instead.'); } else { return this.error(`Expected an array, but found ${typeof expr} instead.`); } } /** * Returns a copy of this context suitable for parsing the subexpression at * index `index`, optionally appending to 'let' binding map. * * Note that `errors` property, intended for collecting errors while * parsing, is copied by reference rather than cloned. * @private */ concat(index, expectedType, bindings) { const path = typeof index === 'number' ? this.path.concat(index) : this.path; const scope = bindings ? this.scope.concat(bindings) : this.scope; return new ParsingContext(this.registry, this._isConstant, path, expectedType || null, scope, this.errors); } /** * Push a parsing (or type checking) error into the `this.errors` * @param error The message * @param keys Optionally specify the source of the error at a child * of the current expression at `this.key`. * @private */ error(error, ...keys) { const key = `${this.key}${keys.map(k => `[${k}]`).join('')}`; this.errors.push(new ExpressionParsingError(key, error)); } /** * Returns null if `t` is a subtype of `expected`; otherwise returns an * error message and also pushes it to `this.errors`. * @param expected The expected type * @param t The actual type * @returns null if `t` is a subtype of `expected`; otherwise returns an error message */ checkSubtype(expected, t) { const error = checkSubtype(expected, t); if (error) this.error(error); return error; } } class Let { constructor(bindings, result) { this.type = result.type; this.bindings = [].concat(bindings); this.result = result; } evaluate(ctx) { return this.result.evaluate(ctx); } eachChild(fn) { for (const binding of this.bindings) { fn(binding[1]); } fn(this.result); } static parse(args, context) { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`); const bindings = []; for (let i = 1; i < args.length - 1; i += 2) { const name = args[i]; if (typeof name !== 'string') { return context.error(`Expected string, but found ${typeof name} instead.`, i); } if (/[^a-zA-Z0-9_]/.test(name)) { return context.error('Variable names must contain only alphanumeric characters or \'_\'.', i); } const value = context.parse(args[i + 1], i + 1); if (!value) return null; bindings.push([name, value]); } const result = context.parse(args[args.length - 1], args.length - 1, context.expectedType, bindings); if (!result) return null; return new Let(bindings, result); } outputDefined() { return this.result.outputDefined(); } } class Var { constructor(name, boundExpression) { this.type = boundExpression.type; this.name = name; this.boundExpression = boundExpression; } static parse(args, context) { if (args.length !== 2 || typeof args[1] !== 'string') return context.error('\'var\' expression requires exactly one string literal argument.'); const name = args[1]; if (!context.scope.has(name)) { return context.error(`Unknown variable "${name}". Make sure "${name}" has been bound in an enclosing "let" expression before using it.`, 1); } return new Var(name, context.scope.get(name)); } evaluate(ctx) { return this.boundExpression.evaluate(ctx); } eachChild() { } outputDefined() { return false; } } class At { constructor(type, index, input) { this.type = type; this.index = index; this.input = input; } static parse(args, context) { if (args.length !== 3) return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); const index = context.parse(args[1], 1, NumberType); const input = context.parse(args[2], 2, array$1(context.expectedType || ValueType)); if (!index || !input) return null; const t = input.type; return new At(t.itemType, index, input); } evaluate(ctx) { const index = this.index.evaluate(ctx); const array = this.input.evaluate(ctx); if (index < 0) { throw new RuntimeError(`Array index out of bounds: ${index} < 0.`); } if (index >= array.length) { throw new RuntimeError(`Array index out of bounds: ${index} > ${array.length - 1}.`); } if (index !== Math.floor(index)) { throw new RuntimeError(`Array index must be an integer, but found ${index} instead.`); } return array[index]; } eachChild(fn) { fn(this.index); fn(this.input); } outputDefined() { return false; } } class In { constructor(needle, haystack) { this.type = BooleanType; this.needle = needle; this.haystack = haystack; } static parse(args, context) { if (args.length !== 3) { return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); } const needle = context.parse(args[1], 1, ValueType); const haystack = context.parse(args[2], 2, ValueType); if (!needle || !haystack) return null; if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) { return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${toString$1(needle.type)} instead`); } return new In(needle, haystack); } evaluate(ctx) { const needle = this.needle.evaluate(ctx); const haystack = this.haystack.evaluate(ctx); if (!haystack) return false; if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) { throw new RuntimeError(`Expected first argument to be of type boolean, string, number or null, but found ${toString$1(typeOf(needle))} instead.`); } if (!isValidNativeType(haystack, ['string', 'array'])) { throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString$1(typeOf(haystack))} instead.`); } return haystack.indexOf(needle) >= 0; } eachChild(fn) { fn(this.needle); fn(this.haystack); } outputDefined() { return true; } } class IndexOf { constructor(needle, haystack, fromIndex) { this.type = NumberType; this.needle = needle; this.haystack = haystack; this.fromIndex = fromIndex; } static parse(args, context) { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } const needle = context.parse(args[1], 1, ValueType); const haystack = context.parse(args[2], 2, ValueType); if (!needle || !haystack) return null; if (!isValidType(needle.type, [BooleanType, StringType, NumberType, NullType, ValueType])) { return context.error(`Expected first argument to be of type boolean, string, number or null, but found ${toString$1(needle.type)} instead`); } if (args.length === 4) { const fromIndex = context.parse(args[3], 3, NumberType); if (!fromIndex) return null; return new IndexOf(needle, haystack, fromIndex); } else { return new IndexOf(needle, haystack); } } evaluate(ctx) { const needle = this.needle.evaluate(ctx); const haystack = this.haystack.evaluate(ctx); if (!isValidNativeType(needle, ['boolean', 'string', 'number', 'null'])) { throw new RuntimeError(`Expected first argument to be of type boolean, string, number or null, but found ${toString$1(typeOf(needle))} instead.`); } let fromIndex; if (this.fromIndex) { fromIndex = this.fromIndex.evaluate(ctx); } if (isValidNativeType(haystack, ['string'])) { const rawIndex = haystack.indexOf(needle, fromIndex); if (rawIndex === -1) { return -1; } else { // The index may be affected by surrogate pairs, so get the length of the preceding substring. return [...haystack.slice(0, rawIndex)].length; } } else if (isValidNativeType(haystack, ['array'])) { return haystack.indexOf(needle, fromIndex); } else { throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString$1(typeOf(haystack))} instead.`); } } eachChild(fn) { fn(this.needle); fn(this.haystack); if (this.fromIndex) { fn(this.fromIndex); } } outputDefined() { return false; } } class Match { constructor(inputType, outputType, input, cases, outputs, otherwise) { this.inputType = inputType; this.type = outputType; this.input = input; this.cases = cases; this.outputs = outputs; this.otherwise = otherwise; } static parse(args, context) { if (args.length < 5) return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 1) return context.error('Expected an even number of arguments.'); let inputType; let outputType; if (context.expectedType && context.expectedType.kind !== 'value') { outputType = context.expectedType; } const cases = {}; const outputs = []; for (let i = 2; i < args.length - 1; i += 2) { let labels = args[i]; const value = args[i + 1]; if (!Array.isArray(labels)) { labels = [labels]; } const labelContext = context.concat(i); if (labels.length === 0) { return labelContext.error('Expected at least one branch label.'); } for (const label of labels) { if (typeof label !== 'number' && typeof label !== 'string') { return labelContext.error('Branch labels must be numbers or strings.'); } else if (typeof label === 'number' && Math.abs(label) > Number.MAX_SAFE_INTEGER) { return labelContext.error(`Branch labels must be integers no larger than ${Number.MAX_SAFE_INTEGER}.`); } else if (typeof label === 'number' && Math.floor(label) !== label) { return labelContext.error('Numeric branch labels must be integer values.'); } else if (!inputType) { inputType = typeOf(label); } else if (labelContext.checkSubtype(inputType, typeOf(label))) { return null; } if (typeof cases[String(label)] !== 'undefined') { return labelContext.error('Branch labels must be unique.'); } cases[String(label)] = outputs.length; } const result = context.parse(value, i, outputType); if (!result) return null; outputType = outputType || result.type; outputs.push(result); } const input = context.parse(args[1], 1, ValueType); if (!input) return null; const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType); if (!otherwise) return null; if (input.type.kind !== 'value' && context.concat(1).checkSubtype(inputType, input.type)) { return null; } return new Match(inputType, outputType, input, cases, outputs, otherwise); } evaluate(ctx) { const input = this.input.evaluate(ctx); const output = (typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise; return output.evaluate(ctx); } eachChild(fn) { fn(this.input); this.outputs.forEach(fn); fn(this.otherwise); } outputDefined() { return this.outputs.every(out => out.outputDefined()) && this.otherwise.outputDefined(); } } class Case { constructor(type, branches, otherwise) { this.type = type; this.branches = branches; this.otherwise = otherwise; } static parse(args, context) { if (args.length < 4) return context.error(`Expected at least 3 arguments, but found only ${args.length - 1}.`); if (args.length % 2 !== 0) return context.error('Expected an odd number of arguments.'); let outputType; if (context.expectedType && context.expectedType.kind !== 'value') { outputType = context.expectedType; } const branches = []; for (let i = 1; i < args.length - 1; i += 2) { const test = context.parse(args[i], i, BooleanType); if (!test) return null; const result = context.parse(args[i + 1], i + 1, outputType); if (!result) return null; branches.push([test, result]); outputType = outputType || result.type; } const otherwise = context.parse(args[args.length - 1], args.length - 1, outputType); if (!otherwise) return null; if (!outputType) throw new Error('Can\'t infer output type'); return new Case(outputType, branches, otherwise); } evaluate(ctx) { for (const [test, expression] of this.branches) { if (test.evaluate(ctx)) { return expression.evaluate(ctx); } } return this.otherwise.evaluate(ctx); } eachChild(fn) { for (const [test, expression] of this.branches) { fn(test); fn(expression); } fn(this.otherwise); } outputDefined() { return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined(); } } class Slice { constructor(type, input, beginIndex, endIndex) { this.type = type; this.input = input; this.beginIndex = beginIndex; this.endIndex = endIndex; } static parse(args, context) { if (args.length <= 2 || args.length >= 5) { return context.error(`Expected 3 or 4 arguments, but found ${args.length - 1} instead.`); } const input = context.parse(args[1], 1, ValueType); const beginIndex = context.parse(args[2], 2, NumberType); if (!input || !beginIndex) return null; if (!isValidType(input.type, [array$1(ValueType), StringType, ValueType])) { return context.error(`Expected first argument to be of type array or string, but found ${toString$1(input.type)} instead`); } if (args.length === 4) { const endIndex = context.parse(args[3], 3, NumberType); if (!endIndex) return null; return new Slice(input.type, input, beginIndex, endIndex); } else { return new Slice(input.type, input, beginIndex); } } evaluate(ctx) { const input = this.input.evaluate(ctx); const beginIndex = this.beginIndex.evaluate(ctx); let endIndex; if (this.endIndex) { endIndex = this.endIndex.evaluate(ctx); } if (isValidNativeType(input, ['string'])) { // Indices may be affected by surrogate pairs. return [...input].slice(beginIndex, endIndex).join(''); } else if (isValidNativeType(input, ['array'])) { return input.slice(beginIndex, endIndex); } else { throw new RuntimeError(`Expected first argument to be of type array or string, but found ${toString$1(typeOf(input))} instead.`); } } eachChild(fn) { fn(this.input); fn(this.beginIndex); if (this.endIndex) { fn(this.endIndex); } } outputDefined() { return false; } } /** * Returns the index of the last stop <= input, or 0 if it doesn't exist. * @private */ function findStopLessThanOrEqualTo(stops, input) { const lastIndex = stops.length - 1; let lowerIndex = 0; let upperIndex = lastIndex; let currentIndex = 0; let currentValue, nextValue; while (lowerIndex <= upperIndex) { currentIndex = Math.floor((lowerIndex + upperIndex) / 2); currentValue = stops[currentIndex]; nextValue = stops[currentIndex + 1]; if (currentValue <= input) { if (currentIndex === lastIndex || input < nextValue) { // Search complete return currentIndex; } lowerIndex = currentIndex + 1; } else if (currentValue > input) { upperIndex = currentIndex - 1; } else { throw new RuntimeError('Input is not a number.'); } } return 0; } class Step { constructor(type, input, stops) { this.type = type; this.input = input; this.labels = []; this.outputs = []; for (const [label, expression] of stops) { this.labels.push(label); this.outputs.push(expression); } } static parse(args, context) { if (args.length - 1 < 4) { return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); } if ((args.length - 1) % 2 !== 0) { return context.error('Expected an even number of arguments.'); } const input = context.parse(args[1], 1, NumberType); if (!input) return null; const stops = []; let outputType = null; if (context.expectedType && context.expectedType.kind !== 'value') { outputType = context.expectedType; } for (let i = 1; i < args.length; i += 2) { const label = i === 1 ? -Infinity : args[i]; const value = args[i + 1]; const labelKey = i; const valueKey = i + 1; if (typeof label !== 'number') { return context.error('Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values.', labelKey); } if (stops.length && stops[stops.length - 1][0] >= label) { return context.error('Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.', labelKey); } const parsed = context.parse(value, valueKey, outputType); if (!parsed) return null; outputType = outputType || parsed.type; stops.push([label, parsed]); } return new Step(outputType, input, stops); } evaluate(ctx) { const labels = this.labels; const outputs = this.outputs; if (labels.length === 1) { return outputs[0].evaluate(ctx); } const value = this.input.evaluate(ctx); if (value <= labels[0]) { return outputs[0].evaluate(ctx); } const stopCount = labels.length; if (value >= labels[stopCount - 1]) { return outputs[stopCount - 1].evaluate(ctx); } const index = findStopLessThanOrEqualTo(labels, value); return outputs[index].evaluate(ctx); } eachChild(fn) { fn(this.input); for (const expression of this.outputs) { fn(expression); } } outputDefined() { return this.outputs.every(out => out.outputDefined()); } } var unitbezier = UnitBezier; function UnitBezier(p1x, p1y, p2x, p2y) { // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). this.cx = 3.0 * p1x; this.bx = 3.0 * (p2x - p1x) - this.cx; this.ax = 1.0 - this.cx - this.bx; this.cy = 3.0 * p1y; this.by = 3.0 * (p2y - p1y) - this.cy; this.ay = 1.0 - this.cy - this.by; this.p1x = p1x; this.p1y = p1y; this.p2x = p2x; this.p2y = p2y; } UnitBezier.prototype = { sampleCurveX: function (t) { // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. return ((this.ax * t + this.bx) * t + this.cx) * t; }, sampleCurveY: function (t) { return ((this.ay * t + this.by) * t + this.cy) * t; }, sampleCurveDerivativeX: function (t) { return (3.0 * this.ax * t + 2.0 * this.bx) * t + this.cx; }, solveCurveX: function (x, epsilon) { if (epsilon === undefined) epsilon = 1e-6; if (x < 0.0) return 0.0; if (x > 1.0) return 1.0; var t = x; // First try a few iterations of Newton's method - normally very fast. for (var i = 0; i < 8; i++) { var x2 = this.sampleCurveX(t) - x; if (Math.abs(x2) < epsilon) return t; var d2 = this.sampleCurveDerivativeX(t); if (Math.abs(d2) < 1e-6) break; t = t - x2 / d2; } // Fall back to the bisection method for reliability. var t0 = 0.0; var t1 = 1.0; t = x; for (i = 0; i < 20; i++) { x2 = this.sampleCurveX(t); if (Math.abs(x2 - x) < epsilon) break; if (x > x2) { t0 = t; } else { t1 = t; } t = (t1 - t0) * 0.5 + t0; } return t; }, solve: function (x, epsilon) { return this.sampleCurveY(this.solveCurveX(x, epsilon)); } }; var UnitBezier$1 = /*@__PURE__*/getDefaultExportFromCjs(unitbezier); function number(from, to, t) { return from + t * (to - from); } function color(from, to, t, spaceKey = 'rgb') { switch (spaceKey) { case 'rgb': { const [r, g, b, alpha] = array(from.rgb, to.rgb, t); return new Color(r, g, b, alpha, false); } case 'hcl': { const [hue0, chroma0, light0, alphaF] = from.hcl; const [hue1, chroma1, light1, alphaT] = to.hcl; // https://github.com/gka/chroma.js/blob/cd1b3c0926c7a85cbdc3b1453b3a94006de91a92/src/interpolator/_hsx.js let hue, chroma; if (!isNaN(hue0) && !isNaN(hue1)) { let dh = hue1 - hue0; if (hue1 > hue0 && dh > 180) { dh -= 360; } else if (hue1 < hue0 && hue0 - hue1 > 180) { dh += 360; } hue = hue0 + t * dh; } else if (!isNaN(hue0)) { hue = hue0; if (light1 === 1 || light1 === 0) chroma = chroma0; } else if (!isNaN(hue1)) { hue = hue1; if (light0 === 1 || light0 === 0) chroma = chroma1; } else { hue = NaN; } const [r, g, b, alpha] = hclToRgb([ hue, chroma !== null && chroma !== void 0 ? chroma : number(chroma0, chroma1, t), number(light0, light1, t), number(alphaF, alphaT, t), ]); return new Color(r, g, b, alpha, false); } case 'lab': { const [r, g, b, alpha] = labToRgb(array(from.lab, to.lab, t)); return new Color(r, g, b, alpha, false); } } } function array(from, to, t) { return from.map((d, i) => { return number(d, to[i], t); }); } function padding(from, to, t) { return new Padding(array(from.values, to.values, t)); } function variableAnchorOffsetCollection(from, to, t) { const fromValues = from.values; const toValues = to.values; if (fromValues.length !== toValues.length) { throw new RuntimeError(`Cannot interpolate values of different length. from: ${from.toString()}, to: ${to.toString()}`); } const output = []; for (let i = 0; i < fromValues.length; i += 2) { // Anchor entries must match if (fromValues[i] !== toValues[i]) { throw new RuntimeError(`Cannot interpolate values containing mismatched anchors. from[${i}]: ${fromValues[i]}, to[${i}]: ${toValues[i]}`); } output.push(fromValues[i]); // Interpolate the offset values for each anchor const [fx, fy] = fromValues[i + 1]; const [tx, ty] = toValues[i + 1]; output.push([number(fx, tx, t), number(fy, ty, t)]); } return new VariableAnchorOffsetCollection(output); } const interpolate = { number, color, array, padding, variableAnchorOffsetCollection }; class Interpolate { constructor(type, operator, interpolation, input, stops) { this.type = type; this.operator = operator; this.interpolation = interpolation; this.input = input; this.labels = []; this.outputs = []; for (const [label, expression] of stops) { this.labels.push(label); this.outputs.push(expression); } } static interpolationFactor(interpolation, input, lower, upper) { let t = 0; if (interpolation.name === 'exponential') { t = exponentialInterpolation(input, interpolation.base, lower, upper); } else if (interpolation.name === 'linear') { t = exponentialInterpolation(input, 1, lower, upper); } else if (interpolation.name === 'cubic-bezier') { const c = interpolation.controlPoints; const ub = new UnitBezier$1(c[0], c[1], c[2], c[3]); t = ub.solve(exponentialInterpolation(input, 1, lower, upper)); } return t; } static parse(args, context) { let [operator, interpolation, input, ...rest] = args; if (!Array.isArray(interpolation) || interpolation.length === 0) { return context.error('Expected an interpolation type expression.', 1); } if (interpolation[0] === 'linear') { interpolation = { name: 'linear' }; } else if (interpolation[0] === 'exponential') { const base = interpolation[1]; if (typeof base !== 'number') return context.error('Exponential interpolation requires a numeric base.', 1, 1); interpolation = { name: 'exponential', base }; } else if (interpolation[0] === 'cubic-bezier') { const controlPoints = interpolation.slice(1); if (controlPoints.length !== 4 || controlPoints.some(t => typeof t !== 'number' || t < 0 || t > 1)) { return context.error('Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.', 1); } interpolation = { name: 'cubic-bezier', controlPoints: controlPoints }; } else { return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0); } if (args.length - 1 < 4) { return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`); } if ((args.length - 1) % 2 !== 0) { return context.error('Expected an even number of arguments.'); } input = context.parse(input, 2, NumberType); if (!input) return null; const stops = []; let outputType = null; if (operator === 'interpolate-hcl' || operator === 'interpolate-lab') { outputType = ColorType; } else if (context.expectedType && context.expectedType.kind !== 'value') { outputType = context.expectedType; } for (let i = 0; i < rest.length; i += 2) { const label = rest[i]; const value = rest[i + 1]; const labelKey = i + 3; const valueKey = i + 4; if (typeof label !== 'number') { return context.error('Input/output pairs for "interpolate" expressions must be defined using literal numeric values (not computed expressions) for the input values.', labelKey); } if (stops.length && stops[stops.length - 1][0] >= label) { return context.error('Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.', labelKey); } const parsed = context.parse(value, valueKey, outputType); if (!parsed) return null; outputType = outputType || parsed.type; stops.push([label, parsed]); } if (!verifyType(outputType, NumberType) && !verifyType(outputType, ColorType) && !verifyType(outputType, PaddingType) && !verifyType(outputType, VariableAnchorOffsetCollectionType) && !verifyType(outputType, array$1(NumberType))) { return context.error(`Type ${toString$1(outputType)} is not interpolatable.`); } return new Interpolate(outputType, operator, interpolation, input, stops); } evaluate(ctx) { const labels = this.labels; const outputs = this.outputs; if (labels.length === 1) { return outputs[0].evaluate(ctx); } const value = this.input.evaluate(ctx); if (value <= labels[0]) { return outputs[0].evaluate(ctx); } const stopCount = labels.length; if (value >= labels[stopCount - 1]) { return outputs[stopCount - 1].evaluate(ctx); } const index = findStopLessThanOrEqualTo(labels, value); const lower = labels[index]; const upper = labels[index + 1]; const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper); const outputLower = outputs[index].evaluate(ctx); const outputUpper = outputs[index + 1].evaluate(ctx); switch (this.operator) { case 'interpolate': return interpolate[this.type.kind](outputLower, outputUpper, t); case 'interpolate-hcl': return interpolate.color(outputLower, outputUpper, t, 'hcl'); case 'interpolate-lab': return interpolate.color(outputLower, outputUpper, t, 'lab'); } } eachChild(fn) { fn(this.input); for (const expression of this.outputs) { fn(expression); } } outputDefined() { return this.outputs.every(out => out.outputDefined()); } } /** * Returns a ratio that can be used to interpolate between exponential function * stops. * How it works: Two consecutive stop values define a (scaled and shifted) exponential function `f(x) = a * base^x + b`, where `base` is the user-specified base, * and `a` and `b` are constants affording sufficient degrees of freedom to fit * the function to the given stops. * * Here's a bit of algebra that lets us compute `f(x)` directly from the stop * values without explicitly solving for `a` and `b`: * * First stop value: `f(x0) = y0 = a * base^x0 + b` * Second stop value: `f(x1) = y1 = a * base^x1 + b` * => `y1 - y0 = a(base^x1 - base^x0)` * => `a = (y1 - y0)/(base^x1 - base^x0)` * * Desired value: `f(x) = y = a * base^x + b` * => `f(x) = y0 + a * (base^x - base^x0)` * * From the above, we can replace the `a` in `a * (base^x - base^x0)` and do a * little algebra: * ``` * a * (base^x - base^x0) = (y1 - y0)/(base^x1 - base^x0) * (base^x - base^x0) * = (y1 - y0) * (base^x - base^x0) / (base^x1 - base^x0) * ``` * * If we let `(base^x - base^x0) / (base^x1 base^x0)`, then we have * `f(x) = y0 + (y1 - y0) * ratio`. In other words, `ratio` may be treated as * an interpolation factor between the two stops' output values. * * (Note: a slightly different form for `ratio`, * `(base^(x-x0) - 1) / (base^(x1-x0) - 1) `, is equivalent, but requires fewer * expensive `Math.pow()` operations.) * * @private */ function exponentialInterpolation(input, base, lowerValue, upperValue) { const difference = upperValue - lowerValue; const progress = input - lowerValue; if (difference === 0) { return 0; } else if (base === 1) { return progress / difference; } else { return (Math.pow(base, progress) - 1) / (Math.pow(base, difference) - 1); } } class Coalesce { constructor(type, args) { this.type = type; this.args = args; } static parse(args, context) { if (args.length < 2) { return context.error('Expectected at least one argument.'); } let outputType = null; const expectedType = context.expectedType; if (expectedType && expectedType.kind !== 'value') { outputType = expectedType; } const parsedArgs = []; for (const arg of args.slice(1)) { const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, { typeAnnotation: 'omit' }); if (!parsed) return null; outputType = outputType || parsed.type; parsedArgs.push(parsed); } if (!outputType) throw new Error('No output type'); // Above, we parse arguments without inferred type annotation so that // they don't produce a runtime error for `null` input, which would // preempt the desired null-coalescing behavior. // Thus, if any of our arguments would have needed an annotation, we // need to wrap the enclosing coalesce expression with it instead. const needsAnnotation = expectedType && parsedArgs.some(arg => checkSubtype(expectedType, arg.type)); return needsAnnotation ? new Coalesce(ValueType, parsedArgs) : new Coalesce(outputType, parsedArgs); } evaluate(ctx) { let result = null; let argCount = 0; let requestedImageName; for (const arg of this.args) { argCount++; result = arg.evaluate(ctx); // we need to keep track of the first requested image in a coalesce statement // if coalesce can't find a valid image, we return the first image name so styleimagemissing can fire if (result && result instanceof ResolvedImage && !result.available) { if (!requestedImageName) { requestedImageName = result.name; } result = null; if (argCount === this.args.length) { result = requestedImageName; } } if (result !== null) break; } return result; } eachChild(fn) { this.args.forEach(fn); } outputDefined() { return this.args.every(arg => arg.outputDefined()); } } function isComparableType(op, type) { if (op === '==' || op === '!=') { // equality operator return type.kind === 'boolean' || type.kind === 'string' || type.kind === 'number' || type.kind === 'null' || type.kind === 'value'; } else { // ordering operator return type.kind === 'string' || type.kind === 'number' || type.kind === 'value'; } } function eq(ctx, a, b) { return a === b; } function neq(ctx, a, b) { return a !== b; } function lt(ctx, a, b) { return a < b; } function gt(ctx, a, b) { return a > b; } function lteq(ctx, a, b) { return a <= b; } function gteq(ctx, a, b) { return a >= b; } function eqCollate(ctx, a, b, c) { return c.compare(a, b) === 0; } function neqCollate(ctx, a, b, c) { return !eqCollate(ctx, a, b, c); } function ltCollate(ctx, a, b, c) { return c.compare(a, b) < 0; } function gtCollate(ctx, a, b, c) { return c.compare(a, b) > 0; } function lteqCollate(ctx, a, b, c) { return c.compare(a, b) <= 0; } function gteqCollate(ctx, a, b, c) { return c.compare(a, b) >= 0; } /** * Special form for comparison operators, implementing the signatures: * - (T, T, ?Collator) => boolean * - (T, value, ?Collator) => boolean * - (value, T, ?Collator) => boolean * * For inequalities, T must be either value, string, or number. For ==/!=, it * can also be boolean or null. * * Equality semantics are equivalent to Javascript's strict equality (===/!==) * -- i.e., when the arguments' types don't match, == evaluates to false, != to * true. * * When types don't match in an ordering comparison, a runtime error is thrown. * * @private */ function makeComparison(op, compareBasic, compareWithCollator) { const isOrderComparison = op !== '==' && op !== '!='; return class Comparison { constructor(lhs, rhs, collator) { this.type = BooleanType; this.lhs = lhs; this.rhs = rhs; this.collator = collator; this.hasUntypedArgument = lhs.type.kind === 'value' || rhs.type.kind === 'value'; } static parse(args, context) { if (args.length !== 3 && args.length !== 4) return context.error('Expected two or three arguments.'); const op = args[0]; let lhs = context.parse(args[1], 1, ValueType); if (!lhs) return null; if (!isComparableType(op, lhs.type)) { return context.concat(1).error(`"${op}" comparisons are not supported for type '${toString$1(lhs.type)}'.`); } let rhs = context.parse(args[2], 2, ValueType); if (!rhs) return null; if (!isComparableType(op, rhs.type)) { return context.concat(2).error(`"${op}" comparisons are not supported for type '${toString$1(rhs.type)}'.`); } if (lhs.type.kind !== rhs.type.kind && lhs.type.kind !== 'value' && rhs.type.kind !== 'value') { return context.error(`Cannot compare types '${toString$1(lhs.type)}' and '${toString$1(rhs.type)}'.`); } if (isOrderComparison) { // typing rules specific to less/greater than operators if (lhs.type.kind === 'value' && rhs.type.kind !== 'value') { // (value, T) lhs = new Assertion(rhs.type, [lhs]); } else if (lhs.type.kind !== 'value' && rhs.type.kind === 'value') { // (T, value) rhs = new Assertion(lhs.type, [rhs]); } } let collator = null; if (args.length === 4) { if (lhs.type.kind !== 'string' && rhs.type.kind !== 'string' && lhs.type.kind !== 'value' && rhs.type.kind !== 'value') { return context.error('Cannot use collator to compare non-string types.'); } collator = context.parse(args[3], 3, CollatorType); if (!collator) return null; } return new Comparison(lhs, rhs, collator); } evaluate(ctx) { const lhs = this.lhs.evaluate(ctx); const rhs = this.rhs.evaluate(ctx); if (isOrderComparison && this.hasUntypedArgument) { const lt = typeOf(lhs); const rt = typeOf(rhs); // check that type is string or number, and equal if (lt.kind !== rt.kind || !(lt.kind === 'string' || lt.kind === 'number')) { throw new RuntimeError(`Expected arguments for "${op}" to be (string, string) or (number, number), but found (${lt.kind}, ${rt.kind}) instead.`); } } if (this.collator && !isOrderComparison && this.hasUntypedArgument) { const lt = typeOf(lhs); const rt = typeOf(rhs); if (lt.kind !== 'string' || rt.kind !== 'string') { return compareBasic(ctx, lhs, rhs); } } return this.collator ? compareWithCollator(ctx, lhs, rhs, this.collator.evaluate(ctx)) : compareBasic(ctx, lhs, rhs); } eachChild(fn) { fn(this.lhs); fn(this.rhs); if (this.collator) { fn(this.collator); } } outputDefined() { return true; } }; } const Equals = makeComparison('==', eq, eqCollate); const NotEquals = makeComparison('!=', neq, neqCollate); const LessThan = makeComparison('<', lt, ltCollate); const GreaterThan = makeComparison('>', gt, gtCollate); const LessThanOrEqual = makeComparison('<=', lteq, lteqCollate); const GreaterThanOrEqual = makeComparison('>=', gteq, gteqCollate); class CollatorExpression { constructor(caseSensitive, diacriticSensitive, locale) { this.type = CollatorType; this.locale = locale; this.caseSensitive = caseSensitive; this.diacriticSensitive = diacriticSensitive; } static parse(args, context) { if (args.length !== 2) return context.error('Expected one argument.'); const options = args[1]; if (typeof options !== 'object' || Array.isArray(options)) return context.error('Collator options argument must be an object.'); const caseSensitive = context.parse(options['case-sensitive'] === undefined ? false : options['case-sensitive'], 1, BooleanType); if (!caseSensitive) return null; const diacriticSensitive = context.parse(options['diacritic-sensitive'] === undefined ? false : options['diacritic-sensitive'], 1, BooleanType); if (!diacriticSensitive) return null; let locale = null; if (options['locale']) { locale = context.parse(options['locale'], 1, StringType); if (!locale) return null; } return new CollatorExpression(caseSensitive, diacriticSensitive, locale); } evaluate(ctx) { return new Collator(this.caseSensitive.evaluate(ctx), this.diacriticSensitive.evaluate(ctx), this.locale ? this.locale.evaluate(ctx) : null); } eachChild(fn) { fn(this.caseSensitive); fn(this.diacriticSensitive); if (this.locale) { fn(this.locale); } } outputDefined() { // Technically the set of possible outputs is the combinatoric set of Collators produced // by all possible outputs of locale/caseSensitive/diacriticSensitive // But for the primary use of Collators in comparison operators, we ignore the Collator's // possible outputs anyway, so we can get away with leaving this false for now. return false; } } class NumberFormat { constructor(number, locale, currency, minFractionDigits, maxFractionDigits) { this.type = StringType; this.number = number; this.locale = locale; this.currency = currency; this.minFractionDigits = minFractionDigits; this.maxFractionDigits = maxFractionDigits; } static parse(args, context) { if (args.length !== 3) return context.error('Expected two arguments.'); const number = context.parse(args[1], 1, NumberType); if (!number) return null; const options = args[2]; if (typeof options !== 'object' || Array.isArray(options)) return context.error('NumberFormat options argument must be an object.'); let locale = null; if (options['locale']) { locale = context.parse(options['locale'], 1, StringType); if (!locale) return null; } let currency = null; if (options['currency']) { currency = context.parse(options['currency'], 1, StringType); if (!currency) return null; } let minFractionDigits = null; if (options['min-fraction-digits']) { minFractionDigits = context.parse(options['min-fraction-digits'], 1, NumberType); if (!minFractionDigits) return null; } let maxFractionDigits = null; if (options['max-fraction-digits']) { maxFractionDigits = context.parse(options['max-fraction-digits'], 1, NumberType); if (!maxFractionDigits) return null; } return new NumberFormat(number, locale, currency, minFractionDigits, maxFractionDigits); } evaluate(ctx) { return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [], { style: this.currency ? 'currency' : 'decimal', currency: this.currency ? this.currency.evaluate(ctx) : undefined, minimumFractionDigits: this.minFractionDigits ? this.minFractionDigits.evaluate(ctx) : undefined, maximumFractionDigits: this.maxFractionDigits ? this.maxFractionDigits.evaluate(ctx) : undefined, }).format(this.number.evaluate(ctx)); } eachChild(fn) { fn(this.number); if (this.locale) { fn(this.locale); } if (this.currency) { fn(this.currency); } if (this.minFractionDigits) { fn(this.minFractionDigits); } if (this.maxFractionDigits) { fn(this.maxFractionDigits); } } outputDefined() { return false; } } class FormatExpression { constructor(sections) { this.type = FormattedType; this.sections = sections; } static parse(args, context) { if (args.length < 2) { return context.error('Expected at least one argument.'); } const firstArg = args[1]; if (!Array.isArray(firstArg) && typeof firstArg === 'object') { return context.error('First argument must be an image or text section.'); } const sections = []; let nextTokenMayBeObject = false; for (let i = 1; i <= args.length - 1; ++i) { const arg = args[i]; if (nextTokenMayBeObject && typeof arg === 'object' && !Array.isArray(arg)) { nextTokenMayBeObject = false; let scale = null; if (arg['font-scale']) { scale = context.parse(arg['font-scale'], 1, NumberType); if (!scale) return null; } let font = null; if (arg['text-font']) { font = context.parse(arg['text-font'], 1, array$1(StringType)); if (!font) return null; } let textColor = null; if (arg['text-color']) { textColor = context.parse(arg['text-color'], 1, ColorType); if (!textColor) return null; } const lastExpression = sections[sections.length - 1]; lastExpression.scale = scale; lastExpression.font = font; lastExpression.textColor = textColor; } else { const content = context.parse(args[i], 1, ValueType); if (!content) return null; const kind = content.type.kind; if (kind !== 'string' && kind !== 'value' && kind !== 'null' && kind !== 'resolvedImage') return context.error('Formatted text type must be \'string\', \'value\', \'image\' or \'null\'.'); nextTokenMayBeObject = true; sections.push({ content, scale: null, font: null, textColor: null }); } } return new FormatExpression(sections); } evaluate(ctx) { const evaluateSection = section => { const evaluatedContent = section.content.evaluate(ctx); if (typeOf(evaluatedContent) === ResolvedImageType) { return new FormattedSection('', evaluatedContent, null, null, null); } return new FormattedSection(toString(evaluatedContent), null, section.scale ? section.scale.evaluate(ctx) : null, section.font ? section.font.evaluate(ctx).join(',') : null, section.textColor ? section.textColor.evaluate(ctx) : null); }; return new Formatted(this.sections.map(evaluateSection)); } eachChild(fn) { for (const section of this.sections) { fn(section.content); if (section.scale) { fn(section.scale); } if (section.font) { fn(section.font); } if (section.textColor) { fn(section.textColor); } } } outputDefined() { // Technically the combinatoric set of all children // Usually, this.text will be undefined anyway return false; } } class ImageExpression { constructor(input) { this.type = ResolvedImageType; this.input = input; } static parse(args, context) { if (args.length !== 2) { return context.error('Expected two arguments.'); } const name = context.parse(args[1], 1, StringType); if (!name) return context.error('No image name provided.'); return new ImageExpression(name); } evaluate(ctx) { const evaluatedImageName = this.input.evaluate(ctx); const value = ResolvedImage.fromString(evaluatedImageName); if (value && ctx.availableImages) value.available = ctx.availableImages.indexOf(evaluatedImageName) > -1; return value; } eachChild(fn) { fn(this.input); } outputDefined() { // The output of image is determined by the list of available images in the evaluation context return false; } } class Length { constructor(input) { this.type = NumberType; this.input = input; } static parse(args, context) { if (args.length !== 2) return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`); const input = context.parse(args[1], 1); if (!input) return null; if (input.type.kind !== 'array' && input.type.kind !== 'string' && input.type.kind !== 'value') return context.error(`Expected argument of type string or array, but found ${toString$1(input.type)} instead.`); return new Length(input); } evaluate(ctx) { const input = this.input.evaluate(ctx); if (typeof input === 'string') { // The length may be affected by surrogate pairs. return [...input].length; } else if (Array.isArray(input)) { return input.length; } else { throw new RuntimeError(`Expected value to be of type string or array, but found ${toString$1(typeOf(input))} instead.`); } } eachChild(fn) { fn(this.input); } outputDefined() { return false; } } const EXTENT = 8192; function getTileCoordinates(p, canonical) { const x = mercatorXfromLng(p[0]); const y = mercatorYfromLat(p[1]); const tilesAtZoom = Math.pow(2, canonical.z); return [Math.round(x * tilesAtZoom * EXTENT), Math.round(y * tilesAtZoom * EXTENT)]; } function getLngLatFromTileCoord(coord, canonical) { const tilesAtZoom = Math.pow(2, canonical.z); const x = (coord[0] / EXTENT + canonical.x) / tilesAtZoom; const y = (coord[1] / EXTENT + canonical.y) / tilesAtZoom; return [lngFromMercatorXfromLng(x), latFromMercatorY(y)]; } function mercatorXfromLng(lng) { return (180 + lng) / 360; } function lngFromMercatorXfromLng(mercatorX) { return mercatorX * 360 - 180; } function mercatorYfromLat(lat) { return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360; } function latFromMercatorY(mercatorY) { return 360 / Math.PI * Math.atan(Math.exp((180 - mercatorY * 360) * Math.PI / 180)) - 90; } function updateBBox(bbox, coord) { bbox[0] = Math.min(bbox[0], coord[0]); bbox[1] = Math.min(bbox[1], coord[1]); bbox[2] = Math.max(bbox[2], coord[0]); bbox[3] = Math.max(bbox[3], coord[1]); } function boxWithinBox(bbox1, bbox2) { if (bbox1[0] <= bbox2[0]) return false; if (bbox1[2] >= bbox2[2]) return false; if (bbox1[1] <= bbox2[1]) return false; if (bbox1[3] >= bbox2[3]) return false; return true; } function rayIntersect(p, p1, p2) { return ((p1[1] > p[1]) !== (p2[1] > p[1])) && (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0]); } function pointOnBoundary(p, p1, p2) { const x1 = p[0] - p1[0]; const y1 = p[1] - p1[1]; const x2 = p[0] - p2[0]; const y2 = p[1] - p2[1]; return (x1 * y2 - x2 * y1 === 0) && (x1 * x2 <= 0) && (y1 * y2 <= 0); } // a, b are end points for line segment1, c and d are end points for line segment2 function segmentIntersectSegment(a, b, c, d) { // check if two segments are parallel or not // precondition is end point a, b is inside polygon, if line a->b is // parallel to polygon edge c->d, then a->b won't intersect with c->d const vectorP = [b[0] - a[0], b[1] - a[1]]; const vectorQ = [d[0] - c[0], d[1] - c[1]]; if (perp(vectorQ, vectorP) === 0) return false; // If lines are intersecting with each other, the relative location should be: // a and b lie in different sides of segment c->d // c and d lie in different sides of segment a->b if (twoSided(a, b, c, d) && twoSided(c, d, a, b)) return true; return false; } function lineIntersectPolygon(p1, p2, polygon) { for (const ring of polygon) { // loop through every edge of the ring for (let j = 0; j < ring.length - 1; ++j) { if (segmentIntersectSegment(p1, p2, ring[j], ring[j + 1])) { return true; } } } return false; } // ray casting algorithm for detecting if point is in polygon function pointWithinPolygon(point, rings, trueIfOnBoundary = false) { let inside = false; for (const ring of rings) { for (let j = 0; j < ring.length - 1; j++) { if (pointOnBoundary(point, ring[j], ring[j + 1])) return trueIfOnBoundary; if (rayIntersect(point, ring[j], ring[j + 1])) inside = !inside; } } return inside; } function pointWithinPolygons(point, polygons) { for (const polygon of polygons) { if (pointWithinPolygon(point, polygon)) return true; } return false; } function lineStringWithinPolygon(line, polygon) { // First, check if geometry points of line segments are all inside polygon for (const point of line) { if (!pointWithinPolygon(point, polygon)) { return false; } } // Second, check if there is line segment intersecting polygon edge for (let i = 0; i < line.length - 1; ++i) { if (lineIntersectPolygon(line[i], line[i + 1], polygon)) { return false; } } return true; } function lineStringWithinPolygons(line, polygons) { for (const polygon of polygons) { if (lineStringWithinPolygon(line, polygon)) return true; } return false; } function perp(v1, v2) { return (v1[0] * v2[1] - v1[1] * v2[0]); } // check if p1 and p2 are in different sides of line segment q1->q2 function twoSided(p1, p2, q1, q2) { // q1->p1 (x1, y1), q1->p2 (x2, y2), q1->q2 (x3, y3) const x1 = p1[0] - q1[0]; const y1 = p1[1] - q1[1]; const x2 = p2[0] - q1[0]; const y2 = p2[1] - q1[1]; const x3 = q2[0] - q1[0]; const y3 = q2[1] - q1[1]; const det1 = (x1 * y3 - x3 * y1); const det2 = (x2 * y3 - x3 * y2); if ((det1 > 0 && det2 < 0) || (det1 < 0 && det2 > 0)) return true; return false; } function getTilePolygon(coordinates, bbox, canonical) { const polygon = []; for (let i = 0; i < coordinates.length; i++) { const ring = []; for (let j = 0; j < coordinates[i].length; j++) { const coord = getTileCoordinates(coordinates[i][j], canonical); updateBBox(bbox, coord); ring.push(coord); } polygon.push(ring); } return polygon; } function getTilePolygons(coordinates, bbox, canonical) { const polygons = []; for (let i = 0; i < coordinates.length; i++) { const polygon = getTilePolygon(coordinates[i], bbox, canonical); polygons.push(polygon); } return polygons; } function updatePoint(p, bbox, polyBBox, worldSize) { if (p[0] < polyBBox[0] || p[0] > polyBBox[2]) { const halfWorldSize = worldSize * 0.5; let shift = (p[0] - polyBBox[0] > halfWorldSize) ? -worldSize : (polyBBox[0] - p[0] > halfWorldSize) ? worldSize : 0; if (shift === 0) { shift = (p[0] - polyBBox[2] > halfWorldSize) ? -worldSize : (polyBBox[2] - p[0] > halfWorldSize) ? worldSize : 0; } p[0] += shift; } updateBBox(bbox, p); } function resetBBox(bbox) { bbox[0] = bbox[1] = Infinity; bbox[2] = bbox[3] = -Infinity; } function getTilePoints(geometry, pointBBox, polyBBox, canonical) { const worldSize = Math.pow(2, canonical.z) * EXTENT; const shifts = [canonical.x * EXTENT, canonical.y * EXTENT]; const tilePoints = []; for (const points of geometry) { for (const point of points) { const p = [point.x + shifts[0], point.y + shifts[1]]; updatePoint(p, pointBBox, polyBBox, worldSize); tilePoints.push(p); } } return tilePoints; } function getTileLines(geometry, lineBBox, polyBBox, canonical) { const worldSize = Math.pow(2, canonical.z) * EXTENT; const shifts = [canonical.x * EXTENT, canonical.y * EXTENT]; const tileLines = []; for (const line of geometry) { const tileLine = []; for (const point of line) { const p = [point.x + shifts[0], point.y + shifts[1]]; updateBBox(lineBBox, p); tileLine.push(p); } tileLines.push(tileLine); } if (lineBBox[2] - lineBBox[0] <= worldSize / 2) { resetBBox(lineBBox); for (const line of tileLines) { for (const p of line) { updatePoint(p, lineBBox, polyBBox, worldSize); } } } return tileLines; } function pointsWithinPolygons(ctx, polygonGeometry) { const pointBBox = [Infinity, Infinity, -Infinity, -Infinity]; const polyBBox = [Infinity, Infinity, -Infinity, -Infinity]; const canonical = ctx.canonicalID(); if (polygonGeometry.type === 'Polygon') { const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical); const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical); if (!boxWithinBox(pointBBox, polyBBox)) return false; for (const point of tilePoints) { if (!pointWithinPolygon(point, tilePolygon)) return false; } } if (polygonGeometry.type === 'MultiPolygon') { const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical); const tilePoints = getTilePoints(ctx.geometry(), pointBBox, polyBBox, canonical); if (!boxWithinBox(pointBBox, polyBBox)) return false; for (const point of tilePoints) { if (!pointWithinPolygons(point, tilePolygons)) return false; } } return true; } function linesWithinPolygons(ctx, polygonGeometry) { const lineBBox = [Infinity, Infinity, -Infinity, -Infinity]; const polyBBox = [Infinity, Infinity, -Infinity, -Infinity]; const canonical = ctx.canonicalID(); if (polygonGeometry.type === 'Polygon') { const tilePolygon = getTilePolygon(polygonGeometry.coordinates, polyBBox, canonical); const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical); if (!boxWithinBox(lineBBox, polyBBox)) return false; for (const line of tileLines) { if (!lineStringWithinPolygon(line, tilePolygon)) return false; } } if (polygonGeometry.type === 'MultiPolygon') { const tilePolygons = getTilePolygons(polygonGeometry.coordinates, polyBBox, canonical); const tileLines = getTileLines(ctx.geometry(), lineBBox, polyBBox, canonical); if (!boxWithinBox(lineBBox, polyBBox)) return false; for (const line of tileLines) { if (!lineStringWithinPolygons(line, tilePolygons)) return false; } } return true; } class Within { constructor(geojson, geometries) { this.type = BooleanType; this.geojson = geojson; this.geometries = geometries; } static parse(args, context) { if (args.length !== 2) return context.error(`'within' expression requires exactly one argument, but found ${args.length - 1} instead.`); if (isValue(args[1])) { const geojson = args[1]; if (geojson.type === 'FeatureCollection') { const polygonsCoords = []; for (const polygon of geojson.features) { const { type, coordinates } = polygon.geometry; if (type === 'Polygon') { polygonsCoords.push(coordinates); } if (type === 'MultiPolygon') { polygonsCoords.push(...coordinates); } } if (polygonsCoords.length) { const multipolygonWrapper = { type: 'MultiPolygon', coordinates: polygonsCoords }; return new Within(geojson, multipolygonWrapper); } } else if (geojson.type === 'Feature') { const type = geojson.geometry.type; if (type === 'Polygon' || type === 'MultiPolygon') { return new Within(geojson, geojson.geometry); } } else if (geojson.type === 'Polygon' || geojson.type === 'MultiPolygon') { return new Within(geojson, geojson); } } return context.error('\'within\' expression requires valid geojson object that contains polygon geometry type.'); } evaluate(ctx) { if (ctx.geometry() != null && ctx.canonicalID() != null) { if (ctx.geometryType() === 'Point') { return pointsWithinPolygons(ctx, this.geometries); } else if (ctx.geometryType() === 'LineString') { return linesWithinPolygons(ctx, this.geometries); } } return false; } eachChild() { } outputDefined() { return true; } } class TinyQueue { constructor(data = [], compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0)) { this.data = data; this.length = this.data.length; this.compare = compare; if (this.length > 0) { for (let i = (this.length >> 1) - 1; i >= 0; i--) this._down(i); } } push(item) { this.data.push(item); this._up(this.length++); } pop() { if (this.length === 0) return undefined; const top = this.data[0]; const bottom = this.data.pop(); if (--this.length > 0) { this.data[0] = bottom; this._down(0); } return top; } peek() { return this.data[0]; } _up(pos) { const {data, compare} = this; const item = data[pos]; while (pos > 0) { const parent = (pos - 1) >> 1; const current = data[parent]; if (compare(item, current) >= 0) break; data[pos] = current; pos = parent; } data[pos] = item; } _down(pos) { const {data, compare} = this; const halfLength = this.length >> 1; const item = data[pos]; while (pos < halfLength) { let bestChild = (pos << 1) + 1; // initially it is the left child const right = bestChild + 1; if (right < this.length && compare(data[right], data[bestChild]) < 0) { bestChild = right; } if (compare(data[bestChild], item) >= 0) break; data[pos] = data[bestChild]; pos = bestChild; } data[pos] = item; } } /** * Classifies an array of rings into polygons with outer rings and holes * @param rings - the rings to classify * @param maxRings - the maximum number of rings to include in a polygon, use 0 to include all rings * @returns an array of polygons with internal rings as holes */ function classifyRings(rings, maxRings) { const len = rings.length; if (len <= 1) return [rings]; const polygons = []; let polygon; let ccw; for (const ring of rings) { const area = calculateSignedArea(ring); if (area === 0) continue; ring.area = Math.abs(area); if (ccw === undefined) ccw = area < 0; if (ccw === area < 0) { if (polygon) polygons.push(polygon); polygon = [ring]; } else { polygon.push(ring); } } if (polygon) polygons.push(polygon); return polygons; } /** * Returns the signed area for the polygon ring. Positive areas are exterior rings and * have a clockwise winding. Negative areas are interior rings and have a counter clockwise * ordering. * * @param ring - Exterior or interior ring * @returns Signed area */ function calculateSignedArea(ring) { let sum = 0; for (let i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { p1 = ring[i]; p2 = ring[j]; sum += (p2.x - p1.x) * (p1.y + p2.y); } return sum; } // This is taken from https://github.com/mapbox/cheap-ruler/ in order to take only the relevant parts // Values that define WGS84 ellipsoid model of the Earth const RE = 6378.137; // equatorial radius const FE = 1 / 298.257223563; // flattening const E2 = FE * (2 - FE); const RAD = Math.PI / 180; class CheapRuler { constructor(lat) { // Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional const m = RAD * RE * 1000; const coslat = Math.cos(lat * RAD); const w2 = 1 / (1 - E2 * (1 - coslat * coslat)); const w = Math.sqrt(w2); // multipliers for converting longitude and latitude degrees into distance this.kx = m * w * coslat; // based on normal radius of curvature this.ky = m * w * w2 * (1 - E2); // based on meridonal radius of curvature } /** * Given two points of the form [longitude, latitude], returns the distance. * * @param a - point [longitude, latitude] * @param b - point [longitude, latitude] * @returns distance * @example * const distance = ruler.distance([30.5, 50.5], [30.51, 50.49]); * //=distance */ distance(a, b) { const dx = this.wrap(a[0] - b[0]) * this.kx; const dy = (a[1] - b[1]) * this.ky; return Math.sqrt(dx * dx + dy * dy); } /** * Returns an object of the form {point, index, t}, where point is closest point on the line * from the given point, index is the start index of the segment with the closest point, * and t is a parameter from 0 to 1 that indicates where the closest point is on that segment. * * @param line - an array of points that form the line * @param p - point [longitude, latitude] * @returns the nearest point, its index in the array and the proportion along the line * @example * const point = ruler.pointOnLine(line, [-67.04, 50.5]).point; * //=point */ pointOnLine(line, p) { let minDist = Infinity; let minX, minY, minI, minT; for (let i = 0; i < line.length - 1; i++) { let x = line[i][0]; let y = line[i][1]; let dx = this.wrap(line[i + 1][0] - x) * this.kx; let dy = (line[i + 1][1] - y) * this.ky; let t = 0; if (dx !== 0 || dy !== 0) { t = (this.wrap(p[0] - x) * this.kx * dx + (p[1] - y) * this.ky * dy) / (dx * dx + dy * dy); if (t > 1) { x = line[i + 1][0]; y = line[i + 1][1]; } else if (t > 0) { x += (dx / this.kx) * t; y += (dy / this.ky) * t; } } dx = this.wrap(p[0] - x) * this.kx; dy = (p[1] - y) * this.ky; const sqDist = dx * dx + dy * dy; if (sqDist < minDist) { minDist = sqDist; minX = x; minY = y; minI = i; minT = t; } } return { point: [minX, minY], index: minI, t: Math.max(0, Math.min(1, minT)) }; } wrap(deg) { while (deg < -180) deg += 360; while (deg > 180) deg -= 360; return deg; } } const MinPointsSize = 100; const MinLinePointsSize = 50; function compareDistPair(a, b) { return b[0] - a[0]; } function getRangeSize(range) { return range[1] - range[0] + 1; } function isRangeSafe(range, threshold) { return range[1] >= range[0] && range[1] < threshold; } function splitRange(range, isLine) { if (range[0] > range[1]) { return [null, null]; } const size = getRangeSize(range); if (isLine) { if (size === 2) { return [range, null]; } const size1 = Math.floor(size / 2); return [[range[0], range[0] + size1], [range[0] + size1, range[1]]]; } if (size === 1) { return [range, null]; } const size1 = Math.floor(size / 2) - 1; return [[range[0], range[0] + size1], [range[0] + size1 + 1, range[1]]]; } function getBBox(coords, range) { if (!isRangeSafe(range, coords.length)) { return [Infinity, Infinity, -Infinity, -Infinity]; } const bbox = [Infinity, Infinity, -Infinity, -Infinity]; for (let i = range[0]; i <= range[1]; ++i) { updateBBox(bbox, coords[i]); } return bbox; } function getPolygonBBox(polygon) { const bbox = [Infinity, Infinity, -Infinity, -Infinity]; for (const ring of polygon) { for (const coord of ring) { updateBBox(bbox, coord); } } return bbox; } function isValidBBox(bbox) { return bbox[0] !== -Infinity && bbox[1] !== -Infinity && bbox[2] !== Infinity && bbox[3] !== Infinity; } // Calculate the distance between two bounding boxes. // Calculate the delta in x and y direction, and use two fake points {0.0, 0.0} // and {dx, dy} to calculate the distance. Distance will be 0.0 if bounding box are overlapping. function bboxToBBoxDistance(bbox1, bbox2, ruler) { if (!isValidBBox(bbox1) || !isValidBBox(bbox2)) { return NaN; } let dx = 0.0; let dy = 0.0; // bbox1 in left side if (bbox1[2] < bbox2[0]) { dx = bbox2[0] - bbox1[2]; } // bbox1 in right side if (bbox1[0] > bbox2[2]) { dx = bbox1[0] - bbox2[2]; } // bbox1 in above side if (bbox1[1] > bbox2[3]) { dy = bbox1[1] - bbox2[3]; } // bbox1 in down side if (bbox1[3] < bbox2[1]) { dy = bbox2[1] - bbox1[3]; } return ruler.distance([0.0, 0.0], [dx, dy]); } function pointToLineDistance(point, line, ruler) { const nearestPoint = ruler.pointOnLine(line, point); return ruler.distance(point, nearestPoint.point); } function segmentToSegmentDistance(p1, p2, q1, q2, ruler) { const dist1 = Math.min(pointToLineDistance(p1, [q1, q2], ruler), pointToLineDistance(p2, [q1, q2], ruler)); const dist2 = Math.min(pointToLineDistance(q1, [p1, p2], ruler), pointToLineDistance(q2, [p1, p2], ruler)); return Math.min(dist1, dist2); } function lineToLineDistance(line1, range1, line2, range2, ruler) { const rangeSafe = isRangeSafe(range1, line1.length) && isRangeSafe(range2, line2.length); if (!rangeSafe) { return Infinity; } let dist = Infinity; for (let i = range1[0]; i < range1[1]; ++i) { const p1 = line1[i]; const p2 = line1[i + 1]; for (let j = range2[0]; j < range2[1]; ++j) { const q1 = line2[j]; const q2 = line2[j + 1]; if (segmentIntersectSegment(p1, p2, q1, q2)) { return 0.0; } dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler)); } } return dist; } function pointsToPointsDistance(points1, range1, points2, range2, ruler) { const rangeSafe = isRangeSafe(range1, points1.length) && isRangeSafe(range2, points2.length); if (!rangeSafe) { return NaN; } let dist = Infinity; for (let i = range1[0]; i <= range1[1]; ++i) { for (let j = range2[0]; j <= range2[1]; ++j) { dist = Math.min(dist, ruler.distance(points1[i], points2[j])); if (dist === 0.0) { return dist; } } } return dist; } function pointToPolygonDistance(point, polygon, ruler) { if (pointWithinPolygon(point, polygon, true)) { return 0.0; } let dist = Infinity; for (const ring of polygon) { const front = ring[0]; const back = ring[ring.length - 1]; if (front !== back) { dist = Math.min(dist, pointToLineDistance(point, [back, front], ruler)); if (dist === 0.0) { return dist; } } const nearestPoint = ruler.pointOnLine(ring, point); dist = Math.min(dist, ruler.distance(point, nearestPoint.point)); if (dist === 0.0) { return dist; } } return dist; } function lineToPolygonDistance(line, range, polygon, ruler) { if (!isRangeSafe(range, line.length)) { return NaN; } for (let i = range[0]; i <= range[1]; ++i) { if (pointWithinPolygon(line[i], polygon, true)) { return 0.0; } } let dist = Infinity; for (let i = range[0]; i < range[1]; ++i) { const p1 = line[i]; const p2 = line[i + 1]; for (const ring of polygon) { for (let j = 0, len = ring.length, k = len - 1; j < len; k = j++) { const q1 = ring[k]; const q2 = ring[j]; if (segmentIntersectSegment(p1, p2, q1, q2)) { return 0.0; } dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler)); } } } return dist; } function polygonIntersect(poly1, poly2) { for (const ring of poly1) { for (const point of ring) { if (pointWithinPolygon(point, poly2, true)) { return true; } } } return false; } function polygonToPolygonDistance(polygon1, polygon2, ruler, currentMiniDist = Infinity) { const bbox1 = getPolygonBBox(polygon1); const bbox2 = getPolygonBBox(polygon2); if (currentMiniDist !== Infinity && bboxToBBoxDistance(bbox1, bbox2, ruler) >= currentMiniDist) { return currentMiniDist; } if (boxWithinBox(bbox1, bbox2)) { if (polygonIntersect(polygon1, polygon2)) { return 0.0; } } else if (polygonIntersect(polygon2, polygon1)) { return 0.0; } let dist = Infinity; for (const ring1 of polygon1) { for (let i = 0, len1 = ring1.length, l = len1 - 1; i < len1; l = i++) { const p1 = ring1[l]; const p2 = ring1[i]; for (const ring2 of polygon2) { for (let j = 0, len2 = ring2.length, k = len2 - 1; j < len2; k = j++) { const q1 = ring2[k]; const q2 = ring2[j]; if (segmentIntersectSegment(p1, p2, q1, q2)) { return 0.0; } dist = Math.min(dist, segmentToSegmentDistance(p1, p2, q1, q2, ruler)); } } } } return dist; } function updateQueue(distQueue, miniDist, ruler, points, polyBBox, rangeA) { if (!rangeA) { return; } const tempDist = bboxToBBoxDistance(getBBox(points, rangeA), polyBBox, ruler); // Insert new pair to the queue if the bbox distance is less than // miniDist, The pair with biggest distance will be at the top if (tempDist < miniDist) { distQueue.push([tempDist, rangeA, [0, 0]]); } } function updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, range1, range2) { if (!range1 || !range2) { return; } const tempDist = bboxToBBoxDistance(getBBox(pointSet1, range1), getBBox(pointSet2, range2), ruler); // Insert new pair to the queue if the bbox distance is less than // miniDist, The pair with biggest distance will be at the top if (tempDist < miniDist) { distQueue.push([tempDist, range1, range2]); } } // Divide and conquer, the time complexity is O(n*lgn), faster than Brute force // O(n*n) Most of the time, use index for in-place processing. function pointsToPolygonDistance(points, isLine, polygon, ruler, currentMiniDist = Infinity) { let miniDist = Math.min(ruler.distance(points[0], polygon[0][0]), currentMiniDist); if (miniDist === 0.0) { return miniDist; } const distQueue = new TinyQueue([[0, [0, points.length - 1], [0, 0]]], compareDistPair); const polyBBox = getPolygonBBox(polygon); while (distQueue.length > 0) { const distPair = distQueue.pop(); if (distPair[0] >= miniDist) { continue; } const range = distPair[1]; // In case the set size are relatively small, we could use brute-force directly const threshold = isLine ? MinLinePointsSize : MinPointsSize; if (getRangeSize(range) <= threshold) { if (!isRangeSafe(range, points.length)) { return NaN; } if (isLine) { const tempDist = lineToPolygonDistance(points, range, polygon, ruler); if (isNaN(tempDist) || tempDist === 0.0) { return tempDist; } miniDist = Math.min(miniDist, tempDist); } else { for (let i = range[0]; i <= range[1]; ++i) { const tempDist = pointToPolygonDistance(points[i], polygon, ruler); miniDist = Math.min(miniDist, tempDist); if (miniDist === 0.0) { return 0.0; } } } } else { const newRangesA = splitRange(range, isLine); updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[0]); updateQueue(distQueue, miniDist, ruler, points, polyBBox, newRangesA[1]); } } return miniDist; } function pointSetToPointSetDistance(pointSet1, isLine1, pointSet2, isLine2, ruler, currentMiniDist = Infinity) { let miniDist = Math.min(currentMiniDist, ruler.distance(pointSet1[0], pointSet2[0])); if (miniDist === 0.0) { return miniDist; } const distQueue = new TinyQueue([[0, [0, pointSet1.length - 1], [0, pointSet2.length - 1]]], compareDistPair); while (distQueue.length > 0) { const distPair = distQueue.pop(); if (distPair[0] >= miniDist) { continue; } const rangeA = distPair[1]; const rangeB = distPair[2]; const threshold1 = isLine1 ? MinLinePointsSize : MinPointsSize; const threshold2 = isLine2 ? MinLinePointsSize : MinPointsSize; // In case the set size are relatively small, we could use brute-force directly if (getRangeSize(rangeA) <= threshold1 && getRangeSize(rangeB) <= threshold2) { if (!isRangeSafe(rangeA, pointSet1.length) && isRangeSafe(rangeB, pointSet2.length)) { return NaN; } let tempDist; if (isLine1 && isLine2) { tempDist = lineToLineDistance(pointSet1, rangeA, pointSet2, rangeB, ruler); miniDist = Math.min(miniDist, tempDist); } else if (isLine1 && !isLine2) { const sublibe = pointSet1.slice(rangeA[0], rangeA[1] + 1); for (let i = rangeB[0]; i <= rangeB[1]; ++i) { tempDist = pointToLineDistance(pointSet2[i], sublibe, ruler); miniDist = Math.min(miniDist, tempDist); if (miniDist === 0.0) { return miniDist; } } } else if (!isLine1 && isLine2) { const sublibe = pointSet2.slice(rangeB[0], rangeB[1] + 1); for (let i = rangeA[0]; i <= rangeA[1]; ++i) { tempDist = pointToLineDistance(pointSet1[i], sublibe, ruler); miniDist = Math.min(miniDist, tempDist); if (miniDist === 0.0) { return miniDist; } } } else { tempDist = pointsToPointsDistance(pointSet1, rangeA, pointSet2, rangeB, ruler); miniDist = Math.min(miniDist, tempDist); } } else { const newRangesA = splitRange(rangeA, isLine1); const newRangesB = splitRange(rangeB, isLine2); updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[0]); updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[0], newRangesB[1]); updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[0]); updateQueueTwoSets(distQueue, miniDist, ruler, pointSet1, pointSet2, newRangesA[1], newRangesB[1]); } } return miniDist; } function pointToGeometryDistance(ctx, geometries) { const tilePoints = ctx.geometry(); const pointPosition = tilePoints.flat().map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical)); if (tilePoints.length === 0) { return NaN; } const ruler = new CheapRuler(pointPosition[0][1]); let dist = Infinity; for (const geometry of geometries) { switch (geometry.type) { case 'Point': dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, [geometry.coordinates], false, ruler, dist)); break; case 'LineString': dist = Math.min(dist, pointSetToPointSetDistance(pointPosition, false, geometry.coordinates, true, ruler, dist)); break; case 'Polygon': dist = Math.min(dist, pointsToPolygonDistance(pointPosition, false, geometry.coordinates, ruler, dist)); break; } if (dist === 0.0) { return dist; } } return dist; } function lineStringToGeometryDistance(ctx, geometries) { const tileLine = ctx.geometry(); const linePositions = tileLine.flat().map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical)); if (tileLine.length === 0) { return NaN; } const ruler = new CheapRuler(linePositions[0][1]); let dist = Infinity; for (const geometry of geometries) { switch (geometry.type) { case 'Point': dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, [geometry.coordinates], false, ruler, dist)); break; case 'LineString': dist = Math.min(dist, pointSetToPointSetDistance(linePositions, true, geometry.coordinates, true, ruler, dist)); break; case 'Polygon': dist = Math.min(dist, pointsToPolygonDistance(linePositions, true, geometry.coordinates, ruler, dist)); break; } if (dist === 0.0) { return dist; } } return dist; } function polygonToGeometryDistance(ctx, geometries) { const tilePolygon = ctx.geometry(); if (tilePolygon.length === 0 || tilePolygon[0].length === 0) { return NaN; } const polygons = classifyRings(tilePolygon).map(polygon => { return polygon.map(ring => { return ring.map(p => getLngLatFromTileCoord([p.x, p.y], ctx.canonical)); }); }); const ruler = new CheapRuler(polygons[0][0][0][1]); let dist = Infinity; for (const geometry of geometries) { for (const polygon of polygons) { switch (geometry.type) { case 'Point': dist = Math.min(dist, pointsToPolygonDistance([geometry.coordinates], false, polygon, ruler, dist)); break; case 'LineString': dist = Math.min(dist, pointsToPolygonDistance(geometry.coordinates, true, polygon, ruler, dist)); break; case 'Polygon': dist = Math.min(dist, polygonToPolygonDistance(polygon, geometry.coordinates, ruler, dist)); break; } if (dist === 0.0) { return dist; } } } return dist; } function toSimpleGeometry(geometry) { if (geometry.type === 'MultiPolygon') { return geometry.coordinates.map(polygon => { return { type: 'Polygon', coordinates: polygon }; }); } if (geometry.type === 'MultiLineString') { return geometry.coordinates.map(lineString => { return { type: 'LineString', coordinates: lineString }; }); } if (geometry.type === 'MultiPoint') { return geometry.coordinates.map(point => { return { type: 'Point', coordinates: point }; }); } return [geometry]; } class Distance { constructor(geojson, geometries) { this.type = NumberType; this.geojson = geojson; this.geometries = geometries; } static parse(args, context) { if (args.length !== 2) return context.error(`'distance' expression requires exactly one argument, but found ${args.length - 1} instead.`); if (isValue(args[1])) { const geojson = args[1]; if (geojson.type === 'FeatureCollection') { return new Distance(geojson, geojson.features.map(feature => toSimpleGeometry(feature.geometry)).flat()); } else if (geojson.type === 'Feature') { return new Distance(geojson, toSimpleGeometry(geojson.geometry)); } else if ('type' in geojson && 'coordinates' in geojson) { return new Distance(geojson, toSimpleGeometry(geojson)); } } return context.error('\'distance\' expression requires valid geojson object that contains polygon geometry type.'); } evaluate(ctx) { if (ctx.geometry() != null && ctx.canonicalID() != null) { if (ctx.geometryType() === 'Point') { return pointToGeometryDistance(ctx, this.geometries); } else if (ctx.geometryType() === 'LineString') { return lineStringToGeometryDistance(ctx, this.geometries); } else if (ctx.geometryType() === 'Polygon') { return polygonToGeometryDistance(ctx, this.geometries); } } return NaN; } eachChild() { } outputDefined() { return true; } } const expressions = { // special forms '==': Equals, '!=': NotEquals, '>': GreaterThan, '<': LessThan, '>=': GreaterThanOrEqual, '<=': LessThanOrEqual, 'array': Assertion, 'at': At, 'boolean': Assertion, 'case': Case, 'coalesce': Coalesce, 'collator': CollatorExpression, 'format': FormatExpression, 'image': ImageExpression, 'in': In, 'index-of': IndexOf, 'interpolate': Interpolate, 'interpolate-hcl': Interpolate, 'interpolate-lab': Interpolate, 'length': Length, 'let': Let, 'literal': Literal, 'match': Match, 'number': Assertion, 'number-format': NumberFormat, 'object': Assertion, 'slice': Slice, 'step': Step, 'string': Assertion, 'to-boolean': Coercion, 'to-color': Coercion, 'to-number': Coercion, 'to-string': Coercion, 'var': Var, 'within': Within, 'distance': Distance }; class CompoundExpression { constructor(name, type, evaluate, args) { this.name = name; this.type = type; this._evaluate = evaluate; this.args = args; } evaluate(ctx) { return this._evaluate(ctx, this.args); } eachChild(fn) { this.args.forEach(fn); } outputDefined() { return false; } static parse(args, context) { const op = args[0]; const definition = CompoundExpression.definitions[op]; if (!definition) { return context.error(`Unknown expression "${op}". If you wanted a literal array, use ["literal", [...]].`, 0); } // Now check argument types against each signature const type = Array.isArray(definition) ? definition[0] : definition.type; const availableOverloads = Array.isArray(definition) ? [[definition[1], definition[2]]] : definition.overloads; const overloads = availableOverloads.filter(([signature]) => (!Array.isArray(signature) || // varags signature.length === args.length - 1 // correct param count )); let signatureContext = null; for (const [params, evaluate] of overloads) { // Use a fresh context for each attempted signature so that, if // we eventually succeed, we haven't polluted `context.errors`. signatureContext = new ParsingContext(context.registry, isExpressionConstant, context.path, null, context.scope); // First parse all the args, potentially coercing to the // types expected by this overload. const parsedArgs = []; let argParseFailed = false; for (let i = 1; i < args.length; i++) { const arg = args[i]; const expectedType = Array.isArray(params) ? params[i - 1] : params.type; const parsed = signatureContext.parse(arg, 1 + parsedArgs.length, expectedType); if (!parsed) { argParseFailed = true; break; } parsedArgs.push(parsed); } if (argParseFailed) { // Couldn't coerce args of this overload to expected type, move // on to next one. continue; } if (Array.isArray(params)) { if (params.length !== parsedArgs.length) { signatureContext.error(`Expected ${params.length} arguments, but found ${parsedArgs.length} instead.`); continue; } } for (let i = 0; i < parsedArgs.length; i++) { const expected = Array.isArray(params) ? params[i] : params.type; const arg = parsedArgs[i]; signatureContext.concat(i + 1).checkSubtype(expected, arg.type); } if (signatureContext.errors.length === 0) { return new CompoundExpression(op, type, evaluate, parsedArgs); } } if (overloads.length === 1) { context.errors.push(...signatureContext.errors); } else { const expected = overloads.length ? overloads : availableOverloads; const signatures = expected .map(([params]) => stringifySignature(params)) .join(' | '); const actualTypes = []; // For error message, re-parse arguments without trying to // apply any coercions for (let i = 1; i < args.length; i++) { const parsed = context.parse(args[i], 1 + actualTypes.length); if (!parsed) return null; actualTypes.push(toString$1(parsed.type)); } context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`); } return null; } static register(registry, definitions) { CompoundExpression.definitions = definitions; for (const name in definitions) { registry[name] = CompoundExpression; } } } function rgba(ctx, [r, g, b, a]) { r = r.evaluate(ctx); g = g.evaluate(ctx); b = b.evaluate(ctx); const alpha = a ? a.evaluate(ctx) : 1; const error = validateRGBA(r, g, b, alpha); if (error) throw new RuntimeError(error); return new Color(r / 255, g / 255, b / 255, alpha, false); } function has(key, obj) { return key in obj; } function get(key, obj) { const v = obj[key]; return typeof v === 'undefined' ? null : v; } function binarySearch(v, a, i, j) { while (i <= j) { const m = (i + j) >> 1; if (a[m] === v) return true; if (a[m] > v) j = m - 1; else i = m + 1; } return false; } function varargs(type) { return { type }; } CompoundExpression.register(expressions, { 'error': [ ErrorType, [StringType], (ctx, [v]) => { throw new RuntimeError(v.evaluate(ctx)); } ], 'typeof': [ StringType, [ValueType], (ctx, [v]) => toString$1(typeOf(v.evaluate(ctx))) ], 'to-rgba': [ array$1(NumberType, 4), [ColorType], (ctx, [v]) => { const [r, g, b, a] = v.evaluate(ctx).rgb; return [r * 255, g * 255, b * 255, a]; }, ], 'rgb': [ ColorType, [NumberType, NumberType, NumberType], rgba ], 'rgba': [ ColorType, [NumberType, NumberType, NumberType, NumberType], rgba ], 'has': { type: BooleanType, overloads: [ [ [StringType], (ctx, [key]) => has(key.evaluate(ctx), ctx.properties()) ], [ [StringType, ObjectType], (ctx, [key, obj]) => has(key.evaluate(ctx), obj.evaluate(ctx)) ] ] }, 'get': { type: ValueType, overloads: [ [ [StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.properties()) ], [ [StringType, ObjectType], (ctx, [key, obj]) => get(key.evaluate(ctx), obj.evaluate(ctx)) ] ] }, 'feature-state': [ ValueType, [StringType], (ctx, [key]) => get(key.evaluate(ctx), ctx.featureState || {}) ], 'properties': [ ObjectType, [], (ctx) => ctx.properties() ], 'geometry-type': [ StringType, [], (ctx) => ctx.geometryType() ], 'id': [ ValueType, [], (ctx) => ctx.id() ], 'zoom': [ NumberType, [], (ctx) => ctx.globals.zoom ], 'heatmap-density': [ NumberType, [], (ctx) => ctx.globals.heatmapDensity || 0 ], 'line-progress': [ NumberType, [], (ctx) => ctx.globals.lineProgress || 0 ], 'accumulated': [ ValueType, [], (ctx) => ctx.globals.accumulated === undefined ? null : ctx.globals.accumulated ], '+': [ NumberType, varargs(NumberType), (ctx, args) => { let result = 0; for (const arg of args) { result += arg.evaluate(ctx); } return result; } ], '*': [ NumberType, varargs(NumberType), (ctx, args) => { let result = 1; for (const arg of args) { result *= arg.evaluate(ctx); } return result; } ], '-': { type: NumberType, overloads: [ [ [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) - b.evaluate(ctx) ], [ [NumberType], (ctx, [a]) => -a.evaluate(ctx) ] ] }, '/': [ NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) / b.evaluate(ctx) ], '%': [ NumberType, [NumberType, NumberType], (ctx, [a, b]) => a.evaluate(ctx) % b.evaluate(ctx) ], 'ln2': [ NumberType, [], () => Math.LN2 ], 'pi': [ NumberType, [], () => Math.PI ], 'e': [ NumberType, [], () => Math.E ], '^': [ NumberType, [NumberType, NumberType], (ctx, [b, e]) => Math.pow(b.evaluate(ctx), e.evaluate(ctx)) ], 'sqrt': [ NumberType, [NumberType], (ctx, [x]) => Math.sqrt(x.evaluate(ctx)) ], 'log10': [ NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN10 ], 'ln': [ NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) ], 'log2': [ NumberType, [NumberType], (ctx, [n]) => Math.log(n.evaluate(ctx)) / Math.LN2 ], 'sin': [ NumberType, [NumberType], (ctx, [n]) => Math.sin(n.evaluate(ctx)) ], 'cos': [ NumberType, [NumberType], (ctx, [n]) => Math.cos(n.evaluate(ctx)) ], 'tan': [ NumberType, [NumberType], (ctx, [n]) => Math.tan(n.evaluate(ctx)) ], 'asin': [ NumberType, [NumberType], (ctx, [n]) => Math.asin(n.evaluate(ctx)) ], 'acos': [ NumberType, [NumberType], (ctx, [n]) => Math.acos(n.evaluate(ctx)) ], 'atan': [ NumberType, [NumberType], (ctx, [n]) => Math.atan(n.evaluate(ctx)) ], 'min': [ NumberType, varargs(NumberType), (ctx, args) => Math.min(...args.map(arg => arg.evaluate(ctx))) ], 'max': [ NumberType, varargs(NumberType), (ctx, args) => Math.max(...args.map(arg => arg.evaluate(ctx))) ], 'abs': [ NumberType, [NumberType], (ctx, [n]) => Math.abs(n.evaluate(ctx)) ], 'round': [ NumberType, [NumberType], (ctx, [n]) => { const v = n.evaluate(ctx); // Javascript's Math.round() rounds towards +Infinity for halfway // values, even when they're negative. It's more common to round // away from 0 (e.g., this is what python and C++ do) return v < 0 ? -Math.round(-v) : Math.round(v); } ], 'floor': [ NumberType, [NumberType], (ctx, [n]) => Math.floor(n.evaluate(ctx)) ], 'ceil': [ NumberType, [NumberType], (ctx, [n]) => Math.ceil(n.evaluate(ctx)) ], 'filter-==': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => ctx.properties()[k.value] === v.value ], 'filter-id-==': [ BooleanType, [ValueType], (ctx, [v]) => ctx.id() === v.value ], 'filter-type-==': [ BooleanType, [StringType], (ctx, [v]) => ctx.geometryType() === v.value ], 'filter-<': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[k.value]; const b = v.value; return typeof a === typeof b && a < b; } ], 'filter-id-<': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = v.value; return typeof a === typeof b && a < b; } ], 'filter->': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[k.value]; const b = v.value; return typeof a === typeof b && a > b; } ], 'filter-id->': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = v.value; return typeof a === typeof b && a > b; } ], 'filter-<=': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[k.value]; const b = v.value; return typeof a === typeof b && a <= b; } ], 'filter-id-<=': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = v.value; return typeof a === typeof b && a <= b; } ], 'filter->=': [ BooleanType, [StringType, ValueType], (ctx, [k, v]) => { const a = ctx.properties()[k.value]; const b = v.value; return typeof a === typeof b && a >= b; } ], 'filter-id->=': [ BooleanType, [ValueType], (ctx, [v]) => { const a = ctx.id(); const b = v.value; return typeof a === typeof b && a >= b; } ], 'filter-has': [ BooleanType, [ValueType], (ctx, [k]) => k.value in ctx.properties() ], 'filter-has-id': [ BooleanType, [], (ctx) => (ctx.id() !== null && ctx.id() !== undefined) ], 'filter-type-in': [ BooleanType, [array$1(StringType)], (ctx, [v]) => v.value.indexOf(ctx.geometryType()) >= 0 ], 'filter-id-in': [ BooleanType, [array$1(ValueType)], (ctx, [v]) => v.value.indexOf(ctx.id()) >= 0 ], 'filter-in-small': [ BooleanType, [StringType, array$1(ValueType)], // assumes v is an array literal (ctx, [k, v]) => v.value.indexOf(ctx.properties()[k.value]) >= 0 ], 'filter-in-large': [ BooleanType, [StringType, array$1(ValueType)], // assumes v is a array literal with values sorted in ascending order and of a single type (ctx, [k, v]) => binarySearch(ctx.properties()[k.value], v.value, 0, v.value.length - 1) ], 'all': { type: BooleanType, overloads: [ [ [BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) && b.evaluate(ctx) ], [ varargs(BooleanType), (ctx, args) => { for (const arg of args) { if (!arg.evaluate(ctx)) return false; } return true; } ] ] }, 'any': { type: BooleanType, overloads: [ [ [BooleanType, BooleanType], (ctx, [a, b]) => a.evaluate(ctx) || b.evaluate(ctx) ], [ varargs(BooleanType), (ctx, args) => { for (const arg of args) { if (arg.evaluate(ctx)) return true; } return false; } ] ] }, '!': [ BooleanType, [BooleanType], (ctx, [b]) => !b.evaluate(ctx) ], 'is-supported-script': [ BooleanType, [StringType], // At parse time this will always return true, so we need to exclude this expression with isGlobalPropertyConstant (ctx, [s]) => { const isSupportedScript = ctx.globals && ctx.globals.isSupportedScript; if (isSupportedScript) { return isSupportedScript(s.evaluate(ctx)); } return true; } ], 'upcase': [ StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase() ], 'downcase': [ StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase() ], 'concat': [ StringType, varargs(ValueType), (ctx, args) => args.map(arg => toString(arg.evaluate(ctx))).join('') ], 'resolved-locale': [ StringType, [CollatorType], (ctx, [collator]) => collator.evaluate(ctx).resolvedLocale() ] }); function stringifySignature(signature) { if (Array.isArray(signature)) { return `(${signature.map(toString$1).join(', ')})`; } else { return `(${toString$1(signature.type)}...)`; } } function isExpressionConstant(expression) { if (expression instanceof Var) { return isExpressionConstant(expression.boundExpression); } else if (expression instanceof CompoundExpression && expression.name === 'error') { return false; } else if (expression instanceof CollatorExpression) { // Although the results of a Collator expression with fixed arguments // generally shouldn't change between executions, we can't serialize them // as constant expressions because results change based on environment. return false; } else if (expression instanceof Within) { return false; } else if (expression instanceof Distance) { return false; } const isTypeAnnotation = expression instanceof Coercion || expression instanceof Assertion; let childrenConstant = true; expression.eachChild(child => { // We can _almost_ assume that if `expressions` children are constant, // they would already have been evaluated to Literal values when they // were parsed. Type annotations are the exception, because they might // have been inferred and added after a child was parsed. // So we recurse into isConstant() for the children of type annotations, // but otherwise simply check whether they are Literals. if (isTypeAnnotation) { childrenConstant = childrenConstant && isExpressionConstant(child); } else { childrenConstant = childrenConstant && child instanceof Literal; } }); if (!childrenConstant) { return false; } return isFeatureConstant(expression) && isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'accumulated', 'is-supported-script']); } function isFeatureConstant(e) { if (e instanceof CompoundExpression) { if (e.name === 'get' && e.args.length === 1) { return false; } else if (e.name === 'feature-state') { return false; } else if (e.name === 'has' && e.args.length === 1) { return false; } else if (e.name === 'properties' || e.name === 'geometry-type' || e.name === 'id') { return false; } else if (/^filter-/.test(e.name)) { return false; } } if (e instanceof Within) { return false; } if (e instanceof Distance) { return false; } let result = true; e.eachChild(arg => { if (result && !isFeatureConstant(arg)) { result = false; } }); return result; } function isStateConstant(e) { if (e instanceof CompoundExpression) { if (e.name === 'feature-state') { return false; } } let result = true; e.eachChild(arg => { if (result && !isStateConstant(arg)) { result = false; } }); return result; } function isGlobalPropertyConstant(e, properties) { if (e instanceof CompoundExpression && properties.indexOf(e.name) >= 0) { return false; } let result = true; e.eachChild((arg) => { if (result && !isGlobalPropertyConstant(arg, properties)) { result = false; } }); return result; } function success(value) { return { result: 'success', value }; } function error(value) { return { result: 'error', value }; } function supportsPropertyExpression(spec) { return spec['property-type'] === 'data-driven' || spec['property-type'] === 'cross-faded-data-driven'; } function supportsZoomExpression(spec) { return !!spec.expression && spec.expression.parameters.indexOf('zoom') > -1; } function supportsInterpolation(spec) { return !!spec.expression && spec.expression.interpolated; } function getType(val) { if (val instanceof Number) { return 'number'; } else if (val instanceof String) { return 'string'; } else if (val instanceof Boolean) { return 'boolean'; } else if (Array.isArray(val)) { return 'array'; } else if (val === null) { return 'null'; } else { return typeof val; } } function isFunction(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } class StyleExpression { constructor(expression, propertySpec) { this.expression = expression; this._warningHistory = {}; this._evaluator = new EvaluationContext(); this._defaultValue = propertySpec ? getDefaultValue(propertySpec) : null; this._enumValues = propertySpec && propertySpec.type === 'enum' ? propertySpec.values : null; } evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) { this._evaluator.globals = globals; this._evaluator.feature = feature; this._evaluator.featureState = featureState; this._evaluator.canonical = canonical; this._evaluator.availableImages = availableImages || null; this._evaluator.formattedSection = formattedSection; return this.expression.evaluate(this._evaluator); } evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) { this._evaluator.globals = globals; this._evaluator.feature = feature || null; this._evaluator.featureState = featureState || null; this._evaluator.canonical = canonical; this._evaluator.availableImages = availableImages || null; this._evaluator.formattedSection = formattedSection || null; try { const val = this.expression.evaluate(this._evaluator); // eslint-disable-next-line no-self-compare if (val === null || val === undefined || (typeof val === 'number' && val !== val)) { return this._defaultValue; } if (this._enumValues && !(val in this._enumValues)) { throw new RuntimeError(`Expected value to be one of ${Object.keys(this._enumValues).map(v => JSON.stringify(v)).join(', ')}, but found ${JSON.stringify(val)} instead.`); } return val; } catch (e) { if (!this._warningHistory[e.message]) { this._warningHistory[e.message] = true; if (typeof console !== 'undefined') { console.warn(e.message); } } return this._defaultValue; } } } function isExpression(expression) { return Array.isArray(expression) && expression.length > 0 && typeof expression[0] === 'string' && expression[0] in expressions; } /** * Parse and typecheck the given style spec JSON expression. If * options.defaultValue is provided, then the resulting StyleExpression's * `evaluate()` method will handle errors by logging a warning (once per * message) and returning the default value. Otherwise, it will throw * evaluation errors. * * @private */ function createExpression(expression, propertySpec) { const parser = new ParsingContext(expressions, isExpressionConstant, [], propertySpec ? getExpectedType(propertySpec) : undefined); // For string-valued properties, coerce to string at the top level rather than asserting. const parsed = parser.parse(expression, undefined, undefined, undefined, propertySpec && propertySpec.type === 'string' ? { typeAnnotation: 'coerce' } : undefined); if (!parsed) { return error(parser.errors); } return success(new StyleExpression(parsed, propertySpec)); } class ZoomConstantExpression { constructor(kind, expression) { this.kind = kind; this._styleExpression = expression; this.isStateDependent = kind !== 'constant' && !isStateConstant(expression.expression); } evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } } class ZoomDependentExpression { constructor(kind, expression, zoomStops, interpolationType) { this.kind = kind; this.zoomStops = zoomStops; this._styleExpression = expression; this.isStateDependent = kind !== 'camera' && !isStateConstant(expression.expression); this.interpolationType = interpolationType; } evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection) { return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection); } evaluate(globals, feature, featureState, canonical, availableImages, formattedSection) { return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection); } interpolationFactor(input, lower, upper) { if (this.interpolationType) { return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper); } else { return 0; } } } function createPropertyExpression(expressionInput, propertySpec) { const expression = createExpression(expressionInput, propertySpec); if (expression.result === 'error') { return expression; } const parsed = expression.value.expression; const isFeatureConstantResult = isFeatureConstant(parsed); if (!isFeatureConstantResult && !supportsPropertyExpression(propertySpec)) { return error([new ExpressionParsingError('', 'data expressions not supported')]); } const isZoomConstant = isGlobalPropertyConstant(parsed, ['zoom']); if (!isZoomConstant && !supportsZoomExpression(propertySpec)) { return error([new ExpressionParsingError('', 'zoom expressions not supported')]); } const zoomCurve = findZoomCurve(parsed); if (!zoomCurve && !isZoomConstant) { return error([new ExpressionParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')]); } else if (zoomCurve instanceof ExpressionParsingError) { return error([zoomCurve]); } else if (zoomCurve instanceof Interpolate && !supportsInterpolation(propertySpec)) { return error([new ExpressionParsingError('', '"interpolate" expressions cannot be used with this property')]); } if (!zoomCurve) { return success(isFeatureConstantResult ? new ZoomConstantExpression('constant', expression.value) : new ZoomConstantExpression('source', expression.value)); } const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined; return success(isFeatureConstantResult ? new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType) : new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType)); } // Zoom-dependent expressions may only use ["zoom"] as the input to a top-level "step" or "interpolate" // expression (collectively referred to as a "curve"). The curve may be wrapped in one or more "let" or // "coalesce" expressions. function findZoomCurve(expression) { let result = null; if (expression instanceof Let) { result = findZoomCurve(expression.result); } else if (expression instanceof Coalesce) { for (const arg of expression.args) { result = findZoomCurve(arg); if (result) { break; } } } else if ((expression instanceof Step || expression instanceof Interpolate) && expression.input instanceof CompoundExpression && expression.input.name === 'zoom') { result = expression; } if (result instanceof ExpressionParsingError) { return result; } expression.eachChild((child) => { const childResult = findZoomCurve(child); if (childResult instanceof ExpressionParsingError) { result = childResult; } else if (!result && childResult) { result = new ExpressionParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.'); } else if (result && childResult && result !== childResult) { result = new ExpressionParsingError('', 'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.'); } }); return result; } function getExpectedType(spec) { const types = { color: ColorType, string: StringType, number: NumberType, enum: StringType, boolean: BooleanType, formatted: FormattedType, padding: PaddingType, resolvedImage: ResolvedImageType, variableAnchorOffsetCollection: VariableAnchorOffsetCollectionType }; if (spec.type === 'array') { return array$1(types[spec.value] || ValueType, spec.length); } return types[spec.type]; } function getDefaultValue(spec) { if (spec.type === 'color' && isFunction(spec.default)) { // Special case for heatmap-color: it uses the 'default:' to define a // default color ramp, but createExpression expects a simple value to fall // back to in case of runtime errors return new Color(0, 0, 0, 0); } else if (spec.type === 'color') { return Color.parse(spec.default) || null; } else if (spec.type === 'padding') { return Padding.parse(spec.default) || null; } else if (spec.type === 'variableAnchorOffsetCollection') { return VariableAnchorOffsetCollection.parse(spec.default) || null; } else if (spec.default === undefined) { return null; } else { return spec.default; } } function validateObject(options) { const key = options.key; const object = options.value; const elementSpecs = options.valueSpec || {}; const elementValidators = options.objectElementValidators || {}; const style = options.style; const styleSpec = options.styleSpec; const validateSpec = options.validateSpec; let errors = []; const type = getType(object); if (type !== 'object') { return [new ValidationError(key, object, `object expected, ${type} found`)]; } for (const objectKey in object) { const elementSpecKey = objectKey.split('.')[0]; // treat 'paint.*' as 'paint' const elementSpec = elementSpecs[elementSpecKey] || elementSpecs['*']; let validateElement; if (elementValidators[elementSpecKey]) { validateElement = elementValidators[elementSpecKey]; } else if (elementSpecs[elementSpecKey]) { validateElement = validateSpec; } else if (elementValidators['*']) { validateElement = elementValidators['*']; } else if (elementSpecs['*']) { validateElement = validateSpec; } else { errors.push(new ValidationError(key, object[objectKey], `unknown property "${objectKey}"`)); continue; } errors = errors.concat(validateElement({ key: (key ? `${key}.` : key) + objectKey, value: object[objectKey], valueSpec: elementSpec, style, styleSpec, object, objectKey, validateSpec, }, object)); } for (const elementSpecKey in elementSpecs) { // Don't check `required` when there's a custom validator for that property. if (elementValidators[elementSpecKey]) { continue; } if (elementSpecs[elementSpecKey].required && elementSpecs[elementSpecKey]['default'] === undefined && object[elementSpecKey] === undefined) { errors.push(new ValidationError(key, object, `missing required property "${elementSpecKey}"`)); } } return errors; } function validateArray(options) { const array = options.value; const arraySpec = options.valueSpec; const validateSpec = options.validateSpec; const style = options.style; const styleSpec = options.styleSpec; const key = options.key; const validateArrayElement = options.arrayElementValidator || validateSpec; if (getType(array) !== 'array') { return [new ValidationError(key, array, `array expected, ${getType(array)} found`)]; } if (arraySpec.length && array.length !== arraySpec.length) { return [new ValidationError(key, array, `array length ${arraySpec.length} expected, length ${array.length} found`)]; } if (arraySpec['min-length'] && array.length < arraySpec['min-length']) { return [new ValidationError(key, array, `array length at least ${arraySpec['min-length']} expected, length ${array.length} found`)]; } let arrayElementSpec = { 'type': arraySpec.value, 'values': arraySpec.values }; if (styleSpec.$version < 7) { arrayElementSpec['function'] = arraySpec.function; } if (getType(arraySpec.value) === 'object') { arrayElementSpec = arraySpec.value; } let errors = []; for (let i = 0; i < array.length; i++) { errors = errors.concat(validateArrayElement({ array, arrayIndex: i, value: array[i], valueSpec: arrayElementSpec, validateSpec: options.validateSpec, style, styleSpec, key: `${key}[${i}]` })); } return errors; } function validateNumber(options) { const key = options.key; const value = options.value; const valueSpec = options.valueSpec; let type = getType(value); // eslint-disable-next-line no-self-compare if (type === 'number' && value !== value) { type = 'NaN'; } if (type !== 'number') { return [new ValidationError(key, value, `number expected, ${type} found`)]; } if ('minimum' in valueSpec && value < valueSpec.minimum) { return [new ValidationError(key, value, `${value} is less than the minimum value ${valueSpec.minimum}`)]; } if ('maximum' in valueSpec && value > valueSpec.maximum) { return [new ValidationError(key, value, `${value} is greater than the maximum value ${valueSpec.maximum}`)]; } return []; } function validateFunction(options) { const functionValueSpec = options.valueSpec; const functionType = unbundle(options.value.type); let stopKeyType; let stopDomainValues = {}; let previousStopDomainValue; let previousStopDomainZoom; const isZoomFunction = functionType !== 'categorical' && options.value.property === undefined; const isPropertyFunction = !isZoomFunction; const isZoomAndPropertyFunction = getType(options.value.stops) === 'array' && getType(options.value.stops[0]) === 'array' && getType(options.value.stops[0][0]) === 'object'; const errors = validateObject({ key: options.key, value: options.value, valueSpec: options.styleSpec.function, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec, objectElementValidators: { stops: validateFunctionStops, default: validateFunctionDefault } }); if (functionType === 'identity' && isZoomFunction) { errors.push(new ValidationError(options.key, options.value, 'missing required property "property"')); } if (functionType !== 'identity' && !options.value.stops) { errors.push(new ValidationError(options.key, options.value, 'missing required property "stops"')); } if (functionType === 'exponential' && options.valueSpec.expression && !supportsInterpolation(options.valueSpec)) { errors.push(new ValidationError(options.key, options.value, 'exponential functions not supported')); } if (options.styleSpec.$version >= 8) { if (isPropertyFunction && !supportsPropertyExpression(options.valueSpec)) { errors.push(new ValidationError(options.key, options.value, 'property functions not supported')); } else if (isZoomFunction && !supportsZoomExpression(options.valueSpec)) { errors.push(new ValidationError(options.key, options.value, 'zoom functions not supported')); } } if ((functionType === 'categorical' || isZoomAndPropertyFunction) && options.value.property === undefined) { errors.push(new ValidationError(options.key, options.value, '"property" property is required')); } return errors; function validateFunctionStops(options) { if (functionType === 'identity') { return [new ValidationError(options.key, options.value, 'identity function may not have a "stops" property')]; } let errors = []; const value = options.value; errors = errors.concat(validateArray({ key: options.key, value, valueSpec: options.valueSpec, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec, arrayElementValidator: validateFunctionStop })); if (getType(value) === 'array' && value.length === 0) { errors.push(new ValidationError(options.key, value, 'array must have at least one stop')); } return errors; } function validateFunctionStop(options) { let errors = []; const value = options.value; const key = options.key; if (getType(value) !== 'array') { return [new ValidationError(key, value, `array expected, ${getType(value)} found`)]; } if (value.length !== 2) { return [new ValidationError(key, value, `array length 2 expected, length ${value.length} found`)]; } if (isZoomAndPropertyFunction) { if (getType(value[0]) !== 'object') { return [new ValidationError(key, value, `object expected, ${getType(value[0])} found`)]; } if (value[0].zoom === undefined) { return [new ValidationError(key, value, 'object stop key must have zoom')]; } if (value[0].value === undefined) { return [new ValidationError(key, value, 'object stop key must have value')]; } if (previousStopDomainZoom && previousStopDomainZoom > unbundle(value[0].zoom)) { return [new ValidationError(key, value[0].zoom, 'stop zoom values must appear in ascending order')]; } if (unbundle(value[0].zoom) !== previousStopDomainZoom) { previousStopDomainZoom = unbundle(value[0].zoom); previousStopDomainValue = undefined; stopDomainValues = {}; } errors = errors.concat(validateObject({ key: `${key}[0]`, value: value[0], valueSpec: { zoom: {} }, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec, objectElementValidators: { zoom: validateNumber, value: validateStopDomainValue } })); } else { errors = errors.concat(validateStopDomainValue({ key: `${key}[0]`, value: value[0], valueSpec: {}, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec }, value)); } if (isExpression(deepUnbundle(value[1]))) { return errors.concat([new ValidationError(`${key}[1]`, value[1], 'expressions are not allowed in function stops.')]); } return errors.concat(options.validateSpec({ key: `${key}[1]`, value: value[1], valueSpec: functionValueSpec, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec })); } function validateStopDomainValue(options, stop) { const type = getType(options.value); const value = unbundle(options.value); const reportValue = options.value !== null ? options.value : stop; if (!stopKeyType) { stopKeyType = type; } else if (type !== stopKeyType) { return [new ValidationError(options.key, reportValue, `${type} stop domain type must match previous stop domain type ${stopKeyType}`)]; } if (type !== 'number' && type !== 'string' && type !== 'boolean') { return [new ValidationError(options.key, reportValue, 'stop domain value must be a number, string, or boolean')]; } if (type !== 'number' && functionType !== 'categorical') { let message = `number expected, ${type} found`; if (supportsPropertyExpression(functionValueSpec) && functionType === undefined) { message += '\nIf you intended to use a categorical function, specify `"type": "categorical"`.'; } return [new ValidationError(options.key, reportValue, message)]; } if (functionType === 'categorical' && type === 'number' && (!isFinite(value) || Math.floor(value) !== value)) { return [new ValidationError(options.key, reportValue, `integer expected, found ${value}`)]; } if (functionType !== 'categorical' && type === 'number' && previousStopDomainValue !== undefined && value < previousStopDomainValue) { return [new ValidationError(options.key, reportValue, 'stop domain values must appear in ascending order')]; } else { previousStopDomainValue = value; } if (functionType === 'categorical' && value in stopDomainValues) { return [new ValidationError(options.key, reportValue, 'stop domain values must be unique')]; } else { stopDomainValues[value] = true; } return []; } function validateFunctionDefault(options) { return options.validateSpec({ key: options.key, value: options.value, valueSpec: functionValueSpec, validateSpec: options.validateSpec, style: options.style, styleSpec: options.styleSpec }); } } function validateExpression(options) { const expression = (options.expressionContext === 'property' ? createPropertyExpression : createExpression)(deepUnbundle(options.value), options.valueSpec); if (expression.result === 'error') { return expression.value.map((error) => { return new ValidationError(`${options.key}${error.key}`, options.value, error.message); }); } const expressionObj = expression.value.expression || expression.value._styleExpression.expression; if (options.expressionContext === 'property' && (options.propertyKey === 'text-font') && !expressionObj.outputDefined()) { return [new ValidationError(options.key, options.value, `Invalid data expression for "${options.propertyKey}". Output values must be contained as literals within the expression.`)]; } if (options.expressionContext === 'property' && options.propertyType === 'layout' && (!isStateConstant(expressionObj))) { return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with layout properties.')]; } if (options.expressionContext === 'filter' && !isStateConstant(expressionObj)) { return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with filters.')]; } if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) { if (!isGlobalPropertyConstant(expressionObj, ['zoom', 'feature-state'])) { return [new ValidationError(options.key, options.value, '"zoom" and "feature-state" expressions are not supported with cluster properties.')]; } if (options.expressionContext === 'cluster-initial' && !isFeatureConstant(expressionObj)) { return [new ValidationError(options.key, options.value, 'Feature data expressions are not supported with initial expression part of cluster properties.')]; } } return []; } function validateBoolean(options) { const value = options.value; const key = options.key; const type = getType(value); if (type !== 'boolean') { return [new ValidationError(key, value, `boolean expected, ${type} found`)]; } return []; } function validateColor(options) { const key = options.key; const value = options.value; const type = getType(value); if (type !== 'string') { return [new ValidationError(key, value, `color expected, ${type} found`)]; } if (!Color.parse(String(value))) { // cast String object to string primitive return [new ValidationError(key, value, `color expected, "${value}" found`)]; } return []; } function validateEnum(options) { const key = options.key; const value = options.value; const valueSpec = options.valueSpec; const errors = []; if (Array.isArray(valueSpec.values)) { // <=v7 if (valueSpec.values.indexOf(unbundle(value)) === -1) { errors.push(new ValidationError(key, value, `expected one of [${valueSpec.values.join(', ')}], ${JSON.stringify(value)} found`)); } } else { // >=v8 if (Object.keys(valueSpec.values).indexOf(unbundle(value)) === -1) { errors.push(new ValidationError(key, value, `expected one of [${Object.keys(valueSpec.values).join(', ')}], ${JSON.stringify(value)} found`)); } } return errors; } function isExpressionFilter(filter) { if (filter === true || filter === false) { return true; } if (!Array.isArray(filter) || filter.length === 0) { return false; } switch (filter[0]) { case 'has': return filter.length >= 2 && filter[1] !== '$id' && filter[1] !== '$type'; case 'in': return filter.length >= 3 && (typeof filter[1] !== 'string' || Array.isArray(filter[2])); case '!in': case '!has': case 'none': return false; case '==': case '!=': case '>': case '>=': case '<': case '<=': return filter.length !== 3 || (Array.isArray(filter[1]) || Array.isArray(filter[2])); case 'any': case 'all': for (const f of filter.slice(1)) { if (!isExpressionFilter(f) && typeof f !== 'boolean') { return false; } } return true; default: return true; } } function validateFilter(options) { if (isExpressionFilter(deepUnbundle(options.value))) { return validateExpression(extendBy({}, options, { expressionContext: 'filter', valueSpec: { value: 'boolean' } })); } else { return validateNonExpressionFilter(options); } } function validateNonExpressionFilter(options) { const value = options.value; const key = options.key; if (getType(value) !== 'array') { return [new ValidationError(key, value, `array expected, ${getType(value)} found`)]; } const styleSpec = options.styleSpec; let type; let errors = []; if (value.length < 1) { return [new ValidationError(key, value, 'filter array must have at least 1 element')]; } errors = errors.concat(validateEnum({ key: `${key}[0]`, value: value[0], valueSpec: styleSpec.filter_operator, style: options.style, styleSpec: options.styleSpec })); switch (unbundle(value[0])) { case '<': case '<=': case '>': case '>=': if (value.length >= 2 && unbundle(value[1]) === '$type') { errors.push(new ValidationError(key, value, `"$type" cannot be use with operator "${value[0]}"`)); } /* falls through */ case '==': case '!=': if (value.length !== 3) { errors.push(new ValidationError(key, value, `filter array for operator "${value[0]}" must have 3 elements`)); } /* falls through */ case 'in': case '!in': if (value.length >= 2) { type = getType(value[1]); if (type !== 'string') { errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`)); } } for (let i = 2; i < value.length; i++) { type = getType(value[i]); if (unbundle(value[1]) === '$type') { errors = errors.concat(validateEnum({ key: `${key}[${i}]`, value: value[i], valueSpec: styleSpec.geometry_type, style: options.style, styleSpec: options.styleSpec })); } else if (type !== 'string' && type !== 'number' && type !== 'boolean') { errors.push(new ValidationError(`${key}[${i}]`, value[i], `string, number, or boolean expected, ${type} found`)); } } break; case 'any': case 'all': case 'none': for (let i = 1; i < value.length; i++) { errors = errors.concat(validateNonExpressionFilter({ key: `${key}[${i}]`, value: value[i], style: options.style, styleSpec: options.styleSpec })); } break; case 'has': case '!has': type = getType(value[1]); if (value.length !== 2) { errors.push(new ValidationError(key, value, `filter array for "${value[0]}" operator must have 2 elements`)); } else if (type !== 'string') { errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`)); } break; } return errors; } function validateProperty(options, propertyType) { const key = options.key; const validateSpec = options.validateSpec; const style = options.style; const styleSpec = options.styleSpec; const value = options.value; const propertyKey = options.objectKey; const layerSpec = styleSpec[`${propertyType}_${options.layerType}`]; if (!layerSpec) return []; const transitionMatch = propertyKey.match(/^(.*)-transition$/); if (propertyType === 'paint' && transitionMatch && layerSpec[transitionMatch[1]] && layerSpec[transitionMatch[1]].transition) { return validateSpec({ key, value, valueSpec: styleSpec.transition, style, styleSpec }); } const valueSpec = options.valueSpec || layerSpec[propertyKey]; if (!valueSpec) { return [new ValidationError(key, value, `unknown property "${propertyKey}"`)]; } let tokenMatch; if (getType(value) === 'string' && supportsPropertyExpression(valueSpec) && !valueSpec.tokens && (tokenMatch = /^{([^}]+)}$/.exec(value))) { return [new ValidationError(key, value, `"${propertyKey}" does not support interpolation syntax\n` + `Use an identity property function instead: \`{ "type": "identity", "property": ${JSON.stringify(tokenMatch[1])} }\`.`)]; } const errors = []; if (options.layerType === 'symbol') { if (propertyKey === 'text-field' && style && !style.glyphs) { errors.push(new ValidationError(key, value, 'use of "text-field" requires a style "glyphs" property')); } if (propertyKey === 'text-font' && isFunction(deepUnbundle(value)) && unbundle(value.type) === 'identity') { errors.push(new ValidationError(key, value, '"text-font" does not support identity functions')); } } return errors.concat(validateSpec({ key: options.key, value, valueSpec, style, styleSpec, expressionContext: 'property', propertyType, propertyKey })); } function validatePaintProperty(options) { return validateProperty(options, 'paint'); } function validateLayoutProperty(options) { return validateProperty(options, 'layout'); } function validateLayer(options) { let errors = []; const layer = options.value; const key = options.key; const style = options.style; const styleSpec = options.styleSpec; if (!layer.type && !layer.ref) { errors.push(new ValidationError(key, layer, 'either "type" or "ref" is required')); } let type = unbundle(layer.type); const ref = unbundle(layer.ref); if (layer.id) { const layerId = unbundle(layer.id); for (let i = 0; i < options.arrayIndex; i++) { const otherLayer = style.layers[i]; if (unbundle(otherLayer.id) === layerId) { errors.push(new ValidationError(key, layer.id, `duplicate layer id "${layer.id}", previously used at line ${otherLayer.id.__line__}`)); } } } if ('ref' in layer) { ['type', 'source', 'source-layer', 'filter', 'layout'].forEach((p) => { if (p in layer) { errors.push(new ValidationError(key, layer[p], `"${p}" is prohibited for ref layers`)); } }); let parent; style.layers.forEach((layer) => { if (unbundle(layer.id) === ref) parent = layer; }); if (!parent) { errors.push(new ValidationError(key, layer.ref, `ref layer "${ref}" not found`)); } else if (parent.ref) { errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer')); } else { type = unbundle(parent.type); } } else if (type !== 'background') { if (!layer.source) { errors.push(new ValidationError(key, layer, 'missing required property "source"')); } else { const source = style.sources && style.sources[layer.source]; const sourceType = source && unbundle(source.type); if (!source) { errors.push(new ValidationError(key, layer.source, `source "${layer.source}" not found`)); } else if (sourceType === 'vector' && type === 'raster') { errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster source`)); } else if (sourceType !== 'raster-dem' && type === 'hillshade') { errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster-dem source`)); } else if (sourceType === 'raster' && type !== 'raster') { errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a vector source`)); } else if (sourceType === 'vector' && !layer['source-layer']) { errors.push(new ValidationError(key, layer, `layer "${layer.id}" must specify a "source-layer"`)); } else if (sourceType === 'raster-dem' && type !== 'hillshade') { errors.push(new ValidationError(key, layer.source, 'raster-dem source can only be used with layer type \'hillshade\'.')); } else if (type === 'line' && layer.paint && layer.paint['line-gradient'] && (sourceType !== 'geojson' || !source.lineMetrics)) { errors.push(new ValidationError(key, layer, `layer "${layer.id}" specifies a line-gradient, which requires a GeoJSON source with \`lineMetrics\` enabled.`)); } } } errors = errors.concat(validateObject({ key, value: layer, valueSpec: styleSpec.layer, style: options.style, styleSpec: options.styleSpec, validateSpec: options.validateSpec, objectElementValidators: { '*'() { return []; }, // We don't want to enforce the spec's `"requires": true` for backward compatibility with refs; // the actual requirement is validated above. See https://github.com/mapbox/mapbox-gl-js/issues/5772. type() { return options.validateSpec({ key: `${key}.type`, value: layer.type, valueSpec: styleSpec.layer.type, style: options.style, styleSpec: options.styleSpec, validateSpec: options.validateSpec, object: layer, objectKey: 'type' }); }, filter: validateFilter, layout(options) { return validateObject({ layer, key: options.key, value: options.value, style: options.style, styleSpec: options.styleSpec, validateSpec: options.validateSpec, objectElementValidators: { '*'(options) { return validateLayoutProperty(extendBy({ layerType: type }, options)); } } }); }, paint(options) { return validateObject({ layer, key: options.key, value: options.value, style: options.style, styleSpec: options.styleSpec, validateSpec: options.validateSpec, objectElementValidators: { '*'(options) { return validatePaintProperty(extendBy({ layerType: type }, options)); } } }); } } })); return errors; } function validateString(options) { const value = options.value; const key = options.key; const type = getType(value); if (type !== 'string') { return [new ValidationError(key, value, `string expected, ${type} found`)]; } return []; } function validateRasterDEMSource(options) { var _a; const sourceName = (_a = options.sourceName) !== null && _a !== void 0 ? _a : ''; const rasterDEM = options.value; const styleSpec = options.styleSpec; const rasterDEMSpec = styleSpec.source_raster_dem; const style = options.style; let errors = []; const rootType = getType(rasterDEM); if (rasterDEM === undefined) { return errors; } else if (rootType !== 'object') { errors.push(new ValidationError('source_raster_dem', rasterDEM, `object expected, ${rootType} found`)); return errors; } const encoding = unbundle(rasterDEM.encoding); const isCustomEncoding = encoding === 'custom'; const customEncodingKeys = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift']; const encodingName = options.value.encoding ? `"${options.value.encoding}"` : 'Default'; for (const key in rasterDEM) { if (!isCustomEncoding && customEncodingKeys.includes(key)) { errors.push(new ValidationError(key, rasterDEM[key], `In "${sourceName}": "${key}" is only valid when "encoding" is set to "custom". ${encodingName} encoding found`)); } else if (rasterDEMSpec[key]) { errors = errors.concat(options.validateSpec({ key, value: rasterDEM[key], valueSpec: rasterDEMSpec[key], validateSpec: options.validateSpec, style, styleSpec })); } else { errors.push(new ValidationError(key, rasterDEM[key], `unknown property "${key}"`)); } } return errors; } const objectElementValidators = { promoteId: validatePromoteId }; function validateSource(options) { const value = options.value; const key = options.key; const styleSpec = options.styleSpec; const style = options.style; const validateSpec = options.validateSpec; if (!value.type) { return [new ValidationError(key, value, '"type" is required')]; } const type = unbundle(value.type); let errors; switch (type) { case 'vector': case 'raster': errors = validateObject({ key, value, valueSpec: styleSpec[`source_${type.replace('-', '_')}`], style: options.style, styleSpec, objectElementValidators, validateSpec, }); return errors; case 'raster-dem': errors = validateRasterDEMSource({ sourceName: key, value, style: options.style, styleSpec, validateSpec, }); return errors; case 'geojson': errors = validateObject({ key, value, valueSpec: styleSpec.source_geojson, style, styleSpec, validateSpec, objectElementValidators }); if (value.cluster) { for (const prop in value.clusterProperties) { const [operator, mapExpr] = value.clusterProperties[prop]; const reduceExpr = typeof operator === 'string' ? [operator, ['accumulated'], ['get', prop]] : operator; errors.push(...validateExpression({ key: `${key}.${prop}.map`, value: mapExpr, validateSpec, expressionContext: 'cluster-map' })); errors.push(...validateExpression({ key: `${key}.${prop}.reduce`, value: reduceExpr, validateSpec, expressionContext: 'cluster-reduce' })); } } return errors; case 'video': return validateObject({ key, value, valueSpec: styleSpec.source_video, style, validateSpec, styleSpec }); case 'image': return validateObject({ key, value, valueSpec: styleSpec.source_image, style, validateSpec, styleSpec }); case 'canvas': return [new ValidationError(key, null, 'Please use runtime APIs to add canvas sources, rather than including them in stylesheets.', 'source.canvas')]; default: return validateEnum({ key: `${key}.type`, value: value.type, valueSpec: { values: ['vector', 'raster', 'raster-dem', 'geojson', 'video', 'image'] }, style, validateSpec, styleSpec }); } } function validatePromoteId({ key, value }) { if (getType(value) === 'string') { return validateString({ key, value }); } else { const errors = []; for (const prop in value) { errors.push(...validateString({ key: `${key}.${prop}`, value: value[prop] })); } return errors; } } function validateLight(options) { const light = options.value; const styleSpec = options.styleSpec; const lightSpec = styleSpec.light; const style = options.style; let errors = []; const rootType = getType(light); if (light === undefined) { return errors; } else if (rootType !== 'object') { errors = errors.concat([new ValidationError('light', light, `object expected, ${rootType} found`)]); return errors; } for (const key in light) { const transitionMatch = key.match(/^(.*)-transition$/); if (transitionMatch && lightSpec[transitionMatch[1]] && lightSpec[transitionMatch[1]].transition) { errors = errors.concat(options.validateSpec({ key, value: light[key], valueSpec: styleSpec.transition, validateSpec: options.validateSpec, style, styleSpec })); } else if (lightSpec[key]) { errors = errors.concat(options.validateSpec({ key, value: light[key], valueSpec: lightSpec[key], validateSpec: options.validateSpec, style, styleSpec })); } else { errors = errors.concat([new ValidationError(key, light[key], `unknown property "${key}"`)]); } } return errors; } function validateSky(options) { const sky = options.value; const styleSpec = options.styleSpec; const skySpec = styleSpec.sky; const style = options.style; const rootType = getType(sky); if (sky === undefined) { return []; } else if (rootType !== 'object') { return [new ValidationError('sky', sky, `object expected, ${rootType} found`)]; } let errors = []; for (const key in sky) { if (skySpec[key]) { errors = errors.concat(options.validateSpec({ key, value: sky[key], valueSpec: skySpec[key], style, styleSpec })); } else { errors = errors.concat([new ValidationError(key, sky[key], `unknown property "${key}"`)]); } } return errors; } function validateTerrain(options) { const terrain = options.value; const styleSpec = options.styleSpec; const terrainSpec = styleSpec.terrain; const style = options.style; let errors = []; const rootType = getType(terrain); if (terrain === undefined) { return errors; } else if (rootType !== 'object') { errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]); return errors; } for (const key in terrain) { if (terrainSpec[key]) { errors = errors.concat(options.validateSpec({ key, value: terrain[key], valueSpec: terrainSpec[key], validateSpec: options.validateSpec, style, styleSpec })); } else { errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]); } } return errors; } function validateFormatted(options) { if (validateString(options).length === 0) { return []; } return validateExpression(options); } function validateImage(options) { if (validateString(options).length === 0) { return []; } return validateExpression(options); } function validatePadding(options) { const key = options.key; const value = options.value; const type = getType(value); if (type === 'array') { if (value.length < 1 || value.length > 4) { return [new ValidationError(key, value, `padding requires 1 to 4 values; ${value.length} values found`)]; } const arrayElementSpec = { type: 'number' }; let errors = []; for (let i = 0; i < value.length; i++) { errors = errors.concat(options.validateSpec({ key: `${key}[${i}]`, value: value[i], validateSpec: options.validateSpec, valueSpec: arrayElementSpec })); } return errors; } else { return validateNumber({ key, value, valueSpec: {} }); } } function validateVariableAnchorOffsetCollection(options) { const key = options.key; const value = options.value; const type = getType(value); const styleSpec = options.styleSpec; if (type !== 'array' || value.length < 1 || value.length % 2 !== 0) { return [new ValidationError(key, value, 'variableAnchorOffsetCollection requires a non-empty array of even length')]; } let errors = []; for (let i = 0; i < value.length; i += 2) { // Elements in even positions should be values from text-anchor enum errors = errors.concat(validateEnum({ key: `${key}[${i}]`, value: value[i], valueSpec: styleSpec['layout_symbol']['text-anchor'] })); // Elements in odd positions should be points (2-element numeric arrays) errors = errors.concat(validateArray({ key: `${key}[${i + 1}]`, value: value[i + 1], valueSpec: { length: 2, value: 'number' }, validateSpec: options.validateSpec, style: options.style, styleSpec })); } return errors; } function validateSprite(options) { let errors = []; const sprite = options.value; const key = options.key; if (!Array.isArray(sprite)) { return validateString({ key, value: sprite }); } else { const allSpriteIds = []; const allSpriteURLs = []; for (const i in sprite) { if (sprite[i].id && allSpriteIds.includes(sprite[i].id)) errors.push(new ValidationError(key, sprite, `all the sprites' ids must be unique, but ${sprite[i].id} is duplicated`)); allSpriteIds.push(sprite[i].id); if (sprite[i].url && allSpriteURLs.includes(sprite[i].url)) errors.push(new ValidationError(key, sprite, `all the sprites' URLs must be unique, but ${sprite[i].url} is duplicated`)); allSpriteURLs.push(sprite[i].url); const pairSpec = { id: { type: 'string', required: true, }, url: { type: 'string', required: true, } }; errors = errors.concat(validateObject({ key: `${key}[${i}]`, value: sprite[i], valueSpec: pairSpec, validateSpec: options.validateSpec, })); } return errors; } } function validateProjection(options) { const projection = options.value; const styleSpec = options.styleSpec; const projectionSpec = styleSpec.projection; const style = options.style; const rootType = getType(projection); if (projection === undefined) { return []; } else if (rootType !== 'object') { return [new ValidationError('projection', projection, `object expected, ${rootType} found`)]; } let errors = []; for (const key in projection) { if (projectionSpec[key]) { errors = errors.concat(options.validateSpec({ key, value: projection[key], valueSpec: projectionSpec[key], style, styleSpec })); } else { errors = errors.concat([new ValidationError(key, projection[key], `unknown property "${key}"`)]); } } return errors; } const VALIDATORS = { '*'() { return []; }, 'array': validateArray, 'boolean': validateBoolean, 'number': validateNumber, 'color': validateColor, 'constants': validateConstants, 'enum': validateEnum, 'filter': validateFilter, 'function': validateFunction, 'layer': validateLayer, 'object': validateObject, 'source': validateSource, 'light': validateLight, 'sky': validateSky, 'terrain': validateTerrain, 'projection': validateProjection, 'string': validateString, 'formatted': validateFormatted, 'resolvedImage': validateImage, 'padding': validatePadding, 'variableAnchorOffsetCollection': validateVariableAnchorOffsetCollection, 'sprite': validateSprite, }; /** * Main recursive validation function used internally. * You should use `validateStyleMin` in the browser or `validateStyle` in node env. * @param options - the options object * @param options.key - string representing location of validation in style tree. Used only * for more informative error reporting. * @param options.value - current value from style being evaluated. May be anything from a * high level object that needs to be descended into deeper or a simple * scalar value. * @param options.valueSpec - current spec being evaluated. Tracks value. * @param options.styleSpec - current full spec being evaluated. * @param options.validateSpec - the validate function itself * @param options.style - the style object * @param options.objectElementValidators - optional object of functions that will be called * @returns an array of errors, or an empty array if no errors are found. */ function validate(options) { const value = options.value; const valueSpec = options.valueSpec; const styleSpec = options.styleSpec; options.validateSpec = validate; if (valueSpec.expression && isFunction(unbundle(value))) { return validateFunction(options); } else if (valueSpec.expression && isExpression(deepUnbundle(value))) { return validateExpression(options); } else if (valueSpec.type && VALIDATORS[valueSpec.type]) { return VALIDATORS[valueSpec.type](options); } else { const valid = validateObject(extendBy({}, options, { valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec })); return valid; } } var $version = 8; var $root = { version: { required: true, type: "enum", values: [ 8 ] }, name: { type: "string" }, metadata: { type: "*" }, center: { type: "array", value: "number" }, zoom: { type: "number" }, bearing: { type: "number", "default": 0, period: 360, units: "degrees" }, pitch: { type: "number", "default": 0, units: "degrees" }, light: { type: "light" }, sky: { type: "sky" }, projection: { type: "projection" }, terrain: { type: "terrain" }, sources: { required: true, type: "sources" }, sprite: { type: "sprite" }, glyphs: { type: "string" }, transition: { type: "transition" }, layers: { required: true, type: "array", value: "layer" } }; var sources = { "*": { type: "source" } }; var source = [ "source_vector", "source_raster", "source_raster_dem", "source_geojson", "source_video", "source_image" ]; var source_vector = { type: { required: true, type: "enum", values: { vector: { } } }, url: { type: "string" }, tiles: { type: "array", value: "string" }, bounds: { type: "array", value: "number", length: 4, "default": [ -180, -85.051129, 180, 85.051129 ] }, scheme: { type: "enum", values: { xyz: { }, tms: { } }, "default": "xyz" }, minzoom: { type: "number", "default": 0 }, maxzoom: { type: "number", "default": 22 }, attribution: { type: "string" }, promoteId: { type: "promoteId" }, volatile: { type: "boolean", "default": false }, "*": { type: "*" } }; var source_raster = { type: { required: true, type: "enum", values: { raster: { } } }, url: { type: "string" }, tiles: { type: "array", value: "string" }, bounds: { type: "array", value: "number", length: 4, "default": [ -180, -85.051129, 180, 85.051129 ] }, minzoom: { type: "number", "default": 0 }, maxzoom: { type: "number", "default": 22 }, tileSize: { type: "number", "default": 512, units: "pixels" }, scheme: { type: "enum", values: { xyz: { }, tms: { } }, "default": "xyz" }, attribution: { type: "string" }, volatile: { type: "boolean", "default": false }, "*": { type: "*" } }; var source_raster_dem = { type: { required: true, type: "enum", values: { "raster-dem": { } } }, url: { type: "string" }, tiles: { type: "array", value: "string" }, bounds: { type: "array", value: "number", length: 4, "default": [ -180, -85.051129, 180, 85.051129 ] }, minzoom: { type: "number", "default": 0 }, maxzoom: { type: "number", "default": 22 }, tileSize: { type: "number", "default": 512, units: "pixels" }, attribution: { type: "string" }, encoding: { type: "enum", values: { terrarium: { }, mapbox: { }, custom: { } }, "default": "mapbox" }, redFactor: { type: "number", "default": 1 }, blueFactor: { type: "number", "default": 1 }, greenFactor: { type: "number", "default": 1 }, baseShift: { type: "number", "default": 0 }, volatile: { type: "boolean", "default": false }, "*": { type: "*" } }; var source_geojson = { type: { required: true, type: "enum", values: { geojson: { } } }, data: { required: true, type: "*" }, maxzoom: { type: "number", "default": 18 }, attribution: { type: "string" }, buffer: { type: "number", "default": 128, maximum: 512, minimum: 0 }, filter: { type: "*" }, tolerance: { type: "number", "default": 0.375 }, cluster: { type: "boolean", "default": false }, clusterRadius: { type: "number", "default": 50, minimum: 0 }, clusterMaxZoom: { type: "number" }, clusterMinPoints: { type: "number" }, clusterProperties: { type: "*" }, lineMetrics: { type: "boolean", "default": false }, generateId: { type: "boolean", "default": false }, promoteId: { type: "promoteId" } }; var source_video = { type: { required: true, type: "enum", values: { video: { } } }, urls: { required: true, type: "array", value: "string" }, coordinates: { required: true, type: "array", length: 4, value: { type: "array", length: 2, value: "number" } } }; var source_image = { type: { required: true, type: "enum", values: { image: { } } }, url: { required: true, type: "string" }, coordinates: { required: true, type: "array", length: 4, value: { type: "array", length: 2, value: "number" } } }; var layer = { id: { type: "string", required: true }, type: { type: "enum", values: { fill: { }, line: { }, symbol: { }, circle: { }, heatmap: { }, "fill-extrusion": { }, raster: { }, hillshade: { }, background: { } }, required: true }, metadata: { type: "*" }, source: { type: "string" }, "source-layer": { type: "string" }, minzoom: { type: "number", minimum: 0, maximum: 24 }, maxzoom: { type: "number", minimum: 0, maximum: 24 }, filter: { type: "filter" }, layout: { type: "layout" }, paint: { type: "paint" } }; var layout = [ "layout_fill", "layout_line", "layout_circle", "layout_heatmap", "layout_fill-extrusion", "layout_symbol", "layout_raster", "layout_hillshade", "layout_background" ]; var layout_background = { visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_fill = { "fill-sort-key": { type: "number", expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_circle = { "circle-sort-key": { type: "number", expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_heatmap = { visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_line = { "line-cap": { type: "enum", values: { butt: { }, round: { }, square: { } }, "default": "butt", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "line-join": { type: "enum", values: { bevel: { }, round: { }, miter: { } }, "default": "miter", expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "line-miter-limit": { type: "number", "default": 2, requires: [ { "line-join": "miter" } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "line-round-limit": { type: "number", "default": 1.05, requires: [ { "line-join": "round" } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "line-sort-key": { type: "number", expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_symbol = { "symbol-placement": { type: "enum", values: { point: { }, line: { }, "line-center": { } }, "default": "point", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "symbol-spacing": { type: "number", "default": 250, minimum: 1, units: "pixels", requires: [ { "symbol-placement": "line" } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "symbol-avoid-edges": { type: "boolean", "default": false, expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "symbol-sort-key": { type: "number", expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "symbol-z-order": { type: "enum", values: { auto: { }, "viewport-y": { }, source: { } }, "default": "auto", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-allow-overlap": { type: "boolean", "default": false, requires: [ "icon-image", { "!": "icon-overlap" } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-overlap": { type: "enum", values: { never: { }, always: { }, cooperative: { } }, requires: [ "icon-image" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-ignore-placement": { type: "boolean", "default": false, requires: [ "icon-image" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-optional": { type: "boolean", "default": false, requires: [ "icon-image", "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-rotation-alignment": { type: "enum", values: { map: { }, viewport: { }, auto: { } }, "default": "auto", requires: [ "icon-image" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-size": { type: "number", "default": 1, minimum: 0, units: "factor of the original icon size", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-text-fit": { type: "enum", values: { none: { }, width: { }, height: { }, both: { } }, "default": "none", requires: [ "icon-image", "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-text-fit-padding": { type: "array", value: "number", length: 4, "default": [ 0, 0, 0, 0 ], units: "pixels", requires: [ "icon-image", "text-field", { "icon-text-fit": [ "both", "width", "height" ] } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-image": { type: "resolvedImage", tokens: true, expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-rotate": { type: "number", "default": 0, period: 360, units: "degrees", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-padding": { type: "padding", "default": [ 2 ], units: "pixels", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-keep-upright": { type: "boolean", "default": false, requires: [ "icon-image", { "icon-rotation-alignment": "map" }, { "symbol-placement": [ "line", "line-center" ] } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-offset": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-anchor": { type: "enum", values: { center: { }, left: { }, right: { }, top: { }, bottom: { }, "top-left": { }, "top-right": { }, "bottom-left": { }, "bottom-right": { } }, "default": "center", requires: [ "icon-image" ], expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "icon-pitch-alignment": { type: "enum", values: { map: { }, viewport: { }, auto: { } }, "default": "auto", requires: [ "icon-image" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-pitch-alignment": { type: "enum", values: { map: { }, viewport: { }, auto: { } }, "default": "auto", requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-rotation-alignment": { type: "enum", values: { map: { }, viewport: { }, "viewport-glyph": { }, auto: { } }, "default": "auto", requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-field": { type: "formatted", "default": "", tokens: true, expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-font": { type: "array", value: "string", "default": [ "Open Sans Regular", "Arial Unicode MS Regular" ], requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-size": { type: "number", "default": 16, minimum: 0, units: "pixels", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-max-width": { type: "number", "default": 10, minimum: 0, units: "ems", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-line-height": { type: "number", "default": 1.2, units: "ems", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-letter-spacing": { type: "number", "default": 0, units: "ems", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-justify": { type: "enum", values: { auto: { }, left: { }, center: { }, right: { } }, "default": "center", requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-radial-offset": { type: "number", units: "ems", "default": 0, requires: [ "text-field" ], "property-type": "data-driven", expression: { interpolated: true, parameters: [ "zoom", "feature" ] } }, "text-variable-anchor": { type: "array", value: "enum", values: { center: { }, left: { }, right: { }, top: { }, bottom: { }, "top-left": { }, "top-right": { }, "bottom-left": { }, "bottom-right": { } }, requires: [ "text-field", { "symbol-placement": [ "point" ] } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-variable-anchor-offset": { type: "variableAnchorOffsetCollection", requires: [ "text-field", { "symbol-placement": [ "point" ] } ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-anchor": { type: "enum", values: { center: { }, left: { }, right: { }, top: { }, bottom: { }, "top-left": { }, "top-right": { }, "bottom-left": { }, "bottom-right": { } }, "default": "center", requires: [ "text-field", { "!": "text-variable-anchor" } ], expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-max-angle": { type: "number", "default": 45, units: "degrees", requires: [ "text-field", { "symbol-placement": [ "line", "line-center" ] } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-writing-mode": { type: "array", value: "enum", values: { horizontal: { }, vertical: { } }, requires: [ "text-field", { "symbol-placement": [ "point" ] } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-rotate": { type: "number", "default": 0, period: 360, units: "degrees", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-padding": { type: "number", "default": 2, minimum: 0, units: "pixels", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-keep-upright": { type: "boolean", "default": true, requires: [ "text-field", { "text-rotation-alignment": "map" }, { "symbol-placement": [ "line", "line-center" ] } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-transform": { type: "enum", values: { none: { }, uppercase: { }, lowercase: { } }, "default": "none", requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-offset": { type: "array", value: "number", units: "ems", length: 2, "default": [ 0, 0 ], requires: [ "text-field", { "!": "text-radial-offset" } ], expression: { interpolated: true, parameters: [ "zoom", "feature" ] }, "property-type": "data-driven" }, "text-allow-overlap": { type: "boolean", "default": false, requires: [ "text-field", { "!": "text-overlap" } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-overlap": { type: "enum", values: { never: { }, always: { }, cooperative: { } }, requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-ignore-placement": { type: "boolean", "default": false, requires: [ "text-field" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-optional": { type: "boolean", "default": false, requires: [ "text-field", "icon-image" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_raster = { visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var layout_hillshade = { visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }; var filter = { type: "array", value: "*" }; var filter_operator = { type: "enum", values: { "==": { }, "!=": { }, ">": { }, ">=": { }, "<": { }, "<=": { }, "in": { }, "!in": { }, all: { }, any: { }, none: { }, has: { }, "!has": { } } }; var geometry_type = { type: "enum", values: { Point: { }, LineString: { }, Polygon: { } } }; var function_stop = { type: "array", minimum: 0, maximum: 24, value: [ "number", "color" ], length: 2 }; var expression = { type: "array", value: "*", minimum: 1 }; var light = { anchor: { type: "enum", "default": "viewport", values: { map: { }, viewport: { } }, "property-type": "data-constant", transition: false, expression: { interpolated: false, parameters: [ "zoom" ] } }, position: { type: "array", "default": [ 1.15, 210, 30 ], length: 3, value: "number", "property-type": "data-constant", transition: true, expression: { interpolated: true, parameters: [ "zoom" ] } }, color: { type: "color", "property-type": "data-constant", "default": "#ffffff", expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, intensity: { type: "number", "property-type": "data-constant", "default": 0.5, minimum: 0, maximum: 1, expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true } }; var sky = { "sky-color": { type: "color", "property-type": "data-constant", "default": "#88C6FC", expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "horizon-color": { type: "color", "property-type": "data-constant", "default": "#ffffff", expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "fog-color": { type: "color", "property-type": "data-constant", "default": "#ffffff", expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "fog-ground-blend": { type: "number", "property-type": "data-constant", "default": 0.5, minimum: 0, maximum: 1, expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "horizon-fog-blend": { type: "number", "property-type": "data-constant", "default": 0.8, minimum: 0, maximum: 1, expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "sky-horizon-blend": { type: "number", "property-type": "data-constant", "default": 0.8, minimum: 0, maximum: 1, expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true }, "atmosphere-blend": { type: "number", "property-type": "data-constant", "default": 0.8, minimum: 0, maximum: 1, expression: { interpolated: true, parameters: [ "zoom" ] }, transition: true } }; var terrain = { source: { type: "string", required: true }, exaggeration: { type: "number", minimum: 0, "default": 1 } }; var projection = { type: { type: "enum", "default": "mercator", values: { mercator: { }, globe: { } } } }; var paint = [ "paint_fill", "paint_line", "paint_circle", "paint_heatmap", "paint_fill-extrusion", "paint_symbol", "paint_raster", "paint_hillshade", "paint_background" ]; var paint_fill = { "fill-antialias": { type: "boolean", "default": true, expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-color": { type: "color", "default": "#000000", transition: true, requires: [ { "!": "fill-pattern" } ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-outline-color": { type: "color", transition: true, requires: [ { "!": "fill-pattern" }, { "fill-antialias": true } ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "fill-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-pattern": { type: "resolvedImage", transition: true, expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "cross-faded-data-driven" } }; var paint_line = { "line-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-color": { type: "color", "default": "#000000", transition: true, requires: [ { "!": "line-pattern" } ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "line-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "line-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "line-width": { type: "number", "default": 1, minimum: 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-gap-width": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-offset": { type: "number", "default": 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-blur": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "line-dasharray": { type: "array", value: "number", minimum: 0, transition: true, units: "line widths", requires: [ { "!": "line-pattern" } ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "cross-faded" }, "line-pattern": { type: "resolvedImage", transition: true, expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "cross-faded-data-driven" }, "line-gradient": { type: "color", transition: false, requires: [ { "!": "line-dasharray" }, { "!": "line-pattern" }, { source: "geojson", has: { lineMetrics: true } } ], expression: { interpolated: true, parameters: [ "line-progress" ] }, "property-type": "color-ramp" } }; var paint_circle = { "circle-radius": { type: "number", "default": 5, minimum: 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-color": { type: "color", "default": "#000000", transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-blur": { type: "number", "default": 0, transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "circle-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "circle-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "circle-pitch-scale": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "circle-pitch-alignment": { type: "enum", values: { map: { }, viewport: { } }, "default": "viewport", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "circle-stroke-width": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-stroke-color": { type: "color", "default": "#000000", transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "circle-stroke-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" } }; var paint_heatmap = { "heatmap-radius": { type: "number", "default": 30, minimum: 1, transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "heatmap-weight": { type: "number", "default": 1, minimum: 0, transition: false, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "heatmap-intensity": { type: "number", "default": 1, minimum: 0, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "heatmap-color": { type: "color", "default": [ "interpolate", [ "linear" ], [ "heatmap-density" ], 0, "rgba(0, 0, 255, 0)", 0.1, "royalblue", 0.3, "cyan", 0.5, "lime", 0.7, "yellow", 1, "red" ], transition: false, expression: { interpolated: true, parameters: [ "heatmap-density" ] }, "property-type": "color-ramp" }, "heatmap-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" } }; var paint_symbol = { "icon-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "icon-color": { type: "color", "default": "#000000", transition: true, requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "icon-halo-color": { type: "color", "default": "rgba(0, 0, 0, 0)", transition: true, requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "icon-halo-width": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "icon-halo-blur": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "icon-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", requires: [ "icon-image" ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "icon-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "icon-image", "icon-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "text-color": { type: "color", "default": "#000000", transition: true, overridable: true, requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "text-halo-color": { type: "color", "default": "rgba(0, 0, 0, 0)", transition: true, requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "text-halo-width": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "text-halo-blur": { type: "number", "default": 0, minimum: 0, transition: true, units: "pixels", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "text-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", requires: [ "text-field" ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "text-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "text-field", "text-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" } }; var paint_raster = { "raster-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-hue-rotate": { type: "number", "default": 0, period: 360, transition: true, units: "degrees", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-brightness-min": { type: "number", "default": 0, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-brightness-max": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-saturation": { type: "number", "default": 0, minimum: -1, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-contrast": { type: "number", "default": 0, minimum: -1, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-resampling": { type: "enum", values: { linear: { }, nearest: { } }, "default": "linear", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "raster-fade-duration": { type: "number", "default": 300, minimum: 0, transition: false, units: "milliseconds", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" } }; var paint_hillshade = { "hillshade-illumination-direction": { type: "number", "default": 335, minimum: 0, maximum: 359, transition: false, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "hillshade-illumination-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "viewport", expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "hillshade-exaggeration": { type: "number", "default": 0.5, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "hillshade-shadow-color": { type: "color", "default": "#000000", transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "hillshade-highlight-color": { type: "color", "default": "#FFFFFF", transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "hillshade-accent-color": { type: "color", "default": "#000000", transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" } }; var paint_background = { "background-color": { type: "color", "default": "#000000", transition: true, requires: [ { "!": "background-pattern" } ], expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "background-pattern": { type: "resolvedImage", transition: true, expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "cross-faded" }, "background-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" } }; var transition = { duration: { type: "number", "default": 300, minimum: 0, units: "milliseconds" }, delay: { type: "number", "default": 0, minimum: 0, units: "milliseconds" } }; var promoteId = { "*": { type: "string" } }; var v8Spec = { $version: $version, $root: $root, sources: sources, source: source, source_vector: source_vector, source_raster: source_raster, source_raster_dem: source_raster_dem, source_geojson: source_geojson, source_video: source_video, source_image: source_image, layer: layer, layout: layout, layout_background: layout_background, layout_fill: layout_fill, layout_circle: layout_circle, layout_heatmap: layout_heatmap, "layout_fill-extrusion": { visibility: { type: "enum", values: { visible: { }, none: { } }, "default": "visible", "property-type": "constant" } }, layout_line: layout_line, layout_symbol: layout_symbol, layout_raster: layout_raster, layout_hillshade: layout_hillshade, filter: filter, filter_operator: filter_operator, geometry_type: geometry_type, "function": { expression: { type: "expression" }, stops: { type: "array", value: "function_stop" }, base: { type: "number", "default": 1, minimum: 0 }, property: { type: "string", "default": "$zoom" }, type: { type: "enum", values: { identity: { }, exponential: { }, interval: { }, categorical: { } }, "default": "exponential" }, colorSpace: { type: "enum", values: { rgb: { }, lab: { }, hcl: { } }, "default": "rgb" }, "default": { type: "*", required: false } }, function_stop: function_stop, expression: expression, light: light, sky: sky, terrain: terrain, projection: projection, paint: paint, paint_fill: paint_fill, "paint_fill-extrusion": { "fill-extrusion-opacity": { type: "number", "default": 1, minimum: 0, maximum: 1, transition: true, expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-extrusion-color": { type: "color", "default": "#000000", transition: true, requires: [ { "!": "fill-extrusion-pattern" } ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-extrusion-translate": { type: "array", value: "number", length: 2, "default": [ 0, 0 ], transition: true, units: "pixels", expression: { interpolated: true, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-extrusion-translate-anchor": { type: "enum", values: { map: { }, viewport: { } }, "default": "map", requires: [ "fill-extrusion-translate" ], expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" }, "fill-extrusion-pattern": { type: "resolvedImage", transition: true, expression: { interpolated: false, parameters: [ "zoom", "feature" ] }, "property-type": "cross-faded-data-driven" }, "fill-extrusion-height": { type: "number", "default": 0, minimum: 0, units: "meters", transition: true, expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-extrusion-base": { type: "number", "default": 0, minimum: 0, units: "meters", transition: true, requires: [ "fill-extrusion-height" ], expression: { interpolated: true, parameters: [ "zoom", "feature", "feature-state" ] }, "property-type": "data-driven" }, "fill-extrusion-vertical-gradient": { type: "boolean", "default": true, transition: false, expression: { interpolated: false, parameters: [ "zoom" ] }, "property-type": "data-constant" } }, paint_line: paint_line, paint_circle: paint_circle, paint_heatmap: paint_heatmap, paint_symbol: paint_symbol, paint_raster: paint_raster, paint_hillshade: paint_hillshade, paint_background: paint_background, transition: transition, "property-type": { "data-driven": { type: "property-type" }, "cross-faded": { type: "property-type" }, "cross-faded-data-driven": { type: "property-type" }, "color-ramp": { type: "property-type" }, "data-constant": { type: "property-type" }, constant: { type: "property-type" } }, promoteId: promoteId }; function validateGlyphsUrl(options) { const value = options.value; const key = options.key; const errors = validateString(options); if (errors.length) return errors; if (value.indexOf('{fontstack}') === -1) { errors.push(new ValidationError(key, value, '"glyphs" url must include a "{fontstack}" token')); } if (value.indexOf('{range}') === -1) { errors.push(new ValidationError(key, value, '"glyphs" url must include a "{range}" token')); } return errors; } /** * Validate a MapLibre style against the style specification. * Use this when running in the browser. * * @param style - The style to be validated. * @param styleSpec - The style specification to validate against. * If omitted, the latest style spec is used. * @returns an array of errors, or an empty array if no errors are found. * @example * const validate = require('@maplibre/maplibre-gl-style-spec/').validateStyleMin; * const errors = validate(style); */ function validateStyleMin(style, styleSpec = v8Spec) { let errors = []; errors = errors.concat(validate({ key: '', value: style, valueSpec: styleSpec.$root, styleSpec, style, validateSpec: validate, objectElementValidators: { glyphs: validateGlyphsUrl, '*'() { return []; } } })); if (style['constants']) { errors = errors.concat(validateConstants({ key: 'constants', value: style['constants'], style, styleSpec, validateSpec: validate, })); } return sortErrors(errors); } validateStyleMin.source = wrapCleanErrors(injectValidateSpec(validateSource)); validateStyleMin.sprite = wrapCleanErrors(injectValidateSpec(validateSprite)); validateStyleMin.glyphs = wrapCleanErrors(injectValidateSpec(validateGlyphsUrl)); validateStyleMin.light = wrapCleanErrors(injectValidateSpec(validateLight)); validateStyleMin.sky = wrapCleanErrors(injectValidateSpec(validateSky)); validateStyleMin.terrain = wrapCleanErrors(injectValidateSpec(validateTerrain)); validateStyleMin.layer = wrapCleanErrors(injectValidateSpec(validateLayer)); validateStyleMin.filter = wrapCleanErrors(injectValidateSpec(validateFilter)); validateStyleMin.paintProperty = wrapCleanErrors(injectValidateSpec(validatePaintProperty)); validateStyleMin.layoutProperty = wrapCleanErrors(injectValidateSpec(validateLayoutProperty)); function injectValidateSpec(validator) { return function (options) { return validator({ ...options, validateSpec: validate, }); }; } function sortErrors(errors) { return [].concat(errors).sort((a, b) => { return a.line - b.line; }); } function wrapCleanErrors(inner) { return function (...args) { return sortErrors(inner.apply(this, args)); }; } // Note: Do not inherit from Error. It breaks when transpiling to ES5. class ParsingError { constructor(error) { this.error = error; this.message = error.message; const match = error.message.match(/line (\d+)/); this.line = match ? parseInt(match[1], 10) : 0; } } const v8 = v8Spec; function commonjsRequire(path) { throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.'); } var jsonlint$1 = {}; /* parser generated by jison 0.4.15 */ (function (exports) { /* Returns a Parser object of the following structure: Parser: { yy: {} } Parser.prototype: { yy: {}, trace: function(), symbols_: {associative list: name ==> number}, terminals_: {associative list: number ==> name}, productions_: [...], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), table: [...], defaultActions: {...}, parseError: function(str, hash), parse: function(input), lexer: { EOF: 1, parseError: function(str, hash), setInput: function(input), input: function(), unput: function(str), more: function(), less: function(n), pastInput: function(), upcomingInput: function(), showPosition: function(), test_match: function(regex_match_array, rule_index), next: function(), lex: function(), begin: function(condition), popState: function(), _currentRules: function(), topState: function(), pushState: function(condition), options: { ranges: boolean (optional: true ==> token location info will include a .range[] member) flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) }, performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), rules: [...], conditions: {associative list: name ==> set}, } } token location info (@$, _$, etc.): { first_line: n, last_line: n, first_column: n, last_column: n, range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) } the parseError function receives a 'hash' object with these members for lexer and parser errors: { text: (matched text) token: (the produced terminal token, if any) line: (yylineno) } while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { loc: (yylloc) expected: (string describing the set of expected tokens) recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) } */ var parser = (function(){ var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,12],$V1=[1,13],$V2=[1,9],$V3=[1,10],$V4=[1,11],$V5=[1,14],$V6=[1,15],$V7=[14,18,22,24],$V8=[18,22],$V9=[22,24]; var parser = {trace: function trace() { }, yy: {}, symbols_: {"error":2,"JSONString":3,"STRING":4,"JSONNumber":5,"NUMBER":6,"JSONNullLiteral":7,"NULL":8,"JSONBooleanLiteral":9,"TRUE":10,"FALSE":11,"JSONText":12,"JSONValue":13,"EOF":14,"JSONObject":15,"JSONArray":16,"{":17,"}":18,"JSONMemberList":19,"JSONMember":20,":":21,",":22,"[":23,"]":24,"JSONElementList":25,"$accept":0,"$end":1}, terminals_: {2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"}, productions_: [0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]], performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { /* this == yyval */ var $0 = $$.length - 1; switch (yystate) { case 1: // replace escaped characters with actual character this.$ = new String(yytext.replace(/\\(\\|")/g, "$"+"1") .replace(/\\n/g,'\n') .replace(/\\r/g,'\r') .replace(/\\t/g,'\t') .replace(/\\v/g,'\v') .replace(/\\f/g,'\f') .replace(/\\b/g,'\b')); this.$.__line__ = this._$.first_line; break; case 2: this.$ = new Number(yytext); this.$.__line__ = this._$.first_line; break; case 3: this.$ = null; break; case 4: this.$ = new Boolean(true); this.$.__line__ = this._$.first_line; break; case 5: this.$ = new Boolean(false); this.$.__line__ = this._$.first_line; break; case 6: return this.$ = $$[$0-1]; case 13: this.$ = {}; Object.defineProperty(this.$, '__line__', { value: this._$.first_line, enumerable: false }); break; case 14: case 19: this.$ = $$[$0-1]; Object.defineProperty(this.$, '__line__', { value: this._$.first_line, enumerable: false }); break; case 15: this.$ = [$$[$0-2], $$[$0]]; break; case 16: this.$ = {}; this.$[$$[$0][0]] = $$[$0][1]; break; case 17: this.$ = $$[$0-2]; $$[$0-2][$$[$0][0]] = $$[$0][1]; break; case 18: this.$ = []; Object.defineProperty(this.$, '__line__', { value: this._$.first_line, enumerable: false }); break; case 20: this.$ = [$$[$0]]; break; case 21: this.$ = $$[$0-2]; $$[$0-2].push($$[$0]); break; } }, table: [{3:5,4:$V0,5:6,6:$V1,7:3,8:$V2,9:4,10:$V3,11:$V4,12:1,13:2,15:7,16:8,17:$V5,23:$V6},{1:[3]},{14:[1,16]},o($V7,[2,7]),o($V7,[2,8]),o($V7,[2,9]),o($V7,[2,10]),o($V7,[2,11]),o($V7,[2,12]),o($V7,[2,3]),o($V7,[2,4]),o($V7,[2,5]),o([14,18,21,22,24],[2,1]),o($V7,[2,2]),{3:20,4:$V0,18:[1,17],19:18,20:19},{3:5,4:$V0,5:6,6:$V1,7:3,8:$V2,9:4,10:$V3,11:$V4,13:23,15:7,16:8,17:$V5,23:$V6,24:[1,21],25:22},{1:[2,6]},o($V7,[2,13]),{18:[1,24],22:[1,25]},o($V8,[2,16]),{21:[1,26]},o($V7,[2,18]),{22:[1,28],24:[1,27]},o($V9,[2,20]),o($V7,[2,14]),{3:20,4:$V0,20:29},{3:5,4:$V0,5:6,6:$V1,7:3,8:$V2,9:4,10:$V3,11:$V4,13:30,15:7,16:8,17:$V5,23:$V6},o($V7,[2,19]),{3:5,4:$V0,5:6,6:$V1,7:3,8:$V2,9:4,10:$V3,11:$V4,13:31,15:7,16:8,17:$V5,23:$V6},o($V8,[2,17]),o($V8,[2,15]),o($V9,[2,21])], defaultActions: {16:[2,6]}, parseError: function parseError(str, hash) { if (hash.recoverable) { this.trace(str); } else { throw new Error(str); } }, parse: function parse(input) { var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); var lexer = Object.create(this.lexer); var sharedState = { yy: {} }; for (var k in this.yy) { if (Object.prototype.hasOwnProperty.call(this.yy, k)) { sharedState.yy[k] = this.yy[k]; } } lexer.setInput(input, sharedState.yy); sharedState.yy.lexer = lexer; sharedState.yy.parser = this; if (typeof lexer.yylloc == 'undefined') { lexer.yylloc = {}; } var yyloc = lexer.yylloc; lstack.push(yyloc); var ranges = lexer.options && lexer.options.ranges; if (typeof sharedState.yy.parseError === 'function') { this.parseError = sharedState.yy.parseError; } else { this.parseError = Object.getPrototypeOf(this).parseError; } function lex() { var token; token = lexer.lex() || EOF; if (typeof token !== 'number') { token = self.symbols_[token] || token; } return token; } var symbol, state, action, r, yyval = {}, p, len, newState, expected; while (true) { state = stack[stack.length - 1]; if (this.defaultActions[state]) { action = this.defaultActions[state]; } else { if (symbol === null || typeof symbol == 'undefined') { symbol = lex(); } action = table[state] && table[state][symbol]; } if (typeof action === 'undefined' || !action.length || !action[0]) { var errStr = ''; expected = []; for (p in table[state]) { if (this.terminals_[p] && p > TERROR) { expected.push('\'' + this.terminals_[p] + '\''); } } if (lexer.showPosition) { errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; } else { errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); } this.parseError(errStr, { text: lexer.match, token: this.terminals_[symbol] || symbol, line: lexer.yylineno, loc: yyloc, expected: expected }); } if (action[0] instanceof Array && action.length > 1) { throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); } switch (action[0]) { case 1: stack.push(symbol); vstack.push(lexer.yytext); lstack.push(lexer.yylloc); stack.push(action[1]); symbol = null; { yyleng = lexer.yyleng; yytext = lexer.yytext; yylineno = lexer.yylineno; yyloc = lexer.yylloc; } break; case 2: len = this.productions_[action[1]][1]; yyval.$ = vstack[vstack.length - len]; yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column }; if (ranges) { yyval._$.range = [ lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1] ]; } r = this.performAction.apply(yyval, [ yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack ].concat(args)); if (typeof r !== 'undefined') { return r; } if (len) { stack = stack.slice(0, -1 * len * 2); vstack = vstack.slice(0, -1 * len); lstack = lstack.slice(0, -1 * len); } stack.push(this.productions_[action[1]][0]); vstack.push(yyval.$); lstack.push(yyval._$); newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; stack.push(newState); break; case 3: return true; } } return true; }}; /* generated by jison-lex 0.3.4 */ var lexer = (function(){ var lexer = ({ EOF:1, parseError:function parseError(str, hash) { if (this.yy.parser) { this.yy.parser.parseError(str, hash); } else { throw new Error(str); } }, // resets the lexer, sets new input setInput:function (input, yy) { this.yy = yy || this.yy || {}; this._input = input; this._more = this._backtrack = this.done = false; this.yylineno = this.yyleng = 0; this.yytext = this.matched = this.match = ''; this.conditionStack = ['INITIAL']; this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0 }; if (this.options.ranges) { this.yylloc.range = [0,0]; } this.offset = 0; return this; }, // consumes and returns one char from the input input:function () { var ch = this._input[0]; this.yytext += ch; this.yyleng++; this.offset++; this.match += ch; this.matched += ch; var lines = ch.match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno++; this.yylloc.last_line++; } else { this.yylloc.last_column++; } if (this.options.ranges) { this.yylloc.range[1]++; } this._input = this._input.slice(1); return ch; }, // unshifts one char (or a string) into the input unput:function (ch) { var len = ch.length; var lines = ch.split(/(?:\r\n?|\n)/g); this._input = ch + this._input; this.yytext = this.yytext.substr(0, this.yytext.length - len); //this.yyleng -= len; this.offset -= len; var oldLines = this.match.split(/(?:\r\n?|\n)/g); this.match = this.match.substr(0, this.match.length - 1); this.matched = this.matched.substr(0, this.matched.length - 1); if (lines.length - 1) { this.yylineno -= lines.length - 1; } var r = this.yylloc.range; this.yylloc = { first_line: this.yylloc.first_line, last_line: this.yylineno + 1, first_column: this.yylloc.first_column, last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len }; if (this.options.ranges) { this.yylloc.range = [r[0], r[0] + this.yyleng - len]; } this.yyleng = this.yytext.length; return this; }, // When called from action, caches matched text and appends it on next action more:function () { this._more = true; return this; }, // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. reject:function () { if (this.options.backtrack_lexer) { this._backtrack = true; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { text: "", token: null, line: this.yylineno }); } return this; }, // retain first n characters of the match less:function (n) { this.unput(this.match.slice(n)); }, // displays already matched input, i.e. for error messages pastInput:function () { var past = this.matched.substr(0, this.matched.length - this.match.length); return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); }, // displays upcoming input, i.e. for error messages upcomingInput:function () { var next = this.match; if (next.length < 20) { next += this._input.substr(0, 20-next.length); } return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); }, // displays the character position where the lexing error occurred, i.e. for error messages showPosition:function () { var pre = this.pastInput(); var c = new Array(pre.length + 1).join("-"); return pre + this.upcomingInput() + "\n" + c + "^"; }, // test the lexed token: return FALSE when not a match, otherwise return token test_match:function (match, indexed_rule) { var token, lines, backup; if (this.options.backtrack_lexer) { // save context backup = { yylineno: this.yylineno, yylloc: { first_line: this.yylloc.first_line, last_line: this.last_line, first_column: this.yylloc.first_column, last_column: this.yylloc.last_column }, yytext: this.yytext, match: this.match, matches: this.matches, matched: this.matched, yyleng: this.yyleng, offset: this.offset, _more: this._more, _input: this._input, yy: this.yy, conditionStack: this.conditionStack.slice(0), done: this.done }; if (this.options.ranges) { backup.yylloc.range = this.yylloc.range.slice(0); } } lines = match[0].match(/(?:\r\n?|\n).*/g); if (lines) { this.yylineno += lines.length; } this.yylloc = { first_line: this.yylloc.last_line, last_line: this.yylineno + 1, first_column: this.yylloc.last_column, last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length }; this.yytext += match[0]; this.match += match[0]; this.matches = match; this.yyleng = this.yytext.length; if (this.options.ranges) { this.yylloc.range = [this.offset, this.offset += this.yyleng]; } this._more = false; this._backtrack = false; this._input = this._input.slice(match[0].length); this.matched += match[0]; token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); if (this.done && this._input) { this.done = false; } if (token) { return token; } else if (this._backtrack) { // recover context for (var k in backup) { this[k] = backup[k]; } return false; // rule action called reject() implying the next rule should be tested instead. } return false; }, // return next match in input next:function () { if (this.done) { return this.EOF; } if (!this._input) { this.done = true; } var token, match, tempMatch, index; if (!this._more) { this.yytext = ''; this.match = ''; } var rules = this._currentRules(); for (var i = 0; i < rules.length; i++) { tempMatch = this._input.match(this.rules[rules[i]]); if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { match = tempMatch; index = i; if (this.options.backtrack_lexer) { token = this.test_match(tempMatch, rules[i]); if (token !== false) { return token; } else if (this._backtrack) { match = false; continue; // rule action called reject() implying a rule MISmatch. } else { // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } } else if (!this.options.flex) { break; } } } if (match) { token = this.test_match(match, rules[index]); if (token !== false) { return token; } // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) return false; } if (this._input === "") { return this.EOF; } else { return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: "", token: null, line: this.yylineno }); } }, // return next match that has a token lex:function lex() { var r = this.next(); if (r) { return r; } else { return this.lex(); } }, // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) begin:function begin(condition) { this.conditionStack.push(condition); }, // pop the previously active lexer condition state off the condition stack popState:function popState() { var n = this.conditionStack.length - 1; if (n > 0) { return this.conditionStack.pop(); } else { return this.conditionStack[0]; } }, // produce the lexer rule set which is active for the currently active lexer condition state _currentRules:function _currentRules() { if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; } else { return this.conditions["INITIAL"].rules; } }, // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available topState:function topState(n) { n = this.conditionStack.length - 1 - Math.abs(n || 0); if (n >= 0) { return this.conditionStack[n]; } else { return "INITIAL"; } }, // alias for begin(condition) pushState:function pushState(condition) { this.begin(condition); }, // return the number of states currently on the stack stateStackSize:function stateStackSize() { return this.conditionStack.length; }, options: {}, performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { switch($avoiding_name_collisions) { case 0:/* skip whitespace */ break; case 1:return 6 case 2:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 4 case 3:return 17 case 4:return 18 case 5:return 23 case 6:return 24 case 7:return 22 case 8:return 21 case 9:return 10 case 10:return 11 case 11:return 8 case 12:return 14 case 13:return 'INVALID' } }, rules: [/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/], conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"inclusive":true}} }); return lexer; })(); parser.lexer = lexer; function Parser () { this.yy = {}; } Parser.prototype = parser;parser.Parser = Parser; return new Parser; })(); if (typeof commonjsRequire !== 'undefined' && 'object' !== 'undefined') { exports.parser = parser; exports.Parser = parser.Parser; exports.parse = function () { return parser.parse.apply(parser, arguments); }; } } (jsonlint$1)); var jsonlint = /*@__PURE__*/getDefaultExportFromCjs(jsonlint$1); function readStyle(style) { if (style instanceof String || typeof style === 'string' || style instanceof Buffer) { try { return jsonlint.parse(style.toString()); } catch (e) { throw new ParsingError(e); } } return style; } /** * Validate a MapLibre GL style against the style specification. * * @param style - The style to be validated. If a `String` or `Buffer` is provided, * the returned errors will contain line numbers. * @param styleSpec - The style specification to validate against. * If omitted, the spec version is inferred from the stylesheet. * @returns an array of errors, or an empty array if no errors are found. * @example * const validate = require('maplibre-gl-style-spec').validate; * const style = fs.readFileSync('./style.json', 'utf8'); * const errors = validate(style); */ function validateStyle(style, styleSpec = v8) { let s = style; try { s = readStyle(s); } catch (e) { return [e]; } return validateStyleMin(s, styleSpec); } const argv = minimist$1(process.argv.slice(2), { boolean: 'json', }); if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) { help(); } else { let status = 0; if (!argv._.length) { argv._.push('/dev/stdin'); } argv._.forEach((file) => { const errors = validateStyle(rw.readFileSync(file, 'utf8')); if (errors.length) { if (argv.json) { process.stdout.write(JSON.stringify(errors, null, 2)); } else { errors.forEach((e) => { console.log('%s:%d: %s', file, e.line, e.message); }); } status = 1; } }); process.exit(status); } function help() { console.log('usage:'); console.log(' gl-style-validate file.json'); console.log(' gl-style-validate < file.json'); console.log(''); console.log('options:'); console.log('--json output errors as json'); } })); //# sourceMappingURL=gl-style-validate.cjs.map