├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── cli.js ├── docs ├── app.css ├── app.js ├── base-style.json ├── fmt.css ├── fmt.html ├── fmt.js └── index.html ├── examples ├── concat.expr └── concat.json ├── index.js ├── lib ├── compile.js ├── decompile.js ├── errors.js ├── parse.js ├── source-map.js ├── textblock.js ├── tokenize.js ├── transform.js ├── unparse.js ├── untokenize.js ├── untransform.js └── util.js ├── package-lock.json ├── package.json ├── scripts └── build.sh ├── site ├── app.css ├── app.js ├── base-style.json ├── fmt.css ├── fmt.html ├── fmt.js └── index.html └── test ├── index.js └── perf.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-object-rest-spread", 4 | ["transform-runtime", { 5 | "polyfill": false, 6 | "regenerator": true 7 | }] 8 | ], 9 | "presets": ["env"] 10 | } 11 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: circleci/node:8.9 7 | working_directory: ~/repo 8 | steps: 9 | - checkout 10 | # Download and cache dependencies 11 | - restore_cache: 12 | keys: 13 | - v1-dependencies-{{ checksum "package.json" }} 14 | # fallback to using the latest cache if no exact match is found 15 | - v1-dependencies- 16 | 17 | - run: npm install 18 | 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: v1-dependencies-{{ checksum "package.json" }} 23 | 24 | # run tests! 25 | - run: npm test 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /old 2 | /node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jamie Blair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-expr 2 | Simple expression language for [mapbox-gl-js expressions][mapbox-gl-expressions] 3 | 4 | [![stability-experimental](https://img.shields.io/badge/stability-experimental-orange.svg)][stability] 5 | [![Build Status](https://circleci.com/gh/orangemug/simple-expr.png?style=shield)][circleci] 6 | [![Dependency Status](https://david-dm.org/orangemug/simple-expr.svg)][dm-prod] 7 | [![Dev Dependency Status](https://david-dm.org/orangemug/simple-expr/dev-status.svg)][dm-dev] 8 | 9 | [stability]: https://github.com/orangemug/stability-badges#experimental 10 | [circleci]: https://circleci.com/gh/orangemug/simple-expr 11 | [dm-prod]: https://david-dm.org/orangemug/simple-expr 12 | [dm-dev]: https://david-dm.org/orangemug/simple-expr#info=devDependencies 13 | 14 | ‼️ This is an experimental repository, if you find it useful or interesting please leave a comment. There is a general discussion issue in [issue #1](https://github.com/orangemug/simple-expr/issues/1), leave any general thoughts there. Or [open a new issue](https://github.com/orangemug/simple-expr/issues/new) for bugs or to suggest new features. 15 | 16 | 17 | The primary aim of this language is to support [MapboxGL expressions][mapbox-gl-expressions], although it should also be generally useful. 18 | 19 | This is a tiny functional language that gets converted into [MapboxGL expressions][mapbox-gl-expressions]. The idea behind the language is that it can be converted in a lossless way between [MapboxGL expressions][mapbox-gl-expressions] and back again. 20 | 21 | 22 | Basically it's just a nicer way to write MapboxGL expressions. Here's an example, the following JSON 23 | 24 | ```json 25 | [ 26 | "interpolate", 27 | ["linear"], ["get", "score"], 28 | 0, ["rgb", 255, 0, 0], 29 | 50, ["rgb", 0, 255, 0], 30 | 100, ["rgb", 0, 0, 255] 31 | ] 32 | ``` 33 | 34 | Would be written as 35 | 36 | ``` 37 | interpolate( 38 | linear(), @score, 39 | 0, rgb(255, 0, 0), 40 | 50, rgb(0, 255, 0), 41 | 100, rgb(0, 0, 255) 42 | ) 43 | ``` 44 | 45 | **See it in action:** 46 | 47 | 48 | ## Install 49 | To install 50 | 51 | ``` 52 | npm install orangemug/simple-expr --save 53 | ``` 54 | 55 | 56 | ## Syntax 57 | You are only allowed to define a single top level expression. 58 | 59 | **Valid** 60 | ``` 61 | rgb(255, 0, 0) 62 | ``` 63 | 64 | **Invalid!** 65 | ``` 66 | rgb(255, 0, 0) 67 | rgb(0, 0, 255) 68 | ``` 69 | 70 | Although sub expressions are allowed 71 | 72 | ``` 73 | rgb(get("rank"), 0, 0) 74 | ``` 75 | 76 | 77 | ### Types 78 | There are 2 basic types 79 | 80 | 81 | #### number 82 | Any integer or decimal number not in quotes, for example 83 | 84 | ``` 85 | 1 86 | -1 87 | +3 88 | 3.14 89 | -1000 90 | -9.8 91 | ``` 92 | 93 | 94 | #### string 95 | Any characters surrounded in quotes, for example 96 | 97 | ``` 98 | "foo bar" 99 | ``` 100 | 101 | You can escape quotes with `\` for example 102 | 103 | ``` 104 | "They said \"it couldn't be done\"" 105 | ``` 106 | 107 | 108 | ### Functions 109 | Functions are defined as 110 | 111 | ``` 112 | function_name(arg, arg, arg...) 113 | ``` 114 | 115 | Note that arguments can also be functions. This gets compiled into the [MapboxGL expressions][mapbox-gl-expressions] JSON format. 116 | 117 | Lets see an example 118 | 119 | ``` 120 | rgb(get("rating"), 0, 0) 121 | ``` 122 | 123 | Will become 124 | 125 | ``` 126 | ["rgb", ["get", "rating"], 0, 0] 127 | ``` 128 | 129 | 130 | ### Feature references 131 | As well as using the `get` function ([see spec](https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-get)) there is also a shorthand to reference feature data. The following 132 | 133 | ``` 134 | rgb(@rating, 0, 0) 135 | ``` 136 | 137 | Is the same as 138 | 139 | ``` 140 | rgb(get("rating"), 0, 0) 141 | ``` 142 | 143 | 144 | ### Variables 145 | **NOTE: Not yet available** 146 | 147 | You can also define variables before the expressions. Variables are also allowed to define a single expression. A quick example 148 | 149 | ``` 150 | &r = interpolate( 151 | linear(), @score, 152 | 1, 100 153 | 22, 255 154 | ) 155 | 156 | rgb(&r, 0, 0) 157 | ``` 158 | 159 | Variables **must** start with a `&` both in there definition and their usage. 160 | 161 | 162 | ## Usage 163 | You can parse, compile to JSON. It comes in 2 forms, the CLI (command line interface) and the JavaScript API 164 | 165 | 166 | ## JavaScript API 167 | 168 | ### Compile to expression JSON 169 | ```js 170 | var simpleExpr = require("simple-expr"); 171 | var out = simpleExpr.compile('concat("hello", " ", "world")', {format: "json"}) 172 | assert.deepEqual(out, [ 173 | "concat", "hello", " ", "world" 174 | ]) 175 | ``` 176 | 177 | ### Parse to AST 178 | 179 | ```js 180 | var simpleExpr = require("simple-expr"); 181 | var tokens = simpleExpr.tokenize('*(2,2)') 182 | var ast = simpleExpr.parse(tokens); 183 | ``` 184 | 185 | 186 | ## CLI 187 | The available CLI commands are 188 | 189 | ### Parse 190 | ```bash 191 | simple-expr parse examples/concat.expr > /tmp/concat.ast 192 | ``` 193 | 194 | ### Compile 195 | ```bash 196 | simple-expr compile examples/concat.expr > /tmp/concat.json 197 | ## >>/tmp/concat.json 198 | ## [ 199 | ## "concat", 200 | ## "Hello", 201 | ## " ", 202 | ## [ 203 | ## "get", 204 | ## "name" 205 | ## ] 206 | ## ] 207 | ## << 208 | ``` 209 | 210 | ### Decompile 211 | ```bash 212 | simple-expr decompile examples/concat.json > /tmp/concat.expr 213 | ## >>/tmp/concat.expr 214 | ## concat("Hello"," ",@name) 215 | ## << 216 | ``` 217 | 218 | ### Execute 219 | ```bash 220 | simple-expr execute --feature-props name=Maputnik examples/concat.expr > /tmp/concat.log 221 | ## >>/tmp/concat.log 222 | ## Hello Maputnik 223 | ## << 224 | ``` 225 | 226 | 227 | ## License 228 | [MIT](LICENSE) 229 | 230 | [mapbox-gl-expressions]: https://www.mapbox.com/mapbox-gl-js/style-spec#expressions 231 | 232 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require("fs"); 3 | var yargs = require("yargs"); 4 | var simpleExpr = require("../"); 5 | var mgl = require("@mapbox/mapbox-gl-style-spec"); 6 | 7 | 8 | function handleError(err) { 9 | console.log(err); 10 | process.eixt(1); 11 | } 12 | 13 | function parseOpts(str) { 14 | var out = {}; 15 | str = str || ""; 16 | 17 | var items = str 18 | .split(/\s+/) 19 | .filter(function(item) { 20 | return item != ""; 21 | }) 22 | 23 | items.forEach(function(item) { 24 | var parts = item.split("=") 25 | if(parts.length !== 2) { 26 | throw "Invalid format '"+item+"' expected format FOO=bar" 27 | } 28 | out[parts[0]] = parts[1]; 29 | }); 30 | 31 | return out; 32 | } 33 | 34 | function optOrStdin(filepath) { 35 | if(filepath) { 36 | return new Promise(function(resolve, reject) { 37 | fs.readFile(filepath, function(err, data) { 38 | if(err) { 39 | reject(err) 40 | } 41 | else { 42 | try { 43 | resolve(data.toString()); 44 | } 45 | catch(err) { 46 | reject(err); 47 | } 48 | } 49 | }) 50 | }) 51 | } 52 | else { 53 | return new Promise(function(resolve, reject) { 54 | var raw = ""; 55 | process.stdin.on('data', function(data) { 56 | raw += data; 57 | }); 58 | process.stdin.on('end', function() { 59 | try { 60 | resolve(raw) 61 | } catch(err) { 62 | reject(err); 63 | } 64 | }); 65 | }) 66 | } 67 | } 68 | 69 | var argv = yargs 70 | .command( 71 | "parse", 72 | "code -> ast", 73 | function (yargs) { 74 | return yargs 75 | }, 76 | function (argv) { 77 | optOrStdin(argv._[1]) 78 | .then(function(data) { 79 | var ast = simpleExpr.parse( 80 | simpleExpr.tokenize(data) 81 | ) 82 | var json = JSON.stringify(ast, null, 2); 83 | console.log(json) 84 | process.exit(0) 85 | }) 86 | .catch(handleError) 87 | } 88 | ) 89 | .command( 90 | "compile", 91 | "code -> json", 92 | function (yargs) { 93 | return yargs 94 | }, 95 | function(argv) { 96 | optOrStdin(argv._[1]) 97 | .then(function(data) { 98 | var json = simpleExpr.compile(data) 99 | console.log(JSON.stringify(json, null, 2)) 100 | process.exit(0) 101 | }) 102 | .catch(handleError) 103 | } 104 | ) 105 | .command( 106 | "decompile", 107 | "json -> code", 108 | function (yargs) { 109 | return yargs 110 | }, 111 | function(argv) { 112 | optOrStdin(argv._[1]) 113 | .then(function(data) { 114 | var json; 115 | try { 116 | json = JSON.parse(data); 117 | } 118 | catch(err) { 119 | console.error("Invalid JSON"); 120 | console.error(err); 121 | process.exit(1); 122 | } 123 | 124 | var code = simpleExpr.decompile(json); 125 | console.log(code); 126 | process.exit(0); 127 | }) 128 | .catch(handleError) 129 | } 130 | ) 131 | .command( 132 | "execute", 133 | "execute as js function", 134 | function (yargs) { 135 | return yargs 136 | .describe("feature-props", "feature properties") 137 | .describe("feature-id", "feature id") 138 | .describe("feature-type", "feature type") 139 | .describe("globals", "globals data") 140 | }, 141 | function(argv) { 142 | var featureOpts = parseOpts(argv["feature-props"]); 143 | var globalOpts = parseOpts(argv.globals); 144 | 145 | optOrStdin(argv._[1]) 146 | .then(function(data) { 147 | var json = simpleExpr.compile(data) 148 | 149 | var out = mgl.expression.createExpression(json, {}) 150 | var result = out.value.evaluate(globalOpts, { 151 | id: argv["feature-id"], 152 | type: argv["feature-type"], 153 | properties: featureOpts 154 | }) 155 | 156 | console.log(result); 157 | process.exit(0); 158 | }) 159 | .catch(handleError) 160 | } 161 | ) 162 | .example("simple-expr compile input.expr", "compile an expression") 163 | .example("simple-expr decompile input.json", "decompile an expression") 164 | .example("simple-expr compile input.expr | mgl-exec execute --data foo=1 bar=2", "Compile and run with some data") 165 | .argv; 166 | 167 | 168 | if(argv._.length < 1) { 169 | yargs.showHelp(); 170 | } 171 | 172 | -------------------------------------------------------------------------------- /docs/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | 8 | html, body { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | overflow: hidden; 15 | } 16 | 17 | a { 18 | color: inherit; 19 | } 20 | 21 | .app { 22 | height: 100%; 23 | display: flex; 24 | flex-direction: column; 25 | overflow: hidden; 26 | } 27 | 28 | .app__header { 29 | display: flex; 30 | align-items: center; 31 | padding: 4px 6px; 32 | } 33 | 34 | .app__header__title { 35 | flex: 1; 36 | } 37 | 38 | .app__header__title h1 { 39 | font-size: 28px; 40 | margin: 0; 41 | } 42 | 43 | .app__header__subtitle { 44 | font-size: 14px; 45 | } 46 | 47 | .toolbox { 48 | padding: 0 6px; 49 | } 50 | 51 | .toolbox .icon { 52 | margin-right: 4px; 53 | } 54 | 55 | .icon { 56 | vertical-align: middle; 57 | text-decoration: none; 58 | } 59 | 60 | 61 | .app__main { 62 | flex: 1; 63 | position: relative; 64 | overflow: hidden; 65 | display: flex; 66 | flex-direction: column; 67 | } 68 | 69 | .row { 70 | flex: 1 1 50%; 71 | max-height: 50%; 72 | display: flex; 73 | } 74 | 75 | .row:nth-child(n+2) { 76 | border-top: solid 1px #555; 77 | } 78 | 79 | .cell { 80 | position: relative; 81 | display: flex; 82 | flex-direction: column; 83 | width: 50%; 84 | } 85 | 86 | .cell:nth-child(n+2) { 87 | border-left: solid 1px #555; 88 | } 89 | 90 | .cell__header { 91 | flex: 0; 92 | background: black; 93 | color: white; 94 | padding: 4px 6px; 95 | } 96 | 97 | .cell__content { 98 | flex: 1; 99 | position: relative; 100 | overflow-x: auto; 101 | } 102 | 103 | .editor { 104 | width: 100%; 105 | height: 100%; 106 | overflow-y: scroll; 107 | border: none; 108 | resize: none; 109 | font-family: monospace; 110 | font-size: 13px; 111 | } 112 | 113 | .editor__message { 114 | background: #eee; 115 | padding: 4px 6px; 116 | border-bottom: solid 1px #ddd; 117 | } 118 | 119 | .result { 120 | margin: 0; 121 | overflow-y: scroll; 122 | } 123 | 124 | .geojson { 125 | margin: 0; 126 | overflow-y: scroll; 127 | } 128 | 129 | .map { 130 | background: #333; 131 | height: 100%; 132 | } 133 | 134 | .examples { 135 | float: right; 136 | } 137 | -------------------------------------------------------------------------------- /docs/base-style.json: -------------------------------------------------------------------------------- 1 | {"version":8,"name":"Positron","metadata":{"mapbox:autocomposite":false,"mapbox:type":"template","mapbox:groups":{"b6371a3f2f5a9932464fa3867530a2e5":{"name":"Transportation","collapsed":false},"a14c9607bc7954ba1df7205bf660433f":{"name":"Boundaries"},"101da9f13b64a08fa4b6ac1168e89e5f":{"name":"Places","collapsed":false}},"openmaptiles:version":"3.x","openmaptiles:mapbox:owner":"openmaptiles","openmaptiles:mapbox:source:url":"mapbox://openmaptiles.4qljc88t"},"center":[10.184401828277089,-1.1368683772161603e-13],"zoom":0.8902641636539237,"bearing":0,"pitch":0,"sources":{"openmaptiles":{"type":"vector","url":"https://free.tilehosting.com/data/v3.json?key=ozKuiN7rRsPFArLI4gsv"}},"sprite":"https://free.tilehosting.com/styles/positron/sprite","glyphs":"https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=ozKuiN7rRsPFArLI4gsv","layers":[{"id":"background","type":"background","paint":{"background-color":"rgb(242,243,240)"}},{"id":"park","type":"fill","source":"openmaptiles","source-layer":"park","filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(230, 233, 229)"}},{"id":"water","type":"fill","source":"openmaptiles","source-layer":"water","filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(194, 200, 202)","fill-antialias":true,"fill-outline-color":{"base":1,"stops":[[0,"hsla(180, 6%, 63%, 0.82)"],[22,"hsla(180, 6%, 63%, 0.18)"]]}}},{"id":"landcover_ice_shelf","type":"fill","source":"openmaptiles","source-layer":"landcover","maxzoom":8,"filter":["all",["==","$type","Polygon"],["==","subclass","ice_shelf"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(0, 0%, 98%)","fill-opacity":0.7}},{"id":"landcover_glacier","type":"fill","source":"openmaptiles","source-layer":"landcover","maxzoom":8,"filter":["all",["==","$type","Polygon"],["==","subclass","glacier"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(0, 0%, 98%)","fill-opacity":{"base":1,"stops":[[0,1],[8,0.5]]}}},{"id":"landuse_residential","type":"fill","source":"openmaptiles","source-layer":"landuse","maxzoom":16,"filter":["all",["==","$type","Polygon"],["==","class","residential"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(234, 234, 230)","fill-opacity":{"base":0.6,"stops":[[8,0.8],[9,0.6]]}}},{"id":"landcover_wood","type":"fill","source":"openmaptiles","source-layer":"landcover","minzoom":10,"filter":["all",["==","$type","Polygon"],["==","class","wood"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(220,224,220)","fill-opacity":{"base":1,"stops":[[8,0],[12,1]]}}},{"id":"waterway","type":"line","source":"openmaptiles","source-layer":"waterway","filter":["==","$type","LineString"],"layout":{"visibility":"visible"},"paint":{"line-color":"hsl(195, 17%, 78%)"}},{"id":"water_name","type":"symbol","source":"openmaptiles","source-layer":"water_name","filter":["all",["==","$type","LineString"],["!has","name:en"]],"layout":{"text-field":"{name:latin} {name:nonlatin}","symbol-placement":"line","text-rotation-alignment":"map","symbol-spacing":500,"text-font":["Metropolis Medium Italic","Klokantech Noto Sans Italic","Klokantech Noto Sans CJK Regular"],"text-size":12},"paint":{"text-color":"rgb(157,169,177)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"water_name-en","type":"symbol","source":"openmaptiles","source-layer":"water_name","filter":["all",["==","$type","LineString"],["has","name:en"]],"layout":{"text-field":"{name:en} {name:nonlatin}","symbol-placement":"line","text-rotation-alignment":"map","symbol-spacing":500,"text-font":["Metropolis Medium Italic","Klokantech Noto Sans Italic","Klokantech Noto Sans CJK Regular"],"text-size":12},"paint":{"text-color":"rgb(157,169,177)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"building","type":"fill","source":"openmaptiles","source-layer":"building","minzoom":12,"filter":["==","$type","Polygon"],"paint":{"fill-color":"rgb(234, 234, 229)","fill-outline-color":"rgb(219, 219, 218)","fill-antialias":true}},{"id":"tunnel_motorway_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,3],[20,40]]},"line-opacity":1}},{"id":"tunnel_motorway_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(234,234,234)","line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"aeroway-taxiway","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":12,"filter":["all",["in","class","taxiway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.55,"stops":[[13,1.8],[20,20]]},"line-opacity":1}},{"id":"aeroway-runway-casing","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":11,"filter":["all",["in","class","runway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.5,"stops":[[11,6],[17,55]]},"line-opacity":1}},{"id":"aeroway-area","type":"fill","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":4,"filter":["all",["==","$type","Polygon"],["in","class","runway","taxiway"]],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[13,0],[14,1]]},"fill-color":"rgba(255, 255, 255, 1)"}},{"id":"aeroway-runway","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":11,"filter":["all",["in","class","runway"],["==","$type","LineString"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgba(255, 255, 255, 1)","line-width":{"base":1.5,"stops":[[11,4],[17,50]]},"line-opacity":1},"maxzoom":24},{"id":"highway_path","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","filter":["all",["==","$type","LineString"],["==","class","path"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(234, 234, 234)","line-width":{"base":1.2,"stops":[[13,1],[20,10]]},"line-opacity":0.9}},{"id":"highway_minor","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":8,"filter":["all",["==","$type","LineString"],["in","class","minor","service","track"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.55,"stops":[[13,1.8],[20,20]]},"line-opacity":0.9}},{"id":"highway_major_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-dasharray":[12,0],"line-width":{"base":1.3,"stops":[[10,3],[20,23]]}}},{"id":"highway_major_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.3,"stops":[[10,2],[20,20]]}}},{"id":"highway_major_subtle","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","maxzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsla(0, 0%, 85%, 0.69)","line-width":2}},{"id":"highway_motorway_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["!in","brunnel","bridge","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,3],[20,40]]},"line-dasharray":[2,0],"line-opacity":1}},{"id":"highway_motorway_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["!in","brunnel","bridge","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"base":1,"stops":[[5.8,"hsla(0, 0%, 85%, 0.53)"],[6,"#fff"]]},"line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"highway_motorway_subtle","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","maxzoom":6,"filter":["all",["==","$type","LineString"],["==","class","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsla(0, 0%, 85%, 0.53)","line-width":{"base":1.4,"stops":[[4,2],[6,1.3]]}}},{"id":"railway_service","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":16,"filter":["all",["==","$type","LineString"],["all",["==","class","rail"],["has","service"]]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#dddddd","line-width":3}},{"id":"railway_service_dashline","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":16,"filter":["all",["==","$type","LineString"],["==","class","rail"],["has","service"]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#fafafa","line-width":2,"line-dasharray":[3,3]}},{"id":"railway","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":13,"filter":["all",["==","$type","LineString"],["all",["!has","service"],["==","class","rail"]]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#dddddd","line-width":{"base":1.3,"stops":[[16,3],[20,7]]}}},{"id":"railway_dashline","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"paint":{"line-color":"#fafafa","line-width":{"base":1.3,"stops":[[16,2],[20,6]]},"line-dasharray":[3,3]},"type":"line","source":"openmaptiles","source-layer":"transportation","minzoom":13,"filter":["all",["==","$type","LineString"],["all",["!has","service"],["==","class","rail"]]],"layout":{"visibility":"visible","line-join":"round"}},{"id":"highway_motorway_bridge_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","bridge"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,5],[20,45]]},"line-dasharray":[2,0],"line-opacity":1}},{"id":"highway_motorway_bridge_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","bridge"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"base":1,"stops":[[5.8,"hsla(0, 0%, 85%, 0.53)"],[6,"#fff"]]},"line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"highway_name_other","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["!=","class","motorway"],["==","$type","LineString"],["!has","name:en"]],"layout":{"text-size":10,"text-max-angle":30,"text-transform":"uppercase","symbol-spacing":350,"text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"map","text-pitch-alignment":"viewport","text-field":"{name:latin} {name:nonlatin}"},"paint":{"text-color":"#bbb","text-halo-color":"#fff","text-translate":[0,0],"text-halo-width":2,"text-halo-blur":1}},{"id":"highway_name_other-en","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["!=","class","motorway"],["==","$type","LineString"],["has","name:en"]],"layout":{"text-size":10,"text-max-angle":30,"text-transform":"uppercase","symbol-spacing":350,"text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"map","text-pitch-alignment":"viewport","text-field":"{name:en} {name:nonlatin}"},"paint":{"text-color":"#bbb","text-halo-color":"#fff","text-translate":[0,0],"text-halo-width":2,"text-halo-blur":1}},{"id":"highway_name_motorway","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["==","$type","LineString"],["==","class","motorway"]],"layout":{"text-size":10,"symbol-spacing":350,"text-font":["Metropolis Light","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"viewport","text-pitch-alignment":"viewport","text-field":"{ref}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"hsl(0, 0%, 100%)","text-translate":[0,2],"text-halo-width":1,"text-halo-blur":1}},{"id":"boundary_state","type":"line","metadata":{"mapbox:group":"a14c9607bc7954ba1df7205bf660433f"},"source":"openmaptiles","source-layer":"boundary","filter":["==","admin_level",4],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(230, 204, 207)","line-width":{"base":1.3,"stops":[[3,1],[22,15]]},"line-blur":0.4,"line-dasharray":[2,2],"line-opacity":1}},{"id":"boundary_country","type":"line","metadata":{"mapbox:group":"a14c9607bc7954ba1df7205bf660433f"},"source":"openmaptiles","source-layer":"boundary","filter":["==","admin_level",2],"layout":{"line-cap":"round","line-join":"round"},"paint":{"line-color":"rgb(230, 204, 207)","line-width":{"base":1.1,"stops":[[3,1],[22,20]]},"line-blur":{"base":1,"stops":[[0,0.4],[22,4]]},"line-opacity":1}},{"id":"place_other","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["!in","class","city","suburb","town","village"],["==","$type","Point"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_other-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["!in","class","city","suburb","town","village"],["==","$type","Point"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_suburb","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","suburb"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_suburb-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","suburb"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_village","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["==","class","village"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":"left","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_village-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["==","class","village"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":"left","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_town","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","town"],["!has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_town-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","town"],["has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["==","class","city"],[">","rank",3]],["!has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["==","class","city"],[">","rank",3]],["has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_capital","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["==","capital",2],["==","class","city"]],["!has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"star-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":1,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_capital-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["==","capital",2],["==","class","city"]],["has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"star-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":1,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city_large","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["<=","rank",3],["==","class","city"]],["!has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city_large-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["<=","rank",3],["==","class","city"]],["has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_state","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["==","class","state"],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":10},"paint":{"text-color":"rgb(113, 129, 144)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_state-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["==","class","state"],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":10},"paint":{"text-color":"rgb(113, 129, 144)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_country_other","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":8,"filter":["all",["==","$type","Point"],["all",["==","class","country"],[">=","rank",2]],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1,"stops":[[0,10],[6,12]]}},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_other-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":8,"filter":["all",["==","$type","Point"],["all",["==","class","country"],[">=","rank",2]],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1,"stops":[[0,10],[6,12]]}},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_major","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":6,"filter":["all",["==","$type","Point"],["all",["<=","rank",1],["==","class","country"]],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1.4,"stops":[[0,10],[3,12],[4,14]]},"text-anchor":"center"},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_major-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":6,"filter":["all",["==","$type","Point"],["all",["<=","rank",1],["==","class","country"]],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1.4,"stops":[[0,10],[3,12],[4,14]]},"text-anchor":"center"},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}}]} 2 | -------------------------------------------------------------------------------- /docs/fmt.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | 8 | .input { 9 | width: 100%; 10 | height: 120px; 11 | font-family: monospace; 12 | font-size: 13px; 13 | border: solid 1px #ccc; 14 | padding: 4px; 15 | resize: vertical; 16 | } 17 | 18 | .debug { 19 | background: #eee; 20 | border: solid 1px #ccc; 21 | border-top-width: 0; 22 | padding: 4px; 23 | } 24 | 25 | .mappings-generated { 26 | margin: 0; 27 | border: solid 1px #ccc; 28 | padding: 4px; 29 | } 30 | 31 | h2 { 32 | font-size: 1.2em; 33 | margin-bottom: 0.2em; 34 | } 35 | 36 | h3 { 37 | font-size: 1em; 38 | margin-bottom: 0.2em; 39 | } 40 | 41 | .mappings-source { 42 | margin: 0; 43 | border: solid 1px #ccc; 44 | padding: 4px; 45 | } 46 | 47 | .mapping { 48 | width: 1px; 49 | height: 1em; 50 | background: red; 51 | display: inline-block; 52 | position: absolute; 53 | z-index: 1; 54 | } 55 | 56 | .output { 57 | width: 100%; 58 | height: 120px; 59 | border: solid 1px #ccc; 60 | padding: 4px; 61 | margin: 0; 62 | } 63 | 64 | .debug-selected { 65 | background-color: hsla(60, 95%, 50%, 0.8); 66 | border-left: solid 1px #000; 67 | margin-left: -1px; 68 | } 69 | -------------------------------------------------------------------------------- /docs/fmt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fmt 7 | 8 | 9 | 10 | 11 |
12 |

