planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,17 @@
all: build test site
build:
./node_modules/jison/lib/cli.js src/jsonlint.y src/jsonlint.l
mv jsonlint.js lib/jsonlint.js
node scripts/bundle.js | ./node_modules/uglify-js/bin/uglifyjs > web/jsonlint.js
site:
cp web/jsonlint.js ../jsonlint-pages/jsonlint.js
deploy: site
cd ../jsonlint-pages && git commit -a -m 'deploy site updates' && git push origin gh-pages
test: lib/jsonlint.js test/all-tests.js
node test/all-tests.js

View File

@@ -0,0 +1,7 @@
JSON Lint
=========
A fork of the `lines-primitive` branch of [tmcw/jsonlint](https://github.com/tmcw/jsonlint), which is a fork of [zaach/jsonlint](https://github.com/zaach/jsonlint) that adds line number annotations to the parsed JSON.
This fork is used by Mapbox GL JS, specifically for providing helpful error messages when validating Mapbox GL style JSON documents.

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env node
/**
* Manual formatter taken straight from https://github.com/umbrae/jsonlintdotcom
**/
/*jslint white: true, devel: true, onevar: true, browser: true, undef: true, nomen: true, regexp: true, plusplus: false, bitwise: true, newcap: true, maxerr: 50, indent: 4 */
/**
* jsl.format - Provide json reformatting in a character-by-character approach, so that even invalid JSON may be reformatted (to the best of its ability).
*
**/
var formatter = (function () {
function repeat(s, count) {
return new Array(count + 1).join(s);
}
function formatJson(json, indentChars) {
var i = 0,
il = 0,
tab = (typeof indentChars !== "undefined") ? indentChars : " ",
newJson = "",
indentLevel = 0,
inString = false,
currentChar = null;
for (i = 0, il = json.length; i < il; i += 1) {
currentChar = json.charAt(i);
switch (currentChar) {
case '{':
case '[':
if (!inString) {
newJson += currentChar + "\n" + repeat(tab, indentLevel + 1);
indentLevel += 1;
} else {
newJson += currentChar;
}
break;
case '}':
case ']':
if (!inString) {
indentLevel -= 1;
newJson += "\n" + repeat(tab, indentLevel) + currentChar;
} else {
newJson += currentChar;
}
break;
case ',':
if (!inString) {
newJson += ",\n" + repeat(tab, indentLevel);
} else {
newJson += currentChar;
}
break;
case ':':
if (!inString) {
newJson += ": ";
} else {
newJson += currentChar;
}
break;
case ' ':
case "\n":
case "\t":
if (inString) {
newJson += currentChar;
}
break;
case '"':
if (i > 0 && json.charAt(i - 1) !== '\\') {
inString = !inString;
}
newJson += currentChar;
break;
default:
newJson += currentChar;
break;
}
}
return newJson;
}
return { "formatJson": formatJson };
}());
if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
exports.formatter = formatter;
}

View File