fmt

13 |

14 | An attempt at an auto formatter for simple-expr. 15 |

16 |

17 | The steps are 18 |

19 |
    20 |
  1. Compile the code
  2. 21 |
  3. Decompile the code again, whitespace will differ
  4. 22 |
  5. Replace cursor to correct position on selection with sourcemap
  6. 23 |
24 |
25 | 26 |

Formatter

27 |

28 | Reformat the code, moving the cursor will translate to the generated (reformated) code. 29 |

30 |

Original (editable)

31 | 36 | 37 |

Reformatted

38 |

39 |     
40 | 41 |

Source maps

42 |

43 | The red lines show the soure map markers. 44 |

45 |

Source

46 |

47 |     

Generated

48 |

49 | 
50 |     
51 |     
52 |   
53 | 
54 | 
55 | 


--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     simple-expr
 7 |     
 8 |   
 9 |   
10 | 
11 |     
12 |
13 |
14 |

simple-expr

15 |
Simple expression language for mapbox-gl-js expressions
16 |
17 | 28 |
29 |
30 |
31 |
32 |
33 | Simple-expr editor 34 | 36 |
37 |
38 | Defines a circle-color property for layer without filter for the below GeoJSON source 39 |
40 | 47 |
48 |
49 |
MapboxGL expression
50 |
51 |
52 |               
53 |
54 |
55 |
56 |
57 |
58 |
GeoJSON
59 |
60 |
61 |               
62 |
63 |
64 |
65 |
Map with expression
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/concat.expr: -------------------------------------------------------------------------------- 1 | concat("Hello", " ", @name) 2 | -------------------------------------------------------------------------------- /examples/concat.json: -------------------------------------------------------------------------------- 1 | [ 2 | "concat", 3 | "Hello", 4 | " ", 5 | [ 6 | "get", 7 | "name" 8 | ] 9 | ] 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var sourceMap = require("source-map"); 2 | 3 | 4 | function decompileFromAst(node) { 5 | var map = new sourceMap.SourceMapGenerator({}); 6 | 7 | function genMapping(name, opts) { 8 | map.addMapping({ 9 | generated: { 10 | line: opts.generated.line, 11 | column: opts.generated.column 12 | }, 13 | source: "foo.js", 14 | original: { 15 | line: opts.original.line, 16 | column: opts.original.column 17 | }, 18 | name: "foo" 19 | }); 20 | } 21 | 22 | var outStr = ""; 23 | 24 | function go(node, depth) { 25 | depth = depth || 0; 26 | 27 | if(node.type === "CallExpression") { 28 | genMapping(node.type, { 29 | original: { 30 | line: node.row+1, 31 | column: node.col 32 | }, 33 | generated: {line: 1, column: outStr.length} 34 | }) 35 | outStr = outStr + `${node.name}(`; 36 | node.params.forEach(function(_node, idx) { 37 | go(_node, depth) 38 | if(idx < node.params.length -1) { 39 | outStr += ", " 40 | } 41 | }) 42 | 43 | outStr = outStr + `)`; 44 | } 45 | else if(node.type === "NumberLiteral") { 46 | genMapping(node.type, { 47 | original: { 48 | line: node.row+1, 49 | column: node.col 50 | }, 51 | generated: {line: 1, column: outStr.length} 52 | }) 53 | outStr = outStr + node.value; 54 | } 55 | else if(node.type === "StringLiteral") { 56 | genMapping(node.type, { 57 | original: { 58 | line: node.row+1, 59 | column: node.col 60 | }, 61 | generated: {line: 1, column: outStr.length} 62 | }) 63 | outStr = outStr + JSON.stringify(node.value); 64 | } 65 | else if(node.type === "FeatureRef") { 66 | genMapping(node.type, { 67 | original: { 68 | line: node.row+1, 69 | column: node.col 70 | }, 71 | generated: {line: 1, column: outStr.length} 72 | }) 73 | outStr = outStr + "@"+node.value; 74 | } 75 | 76 | } 77 | 78 | genMapping(node.type, { 79 | original: {line: 1, column: 0}, 80 | generated: {line: 1, column: 0} 81 | }) 82 | 83 | go(node.body[0]) 84 | return { 85 | code: outStr, 86 | map: map.toJSON() 87 | }; 88 | } 89 | 90 | module.exports = { 91 | compile: require("./lib/compile"), 92 | decompile: require("./lib/decompile"), 93 | // Compile steps 94 | tokenize: require("./lib/tokenize"), 95 | parse: require("./lib/parse"), 96 | transform: require("./lib/transform"), 97 | // Decompile steps 98 | untransform: require("./lib/untransform"), 99 | unparse: require("./lib/unparse"), 100 | untokenize: require("./lib/untokenize"), 101 | // Deprecated APIs 102 | decompileFromAst, 103 | }; 104 | 105 | -------------------------------------------------------------------------------- /lib/compile.js: -------------------------------------------------------------------------------- 1 | const tokenize = require("./tokenize"); 2 | const parse = require("./parse"); 3 | const transform = require("./transform"); 4 | 5 | 6 | module.exports = function(code) { 7 | const tokens = tokenize(code); 8 | const ast = parse(tokens); 9 | const mglJSON = transform(ast); 10 | return mglJSON; 11 | } 12 | -------------------------------------------------------------------------------- /lib/decompile.js: -------------------------------------------------------------------------------- 1 | const untransform = require("./untransform"); 2 | const unparse = require("./unparse"); 3 | const untokenize = require("./untokenize"); 4 | 5 | 6 | module.exports = function(mglJSON) { 7 | const ast = untransform(mglJSON); 8 | const tokens = unparse(ast); 9 | const rslt = untokenize(tokens); 10 | return rslt.code; 11 | } 12 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | class ParseError extends Error { 2 | constructor(message, opts={}) { 3 | super(message); 4 | this.line = opts.token.row; 5 | this.column = opts.token.col; 6 | } 7 | } 8 | 9 | 10 | module.exports = { 11 | ParseError 12 | } 13 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | const util = require("./util") 2 | const ParseError = require("./errors").ParseError; 3 | 4 | 5 | function fetchAdditionalTokens(cursor, types) { 6 | const out = []; 7 | 8 | // Look ahead 9 | let idx = 0; 10 | let checkType; 11 | while(checkType = types[idx++]) { 12 | let token = cursor.fetch(); 13 | if(!token) { 14 | break; 15 | } 16 | 17 | let optional; 18 | if(checkType.match(/^(.*)(\?)$/)) { 19 | optional = true; 20 | checkType = RegExp.$1; 21 | } 22 | 23 | if(checkType == token.type) { 24 | out.push(token); 25 | cursor.move(+1) 26 | } 27 | else if(!optional) { 28 | throw new ParseError("Expected '"+checkType+"'", { 29 | token: token 30 | }); 31 | } 32 | } 33 | 34 | return out; 35 | } 36 | 37 | function buildNode(obj, token) { 38 | return Object.assign({}, obj, { 39 | tokens: Object.assign({ 40 | pre: [], 41 | post: [] 42 | }, obj.tokens, { 43 | // Always override... 44 | current: [ 45 | token 46 | ] 47 | }) 48 | }); 49 | } 50 | 51 | const parsers = { 52 | "open_paren": function(cursor) { 53 | throw new ParseError("Incorrectly placed open_paren", { 54 | token: cursor.fetch() 55 | }); 56 | }, 57 | "close_paren": function(cursor) { 58 | throw new ParseError("Incorrectly placed close_paren", { 59 | token: cursor.fetch() 60 | }); 61 | }, 62 | "whitespace": function(cursor) { 63 | cursor.move(+1); 64 | return; 65 | }, 66 | "arg_sep": function(cursor) { 67 | throw new ParseError("Misplaced arg_sep", { 68 | token: cursor.fetch() 69 | }); 70 | }, 71 | "number": function(cursor) { 72 | const token = cursor.fetch(); 73 | cursor.move(+1); 74 | 75 | const node = buildNode({ 76 | type: 'NumberLiteral', 77 | tokens: { 78 | post: fetchAdditionalTokens(cursor, ["whitespace?", "arg_sep?", "whitespace?"]) 79 | }, 80 | value: util.parseNumber(token.value) 81 | }, token); 82 | 83 | return node; 84 | }, 85 | "string": function(cursor) { 86 | let token = cursor.fetch(); 87 | cursor.move(+1); 88 | 89 | const node = buildNode({ 90 | type: 'StringLiteral', 91 | tokens: { 92 | post: fetchAdditionalTokens(cursor, ["whitespace?", "arg_sep?", "whitespace?"]) 93 | }, 94 | value: token.value 95 | }, token); 96 | 97 | return node; 98 | }, 99 | "feature_ref": function(cursor) { 100 | let token = cursor.fetch(); 101 | cursor.move(+1); 102 | 103 | const node = buildNode({ 104 | type: 'FeatureRef', 105 | tokens: { 106 | post: fetchAdditionalTokens(cursor, ["whitespace?", "arg_sep?", "whitespace?"]) 107 | }, 108 | value: token.value, 109 | }, token); 110 | 111 | return node; 112 | }, 113 | "command": function(cursor) { 114 | let token = cursor.fetch(); 115 | cursor.move(+1); 116 | 117 | let node = buildNode({ 118 | type: 'CallExpression', 119 | value: token.value, 120 | tokens: { 121 | pre: fetchAdditionalTokens(cursor, ["open_paren", "whitespace?"]) 122 | }, 123 | params: [], 124 | }, token); 125 | 126 | let argSepRequired = false; 127 | while ( 128 | (token = cursor.fetch()) 129 | && ( 130 | (token.type !== 'close_paren') 131 | ) 132 | ) { 133 | // we'll call the `walk` function which will return a `node` and we'll 134 | // push it into our `node.params`. 135 | const arg = parsers[token.type](cursor); 136 | 137 | if(arg) { 138 | if(!cursor.fetch()) { 139 | throw new ParseError("Missing close_paren", { 140 | token: token 141 | }) 142 | } 143 | else if(cursor.fetch().type !== "close_paren") { 144 | let argSep = arg.tokens.post 145 | .find(function(item) { 146 | return item.type === "arg_sep"; 147 | }) 148 | 149 | if(!argSep) { 150 | throw new ParseError("Missing arg_sep", { 151 | token: token 152 | }); 153 | } 154 | } 155 | node.params = node.params.concat(arg); 156 | } 157 | 158 | // Check we found a arg_sep 159 | 160 | argSepRequired = true; 161 | } 162 | 163 | node.tokens.post = fetchAdditionalTokens(cursor, ["close_paren", "arg_sep?", "whitespace?"]); 164 | return node; 165 | } 166 | } 167 | 168 | function Cursor(tokens, idx = 0) { 169 | this._tokens = tokens; 170 | this._idx = idx; 171 | } 172 | 173 | Cursor.prototype.move = function(offset) { 174 | this._idx += offset; 175 | } 176 | 177 | Cursor.prototype.currentIndex = function() { 178 | return this._idx; 179 | } 180 | 181 | Cursor.prototype.fetch = function() { 182 | return this.peek(0); 183 | } 184 | 185 | Cursor.prototype.peek = function(offset) { 186 | return this._tokens[this._idx+offset]; 187 | } 188 | 189 | Cursor.prototype.atEnd = function() { 190 | return this._idx > this._tokens.length-1; 191 | } 192 | 193 | function parse(tokens) { 194 | const cursor = new Cursor(tokens); 195 | let ast = []; 196 | 197 | while(!cursor.atEnd()) { 198 | const token = cursor.fetch(); 199 | let nodes = parsers[token.type](cursor); 200 | 201 | if(nodes) { 202 | ast = ast.concat(nodes); 203 | } 204 | } 205 | 206 | if(ast.length > 1) { 207 | throw new ParseError("Multiple top level functions not allowed", { 208 | token: token 209 | }) 210 | } 211 | 212 | return { 213 | type: 'Program', 214 | body: ast 215 | }; 216 | } 217 | 218 | module.exports = parse; 219 | -------------------------------------------------------------------------------- /lib/source-map.js: -------------------------------------------------------------------------------- 1 | const sourceMap = require("source-map"); 2 | 3 | 4 | async function genOrigPosition(map, origPos, source) { 5 | const consumer = await new sourceMap.SourceMapConsumer(map) 6 | consumer.computeColumnSpans(); 7 | 8 | const genPos = consumer.generatedPositionFor({ 9 | line: origPos.line, 10 | column: origPos.column, 11 | source: source, 12 | }) 13 | 14 | function getOrigPos(genPos, origPos) { 15 | let sourcePos; 16 | let skipRest = false; 17 | let offset = 0; 18 | 19 | consumer.eachMapping(function (m) { 20 | if(skipRest) { 21 | return; 22 | } 23 | // If we've found an exact match stop! 24 | else if( 25 | m.originalLine == origPos.line 26 | && m.originalColumn == origPos.column 27 | ) { 28 | sourcePos = m; 29 | skipRest = true; 30 | offset = 0; 31 | } 32 | // If we're still behind our target 33 | else if( 34 | m.generatedLine <= genPos.line && 35 | m.generatedColumn <= genPos.column 36 | ) { 37 | sourcePos = m; 38 | offset = origPos.column - sourcePos.originalColumn 39 | } 40 | 41 | // If the cursor position is ahead of sourcePos reset the offset 42 | if( 43 | sourcePos && 44 | sourcePos.originalColumn > origPos.column 45 | ) { 46 | offset = 0; 47 | } 48 | }) 49 | 50 | return offset 51 | } 52 | 53 | const sourcePos = consumer.originalPositionFor(genPos); 54 | 55 | var offset = getOrigPos(genPos, origPos); 56 | 57 | const newGenPos = { 58 | line: genPos.line, 59 | column: Math.min(genPos.column+offset) 60 | }; 61 | 62 | return newGenPos; 63 | } 64 | 65 | async function getMappings(map) { 66 | const out = []; 67 | const consumer = await new sourceMap.SourceMapConsumer(map) 68 | consumer.eachMapping(function(m) { 69 | out.push(m); 70 | }) 71 | 72 | return out; 73 | } 74 | 75 | 76 | module.exports = { 77 | genOrigPosition, 78 | getMappings 79 | }; 80 | -------------------------------------------------------------------------------- /lib/textblock.js: -------------------------------------------------------------------------------- 1 | function indexToColRow(input, positions) { 2 | let total = 0; 3 | const out = []; 4 | 5 | input.split("\n") 6 | .forEach(function(line, colIdx) { 7 | const prevTotal = total; 8 | total += line.length + 1/*The removed '\n' char */; 9 | 10 | positions.forEach(function(_pos, idx) { 11 | if(!out.hasOwnProperty(idx) && _pos < total) { 12 | out[idx] = { 13 | col: _pos - prevTotal, 14 | row: colIdx 15 | } 16 | } 17 | }) 18 | }) 19 | 20 | return out; 21 | } 22 | 23 | function colRowToIndex(input, positions) { 24 | let total = 0; 25 | const out = []; 26 | 27 | input.split("\n") 28 | .forEach(function(line, rowIdx) { 29 | positions.forEach(function(_pos, idx) { 30 | if(rowIdx == _pos.row) { 31 | out[idx] = total + _pos.col; 32 | } 33 | }) 34 | 35 | total += line.length + 1/*The removed '\n' char */; 36 | }) 37 | 38 | return out; 39 | } 40 | 41 | module.exports = { 42 | indexToColRow, 43 | colRowToIndex 44 | } 45 | -------------------------------------------------------------------------------- /lib/tokenize.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | let current = 0; 3 | 4 | let tokens = []; 5 | 6 | let row = 0; 7 | let offset = 0; 8 | 9 | function addToken(obj) { 10 | const token = Object.assign({}, obj, { 11 | row: row, 12 | col: (current) - offset 13 | }) 14 | tokens.push(token) 15 | return token; 16 | } 17 | 18 | while (current < source.length) { 19 | let char = source[current]; 20 | 21 | if(char === "\n") { 22 | addToken({ 23 | type: 'whitespace', 24 | value: '\n', 25 | }); 26 | row += 1; 27 | offset = current + 1/*Including the '\n' */; 28 | current++ 29 | continue; 30 | } 31 | 32 | if (char === '(') { 33 | addToken({ 34 | type: 'open_paren', 35 | value: '(', 36 | }); 37 | current++; 38 | continue; 39 | } 40 | if (char === ')') { 41 | addToken({ 42 | type: 'close_paren', 43 | value: ')', 44 | }); 45 | current++; 46 | continue; 47 | } 48 | if(char === ",") { 49 | addToken({ 50 | type: 'arg_sep' 51 | }); 52 | current++; 53 | continue; 54 | } 55 | 56 | // Whitespace is ignored 57 | // Note: whitespace in strings are handled separately in the string handler 58 | let WHITESPACE = /\s/; 59 | if (WHITESPACE.test(char)) { 60 | let value = ""; 61 | let token = addToken({ 62 | type: "whitespace" 63 | }) 64 | 65 | while (WHITESPACE.test(char)) { 66 | value += char; 67 | char = source[++current]; 68 | } 69 | 70 | token.value = value; 71 | continue; 72 | } 73 | 74 | let NUMBERS = /[-+.0-9]/; 75 | if (NUMBERS.test(char)) { 76 | let value = ''; 77 | 78 | const token = addToken({type: 'number'}); 79 | 80 | while (NUMBERS.test(char)) { 81 | value += char; 82 | char = source[++current]; 83 | } 84 | 85 | token.value = value; 86 | 87 | if(!value.match(/^[+-]?([0-9]*\.)?[0-9]+$/)) { 88 | throw "Invalid number '"+value+"'" 89 | } 90 | 91 | continue; 92 | } 93 | 94 | // Feature reference 95 | if (char === '@') { 96 | let value = ''; 97 | 98 | const token = addToken({ type: 'feature_ref'}); 99 | 100 | // Skip the '@' 101 | char = source[++current]; 102 | 103 | while (char.match(/[a-zA-Z0-9_]/)) { 104 | value += char; 105 | char = source[++current]; 106 | } 107 | 108 | token.value = value; 109 | 110 | continue; 111 | } 112 | 113 | if (char === '"') { 114 | // Keep a `value` variable for building up our string token. 115 | let value = ''; 116 | 117 | const token = addToken({ type: 'string'}); 118 | 119 | // We'll skip the opening double quote in our token. 120 | char = source[++current]; 121 | 122 | // Iterate through each character until we reach another double quote. 123 | let prev; 124 | while (prev === "\\" || char !== '"') { 125 | value += char; 126 | prev = char; 127 | char = source[++current]; 128 | if(char === undefined) { 129 | throw "Missing closing quote"; 130 | } 131 | } 132 | 133 | token.value = value; 134 | 135 | if(char !== "\"") { 136 | throw "Missing closing quote" 137 | } 138 | 139 | // Skip the closing double quote. 140 | char = source[++current]; 141 | 142 | continue; 143 | } 144 | 145 | 146 | let LETTERS = /[^)( \t]/i; 147 | if (LETTERS.test(char)) { 148 | let value = ''; 149 | 150 | let token = addToken({ type: 'command'}); 151 | 152 | // This allows for log10 method name but not 10log 153 | while (char && LETTERS.test(char)) { 154 | value += char; 155 | char = source[++current]; 156 | } 157 | 158 | token.value = value; 159 | 160 | continue; 161 | } 162 | 163 | throw new TypeError('I don\'t know what this character is: ' + char); 164 | } 165 | 166 | // Always trailing whitespace so the source maps can map to it. 167 | // TODO: Is this needed? 168 | addToken({ 169 | type: 'whitespace', 170 | value: '\n', 171 | }); 172 | 173 | return tokens; 174 | } 175 | -------------------------------------------------------------------------------- /lib/transform.js: -------------------------------------------------------------------------------- 1 | function removeUndefined(v) { 2 | return v !== undefined; 3 | } 4 | 5 | const IGNORE_NODES = [ 6 | "OpenParen", 7 | "CloseParen", 8 | "WhiteSpace" 9 | ] 10 | 11 | module.exports = function(nodes) { 12 | function walk(node) { 13 | if( 14 | IGNORE_NODES.indexOf(node.type) > -1 15 | ) { 16 | // Ignoring node. 17 | return; 18 | } 19 | if(node.type === "CallExpression") { 20 | const args = node.params 21 | .map(walk) 22 | .filter(removeUndefined) 23 | 24 | return [node.value].concat(args); 25 | } 26 | else if (node.type === "StringLiteral") { 27 | return node.value; 28 | } 29 | else if (node.type === "NumberLiteral") { 30 | return node.value; 31 | } 32 | else if (node.type === "FeatureRef") { 33 | return ["get", node.value]; 34 | } 35 | } 36 | 37 | if(nodes.body.length < 1) { 38 | return []; 39 | } 40 | else { 41 | const out = nodes.body 42 | .map(walk) 43 | .filter(removeUndefined) 44 | 45 | if(out.length > 1) { 46 | throw "Invalid AST" 47 | } 48 | else if(out.length < 1) { 49 | return []; 50 | } 51 | else { 52 | return out[0]; 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /lib/unparse.js: -------------------------------------------------------------------------------- 1 | module.exports = function(ast) { 2 | 3 | function addPosInfo(token, node) { 4 | if(node && node.hasOwnProperty("col") && node.hasOwnProperty("row")) { 5 | return Object.assign({}, token, { 6 | col: node.col, 7 | row: node.row 8 | }); 9 | } 10 | else { 11 | return token; 12 | } 13 | } 14 | 15 | function takeTokens(tokens, defTokens) { 16 | tokens = tokens || []; 17 | 18 | const out = [] 19 | let idx = 0; 20 | 21 | let skip = false; 22 | let defToken; 23 | for(defToken of defTokens) { 24 | let token = tokens[idx++]; 25 | 26 | if(!token) { 27 | if(!defToken.optional) { 28 | out.push({...defToken}) 29 | } 30 | continue; 31 | } 32 | 33 | if(Array.isArray(defToken)) { 34 | defToken = defToken.find(function(item) { 35 | return item.type === token.type; 36 | }) 37 | } 38 | 39 | 40 | let typeCheck = defToken.type == token.type; 41 | 42 | if(!skip && typeCheck) { 43 | out.push( 44 | Object.assign({}, token, defToken) 45 | ) 46 | } 47 | else if(defToken.optional) { 48 | // Skip optional nodes 49 | idx--; 50 | } 51 | else { 52 | skip = true; 53 | out.push({...defToken}) 54 | // Create the rest of the tokens ourselves 55 | } 56 | } 57 | 58 | return out; 59 | } 60 | 61 | 62 | function buildTokens(out, node, def) { 63 | function pushIt(item) { 64 | out.push(item); 65 | } 66 | 67 | const tokens = node.tokens || {} 68 | 69 | takeTokens(tokens.pre, def.pre ).map(pushIt); 70 | takeTokens(tokens.current, def.current).map(pushIt); 71 | takeTokens(tokens.post, def.post ).map(pushIt); 72 | } 73 | 74 | function walk(node) { 75 | let out = []; 76 | 77 | if(node.type === "CallExpression") { 78 | 79 | let args = [] 80 | 81 | node.params.forEach(function(_node, idx) { 82 | let isLast = node.params.length-1 <= idx; 83 | 84 | let out = walk(_node) 85 | let noSep = ( 86 | (out[out.length-1] && out[out.length-1].type === "arg_sep") 87 | || (out[out.length-2] && out[out.length-2].type === "arg_sep") 88 | ); 89 | 90 | if(!isLast && !noSep) { 91 | out.push({type: "arg_sep"}) 92 | } 93 | args = args.concat(out) 94 | }) 95 | 96 | let tokens = node.tokens || {} 97 | 98 | out = out.concat( 99 | takeTokens(tokens.current, [ 100 | { 101 | type: "command", 102 | value: node.value 103 | } 104 | ]), 105 | takeTokens(tokens.pre, [ 106 | { 107 | type: "open_paren", 108 | value: "(" 109 | }, 110 | { 111 | type: "whitespace", 112 | optional: true, 113 | value: "" 114 | } 115 | ]), 116 | args, 117 | takeTokens(tokens.post, [ 118 | { 119 | type: "arg_sep", 120 | optional: true, 121 | value: "," 122 | }, 123 | { 124 | type: "close_paren", 125 | value: ")" 126 | }, 127 | { 128 | type: "whitespace", 129 | optional: true, 130 | value: "" 131 | } 132 | ]) 133 | ) 134 | } 135 | else if(node.type === "FeatureRef") { 136 | buildTokens(out, node, { 137 | pre: [], 138 | current: [{ 139 | type: "feature_ref", 140 | value: node.value 141 | }], 142 | post: [ 143 | { 144 | type: "whitespace", 145 | optional: true, 146 | value: "" 147 | }, 148 | { 149 | type: "arg_sep", 150 | optional: true, 151 | value: "," 152 | } 153 | ] 154 | }); 155 | } 156 | else if(node.type === "StringLiteral") { 157 | buildTokens(out, node, { 158 | pre: [], 159 | current: [ 160 | { 161 | type: "string", 162 | value: node.value 163 | } 164 | ], 165 | post: [ 166 | { 167 | type: "whitespace", 168 | optional: true, 169 | value: "" 170 | }, 171 | { 172 | type: "arg_sep", 173 | optional: true, 174 | value: "," 175 | }, 176 | { 177 | type: "whitespace", 178 | optional: true, 179 | value: "" 180 | } 181 | ] 182 | }); 183 | } 184 | else if(node.type === "NumberLiteral") { 185 | buildTokens(out, node, { 186 | pre: [ 187 | ], 188 | current: [ 189 | { 190 | type: "number", 191 | value: node.value 192 | } 193 | ], 194 | post: [ 195 | { 196 | type: "whitespace", 197 | optional: true, 198 | value: "" 199 | }, 200 | { 201 | type: "arg_sep", 202 | optional: true, 203 | value: "," 204 | }, 205 | { 206 | type: "whitespace", 207 | optional: true, 208 | value: "" 209 | } 210 | ] 211 | }); 212 | } 213 | else { 214 | throw TypeError(node.type); 215 | } 216 | 217 | return out; 218 | } 219 | 220 | if(ast.body[0]) { 221 | return walk(ast.body[0]) 222 | } 223 | else { 224 | return []; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/untokenize.js: -------------------------------------------------------------------------------- 1 | const sourceMap = require("source-map"); 2 | 3 | 4 | module.exports = function(tokens) { 5 | 6 | function walk(node) { 7 | const type = node.type; 8 | 9 | if(type === "feature_ref") { 10 | return "@"+node.value 11 | } 12 | else if(type === "command") { 13 | return node.value 14 | } 15 | else if(type === "string") { 16 | return "\""+node.value+"\""; 17 | } 18 | else if(type === "open_paren") { 19 | return node.value; 20 | } 21 | else if(type === "close_paren") { 22 | return node.value; 23 | } 24 | else if(type === "number") { 25 | return node.value; 26 | } 27 | else if(type === "arg_sep") { 28 | return ","; 29 | } 30 | else if(type === "whitespace") { 31 | return node.value; 32 | } 33 | else { 34 | throw "Unrecognised node type '"+type+"'" 35 | } 36 | } 37 | 38 | 39 | let map = new sourceMap.SourceMapGenerator({}); 40 | let genCol = 0; 41 | let genRow = 0; 42 | 43 | function genMapping(token) { 44 | if(token.hasOwnProperty("row") || token.hasOwnProperty("col")) { 45 | const mapData = { 46 | generated: { 47 | line: genRow+1, 48 | column: genCol 49 | }, 50 | source: "foo.js", 51 | original: { 52 | line: token.row+1, 53 | column: token.col 54 | }, 55 | name: "foo" 56 | }; 57 | map.addMapping(mapData); 58 | } 59 | } 60 | 61 | const rslt = tokens.map(function(token) { 62 | genMapping(token); 63 | const out = String(walk(token)); 64 | genCol += out.length; 65 | return out; 66 | }).join(""); 67 | 68 | return { 69 | code: rslt, 70 | map: map.toJSON() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/untransform.js: -------------------------------------------------------------------------------- 1 | module.exports = function untransform(node) { 2 | function walk(node, depth) { 3 | depth = depth || 0; 4 | 5 | if(node.length < 1) { 6 | if(depth > 0) { 7 | throw "node requires function name"; 8 | } 9 | else { 10 | // Empty expression 11 | return; 12 | } 13 | } 14 | 15 | const command = node[0]; 16 | let args = node.slice(1); 17 | 18 | if(command == "get") { 19 | if(node.length !== 2) { 20 | throw "'get' has too many params"; 21 | } 22 | return { 23 | type: "FeatureRef", 24 | value: node[1] 25 | } 26 | } 27 | else { 28 | args = args.map(function(childNode) { 29 | if(Array.isArray(childNode)) { 30 | return walk(childNode, depth+1) 31 | } 32 | else if(typeof(childNode) === "number") { 33 | return { 34 | type: "NumberLiteral", 35 | value: childNode 36 | } 37 | } 38 | else { 39 | return { 40 | type: "StringLiteral", 41 | value: childNode 42 | } 43 | } 44 | }) 45 | 46 | return { 47 | type: "CallExpression", 48 | value: command, 49 | params: args 50 | } 51 | } 52 | } 53 | 54 | const ast = { 55 | "type": "Program", 56 | "body": [] 57 | } 58 | 59 | const body = walk(node); 60 | if(body) { 61 | ast.body.push(body) 62 | } 63 | 64 | return ast; 65 | } 66 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | function parseNumber(value) { 2 | // Work out the type 3 | if(value.match(/^[+-]?[0-9]+$/)) { 4 | value = parseInt(value, 10); 5 | } 6 | else if (value.match(/^[+-]?[0-9]+[.][0-9]+$/)) { 7 | value = parseFloat(value, 10); 8 | } 9 | else { 10 | throw `'${value}' is invalid number`; 11 | } 12 | 13 | return value; 14 | } 15 | 16 | module.exports = { 17 | parseNumber 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-expr", 3 | "version": "0.1.2", 4 | "bin": { 5 | "simple-expr": "./bin/cli.js" 6 | }, 7 | "scripts": { 8 | "test": "mocha test/index.js", 9 | "perf": "node test/perf.js" 10 | }, 11 | "dependencies": { 12 | "@mapbox/mapbox-gl-style-spec": "^11.1.0", 13 | "source-map": "^0.6.1", 14 | "yargs": "^11.0.0" 15 | }, 16 | "browserify": { 17 | "transform": [ 18 | "babelify" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.26.0", 23 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 24 | "babel-plugin-transform-runtime": "^6.23.0", 25 | "babel-polyfill": "^6.26.0", 26 | "babel-preset-env": "^1.6.1", 27 | "babelify": "^8.0.0", 28 | "benchmark": "^2.1.4", 29 | "browserify": "^16.1.0", 30 | "dom-delegate": "^2.0.3", 31 | "mocha": "^5.0.0", 32 | "readme-tester": "github:orangemug/readme-tester#v0.7.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | DIR="$(dirname "$0")" 2 | 3 | mkdir -p $DIR/../docs 4 | cp $DIR/../site/* $DIR/../docs/ 5 | $DIR/../node_modules/.bin/browserify $DIR/../site/app.js > $DIR/../docs/app.js 6 | $DIR/../node_modules/.bin/browserify $DIR/../site/fmt.js > $DIR/../docs/fmt.js 7 | -------------------------------------------------------------------------------- /site/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | 8 | html, body { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | overflow: hidden; 15 | } 16 | 17 | a { 18 | color: inherit; 19 | } 20 | 21 | .app { 22 | height: 100%; 23 | display: flex; 24 | flex-direction: column; 25 | overflow: hidden; 26 | } 27 | 28 | .app__header { 29 | display: flex; 30 | align-items: center; 31 | padding: 4px 6px; 32 | } 33 | 34 | .app__header__title { 35 | flex: 1; 36 | } 37 | 38 | .app__header__title h1 { 39 | font-size: 28px; 40 | margin: 0; 41 | } 42 | 43 | .app__header__subtitle { 44 | font-size: 14px; 45 | } 46 | 47 | .toolbox { 48 | padding: 0 6px; 49 | } 50 | 51 | .toolbox .icon { 52 | margin-right: 4px; 53 | } 54 | 55 | .icon { 56 | vertical-align: middle; 57 | text-decoration: none; 58 | } 59 | 60 | 61 | .app__main { 62 | flex: 1; 63 | position: relative; 64 | overflow: hidden; 65 | display: flex; 66 | flex-direction: column; 67 | } 68 | 69 | .row { 70 | flex: 1 1 50%; 71 | max-height: 50%; 72 | display: flex; 73 | } 74 | 75 | .row:nth-child(n+2) { 76 | border-top: solid 1px #555; 77 | } 78 | 79 | .cell { 80 | position: relative; 81 | display: flex; 82 | flex-direction: column; 83 | width: 50%; 84 | } 85 | 86 | .cell:nth-child(n+2) { 87 | border-left: solid 1px #555; 88 | } 89 | 90 | .cell__header { 91 | flex: 0; 92 | background: black; 93 | color: white; 94 | padding: 4px 6px; 95 | } 96 | 97 | .cell__content { 98 | flex: 1; 99 | position: relative; 100 | overflow-x: auto; 101 | } 102 | 103 | .editor { 104 | width: 100%; 105 | height: 100%; 106 | overflow-y: scroll; 107 | border: none; 108 | resize: none; 109 | font-family: monospace; 110 | font-size: 13px; 111 | } 112 | 113 | .editor__message { 114 | background: #eee; 115 | padding: 4px 6px; 116 | border-bottom: solid 1px #ddd; 117 | } 118 | 119 | .result { 120 | margin: 0; 121 | overflow-y: scroll; 122 | } 123 | 124 | .geojson { 125 | margin: 0; 126 | overflow-y: scroll; 127 | } 128 | 129 | .map { 130 | background: #333; 131 | height: 100%; 132 | } 133 | 134 | .examples { 135 | float: right; 136 | } 137 | -------------------------------------------------------------------------------- /site/app.js: -------------------------------------------------------------------------------- 1 | var simpleExpr = require("../"); 2 | 3 | function makePoint(lon, lat, props) { 4 | return { 5 | "type": "Feature", 6 | "geometry": { 7 | "type": "Point", 8 | "coordinates": [lon, lat] 9 | }, 10 | "properties": props 11 | } 12 | } 13 | 14 | var state = { 15 | geojson: { 16 | "type": "FeatureCollection", 17 | "features": [ 18 | makePoint(8.5217, 47.3769, {score: 20}), 19 | makePoint(8.5317, 47.3569, {score: 40}), 20 | makePoint(8.5417, 47.3969, {score: 60}), 21 | makePoint(8.5517, 47.3869, {score: 80}), 22 | makePoint(8.5117, 47.3769, {score: 100}) 23 | ] 24 | }, 25 | code: "rgb(@score, 100, 100)", 26 | error: "", 27 | result: "" 28 | } 29 | 30 | var editorEl = document.querySelector("textarea"); 31 | var resultEl = document.querySelector(".result"); 32 | var geojsonEl = document.querySelector(".geojson"); 33 | var mapEl = document.querySelector(".map"); 34 | var examplesEl = document.querySelector(".examples"); 35 | 36 | 37 | var EXAMPLES = [ 38 | { 39 | name: "color on score", 40 | code: [ 41 | "interpolate(", 42 | " linear(), @score,", 43 | " 0, rgb(255, 0, 0),", 44 | " 50, rgb(0, 255, 0),", 45 | " 100, rgb(0, 0, 255)", 46 | ")" 47 | ].join("\n") 48 | }, 49 | { 50 | name: "interpolate on zoom", 51 | code: [ 52 | "interpolate(", 53 | " linear(), zoom(),", 54 | " 0, rgb(0, 0, 0),", 55 | " 11, rgb(255, 0, 0),", 56 | " 16, rgb(0, 255, 0),", 57 | " 22, rgb(0, 0, 255)", 58 | ")" 59 | ].join("\n") 60 | }, 61 | ]; 62 | 63 | examplesEl.innerHTML = EXAMPLES.map(function(example) { 64 | return "" 65 | }).join("") 66 | 67 | editorEl.addEventListener("keyup", change); 68 | function change(e) { 69 | state.code = editorEl.value; 70 | state.error = ""; 71 | 72 | try { 73 | state.result = simpleExpr.compile(state.code) 74 | } 75 | catch(err) { 76 | console.error(err); 77 | state.error = err; 78 | } 79 | 80 | render(); 81 | } 82 | 83 | function render() { 84 | if(state.error) { 85 | resultEl.innerHTML = '
'+state.error+'
' 86 | } 87 | else { 88 | resultEl.innerHTML = JSON.stringify(state.result, null, 2); 89 | } 90 | 91 | geojsonEl.innerHTML = JSON.stringify(state.geojson, null, 2); 92 | 93 | var style = buildStyle({ 94 | sources: { 95 | "demo": { 96 | "type": "geojson", 97 | "data": state.geojson 98 | } 99 | }, 100 | layers: [ 101 | { 102 | "id": "example", 103 | "type": "circle", 104 | "source": "demo", 105 | "paint": { 106 | "circle-color": state.result, 107 | "circle-radius": 6 108 | } 109 | } 110 | ] 111 | }) 112 | 113 | map.setStyle(style); 114 | } 115 | 116 | var BASE_STYLE = require("./base-style.json"); 117 | 118 | function buildStyle(opts) { 119 | var baseStyle = JSON.parse( 120 | JSON.stringify(BASE_STYLE) 121 | ); 122 | 123 | baseStyle.sources = Object.assign(baseStyle.sources, opts.sources) 124 | baseStyle.layers = baseStyle.layers.concat(opts.layers); 125 | return baseStyle; 126 | } 127 | 128 | var map = new mapboxgl.Map({ 129 | container: mapEl, 130 | style: 'https://free.tilehosting.com/styles/positron/style.json?key=ozKuiN7rRsPFArLI4gsv', 131 | center: [8.5456, 47.3739], 132 | zoom: 11 133 | }); 134 | 135 | examplesEl.addEventListener("change", function() { 136 | editorEl.value = EXAMPLES[examplesEl.selectedIndex].code; 137 | change(); 138 | }) 139 | 140 | editorEl.value = EXAMPLES[examplesEl.selectedIndex].code; 141 | change(); 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /site/base-style.json: -------------------------------------------------------------------------------- 1 | {"version":8,"name":"Positron","metadata":{"mapbox:autocomposite":false,"mapbox:type":"template","mapbox:groups":{"b6371a3f2f5a9932464fa3867530a2e5":{"name":"Transportation","collapsed":false},"a14c9607bc7954ba1df7205bf660433f":{"name":"Boundaries"},"101da9f13b64a08fa4b6ac1168e89e5f":{"name":"Places","collapsed":false}},"openmaptiles:version":"3.x","openmaptiles:mapbox:owner":"openmaptiles","openmaptiles:mapbox:source:url":"mapbox://openmaptiles.4qljc88t"},"center":[10.184401828277089,-1.1368683772161603e-13],"zoom":0.8902641636539237,"bearing":0,"pitch":0,"sources":{"openmaptiles":{"type":"vector","url":"https://free.tilehosting.com/data/v3.json?key=ozKuiN7rRsPFArLI4gsv"}},"sprite":"https://free.tilehosting.com/styles/positron/sprite","glyphs":"https://free.tilehosting.com/fonts/{fontstack}/{range}.pbf?key=ozKuiN7rRsPFArLI4gsv","layers":[{"id":"background","type":"background","paint":{"background-color":"rgb(242,243,240)"}},{"id":"park","type":"fill","source":"openmaptiles","source-layer":"park","filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(230, 233, 229)"}},{"id":"water","type":"fill","source":"openmaptiles","source-layer":"water","filter":["==","$type","Polygon"],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(194, 200, 202)","fill-antialias":true,"fill-outline-color":{"base":1,"stops":[[0,"hsla(180, 6%, 63%, 0.82)"],[22,"hsla(180, 6%, 63%, 0.18)"]]}}},{"id":"landcover_ice_shelf","type":"fill","source":"openmaptiles","source-layer":"landcover","maxzoom":8,"filter":["all",["==","$type","Polygon"],["==","subclass","ice_shelf"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(0, 0%, 98%)","fill-opacity":0.7}},{"id":"landcover_glacier","type":"fill","source":"openmaptiles","source-layer":"landcover","maxzoom":8,"filter":["all",["==","$type","Polygon"],["==","subclass","glacier"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"hsl(0, 0%, 98%)","fill-opacity":{"base":1,"stops":[[0,1],[8,0.5]]}}},{"id":"landuse_residential","type":"fill","source":"openmaptiles","source-layer":"landuse","maxzoom":16,"filter":["all",["==","$type","Polygon"],["==","class","residential"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(234, 234, 230)","fill-opacity":{"base":0.6,"stops":[[8,0.8],[9,0.6]]}}},{"id":"landcover_wood","type":"fill","source":"openmaptiles","source-layer":"landcover","minzoom":10,"filter":["all",["==","$type","Polygon"],["==","class","wood"]],"layout":{"visibility":"visible"},"paint":{"fill-color":"rgb(220,224,220)","fill-opacity":{"base":1,"stops":[[8,0],[12,1]]}}},{"id":"waterway","type":"line","source":"openmaptiles","source-layer":"waterway","filter":["==","$type","LineString"],"layout":{"visibility":"visible"},"paint":{"line-color":"hsl(195, 17%, 78%)"}},{"id":"water_name","type":"symbol","source":"openmaptiles","source-layer":"water_name","filter":["all",["==","$type","LineString"],["!has","name:en"]],"layout":{"text-field":"{name:latin} {name:nonlatin}","symbol-placement":"line","text-rotation-alignment":"map","symbol-spacing":500,"text-font":["Metropolis Medium Italic","Klokantech Noto Sans Italic","Klokantech Noto Sans CJK Regular"],"text-size":12},"paint":{"text-color":"rgb(157,169,177)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"water_name-en","type":"symbol","source":"openmaptiles","source-layer":"water_name","filter":["all",["==","$type","LineString"],["has","name:en"]],"layout":{"text-field":"{name:en} {name:nonlatin}","symbol-placement":"line","text-rotation-alignment":"map","symbol-spacing":500,"text-font":["Metropolis Medium Italic","Klokantech Noto Sans Italic","Klokantech Noto Sans CJK Regular"],"text-size":12},"paint":{"text-color":"rgb(157,169,177)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"building","type":"fill","source":"openmaptiles","source-layer":"building","minzoom":12,"filter":["==","$type","Polygon"],"paint":{"fill-color":"rgb(234, 234, 229)","fill-outline-color":"rgb(219, 219, 218)","fill-antialias":true}},{"id":"tunnel_motorway_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,3],[20,40]]},"line-opacity":1}},{"id":"tunnel_motorway_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(234,234,234)","line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"aeroway-taxiway","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":12,"filter":["all",["in","class","taxiway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.55,"stops":[[13,1.8],[20,20]]},"line-opacity":1}},{"id":"aeroway-runway-casing","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":11,"filter":["all",["in","class","runway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.5,"stops":[[11,6],[17,55]]},"line-opacity":1}},{"id":"aeroway-area","type":"fill","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":4,"filter":["all",["==","$type","Polygon"],["in","class","runway","taxiway"]],"layout":{"visibility":"visible"},"paint":{"fill-opacity":{"base":1,"stops":[[13,0],[14,1]]},"fill-color":"rgba(255, 255, 255, 1)"}},{"id":"aeroway-runway","type":"line","metadata":{"mapbox:group":"1444849345966.4436"},"source":"openmaptiles","source-layer":"aeroway","minzoom":11,"filter":["all",["in","class","runway"],["==","$type","LineString"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgba(255, 255, 255, 1)","line-width":{"base":1.5,"stops":[[11,4],[17,50]]},"line-opacity":1},"maxzoom":24},{"id":"highway_path","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","filter":["all",["==","$type","LineString"],["==","class","path"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(234, 234, 234)","line-width":{"base":1.2,"stops":[[13,1],[20,10]]},"line-opacity":0.9}},{"id":"highway_minor","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":8,"filter":["all",["==","$type","LineString"],["in","class","minor","service","track"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsl(0, 0%, 88%)","line-width":{"base":1.55,"stops":[[13,1.8],[20,20]]},"line-opacity":0.9}},{"id":"highway_major_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-dasharray":[12,0],"line-width":{"base":1.3,"stops":[[10,3],[20,23]]}}},{"id":"highway_major_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-width":{"base":1.3,"stops":[[10,2],[20,20]]}}},{"id":"highway_major_subtle","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","maxzoom":11,"filter":["all",["==","$type","LineString"],["in","class","primary","secondary","tertiary","trunk"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsla(0, 0%, 85%, 0.69)","line-width":2}},{"id":"highway_motorway_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["!in","brunnel","bridge","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,3],[20,40]]},"line-dasharray":[2,0],"line-opacity":1}},{"id":"highway_motorway_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["!in","brunnel","bridge","tunnel"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"base":1,"stops":[[5.8,"hsla(0, 0%, 85%, 0.53)"],[6,"#fff"]]},"line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"highway_motorway_subtle","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","maxzoom":6,"filter":["all",["==","$type","LineString"],["==","class","motorway"]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"hsla(0, 0%, 85%, 0.53)","line-width":{"base":1.4,"stops":[[4,2],[6,1.3]]}}},{"id":"railway_service","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":16,"filter":["all",["==","$type","LineString"],["all",["==","class","rail"],["has","service"]]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#dddddd","line-width":3}},{"id":"railway_service_dashline","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":16,"filter":["all",["==","$type","LineString"],["==","class","rail"],["has","service"]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#fafafa","line-width":2,"line-dasharray":[3,3]}},{"id":"railway","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":13,"filter":["all",["==","$type","LineString"],["all",["!has","service"],["==","class","rail"]]],"layout":{"visibility":"visible","line-join":"round"},"paint":{"line-color":"#dddddd","line-width":{"base":1.3,"stops":[[16,3],[20,7]]}}},{"id":"railway_dashline","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"paint":{"line-color":"#fafafa","line-width":{"base":1.3,"stops":[[16,2],[20,6]]},"line-dasharray":[3,3]},"type":"line","source":"openmaptiles","source-layer":"transportation","minzoom":13,"filter":["all",["==","$type","LineString"],["all",["!has","service"],["==","class","rail"]]],"layout":{"visibility":"visible","line-join":"round"}},{"id":"highway_motorway_bridge_casing","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","bridge"],["==","class","motorway"]]],"layout":{"line-cap":"butt","line-join":"miter","visibility":"visible"},"paint":{"line-color":"rgb(213, 213, 213)","line-width":{"base":1.4,"stops":[[5.8,0],[6,5],[20,45]]},"line-dasharray":[2,0],"line-opacity":1}},{"id":"highway_motorway_bridge_inner","type":"line","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation","minzoom":6,"filter":["all",["==","$type","LineString"],["all",["==","brunnel","bridge"],["==","class","motorway"]]],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":{"base":1,"stops":[[5.8,"hsla(0, 0%, 85%, 0.53)"],[6,"#fff"]]},"line-width":{"base":1.4,"stops":[[4,2],[6,1.3],[20,30]]}}},{"id":"highway_name_other","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["!=","class","motorway"],["==","$type","LineString"],["!has","name:en"]],"layout":{"text-size":10,"text-max-angle":30,"text-transform":"uppercase","symbol-spacing":350,"text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"map","text-pitch-alignment":"viewport","text-field":"{name:latin} {name:nonlatin}"},"paint":{"text-color":"#bbb","text-halo-color":"#fff","text-translate":[0,0],"text-halo-width":2,"text-halo-blur":1}},{"id":"highway_name_other-en","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["!=","class","motorway"],["==","$type","LineString"],["has","name:en"]],"layout":{"text-size":10,"text-max-angle":30,"text-transform":"uppercase","symbol-spacing":350,"text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"map","text-pitch-alignment":"viewport","text-field":"{name:en} {name:nonlatin}"},"paint":{"text-color":"#bbb","text-halo-color":"#fff","text-translate":[0,0],"text-halo-width":2,"text-halo-blur":1}},{"id":"highway_name_motorway","type":"symbol","metadata":{"mapbox:group":"b6371a3f2f5a9932464fa3867530a2e5"},"source":"openmaptiles","source-layer":"transportation_name","filter":["all",["==","$type","LineString"],["==","class","motorway"]],"layout":{"text-size":10,"symbol-spacing":350,"text-font":["Metropolis Light","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"symbol-placement":"line","visibility":"visible","text-rotation-alignment":"viewport","text-pitch-alignment":"viewport","text-field":"{ref}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"hsl(0, 0%, 100%)","text-translate":[0,2],"text-halo-width":1,"text-halo-blur":1}},{"id":"boundary_state","type":"line","metadata":{"mapbox:group":"a14c9607bc7954ba1df7205bf660433f"},"source":"openmaptiles","source-layer":"boundary","filter":["==","admin_level",4],"layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"rgb(230, 204, 207)","line-width":{"base":1.3,"stops":[[3,1],[22,15]]},"line-blur":0.4,"line-dasharray":[2,2],"line-opacity":1}},{"id":"boundary_country","type":"line","metadata":{"mapbox:group":"a14c9607bc7954ba1df7205bf660433f"},"source":"openmaptiles","source-layer":"boundary","filter":["==","admin_level",2],"layout":{"line-cap":"round","line-join":"round"},"paint":{"line-color":"rgb(230, 204, 207)","line-width":{"base":1.1,"stops":[[3,1],[22,20]]},"line-blur":{"base":1,"stops":[[0,0.4],[22,4]]},"line-opacity":1}},{"id":"place_other","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["!in","class","city","suburb","town","village"],["==","$type","Point"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_other-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["!in","class","city","suburb","town","village"],["==","$type","Point"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_suburb","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","suburb"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_suburb-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","suburb"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"center","visibility":"visible","text-offset":[0.5,0],"text-anchor":"center","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_village","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["==","class","village"],["!has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":"left","text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_village-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["==","class","village"],["has","name:en"]],"layout":{"text-size":10,"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":"left","text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_town","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","town"],["!has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_town-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":15,"filter":["all",["==","$type","Point"],["==","class","town"],["has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["==","class","city"],[">","rank",3]],["!has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":14,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["==","class","city"],[">","rank",3]],["has","name:en"]],"layout":{"text-size":10,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_capital","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["==","capital",2],["==","class","city"]],["!has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"star-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":1,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_capital-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["==","capital",2],["==","class","city"]],["has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"star-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":1,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city_large","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["<=","rank",3],["==","class","city"]],["!has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:latin}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_city_large-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["all",["!=","capital",2],["<=","rank",3],["==","class","city"]],["has","name:en"]],"layout":{"text-size":14,"icon-image":{"base":1,"stops":[[0,"circle-11"],[8,""]]},"text-transform":"uppercase","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-justify":"left","visibility":"visible","text-offset":[0.5,0.2],"icon-size":0.4,"text-anchor":{"base":1,"stops":[[0,"left"],[8,"center"]]},"text-field":"{name:en}\n{name:nonlatin}"},"paint":{"text-color":"rgb(117, 129, 145)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1,"icon-opacity":0.7}},{"id":"place_state","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["==","class","state"],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":10},"paint":{"text-color":"rgb(113, 129, 144)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_state-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":12,"filter":["all",["==","$type","Point"],["==","class","state"],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":10},"paint":{"text-color":"rgb(113, 129, 144)","text-halo-color":"rgb(242,243,240)","text-halo-width":1,"text-halo-blur":1}},{"id":"place_country_other","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":8,"filter":["all",["==","$type","Point"],["all",["==","class","country"],[">=","rank",2]],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1,"stops":[[0,10],[6,12]]}},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_other-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":8,"filter":["all",["==","$type","Point"],["all",["==","class","country"],[">=","rank",2]],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1,"stops":[[0,10],[6,12]]}},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_major","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":6,"filter":["all",["==","$type","Point"],["all",["<=","rank",1],["==","class","country"]],["!has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:latin}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1.4,"stops":[[0,10],[3,12],[4,14]]},"text-anchor":"center"},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}},{"id":"place_country_major-en","type":"symbol","metadata":{"mapbox:group":"101da9f13b64a08fa4b6ac1168e89e5f"},"source":"openmaptiles","source-layer":"place","maxzoom":6,"filter":["all",["==","$type","Point"],["all",["<=","rank",1],["==","class","country"]],["has","name:en"]],"layout":{"visibility":"visible","text-field":"{name:en}\n{name:nonlatin}","text-font":["Metropolis Regular","Klokantech Noto Sans Regular","Klokantech Noto Sans CJK Regular"],"text-transform":"uppercase","text-size":{"base":1.4,"stops":[[0,10],[3,12],[4,14]]},"text-anchor":"center"},"paint":{"text-halo-width":1.4,"text-halo-color":"rgba(236,236,234,0.7)","text-color":{"base":1,"stops":[[3,"rgb(157,169,177)"],[4,"rgb(153, 153, 153)"]]}}}]} 2 | -------------------------------------------------------------------------------- /site/fmt.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | *, *:before, *:after { 5 | box-sizing: inherit; 6 | } 7 | 8 | .input { 9 | width: 100%; 10 | height: 120px; 11 | font-family: monospace; 12 | font-size: 13px; 13 | border: solid 1px #ccc; 14 | padding: 4px; 15 | resize: vertical; 16 | } 17 | 18 | .debug { 19 | background: #eee; 20 | border: solid 1px #ccc; 21 | border-top-width: 0; 22 | padding: 4px; 23 | } 24 | 25 | .mappings-generated { 26 | margin: 0; 27 | border: solid 1px #ccc; 28 | padding: 4px; 29 | } 30 | 31 | h2 { 32 | font-size: 1.2em; 33 | margin-bottom: 0.2em; 34 | } 35 | 36 | h3 { 37 | font-size: 1em; 38 | margin-bottom: 0.2em; 39 | } 40 | 41 | .mappings-source { 42 | margin: 0; 43 | border: solid 1px #ccc; 44 | padding: 4px; 45 | } 46 | 47 | .mapping { 48 | width: 1px; 49 | height: 1em; 50 | background: red; 51 | display: inline-block; 52 | position: absolute; 53 | z-index: 1; 54 | } 55 | 56 | .output { 57 | width: 100%; 58 | height: 120px; 59 | border: solid 1px #ccc; 60 | padding: 4px; 61 | margin: 0; 62 | } 63 | 64 | .debug-selected { 65 | background-color: hsla(60, 95%, 50%, 0.8); 66 | border-left: solid 1px #000; 67 | margin-left: -1px; 68 | } 69 | -------------------------------------------------------------------------------- /site/fmt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fmt 7 | 8 | 9 | 10 | 11 |
12 |

fmt

13 |

14 | An attempt at an auto formatter for simple-expr. 15 |

16 |

17 | The steps are 18 |

19 |
    20 |
  1. Compile the code
  2. 21 |
  3. Decompile the code again, whitespace will differ
  4. 22 |
  5. Replace cursor to correct position on selection with sourcemap
  6. 23 |
24 |
25 | 26 |

Formatter

27 |

28 | Reformat the code, moving the cursor will translate to the generated (reformated) code. 29 |

30 |

Original (editable)

31 | 36 | 37 |

Reformatted

38 |

39 |     
40 | 41 |

Source maps

42 |

43 | The red lines show the soure map markers. 44 |

45 |

Source

46 |

47 |     

Generated

48 |

49 | 
50 |     
51 |     
52 |   
53 | 
54 | 
55 | 


--------------------------------------------------------------------------------
/site/fmt.js:
--------------------------------------------------------------------------------
  1 | var simpleExpr = require("../");
  2 | var textblock = require("../lib/textblock");
  3 | var delegate = require('dom-delegate');
  4 | 
  5 | 
  6 | var inputEl  = document.querySelector(".input");
  7 | var debugEl  = document.querySelector(".debug");
  8 | var outputEl = document.querySelector(".output");
  9 | var mappingsSourceEl = document.querySelector(".mappings-source");
 10 | var mappingsGeneratedEl = document.querySelector(".mappings-generated");
 11 | 
 12 | var sourceMap = require("../lib/source-map");
 13 | 
 14 | 
 15 | function walk(node, parent, callback, opts={}) {
 16 |   if(node.type === "CallExpression") {
 17 |     const params = node.params;
 18 |     node = callback(node, parent);
 19 | 
 20 |     node.params = params.map(function(_node, idx) {
 21 |       return walk(_node, node, callback, {
 22 |         ...opts,
 23 |       })
 24 |     })
 25 | 
 26 |     return node;
 27 |   }
 28 |   else if(node.type === "FeatureRef") {
 29 |     return callback(node, parent);
 30 |   }
 31 |   else if(node.type === "StringLiteral") {
 32 |     return callback(node, parent);
 33 |   }
 34 |   else if(node.type === "NumberLiteral") {
 35 |     return callback(node, parent);
 36 |   }
 37 |   else {
 38 |     throw TypeError(node.type);
 39 |   }
 40 | }
 41 | 
 42 | function formatAst(ast) {
 43 |   if(ast.body[0]) {
 44 |     ast.body[0] = walk(ast.body[0], null, function(node, parent) {
 45 |       return node;
 46 |     })
 47 |   }
 48 |   else {
 49 |     return [];
 50 |   }
 51 |   return ast;
 52 | }
 53 | 
 54 | async function update() {
 55 |   var input = inputEl.value;
 56 | 
 57 |   var pos = {
 58 |     start: inputEl.selectionStart,
 59 |     end:   inputEl.selectionEnd
 60 |   };
 61 | 
 62 |   var orignalPos = textblock.indexToColRow(input, [pos.start, pos.end]);
 63 | 
 64 |   try {
 65 |     var tokens = simpleExpr.tokenize(input);
 66 |     var ast = simpleExpr.parse(tokens);
 67 | 
 68 |     const formattedAst = formatAst(ast);
 69 | 
 70 |     var newTokens = simpleExpr.unparse(formattedAst);
 71 |     var codeBits = simpleExpr.untokenize(newTokens);
 72 |     var code = codeBits.code;
 73 |     var map = codeBits.map;
 74 | 
 75 | 
 76 |     // Get the original position of the node
 77 |     const startGeneratedPos = await sourceMap.genOrigPosition(map, {
 78 |       line:   orignalPos[0].row+1,
 79 |       // IS this a HACK
 80 |       column: orignalPos[0].col,
 81 |     }, "foo.js");
 82 | 
 83 |     const endGeneratedPos = await sourceMap.genOrigPosition(map, {
 84 |       line:   orignalPos[1].row+1,
 85 |       // IS this a HACK
 86 |       column: orignalPos[1].col,
 87 |     }, "foo.js");
 88 | 
 89 |     var newPos = textblock.colRowToIndex(code, [
 90 |       {
 91 |         row: startGeneratedPos.line-1,
 92 |         col: startGeneratedPos.column
 93 |       },
 94 |       {
 95 |         row: endGeneratedPos.line-1,
 96 |         col: endGeneratedPos.column
 97 |       },
 98 |     ])
 99 | 
100 |     if(newPos.length > 0) {
101 |       if(newPos.length < 2) {
102 |         newPos[1] = code.length;
103 |       }
104 | 
105 |       var hlCode = code.substring(0, newPos[0])
106 |         + ""+code.substring(newPos[0], newPos[1])+""
107 |         + code.substring(newPos[1])
108 |     }
109 | 
110 | 
111 |     var mappings = await sourceMap.getMappings(map)
112 | 
113 |     function insertIntoString(orig, idx, text) {
114 |       return orig.substring(0, idx) + text + orig.substring(idx);
115 |     }
116 | 
117 |     var offset = 0;
118 |     var mappedCode = code + "";
119 |     mappings.forEach(function(mapping, idx) {
120 |       var opts = {row: mapping.generatedLine-1, col: mapping.generatedColumn}
121 |       var pos = textblock.colRowToIndex(code, [opts])[0];
122 | 
123 |       pos += offset;
124 |       var toInsert = "";
125 |       offset += toInsert.length;
126 |       mappedCode = insertIntoString(mappedCode, pos, toInsert)
127 |     })
128 | 
129 |     mappingsGeneratedEl.innerHTML = mappedCode;
130 | 
131 |     var offset = 0;
132 |     var origCode = input + "";
133 |     mappings.forEach(function(mapping, idx) {
134 |       var opts = {row: mapping.originalLine-1, col: mapping.originalColumn}
135 |       var pos = textblock.colRowToIndex(input, [opts])[0];
136 | 
137 |       pos += offset;
138 |       var toInsert = "";
139 |       offset += toInsert.length;
140 |       origCode = insertIntoString(origCode, pos, toInsert)
141 |     })
142 | 
143 |     mappingsSourceEl.innerHTML = origCode;
144 | 
145 |     outputEl.innerHTML = hlCode;
146 |   }
147 |   catch(err) {
148 |     console.error("Error [%s, %s]:", err.line, err.column, String(err));
149 |     outputEl.innerHTML = "ERR: "+err;
150 |   }
151 | 
152 |   debugEl.innerHTML = "cursor: "+JSON.stringify(newPos);
153 | 
154 | }
155 | 
156 | inputEl.addEventListener("keydown", function(e) {
157 |   // // Allow tabs
158 |   // if(e.keyCode === 9) {
159 | 		// var start = this.selectionStart;
160 | 		// var end = this.selectionEnd;
161 | 
162 | 		// var value = inputEl.value;
163 | 
164 | 		// inputEl.value = value.substring(0, start)
165 | 			// + "  "
166 | 			// + value.substring(end);
167 | 
168 | 		// // put caret at right position again (add one for the tab)
169 | 		// inputEl.selectionStart = inputEl.selectionEnd = start + 2;
170 | 
171 | 		// // prevent the focus lose
172 | 		// e.preventDefault();
173 |   // }
174 | });
175 | 
176 | inputEl.addEventListener("keyup", update);
177 | inputEl.addEventListener("keydown", update);
178 | inputEl.addEventListener("click", update);
179 | 
180 | var del = delegate(document.body);
181 | del.on("mouseover", ".mapping", function(e, target) {
182 |   var type = target.getAttribute("data-type");
183 |   var idx  = target.getAttribute("data-idx");
184 | })
185 | 
186 | var downHdl;
187 | inputEl.addEventListener("mousedown", function() {
188 |   downHdl = setInterval(update, 1000/60);
189 | });
190 | 
191 | document.addEventListener("mouseup", function() {
192 |   clearInterval(downHdl);
193 | });
194 | update();
195 | 


--------------------------------------------------------------------------------
/site/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     simple-expr
 7 |     
 8 |   
 9 |   
10 | 
11 |     
12 |
13 |
14 |

simple-expr

15 |
Simple expression language for mapbox-gl-js expressions
16 |
17 | 28 |
29 |
30 |
31 |
32 |
33 | Simple-expr editor 34 | 36 |
37 |
38 | Defines a circle-color property for layer without filter for the below GeoJSON source 39 |
40 | 47 |
48 |
49 |
MapboxGL expression
50 |
51 |
52 |               
53 |
54 |
55 |
56 |
57 |
58 |
GeoJSON
59 |
60 |
61 |               
62 |
63 |
64 |
65 |
Map with expression
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var readmeTester = require("readme-tester"); 3 | var simpleExpr = require("../"); 4 | 5 | 6 | const tests = [ 7 | { 8 | name: "number: integer", 9 | input: ` 10 | number(1) 11 | `, 12 | output: ["number", 1] 13 | }, 14 | { 15 | name: "number: integer with +sign", 16 | input: ` 17 | number(+1) 18 | `, 19 | decompiled: ` 20 | number(1) 21 | `, 22 | output: ["number", 1] 23 | }, 24 | { 25 | name: "number: integer with -sign", 26 | input: ` 27 | number(-1) 28 | `, 29 | output: ["number", -1] 30 | }, 31 | { 32 | name: "number: float", 33 | input: ` 34 | number(3.14) 35 | `, 36 | output: ["number", 3.14] 37 | }, 38 | { 39 | name: "number: float with +sign", 40 | input: ` 41 | number(+3.14) 42 | `, 43 | decompiled: ` 44 | number(3.14) 45 | `, 46 | output: ["number", 3.14] 47 | }, 48 | { 49 | name: "number: float with -sign", 50 | input: ` 51 | number(-3.14) 52 | `, 53 | output: ["number", -3.14] 54 | }, 55 | { 56 | name: "number: invalid float", 57 | input: ` 58 | number(3.14.3) 59 | `, 60 | throw: true 61 | }, 62 | { 63 | name: "string: simple", 64 | input: ` 65 | string("hello") 66 | `, 67 | output: ["string", "hello"] 68 | }, 69 | { 70 | name: "string: escaped", 71 | input: ` 72 | string("\\\"") 73 | `, 74 | output: ["string", "\\\""] 75 | }, 76 | { 77 | name: "string: missing quote", 78 | input: ` 79 | concat("Hello","world) 80 | `, 81 | throw: true 82 | }, 83 | { 84 | name: "function: single arg", 85 | input: ` 86 | foo(1) 87 | `, 88 | output: [ 89 | "foo", 1 90 | ] 91 | }, 92 | { 93 | name: "function: args with spaces", 94 | input: ` 95 | foo( 96 | 1, 97 | bar( 1 , 2 ), 98 | @score 99 | ) 100 | `, 101 | decompiled: `foo(1,bar(1,2),@score)`, 102 | output: [ 103 | "foo", 1, ["bar", 1, 2], ["get", "score"] 104 | ] 105 | }, 106 | { 107 | name: "function: multiple args", 108 | input: ` 109 | foo(1,"2",3,"four") 110 | `, 111 | output: [ 112 | "foo", 1, "2", 3, "four" 113 | ] 114 | }, 115 | { 116 | name: "function: nested function", 117 | input: ` 118 | foo(1,"2",bar(3,baz("four"))) 119 | `, 120 | output: [ 121 | "foo", 1, "2", ["bar", 3, ["baz", "four"]] 122 | ] 123 | }, 124 | { 125 | name: "function: multiple top-level functions", 126 | input: ` 127 | foo() 128 | bar() 129 | `, 130 | throw: true 131 | }, 132 | { 133 | name: "function: no top-level function", 134 | input: ` 135 | `, 136 | output: [ 137 | ] 138 | }, 139 | { 140 | name: "function: missing arg separator", 141 | input: ` 142 | concat("hello" "world") 143 | `, 144 | throw: true, 145 | }, 146 | { 147 | name: "function: context references", 148 | input: ` 149 | rgb(@score,@rank,0) 150 | `, 151 | output: [ 152 | "rgb", ["get", "score"], ["get", "rank"], 0 153 | ] 154 | }, 155 | { 156 | name: "function: context references (alt)", 157 | input: ` 158 | rgb(0,@score,@rank) 159 | `, 160 | output: [ 161 | "rgb", 0, ["get", "score"], ["get", "rank"] 162 | ] 163 | }, 164 | { 165 | name: "function: missing paren", 166 | input: ` 167 | rgb(0, @score, @rank 168 | `, 169 | throw: true 170 | }, 171 | { 172 | name: "function: with numbers", 173 | input: ` 174 | log10(1) 175 | `, 176 | output: [ 177 | "log10", 1 178 | ] 179 | }, 180 | { 181 | name: "function: with starting number", 182 | input: ` 183 | 1func(1) 184 | `, 185 | throw: true 186 | }, 187 | { 188 | name: "function: with dashes", 189 | input: ` 190 | to-color(1) 191 | `, 192 | output: [ 193 | "to-color", 1 194 | ] 195 | }, 196 | { 197 | name: "function: additional close paran", 198 | input: ` 199 | number(1)) 200 | `, 201 | throw: true 202 | }, 203 | ] 204 | 205 | var awkwardNames = [ 206 | {name: "!"}, 207 | {name: "!="}, 208 | {name: "<"}, 209 | {name: "<="}, 210 | {name: "=="}, 211 | {name: ">"}, 212 | {name: ">="}, 213 | {name: "-", skip: true}, 214 | {name: "*"}, 215 | {name: "/"}, 216 | {name: "%"}, 217 | {name: "^"}, 218 | {name: "+", skip: true}, 219 | {name: "e"} 220 | ]; 221 | awkwardNames.forEach(function(def) { 222 | var name = def.name; 223 | tests.push({ 224 | name: "function: awkward name: "+name, 225 | input: name+"(0,1)", 226 | skip: def.skip, 227 | only: def.only, 228 | output: [ 229 | name, 0, 1 230 | ] 231 | }) 232 | }) 233 | 234 | 235 | function buildTest(test, runner) { 236 | var name = test.name; 237 | if(test.throw) { 238 | name += " (throws error)"; 239 | } 240 | 241 | function fn() { 242 | var err, out; 243 | try { 244 | out = runner.fn(); 245 | } 246 | catch(_err) { 247 | err = _err; 248 | } 249 | 250 | if(test.throw) { 251 | assert(err); 252 | } 253 | else if(err) { 254 | throw err; 255 | } 256 | else { 257 | runner.assertion(out); 258 | } 259 | } 260 | 261 | if(test.only) { 262 | it.only(name, fn); 263 | } 264 | else if(test.skip) { 265 | it.skip(name, fn); 266 | } 267 | else { 268 | it(name, fn); 269 | } 270 | } 271 | 272 | 273 | describe("simple-expr", function() { 274 | describe("compile", function() { 275 | tests.forEach(function(test) { 276 | buildTest(test, { 277 | fn: function() { 278 | return simpleExpr.compile(test.input); 279 | }, 280 | assertion: function(actual) { 281 | assert.deepEqual(actual, test.output) 282 | } 283 | }) 284 | }) 285 | }) 286 | 287 | describe("decompile", function() { 288 | tests.forEach(function(test) { 289 | buildTest(test, { 290 | fn: function() { 291 | return simpleExpr.decompile(simpleExpr.compile(test.input)); 292 | }, 293 | assertion: function(actual) { 294 | var expected = test.decompiled || test.input; 295 | expected = expected 296 | .replace(/^\s+/, "") 297 | .replace(/\s+$/, "") 298 | 299 | assert.deepEqual(actual, expected) 300 | } 301 | }) 302 | }) 303 | }) 304 | 305 | describe("stringify", null, function() { 306 | tests.forEach(function(test) { 307 | it(test.name, function() { 308 | assert.deepEqual( 309 | simpleExpr.stringify(test.output, {newline: false}), 310 | test.input 311 | ) 312 | }) 313 | }) 314 | }) 315 | 316 | describe("cli", function() { 317 | it("parse") 318 | it("compile") 319 | it("decompile") 320 | it("execute") 321 | }) 322 | 323 | it("README.md", function(done) { 324 | this.timeout(10*1000); 325 | readmeTester(__dirname+"/../README.md", {bash: true}, function(err, assertions) { 326 | assert.ifError(err); 327 | done(err); 328 | }); 329 | }) 330 | }) 331 | -------------------------------------------------------------------------------- /test/perf.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var simpleExr = require("../"); 3 | 4 | 5 | var suite = new Benchmark.Suite; 6 | 7 | var code = [ 8 | { 9 | name: "simple", 10 | code: `concat("Hello", " ", @name)` 11 | }, 12 | { 13 | name: "complex", 14 | code: ` 15 | interpolate( 16 | linear(), @score, 17 | 0, rgb(255, 0, 0), 18 | 50, rgb(0, 255, 0), 19 | 100, rgb(0, 0, 255) 20 | ) 21 | ` 22 | }, 23 | ] 24 | 25 | // Add in compiled code. 26 | code.forEach(function(item) { 27 | item.json = simpleExr.compiler(item.code); 28 | }) 29 | 30 | // Setup the benchmarks 31 | code.forEach(function(item) { 32 | var name = item.name; 33 | var code = item.code; 34 | var json = item.json; 35 | 36 | suite.add(`simpleExr.compile(${name})`, function() { 37 | simpleExr.compiler(code); 38 | }) 39 | 40 | suite.add(`simpleExr.decompile(${name})`, function() { 41 | simpleExr.decompile(json); 42 | }) 43 | }) 44 | 45 | // Setup logging 46 | suite 47 | .on('cycle', function(event) { 48 | console.log(String(event.target)); 49 | }) 50 | .run({ 'async': true }); 51 | 52 | --------------------------------------------------------------------------------