@@ -0,0 +1,681 @@
/* parser generated by jison 0.4.15 */
/*
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];
break;
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], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 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 popStack(n) {
stack.length = stack.length - 2 * n;
vstack.length = vstack.length - n;
lstack.length = lstack.length - n;
}
_token_stack:
function lex() {
var token;
token = lexer.lex() || EOF;
if (typeof token !== 'number') {
token = self.symbols_[token] || token;
}
return token;
}
var symbol, preErrorSymbol, state, action, a, 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;
if (!preErrorSymbol) {
yyleng = lexer.yyleng;
yytext = lexer.yytext;
yylineno = lexer.yylineno;
yyloc = lexer.yylloc;
if (recovering > 0) {
recovering--;
}
} else {
symbol = preErrorSymbol;
preErrorSymbol = null;
}
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) {
var YYSTATE=YY_START;
switch($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:return 6
break;
case 2:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 4
break;
case 3:return 17
break;
case 4:return 18
break;
case 5:return 23
break;
case 6:return 24
break;
case 7:return 22
break;
case 8:return 21
break;
case 9:return 10
break;
case 10:return 11
break;
case 11:return 8
break;
case 12:return 14
break;
case 13:return 'INVALID'
break;
}
},
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 require !== 'undefined' && typeof exports !== 'undefined') {
exports.parser = parser;
exports.Parser = parser.Parser;
exports.parse = function () { return parser.parse.apply(parser, arguments); };
}

View File

@@ -0,0 +1,34 @@
{
"author": "Zach Carter <zach@carter.name> (http://zaa.ch)",
"name": "@mapbox/jsonlint-lines-primitives",
"description": "Validate JSON",
"keywords": [
"json",
"validation",
"lint",
"jsonlint"
],
"version": "2.0.2",
"preferGlobal": true,
"repository": {
"type": "git",
"url": "git://github.com/mapbox/jsonlint.git"
},
"bugs": {
"url": "http://github.com/mapbox/jsonlint/issues"
},
"main": "lib/jsonlint.js",
"engines": {
"node": ">= 0.6"
},
"dependencies": {},
"devDependencies": {
"test": "*",
"jison": "*",
"uglify-js": "*"
},
"scripts": {
"test": "node test/all-tests.js"
},
"optionalDependencies": {}
}

View File

@@ -0,0 +1,8 @@
var fs = require('fs');
var source = "var jsonlint = (function(){var require=true,module=false;var exports={};" +
fs.readFileSync(__dirname+'/../lib/jsonlint.js', 'utf8') +
"return exports;})()";
console.log(source);

View File

@@ -0,0 +1,24 @@
int "-"?([0-9]|[1-9][0-9]+)
exp [eE][-+]?[0-9]+
frac "."[0-9]+
%%
\s+ /* skip whitespace */
{int}{frac}?{exp}?\b return 'NUMBER'
\"(?:'\\'[\\"bfnrt/]|'\\u'[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*\" yytext = yytext.substr(1,yyleng-2); return 'STRING'
"{" return '{'
"}" return '}'
"[" return '['
"]" return ']'
"," return ','
":" return ':'
"true" return 'TRUE'
"false" return 'FALSE'
"null" return 'NULL'
<<EOF>> return 'EOF'
. return 'INVALID'
%%

View File

@@ -0,0 +1,110 @@
%start JSONText
/*
ECMA-262 5th Edition, 15.12.1 The JSON Grammar.
*/
%%
JSONString
: STRING
{ // replace escaped characters with actual character
$$ = 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'));
$$.__line__ = @$.first_line;
}
;
JSONNumber
: NUMBER
{
$$ = new Number(yytext);
$$.__line__ = @$.first_line;
}
;
JSONNullLiteral
: NULL
{
$$ = null;
}
;
JSONBooleanLiteral
: TRUE
{
$$ = new Boolean(true);
$$.__line__ = @$.first_line;
}
| FALSE
{
$$ = new Boolean(false);
$$.__line__ = @$.first_line;
}
;
JSONText
: JSONValue EOF
{return $$ = $1;}
;
JSONValue
: JSONNullLiteral
| JSONBooleanLiteral
| JSONString
| JSONNumber
| JSONObject
| JSONArray
;
JSONObject
: '{' '}'
{$$ = {}; Object.defineProperty($$, '__line__', {
value: @$.first_line,
enumerable: false
})}
| '{' JSONMemberList '}'
{$$ = $2; Object.defineProperty($$, '__line__', {
value: @$.first_line,
enumerable: false
})}
;
JSONMember
: JSONString ':' JSONValue
{$$ = [$1, $3];}
;
JSONMemberList
: JSONMember
{{$$ = {}; $$[$1[0]] = $1[1];}}
| JSONMemberList ',' JSONMember
{$$ = $1; $1[$3[0]] = $3[1];}
;
JSONArray
: '[' ']'
{$$ = []; Object.defineProperty($$, '__line__', {
value: @$.first_line,
enumerable: false
})}
| '[' JSONElementList ']'
{$$ = $2; Object.defineProperty($$, '__line__', {
value: @$.first_line,
enumerable: false
})}
;
JSONElementList
: JSONValue
{$$ = [$1];}
| JSONElementList ',' JSONValue
{$$ = $1; $1.push($3);}
;

View File

@@ -0,0 +1,210 @@
var fs = require("fs"),
assert = require("assert"),
parser = require("../lib/jsonlint").parser;
exports["test string with line break"] = function () {
var json = '{"foo": "bar\nbar"}';
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test string literal"] = function () {
var json = '"foo"';
assert.equal(parser.parse(json), "foo");
assert.equal(parser.parse(json).__line__, 1);
};
exports["test number literal"] = function () {
var json = '1234';
assert.equal(parser.parse(json), 1234);
assert.equal(parser.parse(json).__line__, 1);
};
exports["test null literal"] = function () {
var json = 'null';
assert.equal(parser.parse(json), null);
};
exports["test boolean literal"] = function () {
var json = 'true';
assert.equal(parser.parse(json), true);
assert.equal(parser.parse(json).__line__, 1);
};
exports["test unclosed array"] = function () {
var json = fs.readFileSync(__dirname + "/fails/2.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test unquotedkey keys must be quoted"] = function () {
var json = fs.readFileSync(__dirname + "/fails/3.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test extra comma"] = function () {
var json = fs.readFileSync(__dirname + "/fails/4.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test double extra comma"] = function () {
var json = fs.readFileSync(__dirname + "/fails/5.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test missing value"] = function () {
var json = fs.readFileSync(__dirname + "/fails/6.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test comma after the close"] = function () {
var json = fs.readFileSync(__dirname + "/fails/7.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test extra close"] = function () {
var json = fs.readFileSync(__dirname + "/fails/8.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test extra comma after value"] = function () {
var json = fs.readFileSync(__dirname + "/fails/9.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test extra value after close with misplaced quotes"] = function () {
var json = fs.readFileSync(__dirname + "/fails/10.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test illegal expression addition"] = function () {
var json = fs.readFileSync(__dirname + "/fails/11.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test illegal invocation of alert"] = function () {
var json = fs.readFileSync(__dirname + "/fails/12.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test numbers cannot have leading zeroes"] = function () {
var json = fs.readFileSync(__dirname + "/fails/13.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test numbers cannot be hex"] = function () {
var json = fs.readFileSync(__dirname + "/fails/14.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test illegal backslash escape \\0"] = function () {
var json = fs.readFileSync(__dirname + "/fails/15.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test unquoted text"] = function () {
var json = fs.readFileSync(__dirname + "/fails/16.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test illegal backslash escape \\x"] = function () {
var json = fs.readFileSync(__dirname + "/fails/17.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test missing colon"] = function () {
var json = fs.readFileSync(__dirname + "/fails/19.json")
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test double colon"] = function () {
var json = fs.readFileSync(__dirname + "/fails/20.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test comma instead of colon"] = function () {
var json = fs.readFileSync(__dirname + "/fails/21.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test colon instead of comma"] = function () {
var json = fs.readFileSync(__dirname + "/fails/22.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test bad raw value"] = function () {
var json = fs.readFileSync(__dirname + "/fails/23.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test single quotes"] = function () {
var json = fs.readFileSync(__dirname + "/fails/24.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test tab character in string"] = function () {
var json = fs.readFileSync(__dirname + "/fails/25.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test tab character in string 2"] = function () {
var json = fs.readFileSync(__dirname + "/fails/26.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test line break in string"] = function () {
var json = fs.readFileSync(__dirname + "/fails/27.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test line break in string in array"] = function () {
var json = fs.readFileSync(__dirname + "/fails/28.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test 0e"] = function () {
var json = fs.readFileSync(__dirname + "/fails/29.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test 0e+"] = function () {
var json = fs.readFileSync(__dirname + "/fails/30.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test 0e+ 1"] = function () {
var json = fs.readFileSync(__dirname + "/fails/31.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test comma instead of closing brace"] = function () {
var json = fs.readFileSync(__dirname + "/fails/32.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
};
exports["test bracket mismatch"] = function () {
var json = fs.readFileSync(__dirname + "/fails/33.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
}
exports["test extra brace"] = function () {
var json = fs.readFileSync(__dirname + "/fails/34.json").toString();
assert["throws"](function () {parser.parse(json)}, "should throw error");
}
exports["test pass-1"] = function () {
var json = fs.readFileSync(__dirname + "/passes/1.json").toString();
assert.doesNotThrow(function () {parser.parse(json)}, "should pass");
}
exports["test pass-2"] = function () {
var json = fs.readFileSync(__dirname + "/passes/2.json").toString();
assert.doesNotThrow(function () {parser.parse(json)}, "should pass");
}
exports["test pass-3"] = function () {
var json = fs.readFileSync(__dirname + "/passes/3.json").toString();
assert.doesNotThrow(function () {parser.parse(json)}, "should pass");
}
if (require.main === module) {
require("test").run(exports);
}

View File

@@ -0,0 +1 @@
{"Extra value after close": true} "misplaced quoted value"

View File

@@ -0,0 +1 @@
{"Illegal expression": 1 + 2}

View File

@@ -0,0 +1 @@
{"Illegal invocation": alert()}

View File

@@ -0,0 +1 @@
{"Numbers cannot have leading zeroes": 013}

View File

@@ -0,0 +1 @@
{"Numbers cannot be hex": 0x14}

View File

@@ -0,0 +1 @@
["Illegal backslash escape: \x15"]

View File

@@ -0,0 +1 @@
[\naked]

View File

@@ -0,0 +1 @@
["Illegal backslash escape: \017"]

View File

@@ -0,0 +1 @@
{"Missing colon" null}

View File

@@ -0,0 +1 @@
["Unclosed array"

View File

@@ -0,0 +1 @@
{"Double colon":: null}

View File

@@ -0,0 +1 @@
{"Comma instead of colon", null}

View File

@@ -0,0 +1 @@
["Colon instead of comma": false]

View File

@@ -0,0 +1 @@
["Bad value", truth]

View File

@@ -0,0 +1 @@
['single quote']

View File

@@ -0,0 +1 @@
[" tab character in string "]

View File

@@ -0,0 +1 @@
["tab\ character\ in\ string\ "]

View File

@@ -0,0 +1,2 @@
["line
break"]

View File

@@ -0,0 +1,2 @@
["line\
break"]

View File

@@ -0,0 +1 @@
[0e]

View File

@@ -0,0 +1 @@
{unquoted_key: "keys must be quoted"}

View File

@@ -0,0 +1 @@
[0e+]

View File

@@ -0,0 +1 @@
[0e+-1]

View File

@@ -0,0 +1 @@
{"Comma instead if closing brace": true,

View File

@@ -0,0 +1 @@
["mismatch"}

View File

@@ -0,0 +1 @@
{"extra brace": 1}}

View File

@@ -0,0 +1 @@
["extra comma",]

View File

@@ -0,0 +1 @@
["double extra comma",,]

View File

@@ -0,0 +1 @@
[ , "<-- missing value"]

View File

@@ -0,0 +1 @@
["Comma after the close"],

View File

@@ -0,0 +1 @@
["Extra close"]]

View File

@@ -0,0 +1 @@
{"Extra comma": true,}

View File

@@ -0,0 +1,58 @@
[
"JSON Test Pattern pass1",
{"object with 1 member":["array with 1 element"]},
{},
[],
-42,
true,
false,
null,
{
"integer": 1234567890,
"real": -9876.543210,
"e": 0.123456789e-12,
"E": 1.234567890E+34,
"": 23456789012E66,
"zero": 0,
"one": 1,
"space": " ",
"quote": "\"",
"backslash": "\\",
"controls": "\b\f\n\r\t",
"slash": "/ & \/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
"0123456789": "digit",
"special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?",
"hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A",
"true": true,
"false": false,
"null": null,
"array":[ ],
"object":{ },
"address": "50 St. James Street",
"url": "http://www.JSON.org/",
"comment": "// /* <!-- --",
"# -- --> */": " ",
" s p a c e d " :[1,2 , 3
,
4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7],
"jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
"quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
"\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
: "A key can be any string"
},
0.5 ,98.6
,
99.44
,
1066,
1e1,
0.1e1,
1e-1,
1e00,2e+00,2e-00
,"rosebud"]

View File

@@ -0,0 +1 @@
[[[[[[[[[[[[[[[[[[["Not too deep"]]]]]]]]]]]]]]]]]]]

View File

@@ -0,0 +1,6 @@
{
"JSON Test Pattern pass3": {
"The outermost value": "must be an object or array.",
"In this test": "It is an object."
}
}

View File

@@ -0,0 +1,334 @@
/*jslint evil: true, strict: false */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
if (!this.JSON) {
this.JSON = {};
}
(function () {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ?
'"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());

View File

@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JSON Lint</title>
<script src="json2.js"></script>
<script src="jsonlint.js"></script>
<script>
window.onload = function () {
document.getElementById("button").onclick = function () {
try {
var result = jsonlint.parse(document.getElementById("source").value);
if (result) {
document.getElementById("result").innerHTML = "JSON is valid!";
document.getElementById("result").className = "pass";
if (document.getElementById("reformat").checked) {
document.getElementById("source").value = JSON.stringify(result, null, " ");
}
}
} catch(e) {
document.getElementById("result").innerHTML = e;
document.getElementById("result").className = "fail";
}
};
}
</script>
<style>
body {font-family: sans-serif;}
#result {
padding: 1em;
}
.pass {
background-color: #efe;
color: #393;
border: 2px solid #393;
}
.fail {
background-color: #fee;
color: #933;
border: 2px solid #933;
}
textarea { width: 100%; }
</style>
</head>
<body>
<h1>JSON Lint</h1>
<p>A pure JavaScript version of the service provided at <a href="http://jsonlint.com/">jsonlint.com</a>.</p>
<textarea id="source" rows="20" cols="50">
</textarea>
<p>
<button id="button">Validate</button>
<input type="checkbox" value="yes" id="reformat" /><label for="reformat">reformat JSON</label>
</p>
<h2>Results</h2>
<pre id="result"></pre>
<p><a href="http://github.com/zaach/jsonlint">project on github</a></p>
</body>
</html>

File diff suppressed because one or more lines are too long

10
node_modules/@mapbox/mapbox-gl-style-spec/.eslintrc generated vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"overrides": [
{
"files": ["rollup.config.js", "test.js"],
"rules": {
"flowtype/require-valid-file-annotation": "off"
}
}
]
}

564
node_modules/@mapbox/mapbox-gl-style-spec/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,564 @@
## 13.27.0
### Bug fixes 🐞
* Fix overwriting all feature ids while setting promoteIds on other layers with an object. ([#12322](https://github.com/mapbox/mapbox-gl-js/pull/12322)) (h/t [yongjun21](https://github.com/yongjun21))
## 13.26.0
### Features ✨
* Add unit option to number-format expression. ([#11839](https://github.com/mapbox/mapbox-gl-js/pull/11839)) (h/t [varna](https://github.com/varna))
### Bug fixes 🐞
* Fix a bug where `id` expression didn't correctly handle a value of 0. ([#12000](https://github.com/mapbox/mapbox-gl-js/pull/12000))
## 13.25.0
### Features ✨
* Extend atmospheric `fog` with three new style specification properties: `high-color`, `space-color` and `star-intensity` to allow the design of atmosphere around the globe and night skies. ([#11590](https://github.com/mapbox/mapbox-gl-js/pull/11590))
* Add a new line layer paint property in the style specification: `line-trim-offset` that can be used to create a custom fade out with improved update performance over `line-gradient`. ([#11570](https://github.com/mapbox/mapbox-gl-js/pull/11570))
### 🐞 Bug fixes
* Add `source` field requirement to terrain exaggeration in the style specification. ([#11664](https://github.com/mapbox/mapbox-gl-js/pull/11664))
## 13.24.0
### 🐞 Bug fixes
* Fix error on `gl-style-validate` script. ([#11538](https://github.com/mapbox/mapbox-gl-js/pull/11538))
* Allow the second argument to the `in` expression operator to be an empty string. ([#11547](https://github.com/mapbox/mapbox-gl-js/pull/11547))
* Fix error on some valid `filter` expressions. ([#11475](https://github.com/mapbox/mapbox-gl-js/pull/11475))
## 13.23.1
### ✨ Features and improvements
* Improve `coalesce` expressions to return a `ResolvedImage` when images are missing. ([#11371](https://github.com/mapbox/mapbox-gl-js/pull/11371))
## 13.23.0
### ✨ Features and improvements
* Add a `projection` root property that allows a non-mercator projection to be set as a style's default projection. ([#11124](https://github.com/mapbox/mapbox-gl-js/pull/11124))
* Add support for using `["pitch"]` and `["distance-from-camera"]` expressions within the `filter` of a symbol layer. ([#10795](https://github.com/mapbox/mapbox-gl-js/pull/10795))
## 13.22.0
### ✨ Features and improvements
* Added `protected` field to mapbox-api-supported validation. ([#10968](https://github.com/mapbox/mapbox-gl-js/pull/10968))
## 13.21.0
### ✨ Features and improvements
* Add support for `text-writing-mode` property when using `symbol-placement: line` text labels. ([#10647](https://github.com/mapbox/mapbox-gl-js/pull/10647))
* Note: This change will bring following changes for CJK text block:
* 1. For vertical CJK text, all the characters including Latin and Numbers will be vertically placed now. Previously, Latin and Numbers are horizontally placed.
* 2. For horizontal CJK text, it may have a slight horizontal shift due to the anchor shift.
## 13.20.1
### 🐞 Bug fixes
* Increase strictness of the style API validation for source types ([#10779](https://github.com/mapbox/mapbox-gl-js/pull/10779))
* Remove strictly-increasing requirement for fog range validation ([#10772](https://github.com/mapbox/mapbox-gl-js/pull/10772))
## 13.20.0
### ✨ Features and improvements
* Add configurable fog as a root style specification ([#10564](https://github.com/mapbox/mapbox-gl-js/pull/10564))
* Add support for data-driven expressions in `line-dasharray` and `line-cap` properties. ([#10591](https://github.com/mapbox/mapbox-gl-js/pull/10591))
* Add support for data-driven `text-line-height` ([#10612](https://github.com/mapbox/mapbox-gl-js/pull/10612))
## 13.19.0
### ✨ Features and improvements
* Added array support to minimums and maximums, allowing for validation of multi-dimensional style-spec value constraints. ([#10272](https://github.com/mapbox/mapbox-gl-js/pull/10272))
## 13.18.1
### 🐞 Bug fixes
* Fixed a bug where `map.setStyle` couldn't be used to enable terrain. ([#10177](https://github.com/mapbox/mapbox-gl-js/pull/10177))
## 13.18.0
### ✨ Features and improvements
* Add 3D terrain feature. All layer types and markers can now be extruded using the new `terrain` root level style-spec property or with the function `map.setTerrain()`. ([#1489](https://github.com/mapbox/mapbox-gl-js/issues/1489))
* Add support for unlocked pitch up to 85° (previously 60°). ([#3731](https://github.com/mapbox/mapbox-gl-js/issues/3731))
* Add a new sky layer acting as an infinite background above the horizon line. This layer can be used from the style-spec and has two types: `atmospheric` and `gradient`.
## 13.17.0
### ✨ Features and improvements
* Add a `filter` option for GeoJSON sources to filter out features prior to processing (e.g. before clustering). [#9864](https://github.com/mapbox/mapbox-gl-js/pull/9864)
## 13.16.0
### ✨ Features and improvements
* Added `volatile` source property to control storing the tiles in local storage. ([9702](https://github.com/mapbox/mapbox-gl-js/pull/9702))
* Added `clusterMinPoints` option for clustered GeoJSON sources that defines the minimum number of points to form a cluster. ([#9748](https://github.com/mapbox/mapbox-gl-js/pull/9748))
## 13.15.0
### ✨ Features and improvements
* Add `distance` expression to `style-spec`. This expression returns the shortest distance between a feature and an input geometry ([#9655](https://github.com/mapbox/mapbox-gl-js/pull/9655))
## 13.14.0
### ✨ Features and improvements
* Add `index-of` and `slice` expressions to search arrays and strings for the first occurrence of a specified value and return a section of the original array or string ([#9450](https://github.com/mapbox/mapbox-gl-js/pull/9450)) (h/t [lbutler](https://github.com/lbutler))
## 13.13.1
### ✨ Features and improvements
* Expose `expression.isExpressionFilter(..)` from the bundle. ([#9530](https://github.com/mapbox/mapbox-gl-js/pull/9530))
### 🐛 Bug fixes
* Fix a broken module import where the `style-spec` package was importing files from `mapbox-gl-js`, it's parent repo, causing downstream build systems to break. ([#9522](https://github.com/mapbox/mapbox-gl-js/pull/9522))
## 13.13.0
### ✨ Features and improvements
* Add `within` expression for testing whether an evaluated feature lies within a given GeoJSON object ([#9352](https://github.com/mapbox/mapbox-gl-js/pull/9352)). For example:<br>
`"icon-opacity": ["case", ["==", ["within", "some-polygon"], true], 1,
["==", ["within", "some-polygon"], false], 0]`
* Improve scaling of patterns used in `line-pattern` on all device resolutions and pixel ratios ([#9266](https://github.com/mapbox/mapbox-gl-js/pull/9266))
### 🐛 Bug fixes
* Allow needle argument to `in` expression to be false ([#9295](https://github.com/mapbox/mapbox-gl-js/pull/9295))
* Fix a bug where `icon-image` expression that evaluates to an empty string (`''`) produced a warning ([#9380](https://github.com/mapbox/mapbox-gl-js/pull/9380))
* Prevent exception resulting from `line-dash-array` of empty length ([#9385](https://github.com/mapbox/mapbox-gl-js/pull/9385))
## 13.12.0
* Update `image` expression SDK support table ([#9228](https://github.com/mapbox/mapbox-gl-js/pull/9228))
* Fix `promoteId` for line layers ([#9210](https://github.com/mapbox/mapbox-gl-js/pull/9210), [#9212](https://github.com/mapbox/mapbox-gl-js/pull/9212))
## 13.11.0
### ✨ Features and improvements
* Add `promoteId` option to use a feature property as ID for feature state ([#8987](https://github.com/mapbox/mapbox-gl-js/pull/8987))
* Update `symbol-avoid-edges` documentation regarding global collision detection ([#9157](https://github.com/mapbox/mapbox-gl-js/pull/9157))
* Remove reference to `in` function which has been replaced by the `in` expression ([#9102](https://github.com/mapbox/mapbox-gl-js/pull/9102))
### 🐛 Bug fixes
* Fix bug where `symbol-sort-key` was not used for collisions that crossed tile boundaries ([#9054](https://github.com/mapbox/mapbox-gl-js/pull/9054))
## 13.10.2
### 🐛 Bug fixes
* Fix style validation error messages not being displayed ([#9073](https://github.com/mapbox/mapbox-gl-js/pull/9073))
## 13.10.0
### ✨ Features and improvements
* Add ability to insert images into text labels using an `image` expression within a `format` expression: `"text-field": ["format", "Some text", ["image", "my-image"], "some more text"]` ([#8904](https://github.com/mapbox/mapbox-gl-js/pull/8904))
* Add `in` expression. It can check if a value is in an array (`["in", value, array]`) or a substring is in a string (`["in", substring, string]`) ([#8876](https://github.com/mapbox/mapbox-gl-js/pull/8876))
* Add support for stretchable images (aka nine-part or nine-patch images). Stretchable images can be used with `icon-text-fit` to draw resized images with unstretched corners and borders. ([#8997](https://github.com/mapbox/mapbox-gl-js/pull/8997))
* Add an es modules build of for mapbox-gl-style-spec in dist/ ([#8247](https://github.com/mapbox/mapbox-gl-js/pull/8247)) (h/t [ahocevar](https://github.com/ahocevar))
## 13.9.1
### ✨ Improvement
* Rename `Image` type to `ResolvedImage`, to better represent the result of an `image` expression evaluation. ([#8901](https://github.com/mapbox/mapbox-gl-js/pull/8901))
## 13.9.0
* Add `image` expression operator to determine image availability ([#8684](https://github.com/mapbox/mapbox-gl-js/pull/8684))
* Add a style-spec function to validate that styles are compatible with the Mapbox API ([#8663](https://github.com/mapbox/mapbox-gl-js/pull/8663))
## 13.8.0
- Introduce `text-writing-mode` symbol layer property to allow placing point labels vertically. [#8399](https://github.com/mapbox/mapbox-gl-js/pull/8399)
- Allow `text-color` to be used in formatted expressions to be able to draw different parts of a label in different colors. [#8068](https://github.com/mapbox/mapbox-gl-js/pull/8068)
- Improve conversion of legacy filters with duplicate values. [#8542](https://github.com/mapbox/mapbox-gl-js/pull/8542)
## 13.7.2
### 🐛 Bug fixes
* Fix SDK support spec section for variable label placement ([#8384](https://github.com/mapbox/mapbox-gl-js/pull/8384)) (h/t [@pozdnyakov](https://github.com/pozdnyakov))
### ✨ Features and improvements
* Add SDK support spec section for text-radial-offset ([#8401](https://github.com/mapbox/mapbox-gl-js/pull/8401))
* Add `*-sort-key` layout property for circle, fill, line ([#8467](https://github.com/mapbox/mapbox-gl-js/pull/8467))
* Expose convertFilter API in the style specification ([#8493](https://github.com/mapbox/mapbox-gl-js/pull/8493))
## 13.7.1
### 🐛 Bug fixes
* Fix format expression options validation ([#8339](https://github.com/mapbox/mapbox-gl-js/pull/8339))
* Fix SDK support information for style properties added in v13.7.0: ([#8384](https://github.com/mapbox/mapbox-gl-js/pull/8384))
* Add missing SDK support section for `text-radial-offset` property
* Assign SDK versions for `text-variable-anchor` and `text-justify: auto`
## 13.7.0
### ✨ Features and improvements
* Add `text-radial-offset` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
* Add `text-variable-anchor` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
* Add `auto` value to `text-justify` style property ([#7596](https://github.com/mapbox/mapbox-gl-js/pull/7596))
## 13.6.0
### ✨ Features and improvements
* Add `clusterProperties` option for aggregated cluster properties ([#2412](https://github.com/mapbox/mapbox-gl-js/issues/2412), fixed by [#7584](https://github.com/mapbox/mapbox-gl-js/pull/7584))
* Add `number-format` expression ([#7626](https://github.com/mapbox/mapbox-gl-js/pull/7626))
* Add `symbol-sort-key` style property ([#7678](https://github.com/mapbox/mapbox-gl-js/pull/7678))
## 13.5.0
### Features and improvements
* Flattens `all` expressions in converted filters ([#7679](https://github.com/mapbox/mapbox-gl-js/pull/7679))
* Compatibility tables are updated ([#7574](https://github.com/mapbox/mapbox-gl-js/pull/7574))
## 13.4.0
### ✨ Features and improvements
* **Tighten style validation**
* Disallow expressions as stop values ([#7396](https://github.com/mapbox/mapbox-gl-js/pull/7396))
* Disallow `feature-state` expressions in filters ([#7366](https://github.com/mapbox/mapbox-gl-js/pull/7366))
## 13.3.0
### 🐛 Bug fixes
* **Expressions**
* Fix `let` expression stripping expected type during parsing ([#7300](https://github.com/mapbox/mapbox-gl-js/issues/7300), fixed by [#7301](https://github.com/mapbox/mapbox-gl-js/pull/7301))
* Fix superfluous wrapping of literals in `literal` expression ([#7336](https://github.com/mapbox/mapbox-gl-js/issues/7336), fixed by [#7337](https://github.com/mapbox/mapbox-gl-js/pull/7337))
* Allow calling `to-color` on values that are already of type `Color` ([#7260](https://github.com/mapbox/mapbox-gl-js/pull/7260))
* Fix `to-array` for empty arrays (([#7261](https://github.com/mapbox/mapbox-gl-js/pull/7261)))
* Fix identity functions for `text-field` when using formatted text ([#7351](https://github.com/mapbox/mapbox-gl-js/pull/7351))
* Fix coercion of `null` to `0` in `to-number` expression ([#7083](https://github.com/mapbox/mapbox-gl-js/issues/7083), fixed by [#7274](https://github.com/mapbox/mapbox-gl-js/pull/7274))
### ✨ Features and improvements
* Add `fill-extrusion-vertical-gradient` property for controlling shading of fill extrusions ([#5768](https://github.com/mapbox/mapbox-gl-js/issues/5768), fixed by [#6841](https://github.com/mapbox/mapbox-gl-js/pull/6841))
## 13.2.0
### 🐛 Bug fixes
* Update the style-spec's old `gl-style-migrate` script to include conversion of legacy functions and filters to their expression equivalents ([#6927](https://github.com/mapbox/mapbox-gl-js/issues/6927), fixed by [#7095](https://github.com/mapbox/mapbox-gl-js/pull/7095))
### ✨ Features and improvements
* Add `symbol-z-order` symbol layout property to style spec ([#7219](https://github.com/mapbox/mapbox-gl-js/pull/7219))
* Implement data-driven styling support for `*-pattern properties` ([#6289](https://github.com/mapbox/mapbox-gl-js/pull/6289))
## 13.1.1
### 🐛 Bug fixes
* Fix broken module import in mapboxgl-style-spec (v13.0.1) ([#6984](https://github.com/mapbox/mapbox-gl-js/issues/6984), fixed by [#6997](https://api.github.com/repos/mapbox/mapbox-gl-js/pulls/6997))
### ✨ Features and improvements
* Improve formatting for style output ([#7029](https://github.com/mapbox/mapbox-gl-js/pull/7029))
## 13.1.0
### ✨ Features and improvements
* Add `raster-resampling` raster paint property ([#6411](https://github.com/mapbox/mapbox-gl-js/pull/6411)) (h/t [andrewharvey](https://github.com/andrewharvey))
* Add `symbol-placement: line-center` ([#6821](https://github.com/mapbox/mapbox-gl-js/pull/6821))
## 13.0.1
### ⚠️ Breaking changes
* Align implicit type behavior of `match` expressions with with `case/==` ([#6684](https://github.com/mapbox/mapbox-gl-js/pull/6684))
* Update spec so that documentation can automatically capture which functions and expressions can be used with which properties ([#6521](https://github.com/mapbox/mapbox-gl-js/pull/6521))
### ✨ Features and improvements
* Add `feature-state` [#6263](https://github.com/mapbox/mapbox-gl-js/pull/6263)
* Add support for GeoJSON attribution ([#6364](https://github.com/mapbox/mapbox-gl-js/pull/6364)) (h/t [andrewharvey](https://github.com/andrewharvey))
* Upgrade to Flow 0.69 ([#6594](https://github.com/mapbox/mapbox-gl-js/pull/6594))
### 🐛 Bug fixes
* Use named exports for style-spec entrypoint module ([#6601](https://github.com/mapbox/mapbox-gl-js/issues/6601)
## 13.0.0
Malformed package published to NPM.
## 12.0.0
### ⚠️ Breaking changes
* The `"to-string"` expression operator now converts null to an empty string rather than to `"null"`. [#6534](https://github.com/mapbox/mapbox-gl-js/pull/6534)
### ✨ Features and improvements
* 🌈 Add line-gradient property [#6303](https://github.com/mapbox/mapbox-gl-js/pull/6303)
* Add collator expression for controlling case and diacritic sensitivity in string comparisons [#6270](https://github.com/mapbox/mapbox-gl-js/pull/6270)
* Add `is-supported-script` expression for determining if a script is supported. [#6260](https://github.com/mapbox/mapbox-gl-js/pull/6260)
* Add `collator` expression for controlling case and diacritic sensitivity in string comparisons [#6270](https://github.com/mapbox/mapbox-gl-js/pull/6270)
* Add `abs`, `round`, `floor`, and `ceil` expression operators [#6496](https://github.com/mapbox/mapbox-gl-js/pull/6496)
* Add support for Mapzen Terrarium tiles in raster-dem sources [#6110](https://github.com/mapbox/mapbox-gl-js/pull/6110)
### 🐛 Bug fixes
- Fix Rollup build [6575](https://github.com/mapbox/mapbox-gl-js/pull/6575)
## 11.1.1
### 🐛 Bug fixes
* Update SDK support information for `text-font`, and `heatmap-color`, and `hillshade` layer properties.
## 11.1.0
### ✨ Features and improvements
* Avoid use of `new Function` for filters by converting old-style filters to expressions [#5665](https://github.com/mapbox/mapbox-gl-js/pull/5665).
## 11.0.0
### ⚠️ Breaking changes
* Remove constants validating code [#5885](https://github.com/mapbox/mapbox-gl-js/pull/5885)
* `"colorSpace": "hcl"` now uses shortest-path interpolation for hue [#5811](https://github.com/mapbox/mapbox-gl-js/issues/5811)
### ✨ Features and improvements
* Introduce client-side hillshading with `raster-dem` source type and `hillshade` layer type [#5286](https://github.com/mapbox/mapbox-gl-js/pull/5286)
* Add Color#toString and expose Color publicly [#5866](https://github.com/mapbox/mapbox-gl-js/pull/5866)
* Improve typing for `==` and `!=` expressions [#5840](https://github.com/mapbox/mapbox-gl-js/pull/5840)
* Made `coalesce` expressions more useful [#5755](https://github.com/mapbox/mapbox-gl-js/issues/5755)
* Enable implicit type assertions for array types [#5738](https://github.com/mapbox/mapbox-gl-js/pull/5738)
### 🐛 Bug fixes
* Fix standalone browser build [#5736](https://github.com/mapbox/mapbox-gl-js/pull/5736), [#5906](https://github.com/mapbox/mapbox-gl-js/pull/5906)
* Handle NaN as input to step and interpolate expressions [#5757](https://github.com/mapbox/mapbox-gl-js/pull/5757)
* Fix style diffing for changes to GeoJSON sources [#5745](https://github.com/mapbox/mapbox-gl-js/pull/5745)
* Mark layer "type" property as required [#5849](https://github.com/mapbox/mapbox-gl-js/pull/5849)
## 10.1.0
* Remove support for validating and migrating v6 styles [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
* Remove support for validating v7 styles [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
* Remove spaces after commas in `to-string` representation of colors [#5480](https://github.com/mapbox/mapbox-gl-js/pull/5480)
* Fix bugs preventing `mapbox-gl-style-spec` package on NPM from being used externally [#5502](https://github.com/mapbox/mapbox-gl-js/pull/5502)
* Fix flow-typed interface generator [#5478](https://github.com/mapbox/mapbox-gl-js/pull/5478)
* Export `function` [#5584](https://github.com/mapbox/mapbox-gl-js/pull/5584)
* Export `StylePropertySpecification` type [#5593](https://github.com/mapbox/mapbox-gl-js/pull/5593)
* Split the `source_tile` entry in the JSON schema into `source_vector` and `source_raster` [#5604](https://github.com/mapbox/mapbox-gl-js/pull/5604)
### Validation Changes
* Require that `heatmap-color` use expressions instead of stop functions [#5624](https://github.com/mapbox/mapbox-gl-js/issues/5624)
* Remove support for including `{tokens}` in expressions for `text-field` and `icon-image` [#5599](https://github.com/mapbox/mapbox-gl-js/issues/5599)
* Disallow interpolation in expressions for `line-dasharray` [#5519](https://github.com/mapbox/mapbox-gl-js/pull/5519)
* Validate that zoom expressions only appear at the top level of an expression [#5609](https://github.com/mapbox/mapbox-gl-js/issues/5609)
* Validate that `step` and `interpolate` expressions don't have any duplicate stops [#5605](https://github.com/mapbox/mapbox-gl-js/issues/5605)
* Split `curve` expression into `step` and `interpolate` expressions [#5542](https://github.com/mapbox/mapbox-gl-js/pull/5542)
* Validate expression values for enum-typed properties [#5589](https://github.com/mapbox/mapbox-gl-js/pull/5589)
* Improve validation to catch uses of deprecated function syntax [#5667](https://github.com/mapbox/mapbox-gl-js/pull/5667)
## 10.0.1
* Fix bug preventing @mapbox/mapbox-gl-style-spec package from being usable outside of mapbox-gl-js (#5502)
## 10.0.0
* Add expression and heatmap layer support. See Mapbox GL JS v0.40.1 changelog entry for details.
## 9.0.1
* Remove `fast-stable-stringify` dependency (#5152)
## 9.0.0
* Fix validation error on categorical zoom-and-property functions (#4220)
* Relax requirement that styles using "icon-image" must have a "sprite"
* Prevent infinite loop in binarySearchForIndex when duplicate stops are present. (#4503)
* Ensure generated composite function stops are in the correct order (#4509)
* Update SDK support matrices to be current as of GL JS v0.35.1, iOS SDK v3.5.0, Android SDK 5.0.0
* Remove support for implicitly-categorical functions
* BREAKING CHANGE: the API for the `function` module has changed. The `interpolated` and `piecewise-constant` exports
were replaced with a single unnamed export, a function which accepts an object conforming to the style spec "function"
definition, and an object defining a style spec property. It handles color parsing and validation of feature values
internally.
* Functions now support a "default" property.
* `parseColor` was promoted from gl-js.
## 8.11.0
* Merge `feature-filter` repository into this repository #639
## 8.10.0
v8.0.0 styles are fully compatible with v8.10.0.
### Style Specification Changes
* Added `colorSpace` option to functions
* Added `fill-extrusion` layer type
* Add top-level `light` property
* Remove increase maximum `maxzoom` to 24
* Deprecate paint classes :warning:
* Increase strictness of function validation
### Reference JSON & API Changes
* Added `deref` utility
* Added `group_by_layout` utility
* Merge `mapbox-gl-function` repository into this repository
## 8.9.0
v8.0.0 styles are fully compatible with v8.9.0.
* Added identity functions
* Added `auto` value which represents the calculated default value
## 8.8.1
v8.0.0 styles are fully compatible with v8.8.1.
* Fixed style validation for layers with invalid types
## 8.8.0
v8.0.0 styles are fully compatible with v8.8.0.
* Clarified documentation around fill-opacity.
* Update function documentation and validation for property functions.
* Add text-pitch-alignment property.
* Add icon-text-fit, icon-text-fit-padding properties.
## 8.7.0
v8.0.0 styles are fully compatible with v8.7.0.
* Add support for has / !has operators.
## 8.6.0
v8.0.0 styles are fully compatible with v8.6.0.
* Added support for zoom and feature driven functions.
## 8.4.2
v8.0.0 styles are fully compatible with v8.4.2.
* Refactored style validator to expose more granular validation methods
## 8.4.1
v8.0.0 styles are fully compatible with v8.4.1.
* Revert ramp validation checks that broke some styles.
## 8.4.0
v8.0.0 styles are fully compatible with v8.4.0.
* Added `cluster`, `clusterRadius`, `clusterMaxZoom` GeoJSON source properties.
## 8.3.0
v8.0.0 styles are fully compatible with v8.3.0.
* Added `line-offset` style property
## 8.2.1
v8.0.0 styles are fully compatible with v8.2.1.
* Enforce that all layers that use a vector source specify a "source-layer"
## 8.2.0
v8.0.0 styles are fully compatible with v8.2.0.
* Add inline `example` property.
* Enforce that all style properties must have documentation in `doc` property.
* Create minified style specs with `doc` and `example` properties removed.
* `validate` now validates against minified style spec.
* `format` now accepts `space` option to use with `JSON.stringify`.
* Remove `gl-style-spritify`. Mapbox GL sprites are now created automatically by
the Mapbox style APIs, or for hand-crafted styles, by [spritezero-cli](https://github.com/mapbox/spritezero-cli).
## 8.1.0
v8.0.0 styles are fully compatible with v8.1.0.
* [BREAKING] Simplified layout/paint layer property types to more closely align
with v7 types.
* Fixed migration script compatibility with newer versions of Node.js and io.js
* Removed `constants` from schema, they were deprecated in v8
* Added style diff utility to generate semantic deltas between two stylesheets
* Added `visibility` property to `circle` layer type
* Added `pitch` property to stylesheet
## 8.0.0
Introduction of Mapbox GL style specification v8. To migrate a v7 style to v8,
use the `gl-style-migrate` script as described in the README.
* [BREAKING] The value of the `text-font` property is now an array of
strings, rather than a single comma separated string.
* [BREAKING] Renamed `symbol-min-distance` to `symbol-spacing`.
* [BREAKING] Renamed `background-image` to `background-pattern`.
* [BREAKING] Renamed `line-image` to `line-pattern`.
* [BREAKING] Renamed `fill-image` to `fill-pattern`.
* [BREAKING] Renamed the `url` property of the video source type to `urls`.
* [BREAKING] Coordinates in video sources are now specified in [lon, lat] order.
* [BREAKING] Removed `text-max-size` and `icon-max-size` properties; these
are now calculated automatically.
* [BREAKING] `text-size` and `icon-size` are now layout properties instead of paint properties.
* [BREAKING] Constants are no longer supported. If you are editing styles by
hand and want to use constants, you can use a preprocessing step with a tool
like [ScreeSS](https://github.com/screee/screess).
* [BREAKING] The format for `mapbox://` glyphs URLs has changed; you should
now use `mapbox://fonts/mapbox/{fontstack}/{range}.pbf`.
* [BREAKING] Reversed the priority of layers for calculating label placement:
labels for layers that appear later in the style now have priority over earlier
layers.
* Added a new `image` source type.
* Added a new `circle` layer type.
* Default map center location can now be set in the style.
* Added `mapbox://` sprite URLs `mapbox://sprite/{user | "mapbox"}/{id}`
## 7.5.0
* Added gl-style-composite script, for auto-compositing sources in a style.
## 7.4.1
* Use JSON.stringify for formatting instead of js-beautify
## 7.0.0
Introduction of Mapbox GL style specification v7.
* [BREAKING] Improve dashed lines (#234)
* [BREAKING] Remove prerendered layers (#232)
* Explicit visibility property (#212)
* Functions for all properties (#237)
## 6.0.0 (Style spec v6)
Introduction of Mapbox GL style specification v6.
* [BREAKING] New filter syntax (#178)
* [BREAKING] Line gap property (#131)
* [BREAKING] Remove dashes from min/max-zoom (#175)
* [BREAKING] New layout/paint terminology (#166)
* [BREAKING] Single text positioning property (#197)
* Added requirements (#200)
* Added minimum, maximum, and period values (#198)
## 0.0.5 (in progress)
* [BREAKING] Switch to suffix for transition properties (`transition-*` -> `*-transition`).
* Added support for remote, non-Mapbox TileJSON sources.
* [BREAKING] Source `minZoom` and `maxZoom` renamed to `minzoom` and `maxzoom to match TileJSON.
* Added support for `mapbox://` glyph URLs.
* [BREAKING] Renamed `raster-fade` to `raster-fade-duration`.
* Added background-opacity property.
* Added "tokens" property to string values that can autocomplete fields from layers
* Added "units" property to describe value types
## 0.0.4 (Aug 8 2014)
* Initial public release

59
node_modules/@mapbox/mapbox-gl-style-spec/README.md generated vendored Normal file
View File

@@ -0,0 +1,59 @@
# Mapbox GL style specification & utilities
This directory contains code and reference files that define the Mapbox GL style specification and provides some utilities for working with Mapbox styles.
## npm package
The Mapbox GL style specification and utilities are published as a seperate npm
package so that they can be installed without the bulk of GL JS.
npm install @mapbox/mapbox-gl-style-spec
## CLI Tools
If you install this package globally, you will have access to several CLI tools.
npm install @mapbox/mapbox-gl-style-spec --global
### `gl-style-composite`
```bash
$ gl-style-composite style.json
```
Will take a non-composited style and produce a [composite style](https://www.mapbox.com/blog/better-label-placement-in-mapbox-studio/).
### `gl-style-migrate`
This repo contains scripts for migrating GL styles of any version to the latest version
(currently v8). Migrate a style like this:
```bash
$ gl-style-migrate bright-v7.json > bright-v8.json
```
To migrate a file in place, you can use the `sponge` utility from the `moreutils` package:
```bash
$ brew install moreutils
$ gl-style-migrate bright.json | sponge bright.json
```
### `gl-style-format`
```bash
$ gl-style-format style.json
```
Will format the given style JSON to use standard indentation and sorted object keys.
### `gl-style-validate`
```bash
$ gl-style-validate style.json
```
Will validate the given style JSON and print errors to stdout. Provide a
`--json` flag to get JSON output.
To validate that a style can be uploaded to the Mapbox Styles API, use the `--mapbox-api-supported` flag.

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format, composite} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(composite(JSON.parse(fs.readFileSync(argv._[0]).toString()))));
function help() {
console.log('usage:');
console.log(' gl-style-composite style.json');
}

View File

@@ -0,0 +1,30 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(JSON.parse(fs.readFileSync(argv._[0]).toString()), argv.space));
function help() {
console.log('usage:');
console.log(' gl-style-format source.json > destination.json');
console.log('');
console.log('options:');
console.log(' --space <num>');
console.log(' Number of spaces in output (default "2")');
console.log(' Pass "0" for minified output.');
}

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import fs from 'fs';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {format, migrate} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2));
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(0);
}
console.log(format(migrate(JSON.parse(fs.readFileSync(argv._[0]).toString()))));
function help() {
console.log('usage:');
console.log(' gl-style-migrate source.json > destination.json');
}

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env node
// @flow
/* eslint-disable no-process-exit */
import rw from 'rw';
import minimist from 'minimist';
/* eslint import/no-unresolved: [error, { ignore: ['^@mapbox/mapbox-gl-style-spec$'] }] */
/* $FlowFixMe[cannot-resolve-module] */
import {validate, validateMapboxApiSupported} from '@mapbox/mapbox-gl-style-spec';
const argv = minimist(process.argv.slice(2), {
boolean: ['json', 'mapbox-api-supported'],
});
let status = 0;
if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) {
help();
process.exit(status);
}
if (!argv._.length) {
argv._.push('/dev/stdin');
}
argv._.forEach((file) => {
let errors = [];
if (argv['mapbox-api-supported']) {
errors = validateMapboxApiSupported(rw.readFileSync(file, 'utf8'));
} else {
errors = validate(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');
console.log('--mapbox-api-supported validate compatibility with Mapbox Styles API');
}

View File

50
node_modules/@mapbox/mapbox-gl-style-spec/composite.js generated vendored Normal file
View File

@@ -0,0 +1,50 @@
export default function (style) {
const styleIDs = [];
const sourceIDs = [];
const compositedSourceLayers = [];
for (const id in style.sources) {
const source = style.sources[id];
if (source.type !== "vector")
continue;
const match = /^mapbox:\/\/(.*)/.exec(source.url);
if (!match)
continue;
styleIDs.push(id);
sourceIDs.push(match[1]);
}
if (styleIDs.length < 2)
return style;
styleIDs.forEach((id) => {
delete style.sources[id];
});
const compositeID = sourceIDs.join(",");
style.sources[compositeID] = {
"type": "vector",
"url": `mapbox://${compositeID}`
};
style.layers.forEach((layer) => {
if (styleIDs.indexOf(layer.source) >= 0) {
layer.source = compositeID;
if ('source-layer' in layer) {
if (compositedSourceLayers.indexOf(layer['source-layer']) >= 0) {
throw new Error('Conflicting source layer names');
} else {
compositedSourceLayers.push(layer['source-layer']);
}
}
}
});
return style;
}

53
node_modules/@mapbox/mapbox-gl-style-spec/deref.js generated vendored Normal file
View File

@@ -0,0 +1,53 @@
// @flow
import refProperties from './util/ref_properties.js';
import type {LayerSpecification} from './types.js';
function deref(layer: LayerSpecification, parent: LayerSpecification): LayerSpecification {
const result = {};
for (const k in layer) {
if (k !== 'ref') {
result[k] = layer[k];
}
}
refProperties.forEach((k) => {
if (k in parent) {
result[k] = (parent: any)[k];
}
});
return ((result: any): LayerSpecification);
}
/**
* Given an array of layers, some of which may contain `ref` properties
* whose value is the `id` of another property, return a new array where
* such layers have been augmented with the 'type', 'source', etc. properties
* from the parent layer, and the `ref` property has been removed.
*
* The input is not modified. The output may contain references to portions
* of the input.
*
* @private
* @param {Array<Layer>} layers
* @returns {Array<Layer>}
*/
export default function derefLayers(layers: Array<LayerSpecification>): Array<LayerSpecification> {
layers = layers.slice();
const map = Object.create(null);
for (let i = 0; i < layers.length; i++) {
map[layers[i].id] = layers[i];
}
for (let i = 0; i < layers.length; i++) {
if ('ref' in layers[i]) {
layers[i] = deref(layers[i], map[(layers[i]: any).ref]);
}
}
return layers;
}

434
node_modules/@mapbox/mapbox-gl-style-spec/diff.js generated vendored Normal file
View File

@@ -0,0 +1,434 @@
// @flow
import isEqual from './util/deep_equal.js';
import type {StyleSpecification} from './types.js';
type Command = {
command: string;
args: Array<any>;
};
export const operations: {[_: string]: string} = {
/*
* { command: 'setStyle', args: [stylesheet] }
*/
setStyle: 'setStyle',
/*
* { command: 'addLayer', args: [layer, 'beforeLayerId'] }
*/
addLayer: 'addLayer',
/*
* { command: 'removeLayer', args: ['layerId'] }
*/
removeLayer: 'removeLayer',
/*
* { command: 'setPaintProperty', args: ['layerId', 'prop', value] }
*/
setPaintProperty: 'setPaintProperty',
/*
* { command: 'setLayoutProperty', args: ['layerId', 'prop', value] }
*/
setLayoutProperty: 'setLayoutProperty',
/*
* { command: 'setFilter', args: ['layerId', filter] }
*/
setFilter: 'setFilter',
/*
* { command: 'addSource', args: ['sourceId', source] }
*/
addSource: 'addSource',
/*
* { command: 'removeSource', args: ['sourceId'] }
*/
removeSource: 'removeSource',
/*
* { command: 'setGeoJSONSourceData', args: ['sourceId', data] }
*/
setGeoJSONSourceData: 'setGeoJSONSourceData',
/*
* { command: 'setLayerZoomRange', args: ['layerId', 0, 22] }
*/
setLayerZoomRange: 'setLayerZoomRange',
/*
* { command: 'setLayerProperty', args: ['layerId', 'prop', value] }
*/
setLayerProperty: 'setLayerProperty',
/*
* { command: 'setCenter', args: [[lon, lat]] }
*/
setCenter: 'setCenter',
/*
* { command: 'setZoom', args: [zoom] }
*/
setZoom: 'setZoom',
/*
* { command: 'setBearing', args: [bearing] }
*/
setBearing: 'setBearing',
/*
* { command: 'setPitch', args: [pitch] }
*/
setPitch: 'setPitch',
/*
* { command: 'setSprite', args: ['spriteUrl'] }
*/
setSprite: 'setSprite',
/*
* { command: 'setGlyphs', args: ['glyphsUrl'] }
*/
setGlyphs: 'setGlyphs',
/*
* { command: 'setTransition', args: [transition] }
*/
setTransition: 'setTransition',
/*
* { command: 'setLighting', args: [lightProperties] }
*/
setLight: 'setLight',
/*
* { command: 'setTerrain', args: [terrainProperties] }
*/
setTerrain: 'setTerrain',
/*
* { command: 'setFog', args: [fogProperties] }
*/
setFog: 'setFog',
/*
* { command: 'setProjection', args: [projectionProperties] }
*/
setProjection: 'setProjection'
};
function addSource(sourceId, after, commands) {
commands.push({command: operations.addSource, args: [sourceId, after[sourceId]]});
}
function removeSource(sourceId, commands, sourcesRemoved) {
commands.push({command: operations.removeSource, args: [sourceId]});
sourcesRemoved[sourceId] = true;
}
function updateSource(sourceId, after, commands, sourcesRemoved) {
removeSource(sourceId, commands, sourcesRemoved);
addSource(sourceId, after, commands);
}
function canUpdateGeoJSON(before, after, sourceId) {
let prop;
for (prop in before[sourceId]) {
if (!before[sourceId].hasOwnProperty(prop)) continue;
if (prop !== 'data' && !isEqual(before[sourceId][prop], after[sourceId][prop])) {
return false;
}
}
for (prop in after[sourceId]) {
if (!after[sourceId].hasOwnProperty(prop)) continue;
if (prop !== 'data' && !isEqual(before[sourceId][prop], after[sourceId][prop])) {
return false;
}
}
return true;
}
function diffSources(before, after, commands, sourcesRemoved) {
before = before || {};
after = after || {};
let sourceId;
// look for sources to remove
for (sourceId in before) {
if (!before.hasOwnProperty(sourceId)) continue;
if (!after.hasOwnProperty(sourceId)) {
removeSource(sourceId, commands, sourcesRemoved);
}
}
// look for sources to add/update
for (sourceId in after) {
if (!after.hasOwnProperty(sourceId)) continue;
if (!before.hasOwnProperty(sourceId)) {
addSource(sourceId, after, commands);
} else if (!isEqual(before[sourceId], after[sourceId])) {
if (before[sourceId].type === 'geojson' && after[sourceId].type === 'geojson' && canUpdateGeoJSON(before, after, sourceId)) {
commands.push({command: operations.setGeoJSONSourceData, args: [sourceId, after[sourceId].data]});
} else {
// no update command, must remove then add
updateSource(sourceId, after, commands, sourcesRemoved);
}
}
}
}
function diffLayerPropertyChanges(before, after, commands, layerId, klass, command) {
before = before || {};
after = after || {};
let prop;
for (prop in before) {
if (!before.hasOwnProperty(prop)) continue;
if (!isEqual(before[prop], after[prop])) {
commands.push({command, args: [layerId, prop, after[prop], klass]});
}
}
for (prop in after) {
if (!after.hasOwnProperty(prop) || before.hasOwnProperty(prop)) continue;
if (!isEqual(before[prop], after[prop])) {
commands.push({command, args: [layerId, prop, after[prop], klass]});
}
}
}
function pluckId(layer) {
return layer.id;
}
function indexById(group, layer) {
group[layer.id] = layer;
return group;
}
function diffLayers(before, after, commands) {
before = before || [];
after = after || [];
// order of layers by id
const beforeOrder = before.map(pluckId);
const afterOrder = after.map(pluckId);
// index of layer by id
const beforeIndex = before.reduce(indexById, {});
const afterIndex = after.reduce(indexById, {});
// track order of layers as if they have been mutated
const tracker = beforeOrder.slice();
// layers that have been added do not need to be diffed
const clean = Object.create(null);
let i, d, layerId, beforeLayer, afterLayer, insertBeforeLayerId, prop;
// remove layers
for (i = 0, d = 0; i < beforeOrder.length; i++) {
layerId = beforeOrder[i];
if (!afterIndex.hasOwnProperty(layerId)) {
commands.push({command: operations.removeLayer, args: [layerId]});
tracker.splice(tracker.indexOf(layerId, d), 1);
} else {
// limit where in tracker we need to look for a match
d++;
}
}
// add/reorder layers
for (i = 0, d = 0; i < afterOrder.length; i++) {
// work backwards as insert is before an existing layer
layerId = afterOrder[afterOrder.length - 1 - i];
if (tracker[tracker.length - 1 - i] === layerId) continue;
if (beforeIndex.hasOwnProperty(layerId)) {
// remove the layer before we insert at the correct position
commands.push({command: operations.removeLayer, args: [layerId]});
tracker.splice(tracker.lastIndexOf(layerId, tracker.length - d), 1);
} else {
// limit where in tracker we need to look for a match
d++;
}
// add layer at correct position
insertBeforeLayerId = tracker[tracker.length - i];
commands.push({command: operations.addLayer, args: [afterIndex[layerId], insertBeforeLayerId]});
tracker.splice(tracker.length - i, 0, layerId);
clean[layerId] = true;
}
// update layers
for (i = 0; i < afterOrder.length; i++) {
layerId = afterOrder[i];
beforeLayer = beforeIndex[layerId];
afterLayer = afterIndex[layerId];
// no need to update if previously added (new or moved)
if (clean[layerId] || isEqual(beforeLayer, afterLayer)) continue;
// If source, source-layer, or type have changes, then remove the layer
// and add it back 'from scratch'.
if (!isEqual(beforeLayer.source, afterLayer.source) || !isEqual(beforeLayer['source-layer'], afterLayer['source-layer']) || !isEqual(beforeLayer.type, afterLayer.type)) {
commands.push({command: operations.removeLayer, args: [layerId]});
// we add the layer back at the same position it was already in, so
// there's no need to update the `tracker`
insertBeforeLayerId = tracker[tracker.lastIndexOf(layerId) + 1];
commands.push({command: operations.addLayer, args: [afterLayer, insertBeforeLayerId]});
continue;
}
// layout, paint, filter, minzoom, maxzoom
diffLayerPropertyChanges(beforeLayer.layout, afterLayer.layout, commands, layerId, null, operations.setLayoutProperty);
diffLayerPropertyChanges(beforeLayer.paint, afterLayer.paint, commands, layerId, null, operations.setPaintProperty);
if (!isEqual(beforeLayer.filter, afterLayer.filter)) {
commands.push({command: operations.setFilter, args: [layerId, afterLayer.filter]});
}
if (!isEqual(beforeLayer.minzoom, afterLayer.minzoom) || !isEqual(beforeLayer.maxzoom, afterLayer.maxzoom)) {
commands.push({command: operations.setLayerZoomRange, args: [layerId, afterLayer.minzoom, afterLayer.maxzoom]});
}
// handle all other layer props, including paint.*
for (prop in beforeLayer) {
if (!beforeLayer.hasOwnProperty(prop)) continue;
if (prop === 'layout' || prop === 'paint' || prop === 'filter' ||
prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom') continue;
if (prop.indexOf('paint.') === 0) {
diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), operations.setPaintProperty);
} else if (!isEqual(beforeLayer[prop], afterLayer[prop])) {
commands.push({command: operations.setLayerProperty, args: [layerId, prop, afterLayer[prop]]});
}
}
for (prop in afterLayer) {
if (!afterLayer.hasOwnProperty(prop) || beforeLayer.hasOwnProperty(prop)) continue;
if (prop === 'layout' || prop === 'paint' || prop === 'filter' ||
prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom') continue;
if (prop.indexOf('paint.') === 0) {
diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), operations.setPaintProperty);
} else if (!isEqual(beforeLayer[prop], afterLayer[prop])) {
commands.push({command: operations.setLayerProperty, args: [layerId, prop, afterLayer[prop]]});
}
}
}
}
/**
* Diff two stylesheet
*
* Creates semanticly aware diffs that can easily be applied at runtime.
* Operations produced by the diff closely resemble the mapbox-gl-js API. Any
* error creating the diff will fall back to the 'setStyle' operation.
*
* Example diff:
* [
* { command: 'setConstant', args: ['@water', '#0000FF'] },
* { command: 'setPaintProperty', args: ['background', 'background-color', 'black'] }
* ]
*
* @private
* @param {*} [before] stylesheet to compare from
* @param {*} after stylesheet to compare to
* @returns Array list of changes
*/
export default function diffStyles(before: StyleSpecification, after: StyleSpecification): Array<Command> {
if (!before) return [{command: operations.setStyle, args: [after]}];
let commands = [];
try {
// Handle changes to top-level properties
if (!isEqual(before.version, after.version)) {
return [{command: operations.setStyle, args: [after]}];
}
if (!isEqual(before.center, after.center)) {
commands.push({command: operations.setCenter, args: [after.center]});
}
if (!isEqual(before.zoom, after.zoom)) {
commands.push({command: operations.setZoom, args: [after.zoom]});
}
if (!isEqual(before.bearing, after.bearing)) {
commands.push({command: operations.setBearing, args: [after.bearing]});
}
if (!isEqual(before.pitch, after.pitch)) {
commands.push({command: operations.setPitch, args: [after.pitch]});
}
if (!isEqual(before.sprite, after.sprite)) {
commands.push({command: operations.setSprite, args: [after.sprite]});
}
if (!isEqual(before.glyphs, after.glyphs)) {
commands.push({command: operations.setGlyphs, args: [after.glyphs]});
}
if (!isEqual(before.transition, after.transition)) {
commands.push({command: operations.setTransition, args: [after.transition]});
}
if (!isEqual(before.light, after.light)) {
commands.push({command: operations.setLight, args: [after.light]});
}
if (!isEqual(before.fog, after.fog)) {
commands.push({command: operations.setFog, args: [after.fog]});
}
if (!isEqual(before.projection, after.projection)) {
commands.push({command: operations.setProjection, args: [after.projection]});
}
// Handle changes to `sources`
// If a source is to be removed, we also--before the removeSource
// command--need to remove all the style layers that depend on it.
const sourcesRemoved = {};
// First collect the {add,remove}Source commands
const removeOrAddSourceCommands = [];
diffSources(before.sources, after.sources, removeOrAddSourceCommands, sourcesRemoved);
// Push a removeLayer command for each style layer that depends on a
// source that's being removed.
// Also, exclude any such layers them from the input to `diffLayers`
// below, so that diffLayers produces the appropriate `addLayers`
// command
const beforeLayers = [];
if (before.layers) {
before.layers.forEach((layer) => {
if (layer.source && sourcesRemoved[layer.source]) {
commands.push({command: operations.removeLayer, args: [layer.id]});
} else {
beforeLayers.push(layer);
}
});
}
// Remove the terrain if the source for that terrain is being removed
let beforeTerrain = before.terrain;
if (beforeTerrain) {
if (sourcesRemoved[beforeTerrain.source]) {
commands.push({command: operations.setTerrain, args: [undefined]});
beforeTerrain = undefined;
}
}
commands = commands.concat(removeOrAddSourceCommands);
// Even though terrain is a top-level property
// Its like a layer in the sense that it depends on a source being present.
if (!isEqual(beforeTerrain, after.terrain)) {
commands.push({command: operations.setTerrain, args: [after.terrain]});
}
// Handle changes to `layers`
diffLayers(beforeLayers, after.layers, commands);
} catch (e) {
// fall back to setStyle
console.warn('Unable to compute style diff:', e);
commands = [{command: operations.setStyle, args: [after]}];
}
return commands;
}

View File

17636
node_modules/@mapbox/mapbox-gl-style-spec/dist/index.cjs generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

17610
node_modules/@mapbox/mapbox-gl-style-spec/dist/index.es.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

10
node_modules/@mapbox/mapbox-gl-style-spec/empty.js generated vendored Normal file
View File

@@ -0,0 +1,10 @@
// @flow
import type {StyleSpecification} from './types.js';
export default function emptyStyle(): StyleSpecification {
return {
version: 8,
layers: [],
sources: {}
};
}

View File

@@ -0,0 +1,16 @@
// @flow
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
export default class ParsingError {
message: string;
error: Error;
line: number;
constructor(error: Error) {
this.error = error;
this.message = error.message;
const match = error.message.match(/line (\d+)/);
this.line = match ? parseInt(match[1], 10) : 0;
}
}

View File

@@ -0,0 +1,18 @@
// @flow
// Note: Do not inherit from Error. It breaks when transpiling to ES5.
export default class ValidationError {
message: string;
identifier: ?string;
line: ?number;
constructor(key: ?string, value: ?{ __line__: number }, message: string, identifier: ?string) {
this.message = (key ? `${key}: ` : '') + message;
if (identifier) this.identifier = identifier;
if (value !== null && value !== undefined && value.__line__) {
this.line = value.__line__;
}
}
}

View File

@@ -0,0 +1,162 @@
// @flow
import {toString} from './types.js';
import ParsingContext from './parsing_context.js';
import EvaluationContext from './evaluation_context.js';
import assert from 'assert';
import type {Expression, ExpressionRegistry} from './expression.js';
import type {Type} from './types.js';
import type {Value} from './values.js';
export type Varargs = {| type: Type |};
type Signature = Array<Type> | Varargs;
type Evaluate = (EvaluationContext, Array<Expression>) => Value;
type Definition = [Type, Signature, Evaluate] |
{|type: Type, overloads: Array<[Signature, Evaluate]>|};
class CompoundExpression implements Expression {
name: string;
type: Type;
_evaluate: Evaluate;
args: Array<Expression>;
static definitions: {[_: string]: Definition };
constructor(name: string, type: Type, evaluate: Evaluate, args: Array<Expression>) {
this.name = name;
this.type = type;
this._evaluate = evaluate;
this.args = args;
}
evaluate(ctx: EvaluationContext): Value {
return this._evaluate(ctx, this.args);
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return false;
}
serialize(): Array<mixed> {
return [this.name].concat(this.args.map(arg => arg.serialize()));
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
const op: string = (args[0]: any);
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: ParsingContext = (null: any);
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, context.path, null, context.scope);
// First parse all the args, potentially coercing to the
// types expected by this overload.
const parsedArgs: Array<Expression> = [];
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);
}
}
assert(!signatureContext || signatureContext.errors.length > 0);
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(parsed.type));
}
context.error(`Expected arguments of type ${signatures}, but found (${actualTypes.join(', ')}) instead.`);
}
return null;
}
static register(
registry: ExpressionRegistry,
definitions: {[_: string]: Definition }
) {
assert(!CompoundExpression.definitions);
CompoundExpression.definitions = definitions;
for (const name in definitions) {
registry[name] = CompoundExpression;
}
}
}
function stringifySignature(signature: Signature): string {
if (Array.isArray(signature)) {
return `(${signature.map(toString).join(', ')})`;
} else {
return `(${toString(signature.type)}...)`;
}
}
export default CompoundExpression;

View File

@@ -0,0 +1,130 @@
// @flow
import assert from 'assert';
import {
ObjectType,
ValueType,
StringType,
NumberType,
BooleanType,
checkSubtype,
toString,
array
} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
const types = {
string: StringType,
number: NumberType,
boolean: BooleanType,
object: ObjectType
};
class Assertion implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length < 2)
return context.error(`Expected at least one argument.`);
let i = 1;
let type;
const name: string = (args[0]: any);
if (name === 'array') {
let itemType;
if (args.length > 2) {
const type = args[1];
if (typeof type !== 'string' || !(type in types) || type === 'object')
return context.error('The item type argument of "array" must be one of string, number, boolean', 1);
itemType = types[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(itemType, N);
} else {
assert(types[name], name);
type = types[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: EvaluationContext): any | null {
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(this.type)}, but found ${toString(typeOf(value))} instead.`);
}
}
assert(false);
return null;
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
const type = this.type;
const serialized = [type.kind];
if (type.kind === 'array') {
const itemType = type.itemType;
if (itemType.kind === 'string' ||
itemType.kind === 'number' ||
itemType.kind === 'boolean') {
serialized.push(itemType.kind);
const N = type.N;
if (typeof N === 'number' || this.args.length > 1) {
serialized.push(N);
}
}
}
return serialized.concat(this.args.map(arg => arg.serialize()));
}
}
export default Assertion;

View File

@@ -0,0 +1,70 @@
// @flow
import {array, ValueType, NumberType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type, ArrayType} from '../types.js';
import type {Value} from '../values.js';
class At implements Expression {
type: Type;
index: Expression;
input: Expression;
constructor(type: Type, index: Expression, input: Expression) {
this.type = type;
this.index = index;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?At {
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(context.expectedType || ValueType));
if (!index || !input) return null;
const t: ArrayType = (input.type: any);
return new At(t.itemType, index, input);
}
evaluate(ctx: EvaluationContext): Value {
const index = ((this.index.evaluate(ctx): any): number);
const array = ((this.input.evaluate(ctx): any): Array<Value>);
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: (_: Expression) => void) {
fn(this.index);
fn(this.input);
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
return ["at", this.index.serialize(), this.input.serialize()];
}
}
export default At;

View File

@@ -0,0 +1,85 @@
// @flow
import assert from 'assert';
import {BooleanType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
type Branches = Array<[Expression, Expression]>;
class Case implements Expression {
type: Type;
branches: Branches;
otherwise: Expression;
constructor(type: Type, branches: Branches, otherwise: Expression) {
this.type = type;
this.branches = branches;
this.otherwise = otherwise;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Case {
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: ?Type;
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;
assert(outputType);
return new Case((outputType: any), branches, otherwise);
}
evaluate(ctx: EvaluationContext): any {
for (const [test, expression] of this.branches) {
if (test.evaluate(ctx)) {
return expression.evaluate(ctx);
}
}
return this.otherwise.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
for (const [test, expression] of this.branches) {
fn(test);
fn(expression);
}
fn(this.otherwise);
}
outputDefined(): boolean {
return this.branches.every(([_, out]) => out.outputDefined()) && this.otherwise.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["case"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Case;

View File

@@ -0,0 +1,95 @@
// @flow
import assert from 'assert';
import {checkSubtype, ValueType} from '../types.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Coalesce implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Coalesce {
if (args.length < 2) {
return context.error("Expectected at least one argument.");
}
let outputType: Type = (null: any);
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);
}
assert(outputType);
// 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: any), parsedArgs);
}
evaluate(ctx: EvaluationContext): any | null {
let result = null;
let argCount = 0;
let firstImage;
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 so styleimagemissing can fire
if (result && result instanceof ResolvedImage && !result.available) {
// set to first image
if (!firstImage) {
firstImage = result;
}
result = null;
// if we reach the end, return the first image
if (argCount === this.args.length) {
return firstImage;
}
}
if (result !== null) break;
}
return result;
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
const serialized = ["coalesce"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Coalesce;

View File

@@ -0,0 +1,133 @@
// @flow
import assert from 'assert';
import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types.js';
import {Color, toString as valueToString, validateRGBA} from '../values.js';
import RuntimeError from '../runtime_error.js';
import Formatted from '../types/formatted.js';
import FormatExpression from '../definitions/format.js';
import ImageExpression from '../definitions/image.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
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 implements Expression {
type: Type;
args: Array<Expression>;
constructor(type: Type, args: Array<Expression>) {
this.type = type;
this.args = args;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length < 2)
return context.error(`Expected at least one argument.`);
const name: string = (args[0]: any);
assert(types[name], name);
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: EvaluationContext): null | boolean | number | string | Color | Formatted | ResolvedImage {
if (this.type.kind === 'boolean') {
return Boolean(this.args[0].evaluate(ctx));
} else if (this.type.kind === '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]: any) / 255, (input[1]: any) / 255, (input[2]: any) / 255, (input[3]: any));
}
}
}
throw new RuntimeError(error || `Could not parse color from value '${typeof input === 'string' ? input : String(JSON.stringify(input))}'`);
} else if (this.type.kind === '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.`);
} else if (this.type.kind === 'formatted') {
// There is no explicit 'to-formatted' but this coercion can be implicitly
// created by properties that expect the 'formatted' type.
return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
} else if (this.type.kind === 'resolvedImage') {
return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
} else {
return valueToString(this.args[0].evaluate(ctx));
}
}
eachChild(fn: (_: Expression) => void) {
this.args.forEach(fn);
}
outputDefined(): boolean {
return this.args.every(arg => arg.outputDefined());
}
serialize(): SerializedExpression {
if (this.type.kind === 'formatted') {
return new FormatExpression([{content: this.args[0], scale: null, font: null, textColor: null}]).serialize();
}
if (this.type.kind === 'resolvedImage') {
return new ImageExpression(this.args[0]).serialize();
}
const serialized = [`to-${this.type.kind}`];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Coercion;

View File

@@ -0,0 +1,78 @@
// @flow
import {StringType, BooleanType, CollatorType} from '../types.js';
import Collator from '../types/collator.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
export default class CollatorExpression implements Expression {
type: Type;
caseSensitive: Expression;
diacriticSensitive: Expression;
locale: Expression | null;
constructor(caseSensitive: Expression, diacriticSensitive: Expression, locale: Expression | null) {
this.type = CollatorType;
this.locale = locale;
this.caseSensitive = caseSensitive;
this.diacriticSensitive = diacriticSensitive;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length !== 2)
return context.error(`Expected one argument.`);
const options = (args[1]: any);
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: EvaluationContext): Collator {
return new Collator(this.caseSensitive.evaluate(ctx), this.diacriticSensitive.evaluate(ctx), this.locale ? this.locale.evaluate(ctx) : null);
}
eachChild(fn: (_: Expression) => void) {
fn(this.caseSensitive);
fn(this.diacriticSensitive);
if (this.locale) {
fn(this.locale);
}
}
outputDefined(): boolean {
// 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;
}
serialize(): SerializedExpression {
const options = {};
options['case-sensitive'] = this.caseSensitive.serialize();
options['diacritic-sensitive'] = this.diacriticSensitive.serialize();
if (this.locale) {
options['locale'] = this.locale.serialize();
}
return ["collator", options];
}
}

View File

@@ -0,0 +1,184 @@
// @flow
import {toString, ValueType, BooleanType, CollatorType} from '../types.js';
import Assertion from './assertion.js';
import {typeOf} from '../values.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression, ExpressionRegistration} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
type ComparisonOperator = '==' | '!=' | '<' | '>' | '<=' | '>=' ;
function isComparableType(op: ComparisonOperator, type: 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: EvaluationContext, a: any, b: any): boolean { return a === b; }
function neq(ctx: EvaluationContext, a: any, b: any): boolean { return a !== b; }
function lt(ctx: EvaluationContext, a: any, b: any): boolean { return a < b; }
function gt(ctx: EvaluationContext, a: any, b: any): boolean { return a > b; }
function lteq(ctx: EvaluationContext, a: any, b: any): boolean { return a <= b; }
function gteq(ctx: EvaluationContext, a: any, b: any): boolean { return a >= b; }
function eqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) === 0; }
function neqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return !eqCollate(ctx, a, b, c); }
function ltCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) < 0; }
function gtCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) > 0; }
function lteqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { return c.compare(a, b) <= 0; }
function gteqCollate(ctx: EvaluationContext, a: any, b: any, c: any): boolean { 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: ComparisonOperator, compareBasic: (EvaluationContext, any, any) => boolean, compareWithCollator: (EvaluationContext, any, any, any) => boolean): ExpressionRegistration {
const isOrderComparison = op !== '==' && op !== '!=';
return class Comparison implements Expression {
type: Type;
lhs: Expression;
rhs: Expression;
collator: ?Expression;
hasUntypedArgument: boolean;
constructor(lhs: Expression, rhs: Expression, collator: ?Expression) {
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: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
if (args.length !== 3 && args.length !== 4)
return context.error(`Expected two or three arguments.`);
const op: ComparisonOperator = (args[0]: any);
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(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(rhs.type)}'.`);
}
if (
lhs.type.kind !== rhs.type.kind &&
lhs.type.kind !== 'value' &&
rhs.type.kind !== 'value'
) {
return context.error(`Cannot compare types '${toString(lhs.type)}' and '${toString(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: EvaluationContext): boolean {
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: (_: Expression) => void) {
fn(this.lhs);
fn(this.rhs);
if (this.collator) {
fn(this.collator);
}
}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
const serialized = [op];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
};
}
export const Equals: $Call<typeof makeComparison, '==', typeof eq, typeof eqCollate> = makeComparison('==', eq, eqCollate);
export const NotEquals: $Call<typeof makeComparison, '!=', typeof neq, typeof neqCollate> = makeComparison('!=', neq, neqCollate);
export const LessThan: $Call<typeof makeComparison, '<', typeof lt, typeof ltCollate> = makeComparison('<', lt, ltCollate);
export const GreaterThan: $Call<typeof makeComparison, '>', typeof gt, typeof gtCollate> = makeComparison('>', gt, gtCollate);
export const LessThanOrEqual: $Call<typeof makeComparison, '<=', typeof lteq, typeof lteqCollate> = makeComparison('<=', lteq, lteqCollate);
export const GreaterThanOrEqual: $Call<typeof makeComparison, '>=', typeof gteq, typeof gteqCollate> = makeComparison('>=', gteq, gteqCollate);

View File

@@ -0,0 +1,144 @@
// @flow
import {NumberType, ValueType, FormattedType, array, StringType, ColorType, ResolvedImageType} from '../types.js';
import Formatted, {FormattedSection} from '../types/formatted.js';
import {toString, typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
type FormattedSectionExpression = {
// Content of a section may be Image expression or other
// type of expression that is coercable to 'string'.
content: Expression,
scale: Expression | null;
font: Expression | null;
textColor: Expression | null;
}
export default class FormatExpression implements Expression {
type: Type;
sections: Array<FormattedSectionExpression>;
constructor(sections: Array<FormattedSectionExpression>) {
this.type = FormattedType;
this.sections = sections;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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: Array<FormattedSectionExpression> = [];
let nextTokenMayBeObject = false;
for (let i = 1; i <= args.length - 1; ++i) {
const arg = (args[i]: any);
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(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: EvaluationContext): Formatted {
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: (_: Expression) => void) {
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(): boolean {
// Technically the combinatoric set of all children
// Usually, this.text will be undefined anyway
return false;
}
serialize(): SerializedExpression {
const serialized = ["format"];
for (const section of this.sections) {
serialized.push(section.content.serialize());
const options = {};
if (section.scale) {
options['font-scale'] = section.scale.serialize();
}
if (section.font) {
options['text-font'] = section.font.serialize();
}
if (section.textColor) {
options['text-color'] = section.textColor.serialize();
}
serialized.push(options);
}
return serialized;
}
}

View File

@@ -0,0 +1,52 @@
// @flow
import {ResolvedImageType, StringType} from '../types.js';
import ResolvedImage from '../types/resolved_image.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
export default class ImageExpression implements Expression {
type: Type;
input: Expression;
constructor(input: Expression) {
this.type = ResolvedImageType;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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: EvaluationContext): null | ResolvedImage {
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: (_: Expression) => void) {
fn(this.input);
}
outputDefined(): boolean {
// The output of image is determined by the list of available images in the evaluation context
return false;
}
serialize(): SerializedExpression {
return ["image", this.input.serialize()];
}
}

View File

@@ -0,0 +1,72 @@
// @flow
import {BooleanType, StringType, ValueType, NullType, toString, NumberType, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class In implements Expression {
type: Type;
needle: Expression;
haystack: Expression;
constructor(needle: Expression, haystack: Expression) {
this.type = BooleanType;
this.needle = needle;
this.haystack = haystack;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?In {
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(needle.type)} instead`);
}
return new In(needle, haystack);
}
evaluate(ctx: EvaluationContext): boolean {
const needle = (this.needle.evaluate(ctx): any);
const haystack = (this.haystack.evaluate(ctx): any);
if (haystack == null) 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(typeOf(needle))} instead.`);
}
if (!isValidNativeType(haystack, ['string', 'array'])) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
}
return haystack.indexOf(needle) >= 0;
}
eachChild(fn: (_: Expression) => void) {
fn(this.needle);
fn(this.haystack);
}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
return ["in", this.needle.serialize(), this.haystack.serialize()];
}
}
export default In;

View File

@@ -0,0 +1,580 @@
// @flow
import {
type Type,
NumberType,
StringType,
BooleanType,
ColorType,
ObjectType,
ValueType,
ErrorType,
CollatorType,
array,
toString as typeToString
} from '../types.js';
import {typeOf, Color, validateRGBA, toString as valueToString} from '../values.js';
import CompoundExpression from '../compound_expression.js';
import RuntimeError from '../runtime_error.js';
import Let from './let.js';
import Var from './var.js';
import Literal from './literal.js';
import Assertion from './assertion.js';
import Coercion from './coercion.js';
import At from './at.js';
import In from './in.js';
import IndexOf from './index_of.js';
import Match from './match.js';
import Case from './case.js';
import Slice from './slice.js';
import Step from './step.js';
import Interpolate from './interpolate.js';
import Coalesce from './coalesce.js';
import {
Equals,
NotEquals,
LessThan,
GreaterThan,
LessThanOrEqual,
GreaterThanOrEqual
} from './comparison.js';
import CollatorExpression from './collator.js';
import NumberFormat from './number_format.js';
import FormatExpression from './format.js';
import ImageExpression from './image.js';
import Length from './length.js';
import Within from './within.js';
import type {Varargs} from '../compound_expression.js';
import type {ExpressionRegistry} from '../expression.js';
const expressions: ExpressionRegistry = {
// 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
};
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 * alpha, g / 255 * alpha, b / 255 * alpha, alpha);
}
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: Type): Varargs {
return {type};
}
CompoundExpression.register(expressions, {
'error': [
ErrorType,
[StringType],
(ctx, [v]) => { throw new RuntimeError(v.evaluate(ctx)); }
],
'typeof': [
StringType,
[ValueType],
(ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))
],
'to-rgba': [
array(NumberType, 4),
[ColorType],
(ctx, [v]) => {
return v.evaluate(ctx).toArray();
}
],
'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
],
'pitch': [
NumberType,
[],
(ctx) => ctx.globals.pitch || 0
],
'distance-from-center': [
NumberType,
[],
(ctx) => ctx.distanceFromCenter()
],
'heatmap-density': [
NumberType,
[],
(ctx) => ctx.globals.heatmapDensity || 0
],
'line-progress': [
NumberType,
[],
(ctx) => ctx.globals.lineProgress || 0
],
'sky-radial-progress': [
NumberType,
[],
(ctx) => ctx.globals.skyRadialProgress || 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: any).value] === (v: any).value
],
'filter-id-==': [
BooleanType,
[ValueType],
(ctx, [v]) => ctx.id() === (v: any).value
],
'filter-type-==': [
BooleanType,
[StringType],
(ctx, [v]) => ctx.geometryType() === (v: any).value
],
'filter-<': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a < b;
}
],
'filter-id-<': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a < b;
}
],
'filter->': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a > b;
}
],
'filter-id->': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a > b;
}
],
'filter-<=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a <= b;
}
],
'filter-id-<=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a <= b;
}
],
'filter->=': [
BooleanType,
[StringType, ValueType],
(ctx, [k, v]) => {
const a = ctx.properties()[(k: any).value];
const b = (v: any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-id->=': [
BooleanType,
[ValueType],
(ctx, [v]) => {
const a = ctx.id();
const b = (v: any).value;
return typeof a === typeof b && a >= b;
}
],
'filter-has': [
BooleanType,
[ValueType],
(ctx, [k]) => (k: any).value in ctx.properties()
],
'filter-has-id': [
BooleanType,
[],
(ctx) => (ctx.id() !== null && ctx.id() !== undefined)
],
'filter-type-in': [
BooleanType,
[array(StringType)],
(ctx, [v]) => (v: any).value.indexOf(ctx.geometryType()) >= 0
],
'filter-id-in': [
BooleanType,
[array(ValueType)],
(ctx, [v]) => (v: any).value.indexOf(ctx.id()) >= 0
],
'filter-in-small': [
BooleanType,
[StringType, array(ValueType)],
// assumes v is an array literal
(ctx, [k, v]) => (v: any).value.indexOf(ctx.properties()[(k: any).value]) >= 0
],
'filter-in-large': [
BooleanType,
[StringType, array(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: any).value], (v: any).value, 0, (v: any).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 => valueToString(arg.evaluate(ctx))).join('')
],
'resolved-locale': [
StringType,
[CollatorType],
(ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()
]
});
export default expressions;

View File

@@ -0,0 +1,89 @@
// @flow
import {BooleanType, StringType, ValueType, NullType, toString, NumberType, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class IndexOf implements Expression {
type: Type;
needle: Expression;
haystack: Expression;
fromIndex: ?Expression;
constructor(needle: Expression, haystack: Expression, fromIndex?: Expression) {
this.type = NumberType;
this.needle = needle;
this.haystack = haystack;
this.fromIndex = fromIndex;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?IndexOf {
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(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: EvaluationContext): any {
const needle = (this.needle.evaluate(ctx): any);
const haystack = (this.haystack.evaluate(ctx): any);
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(typeOf(needle))} instead.`);
}
if (!isValidNativeType(haystack, ['string', 'array'])) {
throw new RuntimeError(`Expected second argument to be of type array or string, but found ${toString(typeOf(haystack))} instead.`);
}
if (this.fromIndex) {
const fromIndex = (this.fromIndex.evaluate(ctx): number);
return haystack.indexOf(needle, fromIndex);
}
return haystack.indexOf(needle);
}
eachChild(fn: (_: Expression) => void) {
fn(this.needle);
fn(this.haystack);
if (this.fromIndex) {
fn(this.fromIndex);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
if (this.fromIndex != null && this.fromIndex !== undefined) {
const fromIndex = this.fromIndex.serialize();
return ["index-of", this.needle.serialize(), this.haystack.serialize(), fromIndex];
}
return ["index-of", this.needle.serialize(), this.haystack.serialize()];
}
}
export default IndexOf;

View File

@@ -0,0 +1,268 @@
// @flow
import UnitBezier from '@mapbox/unitbezier';
import * as interpolate from '../../util/interpolate.js';
import {toString, NumberType, ColorType} from '../types.js';
import {findStopLessThanOrEqualTo} from '../stops.js';
import {hcl, lab} from '../../util/color_spaces.js';
import Color from '../../util/color.js';
import type {Stops} from '../stops.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
export type InterpolationType =
{ name: 'linear' } |
{ name: 'exponential', base: number } |
{ name: 'cubic-bezier', controlPoints: [number, number, number, number] };
class Interpolate implements Expression {
type: Type;
operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab';
interpolation: InterpolationType;
input: Expression;
labels: Array<number>;
outputs: Array<Expression>;
constructor(type: Type, operator: 'interpolate' | 'interpolate-hcl' | 'interpolate-lab', interpolation: InterpolationType, input: Expression, stops: 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: InterpolationType, input: number, lower: number, upper: number): number {
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(c[0], c[1], c[2], c[3]);
t = ub.solve(exponentialInterpolation(input, 1, lower, upper));
}
return t;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Interpolate {
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: any)
};
} 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: Stops = [];
let outputType: Type = (null: any);
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 (outputType.kind !== 'number' &&
outputType.kind !== 'color' &&
!(
outputType.kind === 'array' &&
outputType.itemType.kind === 'number' &&
typeof outputType.N === 'number'
)
) {
return context.error(`Type ${toString(outputType)} is not interpolatable.`);
}
return new Interpolate(outputType, (operator: any), interpolation, input, stops);
}
evaluate(ctx: EvaluationContext): Color {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = ((this.input.evaluate(ctx): any): number);
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);
if (this.operator === 'interpolate') {
return (interpolate[this.type.kind.toLowerCase()]: any)(outputLower, outputUpper, t); // eslint-disable-line import/namespace
} else if (this.operator === 'interpolate-hcl') {
return hcl.reverse(hcl.interpolate(hcl.forward(outputLower), hcl.forward(outputUpper), t));
} else {
return lab.reverse(lab.interpolate(lab.forward(outputLower), lab.forward(outputUpper), t));
}
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined());
}
serialize(): SerializedExpression {
let interpolation;
if (this.interpolation.name === 'linear') {
interpolation = ["linear"];
} else if (this.interpolation.name === 'exponential') {
if (this.interpolation.base === 1) {
interpolation = ["linear"];
} else {
interpolation = ["exponential", this.interpolation.base];
}
} else {
interpolation = ["cubic-bezier" ].concat(this.interpolation.controlPoints);
}
const serialized = [this.operator, interpolation, this.input.serialize()];
for (let i = 0; i < this.labels.length; i++) {
serialized.push(
this.labels[i],
this.outputs[i].serialize()
);
}
return serialized;
}
}
/**
* 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);
}
}
export default Interpolate;

View File

@@ -0,0 +1,61 @@
// @flow
import {NumberType, toString} from '../types.js';
import {typeOf} from '../values.js';
import RuntimeError from '../runtime_error.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Length implements Expression {
type: Type;
input: Expression;
constructor(input: Expression) {
this.type = NumberType;
this.input = input;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Length {
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(input.type)} instead.`);
return new Length(input);
}
evaluate(ctx: EvaluationContext): any | number {
const input = this.input.evaluate(ctx);
if (typeof input === 'string') {
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(typeOf(input))} instead.`);
}
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
const serialized = ["length"];
this.eachChild(child => { serialized.push(child.serialize()); });
return serialized;
}
}
export default Length;

View File

@@ -0,0 +1,72 @@
// @flow
import type {Type} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
class Let implements Expression {
type: Type;
bindings: Array<[string, Expression]>;
result: Expression;
constructor(bindings: Array<[string, Expression]>, result: Expression) {
this.type = result.type;
this.bindings = [].concat(bindings);
this.result = result;
}
evaluate(ctx: EvaluationContext): any {
return this.result.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
for (const binding of this.bindings) {
fn(binding[1]);
}
fn(this.result);
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Let {
if (args.length < 4)
return context.error(`Expected at least 3 arguments, but found ${args.length - 1} instead.`);
const bindings: Array<[string, Expression]> = [];
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(): boolean {
return this.result.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["let"];
for (const [name, expr] of this.bindings) {
serialized.push(name, expr.serialize());
}
serialized.push(this.result.serialize());
return serialized;
}
}
export default Let;

View File

@@ -0,0 +1,77 @@
// @flow
import assert from 'assert';
import {isValue, typeOf, Color} from '../values.js';
import Formatted from '../types/formatted.js';
import type {Type} from '../types.js';
import type {Value} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
class Literal implements Expression {
type: Type;
value: Value;
constructor(type: Type, value: Value) {
this.type = type;
this.value = value;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): void | Literal {
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]: any);
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(): Value {
return this.value;
}
eachChild() {}
outputDefined(): boolean {
return true;
}
serialize(): SerializedExpression {
if (this.type.kind === 'array' || this.type.kind === 'object') {
return ["literal", this.value];
} else if (this.value instanceof Color) {
// Constant-folding can generate Literal expressions that you
// couldn't actually generate with a "literal" expression,
// so we have to implement an equivalent serialization here
return ["rgba"].concat(this.value.toArray());
} else if (this.value instanceof Formatted) {
// Same as Color
return this.value.serialize();
} else {
assert(this.value === null ||
typeof this.value === 'string' ||
typeof this.value === 'number' ||
typeof this.value === 'boolean');
return (this.value: any);
}
}
}
export default Literal;

View File

@@ -0,0 +1,158 @@
// @flow
import assert from 'assert';
import {typeOf} from '../values.js';
import {ValueType, type Type} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
// Map input label values to output expression index
type Cases = {[number | string]: number};
class Match implements Expression {
type: Type;
inputType: Type;
input: Expression;
cases: Cases;
outputs: Array<Expression>;
otherwise: Expression;
constructor(inputType: Type, outputType: Type, input: Expression, cases: Cases, outputs: Array<Expression>, otherwise: Expression) {
this.inputType = inputType;
this.type = outputType;
this.input = input;
this.cases = cases;
this.outputs = outputs;
this.otherwise = otherwise;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Match {
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;
assert(inputType && outputType);
if (input.type.kind !== 'value' && context.concat(1).checkSubtype((inputType: any), input.type)) {
return null;
}
return new Match((inputType: any), (outputType: any), input, cases, outputs, otherwise);
}
evaluate(ctx: EvaluationContext): any {
const input = (this.input.evaluate(ctx): any);
const output = (typeOf(input) === this.inputType && this.outputs[this.cases[input]]) || this.otherwise;
return output.evaluate(ctx);
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
this.outputs.forEach(fn);
fn(this.otherwise);
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined()) && this.otherwise.outputDefined();
}
serialize(): SerializedExpression {
const serialized = ["match", this.input.serialize()];
// Sort so serialization has an arbitrary defined order, even though
// branch order doesn't affect evaluation
const sortedLabels = Object.keys(this.cases).sort();
// Group branches by unique match expression to support condensed
// serializations of the form [case1, case2, ...] -> matchExpression
const groupedByOutput: Array<[number, Array<number | string>]> = [];
const outputLookup: {[index: number]: number} = {}; // lookup index into groupedByOutput for a given output expression
for (const label of sortedLabels) {
const outputIndex = outputLookup[this.cases[label]];
if (outputIndex === undefined) {
// First time seeing this output, add it to the end of the grouped list
outputLookup[this.cases[label]] = groupedByOutput.length;
groupedByOutput.push([this.cases[label], [label]]);
} else {
// We've seen this expression before, add the label to that output's group
groupedByOutput[outputIndex][1].push(label);
}
}
const coerceLabel = (label) => this.inputType.kind === 'number' ? Number(label) : label;
for (const [outputIndex, labels] of groupedByOutput) {
if (labels.length === 1) {
// Only a single label matches this output expression
serialized.push(coerceLabel(labels[0]));
} else {
// Array of literal labels pointing to this output expression
serialized.push(labels.map(coerceLabel));
}
serialized.push(this.outputs[outputIndex].serialize());
}
serialized.push(this.otherwise.serialize());
return serialized;
}
}
export default Match;

View File

@@ -0,0 +1,162 @@
// @flow
import {StringType, NumberType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type EvaluationContext from '../evaluation_context.js';
import type ParsingContext from '../parsing_context.js';
import type {Type} from '../types.js';
declare var Intl: {
NumberFormat: Class<Intl$NumberFormat>
};
declare class Intl$NumberFormat {
constructor (
locales?: string | string[],
options?: NumberFormatOptions
): Intl$NumberFormat;
static (
locales?: string | string[],
options?: NumberFormatOptions
): Intl$NumberFormat;
format(a: number): string;
resolvedOptions(): any;
}
type NumberFormatOptions = {
style?: 'decimal' | 'currency' | 'percent' | 'unit';
currency?: null | string;
unit?: null | string;
minimumFractionDigits?: null | string;
maximumFractionDigits?: null | string;
};
export default class NumberFormat implements Expression {
type: Type;
number: Expression;
locale: Expression | null; // BCP 47 language tag
currency: Expression | null; // ISO 4217 currency code, required if style=currency
unit: Expression | null; // Simple units sanctioned for use in ECMAScript, required if style=unit. https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier
minFractionDigits: Expression | null; // Default 0
maxFractionDigits: Expression | null; // Default 3
constructor(number: Expression,
locale: Expression | null,
currency: Expression | null,
unit: Expression | null,
minFractionDigits: Expression | null,
maxFractionDigits: Expression | null) {
this.type = StringType;
this.number = number;
this.locale = locale;
this.currency = currency;
this.unit = unit;
this.minFractionDigits = minFractionDigits;
this.maxFractionDigits = maxFractionDigits;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Expression {
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]: any);
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 unit = null;
if (options['unit']) {
unit = context.parse(options['unit'], 1, StringType);
if (!unit) 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, unit, minFractionDigits, maxFractionDigits);
}
evaluate(ctx: EvaluationContext): string {
return new Intl.NumberFormat(this.locale ? this.locale.evaluate(ctx) : [],
{
style:
(this.currency && "currency") ||
(this.unit && "unit") ||
"decimal",
currency: this.currency ? this.currency.evaluate(ctx) : undefined,
unit: this.unit ? this.unit.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: (_: Expression) => void) {
fn(this.number);
if (this.locale) {
fn(this.locale);
}
if (this.currency) {
fn(this.currency);
}
if (this.unit) {
fn(this.unit);
}
if (this.minFractionDigits) {
fn(this.minFractionDigits);
}
if (this.maxFractionDigits) {
fn(this.maxFractionDigits);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
const options = {};
if (this.locale) {
options['locale'] = this.locale.serialize();
}
if (this.currency) {
options['currency'] = this.currency.serialize();
}
if (this.unit) {
options['unit'] = this.unit.serialize();
}
if (this.minFractionDigits) {
options['min-fraction-digits'] = this.minFractionDigits.serialize();
}
if (this.maxFractionDigits) {
options['max-fraction-digits'] = this.maxFractionDigits.serialize();
}
return ["number-format", this.number.serialize(), options];
}
}

View File

@@ -0,0 +1,86 @@
// @flow
import {ValueType, NumberType, StringType, array, toString, isValidType, isValidNativeType} from '../types.js';
import RuntimeError from '../runtime_error.js';
import {typeOf} from '../values.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Slice implements Expression {
type: Type;
input: Expression;
beginIndex: Expression;
endIndex: ?Expression;
constructor(type: Type, input: Expression, beginIndex: Expression, endIndex?: Expression) {
this.type = type;
this.input = input;
this.beginIndex = beginIndex;
this.endIndex = endIndex;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Slice {
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(ValueType), StringType, ValueType])) {
return context.error(`Expected first argument to be of type array or string, but found ${toString(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: EvaluationContext): any {
const input = (this.input.evaluate(ctx): any);
const beginIndex = (this.beginIndex.evaluate(ctx): number);
if (!isValidNativeType(input, ['string', 'array'])) {
throw new RuntimeError(`Expected first argument to be of type array or string, but found ${toString(typeOf(input))} instead.`);
}
if (this.endIndex) {
const endIndex = (this.endIndex.evaluate(ctx): number);
return input.slice(beginIndex, endIndex);
}
return input.slice(beginIndex);
}
eachChild(fn: (_: Expression) => void) {
fn(this.input);
fn(this.beginIndex);
if (this.endIndex) {
fn(this.endIndex);
}
}
outputDefined(): boolean {
return false;
}
serialize(): SerializedExpression {
if (this.endIndex != null && this.endIndex !== undefined) {
const endIndex = this.endIndex.serialize();
return ["slice", this.input.serialize(), this.beginIndex.serialize(), endIndex];
}
return ["slice", this.input.serialize(), this.beginIndex.serialize()];
}
}
export default Slice;

View File

@@ -0,0 +1,120 @@
// @flow
import {NumberType} from '../types.js';
import {findStopLessThanOrEqualTo} from '../stops.js';
import type {Stops} from '../stops.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {Type} from '../types.js';
class Step implements Expression {
type: Type;
input: Expression;
labels: Array<number>;
outputs: Array<Expression>;
constructor(type: Type, input: Expression, stops: 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: $ReadOnlyArray<mixed>, context: ParsingContext): ?Step {
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: Stops = [];
let outputType: Type = (null: any);
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: EvaluationContext): any {
const labels = this.labels;
const outputs = this.outputs;
if (labels.length === 1) {
return outputs[0].evaluate(ctx);
}
const value = ((this.input.evaluate(ctx): any): number);
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: (_: Expression) => void) {
fn(this.input);
for (const expression of this.outputs) {
fn(expression);
}
}
outputDefined(): boolean {
return this.outputs.every(out => out.outputDefined());
}
serialize(): SerializedExpression {
const serialized = ["step", this.input.serialize()];
for (let i = 0; i < this.labels.length; i++) {
if (i > 0) {
serialized.push(this.labels[i]);
}
serialized.push(this.outputs[i].serialize());
}
return serialized;
}
}
export default Step;

View File

@@ -0,0 +1,46 @@
// @flow
import type {Type} from '../types.js';
import type {Expression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
class Var implements Expression {
type: Type;
name: string;
boundExpression: Expression;
constructor(name: string, boundExpression: Expression) {
this.type = boundExpression.type;
this.name = name;
this.boundExpression = boundExpression;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): void | Var {
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: EvaluationContext): any {
return this.boundExpression.evaluate(ctx);
}
eachChild() {}
outputDefined(): boolean {
return false;
}
serialize(): Array<string> {
return ["var", this.name];
}
}
export default Var;

View File

@@ -0,0 +1,349 @@
// @flow
import {isValue} from '../values.js';
import type {Type} from '../types.js';
import {BooleanType} from '../types.js';
import type {Expression, SerializedExpression} from '../expression.js';
import type ParsingContext from '../parsing_context.js';
import type EvaluationContext from '../evaluation_context.js';
import type {GeoJSON, GeoJSONPolygon, GeoJSONMultiPolygon} from '@mapbox/geojson-types';
import type {CanonicalTileID} from '../../../source/tile_id.js';
type GeoJSONPolygons =| GeoJSONPolygon | GeoJSONMultiPolygon;
// minX, minY, maxX, maxY
type BBox = [number, number, number, number];
const EXTENT = 8192;
function updateBBox(bbox: BBox, coord: [number, number]) {
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 mercatorXfromLng(lng: number) {
return (180 + lng) / 360;
}
function mercatorYfromLat(lat: number) {
return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
}
function boxWithinBox(bbox1: BBox, bbox2: BBox) {
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 getTileCoordinates(p, canonical: CanonicalTileID) {
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 onBoundary(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);
}
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]);
}
// ray casting algorithm for detecting if point is in polygon
function pointWithinPolygon(point, rings) {
let inside = false;
for (let i = 0, len = rings.length; i < len; i++) {
const ring = rings[i];
for (let j = 0, len2 = ring.length; j < len2 - 1; j++) {
if (onBoundary(point, ring[j], ring[j + 1])) return false;
if (rayIntersect(point, ring[j], ring[j + 1])) inside = !inside;
}
}
return inside;
}
function pointWithinPolygons(point, polygons) {
for (let i = 0; i < polygons.length; i++) {
if (pointWithinPolygon(point, polygons[i])) 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;
}
// a, b are end points for line segment1, c and d are end points for line segment2
function lineIntersectLine(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 (lineIntersectLine(p1, p2, ring[j], ring[j + 1])) {
return true;
}
}
}
return false;
}
function lineStringWithinPolygon(line, polygon) {
// First, check if geometry points of line segments are all inside polygon
for (let i = 0; i < line.length; ++i) {
if (!pointWithinPolygon(line[i], 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 (let i = 0; i < polygons.length; i++) {
if (lineStringWithinPolygon(line, polygons[i])) return true;
}
return false;
}
function getTilePolygon(coordinates, bbox: BBox, canonical: CanonicalTileID) {
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: CanonicalTileID) {
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: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tilePoints = [];
if (!geometry) return 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: CanonicalTileID) {
const worldSize = Math.pow(2, canonical.z) * EXTENT;
const shifts = [canonical.x * EXTENT, canonical.y * EXTENT];
const tileLines = [];
if (!geometry) return 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: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const pointBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
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: EvaluationContext, polygonGeometry: GeoJSONPolygons) {
const lineBBox = [Infinity, Infinity, -Infinity, -Infinity];
const polyBBox = [Infinity, Infinity, -Infinity, -Infinity];
const canonical = ctx.canonicalID();
if (!canonical) {
return false;
}
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 implements Expression {
type: Type;
geojson: GeoJSON
geometries: GeoJSONPolygons;
constructor(geojson: GeoJSON, geometries: GeoJSONPolygons) {
this.type = BooleanType;
this.geojson = geojson;
this.geometries = geometries;
}
static parse(args: $ReadOnlyArray<mixed>, context: ParsingContext): ?Within {
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]: Object);
if (geojson.type === 'FeatureCollection') {
for (let i = 0; i < geojson.features.length; ++i) {
const type = geojson.features[i].geometry.type;
if (type === 'Polygon' || type === 'MultiPolygon') {
return new Within(geojson, geojson.features[i].geometry);
}
}
} 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: EvaluationContext): boolean {
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(): boolean {
return true;
}
serialize(): SerializedExpression {
return ["within", this.geojson];
}
}
export default Within;

View File

@@ -0,0 +1,89 @@
// @flow
import {Color} from './values.js';
import type Point from '@mapbox/point-geometry';
import type {FormattedSection} from './types/formatted.js';
import type {GlobalProperties, Feature, FeatureState} from './index.js';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {FeatureDistanceData} from '../feature_filter/index.js';
const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
class EvaluationContext {
globals: GlobalProperties;
feature: ?Feature;
featureState: ?FeatureState;
formattedSection: ?FormattedSection;
availableImages: ?Array<string>;
canonical: null | CanonicalTileID;
featureTileCoord: ?Point;
featureDistanceData: ?FeatureDistanceData;
_parseColorCache: {[_: string]: ?Color};
constructor() {
this.globals = (null: any);
this.feature = null;
this.featureState = null;
this.formattedSection = null;
this._parseColorCache = {};
this.availableImages = null;
this.canonical = null;
this.featureTileCoord = null;
this.featureDistanceData = null;
}
id(): number | null {
return this.feature && this.feature.id !== undefined ? this.feature.id : null;
}
geometryType(): null | string {
return this.feature ? typeof this.feature.type === 'number' ? geometryTypes[this.feature.type] : this.feature.type : null;
}
geometry(): ?Array<Array<Point>> {
return this.feature && 'geometry' in this.feature ? this.feature.geometry : null;
}
canonicalID(): null | CanonicalTileID {
return this.canonical;
}
properties(): {[string]: any} {
return (this.feature && this.feature.properties) || {};
}
distanceFromCenter(): number {
if (this.featureTileCoord && this.featureDistanceData) {
const c = this.featureDistanceData.center;
const scale = this.featureDistanceData.scale;
const {x, y} = this.featureTileCoord;
// Calculate the distance vector `d` (left handed)
const dX = x * scale - c[0];
const dY = y * scale - c[1];
// The bearing vector `b` (left handed)
const bX = this.featureDistanceData.bearing[0];
const bY = this.featureDistanceData.bearing[1];
// Distance is calculated as `dot(d, v)`
const dist = (bX * dX + bY * dY);
return dist;
}
return 0;
}
parseColor(input: string): ?Color {
let cached = this._parseColorCache[input];
if (!cached) {
cached = this._parseColorCache[input] = Color.parse(input);
}
return cached;
}
}
export default EvaluationContext;

View File

@@ -0,0 +1,27 @@
// @flow
import type {Type} from './types.js';
import type ParsingContext from './parsing_context.js';
import type EvaluationContext from './evaluation_context.js';
export type SerializedExpression = Array<mixed> | Array<string> | string | number | boolean | null;
export interface Expression {
+type: Type;
evaluate(ctx: EvaluationContext): any;
eachChild(fn: Expression => void): void;
/**
* Statically analyze the expression, attempting to enumerate possible outputs. Returns
* false if the complete set of outputs is statically undecidable, otherwise true.
*/
outputDefined(): boolean;
serialize(): SerializedExpression;
}
export type ExpressionParser = (args: $ReadOnlyArray<mixed>, context: ParsingContext) => ?Expression;
export type ExpressionRegistration = Class<Expression> & { +parse: ExpressionParser };
export type ExpressionRegistry = {[_: string]: ExpressionRegistration};

View File

@@ -0,0 +1,399 @@
// @flow
import assert from 'assert';
import extend from '../util/extend.js';
import ParsingError from './parsing_error.js';
import ParsingContext from './parsing_context.js';
import EvaluationContext from './evaluation_context.js';
import CompoundExpression from './compound_expression.js';
import Step from './definitions/step.js';
import Interpolate from './definitions/interpolate.js';
import Coalesce from './definitions/coalesce.js';
import Let from './definitions/let.js';
import definitions from './definitions/index.js';
import * as isConstant from './is_constant.js';
import RuntimeError from './runtime_error.js';
import {success, error} from '../util/result.js';
import {supportsPropertyExpression, supportsZoomExpression, supportsInterpolation} from '../util/properties.js';
import type {Type, EvaluationKind} from './types.js';
import type {Value} from './values.js';
import type {Expression} from './expression.js';
import type {StylePropertySpecification} from '../style-spec.js';
import type {Result} from '../util/result.js';
import type {InterpolationType} from './definitions/interpolate.js';
import type {PropertyValueSpecification} from '../types.js';
import type {FormattedSection} from './types/formatted.js';
import type Point from '@mapbox/point-geometry';
import type {CanonicalTileID} from '../../source/tile_id.js';
import type {FeatureDistanceData} from '../feature_filter/index.js';
export type Feature = {
+type: 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon',
+id?: number | null,
+properties: {[_: string]: any},
+patterns?: {[_: string]: string},
+geometry?: Array<Array<Point>>
};
export type FeatureState = {[_: string]: any};
export type GlobalProperties = $ReadOnly<{
zoom: number,
pitch?: number,
heatmapDensity?: number,
lineProgress?: number,
skyRadialProgress?: number,
isSupportedScript?: (_: string) => boolean,
accumulated?: Value
}>;
export class StyleExpression {
expression: Expression;
_evaluator: EvaluationContext;
_defaultValue: Value;
_warningHistory: {[key: string]: boolean};
_enumValues: ?{[_: string]: any};
constructor(expression: Expression, propertySpec: ?StylePropertySpecification) {
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: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
this._evaluator.globals = globals;
this._evaluator.feature = feature;
this._evaluator.featureState = featureState;
this._evaluator.canonical = canonical || null;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection;
this._evaluator.featureTileCoord = featureTileCoord || null;
this._evaluator.featureDistanceData = featureDistanceData || null;
return this.expression.evaluate(this._evaluator);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection, featureTileCoord?: Point, featureDistanceData?: FeatureDistanceData): any {
this._evaluator.globals = globals;
this._evaluator.feature = feature || null;
this._evaluator.featureState = featureState || null;
this._evaluator.canonical = canonical || null;
this._evaluator.availableImages = availableImages || null;
this._evaluator.formattedSection = formattedSection || null;
this._evaluator.featureTileCoord = featureTileCoord || null;
this._evaluator.featureDistanceData = featureDistanceData || 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;
}
}
}
export function isExpression(expression: mixed): boolean {
return Array.isArray(expression) && expression.length > 0 &&
typeof expression[0] === 'string' && expression[0] in definitions;
}
/**
* 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
*/
export function createExpression(expression: mixed, propertySpec: ?StylePropertySpecification): Result<StyleExpression, Array<ParsingError>> {
const parser = new ParsingContext(definitions, [], 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) {
assert(parser.errors.length > 0);
return error(parser.errors);
}
return success(new StyleExpression(parsed, propertySpec));
}
export class ZoomConstantExpression<Kind: EvaluationKind> {
kind: Kind;
isStateDependent: boolean;
_styleExpression: StyleExpression;
constructor(kind: Kind, expression: StyleExpression) {
this.kind = kind;
this._styleExpression = expression;
this.isStateDependent = kind !== ('constant': EvaluationKind) && !isConstant.isStateConstant(expression.expression);
}
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
}
export class ZoomDependentExpression<Kind: EvaluationKind> {
kind: Kind;
zoomStops: Array<number>;
isStateDependent: boolean;
_styleExpression: StyleExpression;
interpolationType: ?InterpolationType;
constructor(kind: Kind, expression: StyleExpression, zoomStops: Array<number>, interpolationType?: InterpolationType) {
this.kind = kind;
this.zoomStops = zoomStops;
this._styleExpression = expression;
this.isStateDependent = kind !== ('camera': EvaluationKind) && !isConstant.isStateConstant(expression.expression);
this.interpolationType = interpolationType;
}
evaluateWithoutErrorHandling(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
}
evaluate(globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection): any {
return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
}
interpolationFactor(input: number, lower: number, upper: number): number {
if (this.interpolationType) {
return Interpolate.interpolationFactor(this.interpolationType, input, lower, upper);
} else {
return 0;
}
}
}
export type ConstantExpression = {
kind: 'constant',
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>) => any,
}
export type SourceExpression = {
kind: 'source',
isStateDependent: boolean,
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection) => any,
};
export type CameraExpression = {
kind: 'camera',
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>) => any,
+interpolationFactor: (input: number, lower: number, upper: number) => number,
zoomStops: Array<number>,
interpolationType: ?InterpolationType
};
export type CompositeExpression = {
kind: 'composite',
isStateDependent: boolean,
+evaluate: (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: CanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection) => any,
+interpolationFactor: (input: number, lower: number, upper: number) => number,
zoomStops: Array<number>,
interpolationType: ?InterpolationType
};
export type StylePropertyExpression =
| ConstantExpression
| SourceExpression
| CameraExpression
| CompositeExpression;
export function createPropertyExpression(expression: mixed, propertySpec: StylePropertySpecification): Result<StylePropertyExpression, Array<ParsingError>> {
expression = createExpression(expression, propertySpec);
if (expression.result === 'error') {
return expression;
}
const parsed = expression.value.expression;
const isFeatureConstant = isConstant.isFeatureConstant(parsed);
if (!isFeatureConstant && !supportsPropertyExpression(propertySpec)) {
return error([new ParsingError('', 'data expressions not supported')]);
}
const isZoomConstant = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'pitch', 'distance-from-center']);
if (!isZoomConstant && !supportsZoomExpression(propertySpec)) {
return error([new ParsingError('', 'zoom expressions not supported')]);
}
const zoomCurve = findZoomCurve(parsed);
if (!zoomCurve && !isZoomConstant) {
return error([new ParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.')]);
} else if (zoomCurve instanceof ParsingError) {
return error([zoomCurve]);
} else if (zoomCurve instanceof Interpolate && !supportsInterpolation(propertySpec)) {
return error([new ParsingError('', '"interpolate" expressions cannot be used with this property')]);
}
if (!zoomCurve) {
return success(isFeatureConstant ?
(new ZoomConstantExpression('constant', expression.value): ConstantExpression) :
(new ZoomConstantExpression('source', expression.value): SourceExpression));
}
const interpolationType = zoomCurve instanceof Interpolate ? zoomCurve.interpolation : undefined;
return success(isFeatureConstant ?
(new ZoomDependentExpression('camera', expression.value, zoomCurve.labels, interpolationType): CameraExpression) :
(new ZoomDependentExpression('composite', expression.value, zoomCurve.labels, interpolationType): CompositeExpression));
}
import {isFunction, createFunction} from '../function/index.js';
import {Color} from './values.js';
// serialization wrapper for old-style stop functions normalized to the
// expression interface
export class StylePropertyFunction<T> {
_parameters: PropertyValueSpecification<T>;
_specification: StylePropertySpecification;
kind: EvaluationKind;
evaluate: (globals: GlobalProperties, feature?: Feature) => any;
interpolationFactor: ?(input: number, lower: number, upper: number) => number;
zoomStops: ?Array<number>;
constructor(parameters: PropertyValueSpecification<T>, specification: StylePropertySpecification) {
this._parameters = parameters;
this._specification = specification;
extend(this, createFunction(this._parameters, this._specification));
}
static deserialize(serialized: {_parameters: PropertyValueSpecification<T>, _specification: StylePropertySpecification}): StylePropertyFunction<T> {
return new StylePropertyFunction(serialized._parameters, serialized._specification);
}
static serialize(input: StylePropertyFunction<T>): {_parameters: PropertyValueSpecification<T>, _specification: StylePropertySpecification} {
return {
_parameters: input._parameters,
_specification: input._specification
};
}
}
export function normalizePropertyExpression<T>(value: PropertyValueSpecification<T>, specification: StylePropertySpecification): StylePropertyExpression {
if (isFunction(value)) {
return (new StylePropertyFunction(value, specification): any);
} else if (isExpression(value)) {
const expression = createPropertyExpression(value, specification);
if (expression.result === 'error') {
// this should have been caught in validation
throw new Error(expression.value.map(err => `${err.key}: ${err.message}`).join(', '));
}
return expression.value;
} else {
let constant: any = value;
if (typeof value === 'string' && specification.type === 'color') {
constant = Color.parse(value);
}
return {
kind: 'constant',
evaluate: () => constant
};
}
}
// 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: Expression): Step | Interpolate | ParsingError | null {
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 ParsingError) {
return result;
}
expression.eachChild((child) => {
const childResult = findZoomCurve(child);
if (childResult instanceof ParsingError) {
result = childResult;
} else if (!result && childResult) {
result = new ParsingError('', '"zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.');
} else if (result && childResult && result !== childResult) {
result = new ParsingError('', 'Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.');
}
});
return result;
}
import {ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, ResolvedImageType, array} from './types.js';
function getExpectedType(spec: StylePropertySpecification): Type {
const types = {
color: ColorType,
string: StringType,
number: NumberType,
enum: StringType,
boolean: BooleanType,
formatted: FormattedType,
resolvedImage: ResolvedImageType
};
if (spec.type === 'array') {
return array(types[spec.value] || ValueType, spec.length);
}
return types[spec.type];
}
function getDefaultValue(spec: StylePropertySpecification): Value {
if (spec.type === 'color' && (isFunction(spec.default) || Array.isArray(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.default === undefined) {
return null;
} else {
return spec.default;
}
}

View File

@@ -0,0 +1,59 @@
// @flow
import CompoundExpression from './compound_expression.js';
import Within from './definitions/within.js';
import type {Expression} from './expression.js';
function isFeatureConstant(e: Expression): boolean {
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;
}
let result = true;
e.eachChild(arg => {
if (result && !isFeatureConstant(arg)) { result = false; }
});
return result;
}
function isStateConstant(e: Expression): boolean {
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: Expression, properties: Array<string>): boolean {
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;
}
export {isFeatureConstant, isGlobalPropertyConstant, isStateConstant};

View File

@@ -0,0 +1,233 @@
// @flow
import Scope from './scope.js';
import {checkSubtype} from './types.js';
import ParsingError from './parsing_error.js';
import Literal from './definitions/literal.js';
import Assertion from './definitions/assertion.js';
import Coercion from './definitions/coercion.js';
import EvaluationContext from './evaluation_context.js';
import CompoundExpression from './compound_expression.js';
import CollatorExpression from './definitions/collator.js';
import Within from './definitions/within.js';
import {isGlobalPropertyConstant, isFeatureConstant} from './is_constant.js';
import Var from './definitions/var.js';
import type {Expression, ExpressionRegistry} from './expression.js';
import type {Type} from './types.js';
/**
* State associated parsing at a given point in an expression tree.
* @private
*/
class ParsingContext {
registry: ExpressionRegistry;
path: Array<number>;
key: string;
scope: Scope;
errors: Array<ParsingError>;
// The expected type of this expression. Provided only to allow Expression
// implementations to infer argument types: Expression#parse() need not
// check that the output type of the parsed expression matches
// `expectedType`.
expectedType: ?Type;
constructor(
registry: ExpressionRegistry,
path: Array<number> = [],
expectedType: ?Type,
scope: Scope = new Scope(),
errors: Array<ParsingError> = []
) {
this.registry = registry;
this.path = path;
this.key = path.map(part => `[${part}]`).join('');
this.scope = scope;
this.errors = errors;
this.expectedType = expectedType;
}
/**
* @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: mixed,
index?: number,
expectedType?: ?Type,
bindings?: Array<[string, Expression]>,
options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'} = {}
): ?Expression {
if (index) {
return this.concat(index, expectedType, bindings)._parse(expr, options);
}
return this._parse(expr, options);
}
_parse(expr: mixed, options: {typeAnnotation?: 'assert' | 'coerce' | 'omit'}): ?Expression {
if (expr === null || typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') {
expr = ['literal', expr];
}
function annotate(parsed, type, typeAnnotation: 'assert' | 'coerce' | 'omit') {
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 (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') && 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: number, expectedType?: ?Type, bindings?: Array<[string, Expression]>): ParsingContext {
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,
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: string, ...keys: Array<number>) {
const key = `${this.key}${keys.map(k => `[${k}]`).join('')}`;
this.errors.push(new ParsingError(key, error));
}
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message and also pushes it to `this.errors`.
*/
checkSubtype(expected: Type, t: Type): ?string {
const error = checkSubtype(expected, t);
if (error) this.error(error);
return error;
}
}
export default ParsingContext;
function isConstant(expression: Expression) {
if (expression instanceof Var) {
return isConstant(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;
}
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 && isConstant(child);
} else {
childrenConstant = childrenConstant && child instanceof Literal;
}
});
if (!childrenConstant) {
return false;
}
return isFeatureConstant(expression) &&
isGlobalPropertyConstant(expression, ['zoom', 'heatmap-density', 'line-progress', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center']);
}

View File

@@ -0,0 +1,13 @@
// @flow
class ParsingError extends Error {
key: string;
message: string;
constructor(key: string, message: string) {
super(message);
this.message = message;
this.key = key;
}
}
export default ParsingError;

View File

@@ -0,0 +1,17 @@
// @flow
class RuntimeError {
name: string;
message: string;
constructor(message: string) {
this.name = 'ExpressionEvaluationError';
this.message = message;
}
toJSON(): string {
return this.message;
}
}
export default RuntimeError;

View File

@@ -0,0 +1,36 @@
// @flow
import type {Expression} from './expression.js';
/**
* Tracks `let` bindings during expression parsing.
* @private
*/
class Scope {
parent: ?Scope;
bindings: {[_: string]: Expression};
constructor(parent?: Scope, bindings: Array<[string, Expression]> = []) {
this.parent = parent;
this.bindings = {};
for (const [name, expression] of bindings) {
this.bindings[name] = expression;
}
}
concat(bindings: Array<[string, Expression]>): Scope {
return new Scope(this, bindings);
}
get(name: string): Expression {
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: string): boolean {
if (this.bindings[name]) return true;
return this.parent ? this.parent.has(name) : false;
}
}
export default Scope;

View File

@@ -0,0 +1,39 @@
// @flow
import RuntimeError from './runtime_error.js';
import type {Expression} from './expression.js';
export type Stops = Array<[number, Expression]>;
/**
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
* @private
*/
export function findStopLessThanOrEqualTo(stops: Array<number>, input: number): number {
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;
}

View File

@@ -0,0 +1,126 @@
// @flow
export type NullTypeT = { kind: 'null' };
export type NumberTypeT = { kind: 'number' };
export type StringTypeT = { kind: 'string' };
export type BooleanTypeT = { kind: 'boolean' };
export type ColorTypeT = { kind: 'color' };
export type ObjectTypeT = { kind: 'object' };
export type ValueTypeT = { kind: 'value' };
export type ErrorTypeT = { kind: 'error' };
export type CollatorTypeT = { kind: 'collator' };
export type FormattedTypeT = { kind: 'formatted' };
export type ResolvedImageTypeT = { kind: 'resolvedImage' };
export type EvaluationKind = 'constant' | 'source' | 'camera' | 'composite';
export type Type =
NullTypeT |
NumberTypeT |
StringTypeT |
BooleanTypeT |
ColorTypeT |
ObjectTypeT |
ValueTypeT |
ArrayType | // eslint-disable-line no-use-before-define
ErrorTypeT |
CollatorTypeT |
FormattedTypeT |
ResolvedImageTypeT
export type ArrayType = {
kind: 'array',
itemType: Type,
N: ?number
}
export type NativeType = 'number' | 'string' | 'boolean' | 'null' | 'array' | 'object'
export const NullType = {kind: 'null'};
export const NumberType = {kind: 'number'};
export const StringType = {kind: 'string'};
export const BooleanType = {kind: 'boolean'};
export const ColorType = {kind: 'color'};
export const ObjectType = {kind: 'object'};
export const ValueType = {kind: 'value'};
export const ErrorType = {kind: 'error'};
export const CollatorType = {kind: 'collator'};
export const FormattedType = {kind: 'formatted'};
export const ResolvedImageType = {kind: 'resolvedImage'};
export function array(itemType: Type, N: ?number): ArrayType {
return {
kind: 'array',
itemType,
N
};
}
export function toString(type: Type): string {
if (type.kind === 'array') {
const itemType = toString(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(ValueType),
ResolvedImageType
];
/**
* Returns null if `t` is a subtype of `expected`; otherwise returns an
* error message.
* @private
*/
export function checkSubtype(expected: Type, t: Type): ?string {
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(expected)} but found ${toString(t)} instead.`;
}
export function isValidType(provided: Type, allowedTypes: Array<Type>): boolean {
return allowedTypes.some(t => t.kind === provided.kind);
}
export function isValidNativeType(provided: any, allowedTypes: Array<NativeType>): boolean {
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;
}
});
}

Some files were not shown because too many files have changed in this diff Show More