├── .eslintrc ├── .gitignore ├── .npmignore ├── API.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── STYLE.md ├── _config.mb-pages.yml ├── _config.yml ├── bin ├── gl-style-composite ├── gl-style-format ├── gl-style-migrate └── gl-style-validate ├── docs ├── _generate │ ├── generate.js │ ├── index.html │ └── item.html ├── _layouts │ └── default.html └── site.css ├── index.js ├── lib ├── composite.js ├── diff.js ├── error │ ├── parsing_error.js │ └── validation_error.js ├── format.js ├── migrate.js ├── util │ ├── extend.js │ ├── get_type.js │ └── unbundle_jsonlint.js ├── validate │ ├── latest.js │ ├── validate.js │ ├── validate_array.js │ ├── validate_boolean.js │ ├── validate_color.js │ ├── validate_constants.js │ ├── validate_enum.js │ ├── validate_filter.js │ ├── validate_function.js │ ├── validate_glyphs_url.js │ ├── validate_layer.js │ ├── validate_layout_property.js │ ├── validate_light.js │ ├── validate_number.js │ ├── validate_object.js │ ├── validate_paint_property.js │ ├── validate_source.js │ └── validate_string.js ├── validate_style.js └── validate_style.min.js ├── migrations ├── v7.js └── v8.js ├── minify.js ├── package.json ├── reference ├── latest.js ├── latest.min.js ├── v6.json ├── v6.min.json ├── v7.json ├── v7.min.json ├── v8.json └── v8.min.json └── test ├── composite.js ├── diff.js ├── fixture ├── bad-color.input.json ├── bad-color.output.json ├── constants-v7.input.json ├── constants-v7.output.json ├── constants-v8.input.json ├── constants-v8.output.json ├── extrakeys.input.json ├── extrakeys.output.json ├── filters.input.json ├── filters.output.json ├── functions.input.json ├── functions.output.json ├── invalidjson.input.json ├── invalidjson.output.json ├── layers.input.json ├── layers.output.json ├── light.input.json ├── light.output.json ├── malformed-glyphs-type.input.json ├── malformed-glyphs-type.output.json ├── malformed-glyphs.input.json ├── malformed-glyphs.output.json ├── map-properties.input.json ├── map-properties.output.json ├── metadata.input.json ├── metadata.output.json ├── missing-glyphs.input.json ├── missing-glyphs.output.json ├── missing-sprite.input.json ├── missing-sprite.output.json ├── no-sources.input.json ├── no-sources.output.json ├── pitch.input.json ├── pitch.output.json ├── properties.input.json ├── properties.output.json ├── required.input.json ├── required.output.json ├── root-properties.input.json ├── root-properties.output.json ├── sources.input.json ├── sources.output.json ├── text-font.input.json ├── text-font.output.json ├── unknown-keys-nested.input.json ├── unknown-keys-nested.output.json ├── v6.input.json ├── v6.output.json └── v7-migrate │ ├── style-basic.input.json │ └── style-basic.output.json ├── format.js ├── migrate.js ├── migrations ├── v7.js └── v8.js ├── spec.js └── validate.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "space-after-function-name": 2, 4 | "space-before-blocks": 2, 5 | "space-after-keywords": 2, 6 | "space-unary-ops": 2, 7 | "no-use-before-define": [2, "nofunc"], 8 | "camelcase": 0, 9 | "comma-style": 2, 10 | "eqeqeq": 0, 11 | "new-cap": 2, 12 | "no-new": 2, 13 | "no-multi-spaces": 2, 14 | "brace-style": 2, 15 | "no-underscore-dangle": [0], 16 | "no-self-compare": 2, 17 | "no-void": 2, 18 | "wrap-iife": 2, 19 | "no-eq-null": 2, 20 | "quotes": [0], 21 | "curly": 0, 22 | "dot-notation": [0], 23 | "no-shadow": 0 24 | }, 25 | "env": { 26 | "node": true, 27 | "browser": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | node_modules 3 | npm-debug.log 4 | /docs/index.html 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | _site 3 | node_modules 4 | /docs/_generate/node_modules 5 | CHANGELOG.md 6 | README.md 7 | STYLE.md 8 | API.md 9 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | ### `diffStyles([before], after)` 3 | 4 | Diff two stylesheet 5 | 6 | Creates semanticly aware diffs that can easily be applied at runtime. 7 | Operations produced by the diff closely resemble the mapbox-gl-js API. Any 8 | error creating the diff will fall back to the 'setStyle' operation. 9 | 10 | Example diff: 11 | [ 12 | { command: 'setConstant', args: ['@water', '#0000FF'] }, 13 | { command: 'setPaintProperty', args: ['background', 'background-color', 'black'] } 14 | ] 15 | 16 | 17 | ### Parameters 18 | 19 | | parameter | type | description | 20 | | ---------- | ---- | -------------------------------------- | 21 | | `[before]` | | _optional:_ stylesheet to compare from | 22 | | `after` | | stylesheet to compare to | 23 | 24 | 25 | 26 | **Returns** `rra`, list of changes 27 | 28 | 29 | ### `format(style, [space])` 30 | 31 | Format a Mapbox GL Style. Returns a stringified style with its keys 32 | sorted in the same order as the reference style. 33 | 34 | The optional `space` argument is passed to 35 | [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) 36 | to generate formatted output. 37 | 38 | If `space` is unspecified, a default of `2` spaces will be used. 39 | 40 | 41 | ### Parameters 42 | 43 | | parameter | type | description | 44 | | --------- | ------ | ------------------------------------------------------ | 45 | | `style` | Object | a Mapbox GL Style | 46 | | `[space]` | number | _optional:_ space argument to pass to `JSON.stringify` | 47 | 48 | 49 | ### Example 50 | 51 | ```js 52 | var fs = require('fs'); 53 | var format = require('mapbox-gl-style-spec').format; 54 | var style = fs.readFileSync('./source.json', 'utf8'); 55 | fs.writeFileSync('./dest.json', format(style)); 56 | fs.writeFileSync('./dest.min.json', format(style, 0)); 57 | ``` 58 | 59 | 60 | **Returns** `string`, stringified formatted JSON 61 | 62 | 63 | ### `migrate(style)` 64 | 65 | Migrate a Mapbox GL Style to the latest version. 66 | 67 | 68 | ### Parameters 69 | 70 | | parameter | type | description | 71 | | --------- | ------ | ----------------- | 72 | | `style` | object | a Mapbox GL Style | 73 | 74 | 75 | ### Example 76 | 77 | ```js 78 | var fs = require('fs'); 79 | var migrate = require('mapbox-gl-style-spec').migrate; 80 | var style = fs.readFileSync('./style.json', 'utf8'); 81 | fs.writeFileSync('./style.json', JSON.stringify(migrate(style))); 82 | ``` 83 | 84 | 85 | **Returns** `Object`, a migrated style 86 | 87 | 88 | ### `validate(style, [styleSpec])` 89 | 90 | Validate a Mapbox GL style against the style specification. 91 | 92 | 93 | ### Parameters 94 | 95 | | parameter | type | description | 96 | | ------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | 97 | | `style` | Object\,String\,Buffer | The style to be validated. If a `String` or `Buffer` is provided, the returned errors will contain line numbers. | 98 | | `[styleSpec]` | Object | _optional:_ The style specification to validate against. If omitted, the spec version is inferred from the stylesheet. | 99 | 100 | 101 | ### Example 102 | 103 | ```js 104 | var validate = require('mapbox-gl-style-spec').validate; 105 | var style = fs.readFileSync('./style.json', 'utf8'); 106 | var errors = validate(style); 107 | ``` 108 | 109 | 110 | **Returns** `Array.`, 111 | 112 | 113 | ### `validateStyleMin(style, [styleSpec])` 114 | 115 | Validate a Mapbox GL style against the style specification. This entrypoint, 116 | `mapbox-gl-style-spec/lib/validate_style.min`, is designed to produce as 117 | small a browserify bundle as possible by omitting unnecessary functionality 118 | and legacy style specifications. 119 | 120 | 121 | ### Parameters 122 | 123 | | parameter | type | description | 124 | | ------------- | ------ | ------------------------------------------------------------------------------------------------------- | 125 | | `style` | Object | The style to be validated. | 126 | | `[styleSpec]` | Object | _optional:_ The style specification to validate against. If omitted, the latest style spec is used. | 127 | 128 | 129 | ### Example 130 | 131 | ```js 132 | var validate = require('mapbox-gl-style-spec/lib/validate_style.min'); 133 | var errors = validate(style); 134 | ``` 135 | 136 | 137 | **Returns** `Array.`, 138 | 139 | 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master 2 | 3 | ## 8.9.0 4 | 5 | v8.0.0 styles are fully compatible with v8.9.0. 6 | 7 | * Added identity functions 8 | * Added `auto` value which represents the calculated default value 9 | 10 | ## 8.8.1 11 | 12 | v8.0.0 styles are fully compatible with v8.8.1. 13 | 14 | * Fixed style validation for layers with invalid types 15 | 16 | ## 8.8.0 17 | 18 | v8.0.0 styles are fully compatible with v8.8.0. 19 | 20 | * Clarified documentation around fill-opacity. 21 | * Update function documentation and validation for property functions. 22 | * Add text-pitch-alignment property. 23 | * Add icon-text-fit, icon-text-fit-padding properties. 24 | 25 | ## 8.7.0 26 | 27 | v8.0.0 styles are fully compatible with v8.7.0. 28 | 29 | * Add support for has / !has operators. 30 | 31 | ## 8.6.0 32 | 33 | v8.0.0 styles are fully compatible with v8.6.0. 34 | 35 | * Added support for zoom and feature driven functions. 36 | 37 | ## 8.4.2 38 | 39 | v8.0.0 styles are fully compatible with v8.4.2. 40 | 41 | * Refactored style validator to expose more granular validation methods 42 | 43 | ## 8.4.1 44 | 45 | v8.0.0 styles are fully compatible with v8.4.1. 46 | 47 | * Revert ramp validation checks that broke some styles. 48 | 49 | ## 8.4.0 50 | 51 | v8.0.0 styles are fully compatible with v8.4.0. 52 | 53 | * Added `cluster`, `clusterRadius`, `clusterMaxZoom` GeoJSON source properties. 54 | 55 | ## 8.3.0 56 | 57 | v8.0.0 styles are fully compatible with v8.3.0. 58 | 59 | * Added `line-offset` style property 60 | 61 | ## 8.2.1 62 | 63 | v8.0.0 styles are fully compatible with v8.2.1. 64 | 65 | * Enforce that all layers that use a vector source specify a "source-layer" 66 | 67 | ## 8.2.0 68 | 69 | v8.0.0 styles are fully compatible with v8.2.0. 70 | 71 | * Add inline `example` property. 72 | * Enforce that all style properties must have documentation in `doc` property. 73 | * Create minified style specs with `doc` and `example` properties removed. 74 | * `validate` now validates against minified style spec. 75 | * `format` now accepts `space` option to use with `JSON.stringify`. 76 | * Remove `gl-style-spritify`. Mapbox GL sprites are now created automatically by 77 | the Mapbox style APIs, or for hand-crafted styles, by [spritezero-cli](https://github.com/mapbox/spritezero-cli). 78 | 79 | ## 8.1.0 80 | 81 | v8.0.0 styles are fully compatible with v8.1.0. 82 | 83 | * [BREAKING] Simplified layout/paint layer property types to more closely align 84 | with v7 types. 85 | * Fixed migration script compatibility with newer versions of Node.js and io.js 86 | * Removed `constants` from schema, they were deprecated in v8 87 | * Added style diff utility to generate semantic deltas between two stylesheets 88 | * Added `visibility` property to `circle` layer type 89 | * Added `pitch` property to stylesheet 90 | 91 | ## 8.0.0 92 | 93 | Introduction of Mapbox GL style specification v8. To migrate a v7 style to v8, 94 | use the `gl-style-migrate` script as described in the README. 95 | 96 | * [BREAKING] The value of the `text-font` property is now an array of 97 | strings, rather than a single comma separated string. 98 | * [BREAKING] Renamed `symbol-min-distance` to `symbol-spacing`. 99 | * [BREAKING] Renamed `background-image` to `background-pattern`. 100 | * [BREAKING] Renamed `line-image` to `line-pattern`. 101 | * [BREAKING] Renamed `fill-image` to `fill-pattern`. 102 | * [BREAKING] Renamed the `url` property of the video source type to `urls`. 103 | * [BREAKING] Coordinates in video sources are now specified in [lon, lat] order. 104 | * [BREAKING] Removed `text-max-size` and `icon-max-size` properties; these 105 | are now calculated automatically. 106 | * [BREAKING] `text-size` and `icon-size` are now layout properties instead of paint properties. 107 | * [BREAKING] Constants are no longer supported. If you are editing styles by 108 | hand and want to use constants, you can use a preprocessing step with a tool 109 | like [ScreeSS](https://github.com/screee/screess). 110 | * [BREAKING] The format for `mapbox://` glyphs URLs has changed; you should 111 | now use `mapbox://fonts/mapbox/{fontstack}/{range}.pbf`. 112 | * [BREAKING] Reversed the priority of layers for calculating label placement: 113 | labels for layers that appear later in the style now have priority over earlier 114 | layers. 115 | * Added a new `image` source type. 116 | * Added a new `circle` layer type. 117 | * Default map center location can now be set in the style. 118 | * Added `mapbox://` sprite URLs `mapbox://sprite/{user | "mapbox"}/{id}` 119 | 120 | ## 7.5.0 121 | 122 | * Added gl-style-composite script, for auto-compositing sources in a style. 123 | 124 | ## 7.4.1 125 | 126 | * Use JSON.stringify for formatting instead of js-beautify 127 | 128 | ## 7.0.0 129 | 130 | Introduction of Mapbox GL style specification v7. 131 | 132 | * [BREAKING] Improve dashed lines (#234) 133 | * [BREAKING] Remove prerendered layers (#232) 134 | * Explicit visibility property (#212) 135 | * Functions for all properties (#237) 136 | 137 | ## 6.0.0 (Style spec v6) 138 | 139 | Introduction of Mapbox GL style specification v6. 140 | 141 | * [BREAKING] New filter syntax (#178) 142 | * [BREAKING] Line gap property (#131) 143 | * [BREAKING] Remove dashes from min/max-zoom (#175) 144 | * [BREAKING] New layout/paint terminology (#166) 145 | * [BREAKING] Single text positioning property (#197) 146 | * Added requirements (#200) 147 | * Added minimum, maximum, and period values (#198) 148 | 149 | ## 0.0.5 (in progress) 150 | 151 | * [BREAKING] Switch to suffix for transition properties (`transition-*` -> `*-transition`). 152 | * Added support for remote, non-Mapbox TileJSON sources. 153 | * [BREAKING] Source `minZoom` and `maxZoom` renamed to `minzoom` and `maxzoom to match TileJSON. 154 | * Added support for `mapbox://` glyph URLs. 155 | * [BREAKING] Renamed `raster-fade` to `raster-fade-duration`. 156 | * Added background-opacity property. 157 | * Added "tokens" property to string values that can autocomplete fields from layers 158 | * Added "units" property to describe value types 159 | 160 | ## 0.0.4 (Aug 8 2014) 161 | 162 | * Initial public release 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014, Mapbox 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapbox GL Spec & Lint 2 | 3 | [![Circle CI](https://circleci.com/gh/mapbox/mapbox-gl-style-spec.svg?style=svg)](https://circleci.com/gh/mapbox/mapbox-gl-style-spec) 4 | 5 | GL style spec, validation, and migration scripts for [mapbox-gl-js](https://github.com/mapbox/mapbox-gl-js) and 6 | [mapbox-gl-native](https://github.com/mapbox/mapbox-gl-native). 7 | 8 | ### Install 9 | 10 | npm install -g mapbox-gl-style-spec 11 | 12 | Provides the utilities: 13 | 14 | * `gl-style-migrate` 15 | * `gl-style-format` 16 | * `gl-style-validate` 17 | 18 | ### Validation 19 | 20 | ```bash 21 | $ gl-style-validate style.json 22 | ``` 23 | 24 | Will validate the given style JSON and print errors to stdout. Provide a 25 | `--json` flag to get JSON output. 26 | 27 | ### Migrations 28 | 29 | This repo contains scripts for migrating GL styles of any version to the latest version 30 | (currently v8). Migrate a style like this: 31 | 32 | ```bash 33 | $ gl-style-migrate bright-v7.json > bright-v8.json 34 | ``` 35 | 36 | To migrate a file in place, you can use the `sponge` utility from the `moreutils` package: 37 | 38 | ```bash 39 | $ brew install moreutils 40 | $ gl-style-migrate bright.json | sponge bright.json 41 | ``` 42 | 43 | ### [API](API.md) 44 | 45 | ## Tests 46 | 47 | To run tests: 48 | 49 | npm install 50 | npm test 51 | 52 | To update test fixtures 53 | 54 | UPDATE=true npm test 55 | 56 | ### Documentation 57 | 58 | * The utility reference page [API.md](API.md) is generated automatically from inline source documentation. 59 | * The style reference page exists here: `docs/_generate/index.html` and can be edited directly. 60 | 61 | To view the documentation, run 62 | 63 | ```bash 64 | npm start 65 | ``` 66 | 67 | and open the served page 68 | 69 | ```bash 70 | open http://127.0.0.1:4000/mapbox-gl-style-spec 71 | ``` 72 | -------------------------------------------------------------------------------- /STYLE.md: -------------------------------------------------------------------------------- 1 | # gl-style-spec 2 | 3 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 4 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in 5 | this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 6 | 7 | ## 1. Purpose 8 | 9 | This specification attempts to create a standard for styling vector data across multiple clients. 10 | 11 | ## 2. File Format 12 | 13 | Style files must be valid [JSON](http://www.json.org/). 14 | 15 | ## 3. Internal Structure 16 | 17 | Styles must have a `version` property equal to the current specification 18 | version as a **string**. 19 | 20 | Styles must have a `buckets` property as an **object** with mappings from 21 | names to bucket properties. 22 | 23 | Styles must have a `layers` property as an **array**. 24 | 25 | ### Buckets 26 | 27 | Each bucket must have property **filter** that defines its filter from the 28 | dataset. 29 | -------------------------------------------------------------------------------- /_config.mb-pages.yml: -------------------------------------------------------------------------------- 1 | url: https://www.mapbox.com 2 | api: https://www.mapbox.com/core 3 | tileApi: https://api.tiles.mapbox.com 4 | source: docs 5 | permalink: / 6 | baseurl: /mapbox-gl-style-spec 7 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | url: https://122e4e-mapbox.global.ssl.fastly.net 2 | api: https://122e4e-mapbox.global.ssl.fastly.net/core 3 | tileApi: https://api-maps-staging.tilestream.net 4 | source: docs 5 | permalink: / 6 | baseurl: /mapbox-gl-style-spec 7 | -------------------------------------------------------------------------------- /bin/gl-style-composite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'), 6 | argv = require('minimist')(process.argv.slice(2)), 7 | format = require('../').format, 8 | composite = require('../').composite; 9 | 10 | console.log(format(composite(JSON.parse(fs.readFileSync(argv._[0]))))); 11 | -------------------------------------------------------------------------------- /bin/gl-style-format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'), 6 | argv = require('minimist')(process.argv.slice(2)), 7 | format = require('../').format; 8 | 9 | if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) { 10 | return help(); 11 | } 12 | 13 | console.log(format(JSON.parse(fs.readFileSync(argv._[0])), argv.space)); 14 | 15 | function help() { 16 | console.log('usage:'); 17 | console.log(' gl-style-format source.json > destination.json'); 18 | console.log(''); 19 | console.log('options:'); 20 | console.log(' --space '); 21 | console.log(' Number of spaces in output (default "2")'); 22 | console.log(' Pass "0" for minified output.'); 23 | } 24 | -------------------------------------------------------------------------------- /bin/gl-style-migrate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var fs = require('fs'), 6 | argv = require('minimist')(process.argv.slice(2)), 7 | format = require('../').format, 8 | migrate = require('../').migrate; 9 | 10 | console.log(format(migrate(JSON.parse(fs.readFileSync(argv._[0]))))); 11 | -------------------------------------------------------------------------------- /bin/gl-style-validate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var argv = require('minimist')(process.argv.slice(2), { 6 | boolean: 'json' 7 | }), 8 | validate = require('../').validate, 9 | rw = require('rw'), 10 | status = 0; 11 | 12 | if (argv.help || argv.h || (!argv._.length && process.stdin.isTTY)) { 13 | return help(); 14 | } 15 | 16 | if (!argv._.length) { 17 | argv._.push('/dev/stdin'); 18 | } 19 | 20 | argv._.forEach(function(file) { 21 | var errors = validate(rw.readFileSync(file, 'utf8')); 22 | if (errors.length) { 23 | if (argv.json) { 24 | process.stdout.write(JSON.stringify(errors, null, 2)); 25 | } else { 26 | errors.forEach(function (e) { 27 | console.log('%s:%d: %s', file, e.line, e.message); 28 | }); 29 | } 30 | status = 1; 31 | } 32 | }); 33 | 34 | process.exit(status); 35 | 36 | function help() { 37 | console.log('usage:'); 38 | console.log(' gl-style-validate file.json'); 39 | console.log(' gl-style-validate < file.json'); 40 | console.log(''); 41 | console.log('options:'); 42 | console.log('--json output errors as json'); 43 | } 44 | -------------------------------------------------------------------------------- /docs/_generate/generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // GL style reference generator 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var ref = require('../..').latest; 8 | var _ = require('lodash'); 9 | var remark = require('remark'); 10 | var html = require('remark-html'); 11 | 12 | function tmpl(x, options) { 13 | return _.template(fs.readFileSync(path.join(__dirname, x), 'utf-8'), options); 14 | } 15 | 16 | var index = tmpl('index.html', { 17 | imports: { 18 | _: _, 19 | item: tmpl('item.html', { 20 | imports: { 21 | _: _, 22 | md: function(markdown) { 23 | return remark().use(html).process(markdown); 24 | } 25 | } 26 | }) 27 | } 28 | }); 29 | 30 | fs.writeFileSync(path.join(__dirname, '../index.html'), index({ ref: ref })); 31 | -------------------------------------------------------------------------------- /docs/_generate/item.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= name %> 4 | <% if (prop.function == "interpolated" ) { %><% } %> 5 | <% if (prop.function == "piecewise-constant" ) { %><% } %> 6 | <% if (prop.transition) { %><% } %> 7 |
8 |
9 | 10 | <%= prop.required ? '必选' : '可选' %> 11 | <% if (prop.type && prop.type !== '*') { %><%= prop.type %>.<% } %> 12 | 13 | <% if (prop.values && Array.isArray(prop.values) === false) { // skips $root.version %> 14 | 可选的值有 <%= _(Object.keys(prop.values)).map(function(opt) { return '' + opt + '' }).join(', ') %>. 15 | <% } %> 16 | <% if (prop.units) { %> 17 | 单位为 18 | <% if (typeof prop.units === 'object') { %> 19 | <%= _(prop.units).map(function(opt) { return '' + opt + '' }).join(', ') %>. 20 | <% } else { %> 21 | <%= prop.units %>. 22 | <% } %> 23 | <% } %> 24 | <% if (prop.default || prop.default === false || prop.default === 0) { %> 25 | 默认为 <%= prop.default %>. 26 | <% } %> 27 | <% if (prop.requires) { %> 28 | <%= _(prop.requires).map(function(req) { 29 | if (typeof req === 'string') { 30 | return '需要 ' + req + '.'; 31 | } else { 32 | if (req['!']) { 33 | return '设置 ' + req['!'] + ' 后无效.'; 34 | } else if (req['<=']) { 35 | return '必须小于等于 ' + req['<='] + '.'; 36 | } else { 37 | var requiredValue = _.toPairs(req)[0][1]; 38 | var requiredDisplay = _.isArray(requiredValue) 39 | ? 'one of ' + requiredValue.join(', ') 40 | : requiredValue; 41 | return '需要 ' + _.toPairs(req)[0][0] + ' = ' + requiredDisplay + '.'; 42 | } 43 | } 44 | }).join(' ') %> 45 | <% } %> 46 |
47 | <% if (prop.doc) { %> 48 |
<%= md(prop.doc) %>
49 | <% } %> 50 | <% if (prop.values && Array.isArray(prop.values) === false) { // skips $root.version %>
51 | <% for (var v in prop.values) { %> 52 |
<%= v %>
53 |
<%= md(prop.values[v].doc) %>
54 | <% } %> 55 |
<% } %> 56 | <% if (prop.example) { %> 57 |
58 | {% highlight json %} 59 | <%= '"' + name + '": ' + JSON.stringify(prop.example, null, 2) %> 60 | {% endhighlight %} 61 |
62 | <% } %> 63 | <% if (prop['sdk-support']) { 64 | var support = function(type, sdk) { 65 | var support = prop['sdk-support'][type]; 66 | if (!support) return '不支持'; 67 | support = support[sdk]; 68 | if (support === undefined) return '不支持'; 69 | return '>= ' + support; 70 | } 71 | %> 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | <% _.each(Object.keys(prop['sdk-support']), function(key) { %> 84 | 85 | 86 | 87 | 88 | 89 | 90 | <% }); %> 91 | 92 |
SDK 支持Mapbox GL JSiOS SDKAndroid SDK
<%= md(key) %><%- support(key, 'js') %><%- support(key, 'ios') %><%- support(key, 'android') %>
93 |
94 | <% } %> 95 |
96 | -------------------------------------------------------------------------------- /docs/site.css: -------------------------------------------------------------------------------- 1 | .fill-gradient { background-image: linear-gradient( to bottom right, #7474BF, #348AC7); } 2 | .doc .property p { margin-bottom:0; } 3 | .doc .space-right { padding-right:10px; } 4 | .doc .icon.inline:before { vertical-align:middle; } 5 | .doc .uppercase { text-transform: uppercase; } 6 | .doc .indented { border-left: 4px solid rgba(255,255,255,0.2); } 7 | .doc.dark .keyline-bottom { border-color: rgba(0,0,0,0.15); } 8 | 9 | /* Supress `err` styling rouge applies from 10 | * mapbox.com/base/ to favor shorthand documentation 11 | * that doesn't always support formal syntax */ 12 | pre .err { 13 | background-color:transparent; 14 | color:inherit; 15 | } 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.v6 = require('./reference/v6.json'); 2 | exports.v7 = require('./reference/v7.json'); 3 | exports.v8 = require('./reference/v8.json'); 4 | exports.latest = require('./reference/latest'); 5 | 6 | exports.v6min = require('./reference/v6.min.json'); 7 | exports.v7min = require('./reference/v7.min.json'); 8 | exports.v8min = require('./reference/v8.min.json'); 9 | exports.latestmin = require('./reference/latest.min'); 10 | 11 | exports.format = require('./lib/format'); 12 | exports.migrate = require('./lib/migrate'); 13 | exports.composite = require('./lib/composite'); 14 | exports.diff = require('./lib/diff'); 15 | exports.ValidationError = require('./lib/error/validation_error'); 16 | exports.ParsingError = require('./lib/error/parsing_error'); 17 | 18 | exports.validate = require('./lib/validate_style'); 19 | exports.validate.parsed = require('./lib/validate_style'); 20 | exports.validate.latest = require('./lib/validate_style'); 21 | -------------------------------------------------------------------------------- /lib/composite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (style) { 4 | var styleIDs = [], 5 | sourceIDs = []; 6 | 7 | for (var id in style.sources) { 8 | var source = style.sources[id]; 9 | 10 | if (source.type !== "vector") 11 | continue; 12 | 13 | var match = /^mapbox:\/\/(.*)/.exec(source.url); 14 | if (!match) 15 | continue; 16 | 17 | styleIDs.push(id); 18 | sourceIDs.push(match[1]); 19 | } 20 | 21 | if (styleIDs.length < 2) 22 | return style; 23 | 24 | styleIDs.forEach(function (id) { 25 | delete style.sources[id]; 26 | }); 27 | 28 | var compositeID = sourceIDs.join(","); 29 | 30 | style.sources[compositeID] = { 31 | "type": "vector", 32 | "url": "mapbox://" + compositeID 33 | }; 34 | 35 | style.layers.forEach(function (layer) { 36 | if (styleIDs.indexOf(layer.source) >= 0) { 37 | layer.source = compositeID; 38 | } 39 | }); 40 | 41 | return style; 42 | }; 43 | -------------------------------------------------------------------------------- /lib/diff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isEqual = require('lodash.isequal'); 4 | 5 | var operations = { 6 | 7 | /* 8 | * { command: 'setStyle', args: [stylesheet] } 9 | */ 10 | setStyle: 'setStyle', 11 | 12 | /* 13 | * { command: 'addLayer', args: [layer, 'beforeLayerId'] } 14 | */ 15 | addLayer: 'addLayer', 16 | 17 | /* 18 | * { command: 'removeLayer', args: ['layerId'] } 19 | */ 20 | removeLayer: 'removeLayer', 21 | 22 | /* 23 | * { command: 'setPaintProperty', args: ['layerId', 'prop', value] } 24 | */ 25 | setPaintProperty: 'setPaintProperty', 26 | 27 | /* 28 | * { command: 'setLayoutProperty', args: ['layerId', 'prop', value] } 29 | */ 30 | setLayoutProperty: 'setLayoutProperty', 31 | 32 | /* 33 | * { command: 'setFilter', args: ['layerId', filter] } 34 | */ 35 | setFilter: 'setFilter', 36 | 37 | /* 38 | * { command: 'addSource', args: ['sourceId', source] } 39 | */ 40 | addSource: 'addSource', 41 | 42 | /* 43 | * { command: 'removeSource', args: ['sourceId'] } 44 | */ 45 | removeSource: 'removeSource', 46 | 47 | /* 48 | * { command: 'setLayerZoomRange', args: ['layerId', 0, 22] } 49 | */ 50 | setLayerZoomRange: 'setLayerZoomRange', 51 | 52 | /* 53 | * { command: 'setLayerProperty', args: ['layerId', 'prop', value] } 54 | */ 55 | setLayerProperty: 'setLayerProperty', 56 | 57 | /* 58 | * { command: 'setCenter', args: [[lon, lat]] } 59 | */ 60 | setCenter: 'setCenter', 61 | 62 | /* 63 | * { command: 'setZoom', args: [zoom] } 64 | */ 65 | setZoom: 'setZoom', 66 | 67 | /* 68 | * { command: 'setBearing', args: [bearing] } 69 | */ 70 | setBearing: 'setBearing', 71 | 72 | /* 73 | * { command: 'setPitch', args: [pitch] } 74 | */ 75 | setPitch: 'setPitch', 76 | 77 | /* 78 | * { command: 'setSprite', args: ['spriteUrl'] } 79 | */ 80 | setSprite: 'setSprite', 81 | 82 | /* 83 | * { command: 'setGlyphs', args: ['glyphsUrl'] } 84 | */ 85 | setGlyphs: 'setGlyphs', 86 | 87 | /* 88 | * { command: 'setTransition', args: [transition] } 89 | */ 90 | setTransition: 'setTransition', 91 | 92 | /* 93 | * { command: 'setLighting', args: [lightProperties] } 94 | */ 95 | setLight: 'setLight' 96 | 97 | }; 98 | 99 | 100 | function diffSources(before, after, commands) { 101 | before = before || {}; 102 | after = after || {}; 103 | 104 | var sourceId; 105 | 106 | // look for sources to remove 107 | for (sourceId in before) { 108 | if (!before.hasOwnProperty(sourceId)) continue; 109 | if (!after.hasOwnProperty(sourceId)) { 110 | commands.push({ command: operations.removeSource, args: [sourceId] }); 111 | } 112 | } 113 | 114 | // look for sources to add/update 115 | for (sourceId in after) { 116 | if (!after.hasOwnProperty(sourceId)) continue; 117 | if (!before.hasOwnProperty(sourceId)) { 118 | commands.push({ command: operations.addSource, args: [sourceId, after[sourceId]] }); 119 | } else if (!isEqual(before[sourceId], after[sourceId])) { 120 | // no update command, must remove then add 121 | commands.push({ command: operations.removeSource, args: [sourceId] }); 122 | commands.push({ command: operations.addSource, args: [sourceId, after[sourceId]] }); 123 | } 124 | } 125 | } 126 | 127 | function diffLayerPropertyChanges(before, after, commands, layerId, klass, command) { 128 | before = before || {}; 129 | after = after || {}; 130 | 131 | var prop; 132 | 133 | for (prop in before) { 134 | if (!before.hasOwnProperty(prop)) continue; 135 | if (!isEqual(before[prop], after[prop])) { 136 | commands.push({ command: command, args: [layerId, prop, after[prop], klass] }); 137 | } 138 | } 139 | for (prop in after) { 140 | if (!after.hasOwnProperty(prop) || before.hasOwnProperty(prop)) continue; 141 | if (!isEqual(before[prop], after[prop])) { 142 | commands.push({ command: command, args: [layerId, prop, after[prop], klass] }); 143 | } 144 | } 145 | } 146 | 147 | function pluckId(layer) { 148 | return layer.id; 149 | } 150 | function indexById(group, layer) { 151 | group[layer.id] = layer; 152 | return group; 153 | } 154 | 155 | function diffLayers(before, after, commands) { 156 | before = before || []; 157 | after = after || []; 158 | 159 | // order of layers by id 160 | var beforeOrder = before.map(pluckId); 161 | var afterOrder = after.map(pluckId); 162 | 163 | // index of layer by id 164 | var beforeIndex = before.reduce(indexById, {}); 165 | var afterIndex = after.reduce(indexById, {}); 166 | 167 | // track order of layers as if they have been mutated 168 | var tracker = beforeOrder.slice(); 169 | 170 | // layers that have been added do not need to be diffed 171 | var clean = Object.create(null); 172 | 173 | var i, d, layerId, beforeLayer, afterLayer, insertBeforeLayerId, prop; 174 | 175 | // remove layers 176 | for (i = 0, d = 0; i < beforeOrder.length; i++) { 177 | layerId = beforeOrder[i]; 178 | if (!afterIndex.hasOwnProperty(layerId)) { 179 | commands.push({ command: operations.removeLayer, args: [layerId] }); 180 | tracker.splice(tracker.indexOf(layerId, d), 1); 181 | } else { 182 | // limit where in tracker we need to look for a match 183 | d++; 184 | } 185 | } 186 | 187 | // add/reorder layers 188 | for (i = 0, d = 0; i < afterOrder.length; i++) { 189 | // work backwards as insert is before an existing layer 190 | layerId = afterOrder[afterOrder.length - 1 - i]; 191 | 192 | if (tracker[tracker.length - 1 - i] === layerId) continue; 193 | 194 | if (beforeIndex.hasOwnProperty(layerId)) { 195 | // remove the layer before we insert at the correct position 196 | commands.push({ command: operations.removeLayer, args: [layerId] }); 197 | tracker.splice(tracker.lastIndexOf(layerId, tracker.length - d), 1); 198 | } else { 199 | // limit where in tracker we need to look for a match 200 | d++; 201 | } 202 | 203 | // add layer at correct position 204 | insertBeforeLayerId = tracker[tracker.length - i]; 205 | commands.push({ command: operations.addLayer, args: [afterIndex[layerId], insertBeforeLayerId] }); 206 | tracker.splice(tracker.length - i, 0, layerId); 207 | clean[layerId] = true; 208 | } 209 | 210 | // update layers 211 | for (i = 0; i < afterOrder.length; i++) { 212 | layerId = afterOrder[i]; 213 | beforeLayer = beforeIndex[layerId]; 214 | afterLayer = afterIndex[layerId]; 215 | 216 | // no need to update if previously added (new or moved) 217 | if (clean[layerId] || isEqual(beforeLayer, afterLayer)) continue; 218 | 219 | // layout, paint, filter, minzoom, maxzoom 220 | diffLayerPropertyChanges(beforeLayer.layout, afterLayer.layout, commands, layerId, null, operations.setLayoutProperty); 221 | diffLayerPropertyChanges(beforeLayer.paint, afterLayer.paint, commands, layerId, null, operations.setPaintProperty); 222 | if (!isEqual(beforeLayer.filter, afterLayer.filter)) { 223 | commands.push({ command: operations.setFilter, args: [layerId, afterLayer.filter] }); 224 | } 225 | if (!isEqual(beforeLayer.minzoom, afterLayer.minzoom) || !isEqual(beforeLayer.maxzoom, afterLayer.maxzoom)) { 226 | commands.push({ command: operations.setLayerZoomRange, args: [layerId, afterLayer.minzoom, afterLayer.maxzoom] }); 227 | } 228 | 229 | // handle all other layer props, including paint.* 230 | for (prop in beforeLayer) { 231 | if (!beforeLayer.hasOwnProperty(prop)) continue; 232 | if (prop === 'layout' || prop === 'paint' || prop === 'filter' 233 | || prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom') continue; 234 | if (prop.indexOf('paint.') === 0) { 235 | diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), operations.setPaintProperty); 236 | } else if (!isEqual(beforeLayer[prop], afterLayer[prop])) { 237 | commands.push({ command: operations.setLayerProperty, args: [layerId, prop, afterLayer[prop]] }); 238 | } 239 | } 240 | for (prop in afterLayer) { 241 | if (!afterLayer.hasOwnProperty(prop) || beforeLayer.hasOwnProperty(prop)) continue; 242 | if (prop === 'layout' || prop === 'paint' || prop === 'filter' 243 | || prop === 'metadata' || prop === 'minzoom' || prop === 'maxzoom') continue; 244 | if (prop.indexOf('paint.') === 0) { 245 | diffLayerPropertyChanges(beforeLayer[prop], afterLayer[prop], commands, layerId, prop.slice(6), operations.setPaintProperty); 246 | } else if (!isEqual(beforeLayer[prop], afterLayer[prop])) { 247 | commands.push({ command: operations.setLayerProperty, args: [layerId, prop, afterLayer[prop]] }); 248 | } 249 | } 250 | } 251 | } 252 | 253 | /** 254 | * Diff two stylesheet 255 | * 256 | * Creates semanticly aware diffs that can easily be applied at runtime. 257 | * Operations produced by the diff closely resemble the mapbox-gl-js API. Any 258 | * error creating the diff will fall back to the 'setStyle' operation. 259 | * 260 | * Example diff: 261 | * [ 262 | * { command: 'setConstant', args: ['@water', '#0000FF'] }, 263 | * { command: 'setPaintProperty', args: ['background', 'background-color', 'black'] } 264 | * ] 265 | * 266 | * @param {*} [before] stylesheet to compare from 267 | * @param {*} after stylesheet to compare to 268 | * @returns Array list of changes 269 | */ 270 | function diffStyles(before, after) { 271 | if (!before) return [{ command: operations.setStyle, args: [after] }]; 272 | 273 | var commands = []; 274 | 275 | try { 276 | if (!isEqual(before.version, after.version)) { 277 | return [{ command: operations.setStyle, args: [after] }]; 278 | } 279 | if (!isEqual(before.center, after.center)) { 280 | commands.push({ command: operations.setCenter, args: [after.center] }); 281 | } 282 | if (!isEqual(before.zoom, after.zoom)) { 283 | commands.push({ command: operations.setZoom, args: [after.zoom] }); 284 | } 285 | if (!isEqual(before.bearing, after.bearing)) { 286 | commands.push({ command: operations.setBearing, args: [after.bearing] }); 287 | } 288 | if (!isEqual(before.pitch, after.pitch)) { 289 | commands.push({ command: operations.setPitch, args: [after.pitch] }); 290 | } 291 | if (!isEqual(before.sprite, after.sprite)) { 292 | commands.push({ command: operations.setSprite, args: [after.sprite] }); 293 | } 294 | if (!isEqual(before.glyphs, after.glyphs)) { 295 | commands.push({ command: operations.setGlyphs, args: [after.glyphs] }); 296 | } 297 | if (!isEqual(before.transition, after.transition)) { 298 | commands.push({ command: operations.setTransition, args: [after.transition] }); 299 | } 300 | if (!isEqual(before.light, after.light)) { 301 | commands.push({ command: operations.setLight, args: [after.light] }); 302 | } 303 | diffSources(before.sources, after.sources, commands); 304 | diffLayers(before.layers, after.layers, commands); 305 | } catch (e) { 306 | // fall back to setStyle 307 | console.warn('Unable to compute style diff:', e); 308 | commands = [{ command: operations.setStyle, args: [after] }]; 309 | } 310 | 311 | return commands; 312 | } 313 | 314 | module.exports = diffStyles; 315 | module.exports.operations = operations; 316 | -------------------------------------------------------------------------------- /lib/error/parsing_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ParsingError(error) { 4 | this.error = error; 5 | this.message = error.message; 6 | var match = error.message.match(/line (\d+)/); 7 | this.line = match ? parseInt(match[1], 10) : 0; 8 | } 9 | 10 | module.exports = ParsingError; 11 | -------------------------------------------------------------------------------- /lib/error/validation_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var format = require('util').format; 4 | 5 | function ValidationError(key, value /*, message, ...*/) { 6 | this.message = ( 7 | (key ? key + ': ' : '') + 8 | format.apply(format, Array.prototype.slice.call(arguments, 2)) 9 | ); 10 | 11 | if (value !== null && value !== undefined && value.__line__) { 12 | this.line = value.__line__; 13 | } 14 | } 15 | 16 | module.exports = ValidationError; 17 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var reference = require('../reference/latest.js'), 4 | sortObject = require('sort-object'); 5 | 6 | function sameOrderAs(reference) { 7 | var keyOrder = {}; 8 | 9 | Object.keys(reference).forEach(function(k, i) { 10 | keyOrder[k] = i + 1; 11 | }); 12 | 13 | return { 14 | sort: function (a, b) { 15 | return (keyOrder[a] || Infinity) - 16 | (keyOrder[b] || Infinity); 17 | } 18 | }; 19 | } 20 | 21 | /** 22 | * Format a Mapbox GL Style. Returns a stringified style with its keys 23 | * sorted in the same order as the reference style. 24 | * 25 | * The optional `space` argument is passed to 26 | * [`JSON.stringify`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) 27 | * to generate formatted output. 28 | * 29 | * If `space` is unspecified, a default of `2` spaces will be used. 30 | * 31 | * @param {Object} style a Mapbox GL Style 32 | * @param {number} [space] space argument to pass to `JSON.stringify` 33 | * @returns {string} stringified formatted JSON 34 | * @example 35 | * var fs = require('fs'); 36 | * var format = require('mapbox-gl-style-spec').format; 37 | * var style = fs.readFileSync('./source.json', 'utf8'); 38 | * fs.writeFileSync('./dest.json', format(style)); 39 | * fs.writeFileSync('./dest.min.json', format(style, 0)); 40 | */ 41 | function format(style, space) { 42 | if (space === undefined) space = 2; 43 | style = sortObject(style, sameOrderAs(reference.$root)); 44 | 45 | if (style.layers) { 46 | style.layers = style.layers.map(function(layer) { 47 | return sortObject(layer, sameOrderAs(reference.layer)); 48 | }); 49 | } 50 | 51 | return JSON.stringify(style, null, space); 52 | } 53 | 54 | module.exports = format; 55 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Migrate a Mapbox GL Style to the latest version. 5 | * 6 | * @alias migrate 7 | * @param {object} style a Mapbox GL Style 8 | * @returns {Object} a migrated style 9 | * @example 10 | * var fs = require('fs'); 11 | * var migrate = require('mapbox-gl-style-spec').migrate; 12 | * var style = fs.readFileSync('./style.json', 'utf8'); 13 | * fs.writeFileSync('./style.json', JSON.stringify(migrate(style))); 14 | */ 15 | module.exports = function(style) { 16 | var migrated = false; 17 | 18 | if (style.version === 6) { 19 | style = require('../migrations/v7')(style); 20 | migrated = true; 21 | } 22 | 23 | if (style.version === 7 || style.version === 8) { 24 | style = require('../migrations/v8')(style); 25 | migrated = true; 26 | } 27 | 28 | if (!migrated) { 29 | throw new Error('cannot migrate from', style.version); 30 | } 31 | 32 | return style; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/util/extend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (output) { 4 | for (var i = 1; i < arguments.length; i++) { 5 | var input = arguments[i]; 6 | for (var k in input) { 7 | output[k] = input[k]; 8 | } 9 | } 10 | return output; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/util/get_type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function getType(val) { 4 | if (val instanceof Number) { 5 | return 'number'; 6 | } else if (val instanceof String) { 7 | return 'string'; 8 | } else if (val instanceof Boolean) { 9 | return 'boolean'; 10 | } else if (Array.isArray(val)) { 11 | return 'array'; 12 | } else if (val === null) { 13 | return 'null'; 14 | } else { 15 | return typeof val; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/util/unbundle_jsonlint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Turn jsonlint-lines-primitives objects into primitive objects 4 | module.exports = function unbundle(value) { 5 | if (value instanceof Number || value instanceof String || value instanceof Boolean) { 6 | return value.valueOf(); 7 | } else { 8 | return value; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lib/validate/latest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * Validate a style against the latest specification. This method is optimized 5 | * to keep its bundle size small by refraining from requiring jslint or old 6 | * style spec versions. 7 | * @see validateStyleMin 8 | * @deprecated This file exists for backwards compatibility and will be dropped in the next minor release. 9 | */ 10 | module.exports = require('../validate_style.min'); 11 | -------------------------------------------------------------------------------- /lib/validate/validate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | var extend = require('../util/extend'); 6 | 7 | // Main recursive validation function. Tracks: 8 | // 9 | // - key: string representing location of validation in style tree. Used only 10 | // for more informative error reporting. 11 | // - value: current value from style being evaluated. May be anything from a 12 | // high level object that needs to be descended into deeper or a simple 13 | // scalar value. 14 | // - valueSpec: current spec being evaluated. Tracks value. 15 | 16 | module.exports = function validate(options) { 17 | 18 | var validateFunction = require('./validate_function'); 19 | var validateObject = require('./validate_object'); 20 | var VALIDATORS = { 21 | '*': function() { 22 | return []; 23 | }, 24 | 'array': require('./validate_array'), 25 | 'boolean': require('./validate_boolean'), 26 | 'number': require('./validate_number'), 27 | 'color': require('./validate_color'), 28 | 'constants': require('./validate_constants'), 29 | 'enum': require('./validate_enum'), 30 | 'filter': require('./validate_filter'), 31 | 'function': require('./validate_function'), 32 | 'layer': require('./validate_layer'), 33 | 'object': require('./validate_object'), 34 | 'source': require('./validate_source'), 35 | 'string': require('./validate_string') 36 | }; 37 | 38 | var value = options.value; 39 | var valueSpec = options.valueSpec; 40 | var key = options.key; 41 | var styleSpec = options.styleSpec; 42 | var style = options.style; 43 | 44 | if (getType(value) === 'string' && value[0] === '@') { 45 | if (styleSpec.$version > 7) { 46 | return [new ValidationError(key, value, 'constants have been deprecated as of v8')]; 47 | } 48 | if (!(value in style.constants)) { 49 | return [new ValidationError(key, value, 'constant "%s" not found', value)]; 50 | } 51 | options = extend({}, options, { value: style.constants[value] }); 52 | } 53 | 54 | if (valueSpec.function && getType(value) === 'object') { 55 | return validateFunction(options); 56 | 57 | } else if (valueSpec.type && VALIDATORS[valueSpec.type]) { 58 | return VALIDATORS[valueSpec.type](options); 59 | 60 | } else { 61 | return validateObject(extend({}, options, { 62 | valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec 63 | })); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lib/validate/validate_array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getType = require('../util/get_type'); 4 | var validate = require('./validate'); 5 | var ValidationError = require('../error/validation_error'); 6 | 7 | module.exports = function validateArray(options) { 8 | var array = options.value; 9 | var arraySpec = options.valueSpec; 10 | var style = options.style; 11 | var styleSpec = options.styleSpec; 12 | var key = options.key; 13 | var validateArrayElement = options.arrayElementValidator || validate; 14 | 15 | if (getType(array) !== 'array') { 16 | return [new ValidationError(key, array, 'array expected, %s found', getType(array))]; 17 | } 18 | 19 | if (arraySpec.length && array.length !== arraySpec.length) { 20 | return [new ValidationError(key, array, 'array length %d expected, length %d found', arraySpec.length, array.length)]; 21 | } 22 | 23 | if (arraySpec['min-length'] && array.length < arraySpec['min-length']) { 24 | return [new ValidationError(key, array, 'array length at least %d expected, length %d found', arraySpec['min-length'], array.length)]; 25 | } 26 | 27 | var arrayElementSpec = { 28 | "type": arraySpec.value 29 | }; 30 | 31 | if (styleSpec.$version < 7) { 32 | arrayElementSpec.function = arraySpec.function; 33 | } 34 | 35 | if (getType(arraySpec.value) === 'object') { 36 | arrayElementSpec = arraySpec.value; 37 | } 38 | 39 | var errors = []; 40 | for (var i = 0; i < array.length; i++) { 41 | errors = errors.concat(validateArrayElement({ 42 | array: array, 43 | arrayIndex: i, 44 | value: array[i], 45 | valueSpec: arrayElementSpec, 46 | style: style, 47 | styleSpec: styleSpec, 48 | key: key + '[' + i + ']' 49 | })); 50 | } 51 | return errors; 52 | }; 53 | -------------------------------------------------------------------------------- /lib/validate/validate_boolean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getType = require('../util/get_type'); 4 | var ValidationError = require('../error/validation_error'); 5 | 6 | module.exports = function validateBoolean(options) { 7 | var value = options.value; 8 | var key = options.key; 9 | var type = getType(value); 10 | 11 | if (type !== 'boolean') { 12 | return [new ValidationError(key, value, 'boolean expected, %s found', type)]; 13 | } 14 | 15 | return []; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/validate/validate_color.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | var parseCSSColor = require('csscolorparser').parseCSSColor; 6 | 7 | module.exports = function validateColor(options) { 8 | var key = options.key; 9 | var value = options.value; 10 | var type = getType(value); 11 | 12 | if (type !== 'string') { 13 | return [new ValidationError(key, value, 'color expected, %s found', type)]; 14 | } 15 | 16 | if (parseCSSColor(value) === null) { 17 | return [new ValidationError(key, value, 'color expected, "%s" found', value)]; 18 | } 19 | 20 | return []; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/validate/validate_constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | 6 | module.exports = function validateConstants(options) { 7 | var key = options.key; 8 | var constants = options.value; 9 | var styleSpec = options.styleSpec; 10 | 11 | if (styleSpec.$version > 7) { 12 | if (constants) { 13 | return [new ValidationError(key, constants, 'constants have been deprecated as of v8')]; 14 | } else { 15 | return []; 16 | } 17 | } else { 18 | var type = getType(constants); 19 | if (type !== 'object') { 20 | return [new ValidationError(key, constants, 'object expected, %s found', type)]; 21 | } 22 | 23 | var errors = []; 24 | for (var constantName in constants) { 25 | if (constantName[0] !== '@') { 26 | errors.push(new ValidationError(key + '.' + constantName, constants[constantName], 'constants must start with "@"')); 27 | } 28 | } 29 | return errors; 30 | } 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /lib/validate/validate_enum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var unbundle = require('../util/unbundle_jsonlint'); 5 | 6 | module.exports = function validateEnum(options) { 7 | var key = options.key; 8 | var value = options.value; 9 | var valueSpec = options.valueSpec; 10 | var errors = []; 11 | 12 | if (Array.isArray(valueSpec.values)) { // <=v7 13 | if (valueSpec.values.indexOf(unbundle(value)) === -1) { 14 | errors.push(new ValidationError(key, value, 'expected one of [%s], %s found', valueSpec.values.join(', '), value)); 15 | } 16 | } else { // >=v8 17 | if (Object.keys(valueSpec.values).indexOf(unbundle(value)) === -1) { 18 | errors.push(new ValidationError(key, value, 'expected one of [%s], %s found', Object.keys(valueSpec.values).join(', '), value)); 19 | } 20 | } 21 | return errors; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/validate/validate_filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var validateEnum = require('./validate_enum'); 5 | var getType = require('../util/get_type'); 6 | var unbundle = require('../util/unbundle_jsonlint'); 7 | 8 | module.exports = function validateFilter(options) { 9 | var value = options.value; 10 | var key = options.key; 11 | var styleSpec = options.styleSpec; 12 | var type; 13 | 14 | var errors = []; 15 | 16 | if (getType(value) !== 'array') { 17 | return [new ValidationError(key, value, 'array expected, %s found', getType(value))]; 18 | } 19 | 20 | if (value.length < 1) { 21 | return [new ValidationError(key, value, 'filter array must have at least 1 element')]; 22 | } 23 | 24 | errors = errors.concat(validateEnum({ 25 | key: key + '[0]', 26 | value: value[0], 27 | valueSpec: styleSpec.filter_operator, 28 | style: options.style, 29 | styleSpec: options.styleSpec 30 | })); 31 | 32 | switch (unbundle(value[0])) { 33 | case '<': 34 | case '<=': 35 | case '>': 36 | case '>=': 37 | if (value.length >= 2 && value[1] == '$type') { 38 | errors.push(new ValidationError(key, value, '"$type" cannot be use with operator "%s"', value[0])); 39 | } 40 | /* falls through */ 41 | case '==': 42 | case '!=': 43 | if (value.length != 3) { 44 | errors.push(new ValidationError(key, value, 'filter array for operator "%s" must have 3 elements', value[0])); 45 | } 46 | /* falls through */ 47 | case 'in': 48 | case '!in': 49 | if (value.length >= 2) { 50 | type = getType(value[1]); 51 | if (type !== 'string') { 52 | errors.push(new ValidationError(key + '[1]', value[1], 'string expected, %s found', type)); 53 | } else if (value[1][0] === '@') { 54 | errors.push(new ValidationError(key + '[1]', value[1], 'filter key cannot be a constant')); 55 | } 56 | } 57 | for (var i = 2; i < value.length; i++) { 58 | type = getType(value[i]); 59 | if (value[1] == '$type') { 60 | errors = errors.concat(validateEnum({ 61 | key: key + '[' + i + ']', 62 | value: value[i], 63 | valueSpec: styleSpec.geometry_type, 64 | style: options.style, 65 | styleSpec: options.styleSpec 66 | })); 67 | } else if (type === 'string' && value[i][0] === '@') { 68 | errors.push(new ValidationError(key + '[' + i + ']', value[i], 'filter value cannot be a constant')); 69 | } else if (type !== 'string' && type !== 'number' && type !== 'boolean') { 70 | errors.push(new ValidationError(key + '[' + i + ']', value[i], 'string, number, or boolean expected, %s found', type)); 71 | } 72 | } 73 | break; 74 | 75 | case 'any': 76 | case 'all': 77 | case 'none': 78 | for (i = 1; i < value.length; i++) { 79 | errors = errors.concat(validateFilter({ 80 | key: key + '[' + i + ']', 81 | value: value[i], 82 | style: options.style, 83 | styleSpec: options.styleSpec 84 | })); 85 | } 86 | break; 87 | 88 | case 'has': 89 | case '!has': 90 | type = getType(value[1]); 91 | if (value.length !== 2) { 92 | errors.push(new ValidationError(key, value, 'filter array for "%s" operator must have 2 elements', value[0])); 93 | } else if (type !== 'string') { 94 | errors.push(new ValidationError(key + '[1]', value[1], 'string expected, %s found', type)); 95 | } else if (value[1][0] === '@') { 96 | errors.push(new ValidationError(key + '[1]', value[1], 'filter key cannot be a constant')); 97 | } 98 | break; 99 | 100 | } 101 | 102 | return errors; 103 | }; 104 | -------------------------------------------------------------------------------- /lib/validate/validate_function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | var validate = require('./validate'); 6 | var validateObject = require('./validate_object'); 7 | var validateArray = require('./validate_array'); 8 | var validateNumber = require('./validate_number'); 9 | var unbundle = require('../util/unbundle_jsonlint'); 10 | 11 | module.exports = function validateFunction(options) { 12 | var functionValueSpec = options.valueSpec; 13 | var functionValue = options.value; 14 | var stopKeyType; 15 | 16 | var isPropertyFunction = options.value.property !== undefined || stopKeyType === 'object'; 17 | var isZoomFunction = options.value.property === undefined || stopKeyType === 'object'; 18 | 19 | var errors = validateObject({ 20 | key: options.key, 21 | value: options.value, 22 | valueSpec: options.styleSpec.function, 23 | style: options.style, 24 | styleSpec: options.styleSpec, 25 | objectElementValidators: { stops: validateFunctionStops } 26 | }); 27 | 28 | if (unbundle(options.value.type) !== 'identity' && !options.value.stops) { 29 | errors.push(new ValidationError(options.key, options.value, 'missing required property "stops"')); 30 | } 31 | 32 | if (options.styleSpec.$version >= 8) { 33 | if (isPropertyFunction && !options.valueSpec['property-function']) { 34 | errors.push(new ValidationError(options.key, options.value, 'property functions not supported')); 35 | } else if (isZoomFunction && !options.valueSpec['zoom-function']) { 36 | errors.push(new ValidationError(options.key, options.value, 'zoom functions not supported')); 37 | } 38 | } 39 | 40 | return errors; 41 | 42 | function validateFunctionStops(options) { 43 | if (unbundle(functionValue.type) === 'identity') { 44 | return [new ValidationError(options.key, options.value, 'identity function may not have a "stops" property')]; 45 | } 46 | 47 | var errors = []; 48 | var value = options.value; 49 | 50 | errors = errors.concat(validateArray({ 51 | key: options.key, 52 | value: value, 53 | valueSpec: options.valueSpec, 54 | style: options.style, 55 | styleSpec: options.styleSpec, 56 | arrayElementValidator: validateFunctionStop 57 | })); 58 | 59 | if (getType(value) === 'array' && value.length === 0) { 60 | errors.push(new ValidationError(options.key, value, 'array must have at least one stop')); 61 | } 62 | 63 | return errors; 64 | } 65 | 66 | function validateFunctionStop(options) { 67 | var errors = []; 68 | var value = options.value; 69 | var key = options.key; 70 | 71 | if (getType(value) !== 'array') { 72 | return [new ValidationError(key, value, 'array expected, %s found', getType(value))]; 73 | } 74 | 75 | if (value.length !== 2) { 76 | return [new ValidationError(key, value, 'array length %d expected, length %d found', 2, value.length)]; 77 | } 78 | 79 | var type = getType(value[0]); 80 | if (!stopKeyType) stopKeyType = type; 81 | if (type !== stopKeyType) { 82 | return [new ValidationError(key, value, '%s stop key type must match previous stop key type %s', type, stopKeyType)]; 83 | } 84 | 85 | if (type === 'object') { 86 | if (value[0].zoom === undefined) { 87 | return [new ValidationError(key, value, 'object stop key must have zoom')]; 88 | } 89 | if (value[0].value === undefined) { 90 | return [new ValidationError(key, value, 'object stop key must have value')]; 91 | } 92 | errors = errors.concat(validateObject({ 93 | key: key + '[0]', 94 | value: value[0], 95 | valueSpec: { zoom: {} }, 96 | style: options.style, 97 | styleSpec: options.styleSpec, 98 | objectElementValidators: { zoom: validateNumber, value: validateValue } 99 | })); 100 | } else { 101 | errors = errors.concat((isZoomFunction ? validateNumber : validateValue)({ 102 | key: key + '[0]', 103 | value: value[0], 104 | valueSpec: {}, 105 | style: options.style, 106 | styleSpec: options.styleSpec 107 | })); 108 | } 109 | 110 | errors = errors.concat(validate({ 111 | key: key + '[1]', 112 | value: value[1], 113 | valueSpec: functionValueSpec, 114 | style: options.style, 115 | styleSpec: options.styleSpec 116 | })); 117 | 118 | if (getType(value[0]) === 'number') { 119 | if (functionValueSpec.function === 'piecewise-constant' && value[0] % 1 !== 0) { 120 | errors.push(new ValidationError(key + '[0]', value[0], 'zoom level for piecewise-constant functions must be an integer')); 121 | } 122 | 123 | if (options.arrayIndex !== 0) { 124 | if (value[0] < options.array[options.arrayIndex - 1][0]) { 125 | errors.push(new ValidationError(key + '[0]', value[0], 'array stops must appear in ascending order')); 126 | } 127 | } 128 | } 129 | 130 | return errors; 131 | } 132 | 133 | function validateValue(options) { 134 | var errors = []; 135 | var type = getType(options.value); 136 | if (type !== 'number' && type !== 'string' && type !== 'array') { 137 | errors.push(new ValidationError(options.key, options.value, 'property value must be a number, string or array')); 138 | } 139 | return errors; 140 | } 141 | 142 | }; 143 | -------------------------------------------------------------------------------- /lib/validate/validate_glyphs_url.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var validateString = require('./validate_string'); 5 | 6 | module.exports = function(options) { 7 | var value = options.value; 8 | var key = options.key; 9 | 10 | var errors = validateString(options); 11 | if (errors.length) return errors; 12 | 13 | if (value.indexOf('{fontstack}') === -1) { 14 | errors.push(new ValidationError(key, value, '"glyphs" url must include a "{fontstack}" token')); 15 | } 16 | 17 | if (value.indexOf('{range}') === -1) { 18 | errors.push(new ValidationError(key, value, '"glyphs" url must include a "{range}" token')); 19 | } 20 | 21 | return errors; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/validate/validate_layer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var unbundle = require('../util/unbundle_jsonlint'); 5 | var validateObject = require('./validate_object'); 6 | var validateFilter = require('./validate_filter'); 7 | var validatePaintProperty = require('./validate_paint_property'); 8 | var validateLayoutProperty = require('./validate_layout_property'); 9 | var extend = require('../util/extend'); 10 | 11 | module.exports = function validateLayer(options) { 12 | var errors = []; 13 | 14 | var layer = options.value; 15 | var key = options.key; 16 | var style = options.style; 17 | var styleSpec = options.styleSpec; 18 | 19 | if (!layer.type && !layer.ref) { 20 | errors.push(new ValidationError(key, layer, 'either "type" or "ref" is required')); 21 | } 22 | var type = unbundle(layer.type); 23 | var ref = unbundle(layer.ref); 24 | 25 | if (layer.id) { 26 | for (var i = 0; i < options.arrayIndex; i++) { 27 | var otherLayer = style.layers[i]; 28 | if (unbundle(otherLayer.id) === unbundle(layer.id)) { 29 | errors.push(new ValidationError(key, layer.id, 'duplicate layer id "%s", previously used at line %d', layer.id, otherLayer.id.__line__)); 30 | } 31 | } 32 | } 33 | 34 | if ('ref' in layer) { 35 | ['type', 'source', 'source-layer', 'filter', 'layout'].forEach(function (p) { 36 | if (p in layer) { 37 | errors.push(new ValidationError(key, layer[p], '"%s" is prohibited for ref layers', p)); 38 | } 39 | }); 40 | 41 | var parent; 42 | 43 | style.layers.forEach(function(layer) { 44 | if (layer.id == ref) parent = layer; 45 | }); 46 | 47 | if (!parent) { 48 | errors.push(new ValidationError(key, layer.ref, 'ref layer "%s" not found', ref)); 49 | } else if (parent.ref) { 50 | errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer')); 51 | } else { 52 | type = unbundle(parent.type); 53 | } 54 | } else if (type !== 'background') { 55 | if (!layer.source) { 56 | errors.push(new ValidationError(key, layer, 'missing required property "source"')); 57 | } else { 58 | var source = style.sources && style.sources[layer.source]; 59 | if (!source) { 60 | errors.push(new ValidationError(key, layer.source, 'source "%s" not found', layer.source)); 61 | } else if (source.type == 'vector' && type == 'raster') { 62 | errors.push(new ValidationError(key, layer.source, 'layer "%s" requires a raster source', layer.id)); 63 | } else if (source.type == 'raster' && type != 'raster') { 64 | errors.push(new ValidationError(key, layer.source, 'layer "%s" requires a vector source', layer.id)); 65 | } else if (source.type == 'vector' && !layer['source-layer']) { 66 | errors.push(new ValidationError(key, layer, 'layer "%s" must specify a "source-layer"', layer.id)); 67 | } 68 | } 69 | } 70 | 71 | errors = errors.concat(validateObject({ 72 | key: key, 73 | value: layer, 74 | valueSpec: styleSpec.layer, 75 | style: options.style, 76 | styleSpec: options.styleSpec, 77 | objectElementValidators: { 78 | filter: validateFilter, 79 | layout: function(options) { 80 | return validateObject({ 81 | layer: layer, 82 | key: options.key, 83 | value: options.value, 84 | style: options.style, 85 | styleSpec: options.styleSpec, 86 | objectElementValidators: { 87 | '*': function(options) { 88 | return validateLayoutProperty(extend({layerType: type}, options)); 89 | } 90 | } 91 | }); 92 | }, 93 | paint: function(options) { 94 | return validateObject({ 95 | layer: layer, 96 | key: options.key, 97 | value: options.value, 98 | style: options.style, 99 | styleSpec: options.styleSpec, 100 | objectElementValidators: { 101 | '*': function(options) { 102 | return validatePaintProperty(extend({layerType: type}, options)); 103 | } 104 | } 105 | }); 106 | } 107 | } 108 | })); 109 | 110 | return errors; 111 | }; 112 | -------------------------------------------------------------------------------- /lib/validate/validate_layout_property.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validate = require('./validate'); 4 | var ValidationError = require('../error/validation_error'); 5 | 6 | module.exports = function validateLayoutProperty(options) { 7 | var key = options.key; 8 | var style = options.style; 9 | var styleSpec = options.styleSpec; 10 | var value = options.value; 11 | var propertyKey = options.objectKey; 12 | var layerSpec = styleSpec['layout_' + options.layerType]; 13 | 14 | if (!layerSpec) return []; 15 | 16 | if (options.valueSpec || layerSpec[propertyKey]) { 17 | var errors = []; 18 | 19 | if (options.layerType === 'symbol') { 20 | if (propertyKey === 'icon-image' && style && !style.sprite) { 21 | errors.push(new ValidationError(key, value, 'use of "icon-image" requires a style "sprite" property')); 22 | } else if (propertyKey === 'text-field' && style && !style.glyphs) { 23 | errors.push(new ValidationError(key, value, 'use of "text-field" requires a style "glyphs" property')); 24 | } 25 | } 26 | 27 | return errors.concat(validate({ 28 | key: options.key, 29 | value: value, 30 | valueSpec: options.valueSpec || layerSpec[propertyKey], 31 | style: style, 32 | styleSpec: styleSpec 33 | })); 34 | 35 | } else { 36 | return [new ValidationError(key, value, 'unknown property "%s"', propertyKey)]; 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /lib/validate/validate_light.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | var validate = require('./validate'); 6 | var validateTypes = { 7 | enum: require('./validate_enum'), 8 | color: require('./validate_color'), 9 | array: require('./validate_array'), 10 | number: require('./validate_number'), 11 | function: require('./validate_function') 12 | }; 13 | 14 | module.exports = function validateLight(options) { 15 | var light = options.value; 16 | var styleSpec = options.styleSpec; 17 | var lightSpec = styleSpec.$root.light; 18 | var style = options.style; 19 | 20 | var errors = []; 21 | 22 | var type; 23 | for (var key in light) { 24 | var transitionMatch = key.match(/^(.*)-transition$/); 25 | 26 | if (transitionMatch && lightSpec[transitionMatch[1]] && lightSpec[transitionMatch[1]].transition) { 27 | var baseKey = transitionMatch[1]; 28 | 29 | type = lightSpec[baseKey].type; 30 | errors = errors.concat(validate({ 31 | key: key, 32 | value: light[key], 33 | valueSpec: styleSpec.transition, 34 | style: style, 35 | styleSpec: styleSpec 36 | })); 37 | } else if (lightSpec[key]) { 38 | type = lightSpec[key].type; 39 | if (lightSpec[key].function && getType(light[key]) === 'object') type = 'function'; 40 | errors = errors.concat(validateTypes[type]({ 41 | key: key, 42 | value: light[key], 43 | valueSpec: lightSpec[key], 44 | style: style, 45 | styleSpec: styleSpec 46 | })); 47 | } else { 48 | errors = errors.concat([new ValidationError(key, light[key], 'unknown property "%s"', key)]); 49 | } 50 | } 51 | 52 | return errors; 53 | }; 54 | -------------------------------------------------------------------------------- /lib/validate/validate_number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getType = require('../util/get_type'); 4 | var ValidationError = require('../error/validation_error'); 5 | 6 | module.exports = function validateNumber(options) { 7 | var key = options.key; 8 | var value = options.value; 9 | var valueSpec = options.valueSpec; 10 | var type = getType(value); 11 | 12 | if (type !== 'number') { 13 | return [new ValidationError(key, value, 'number expected, %s found', type)]; 14 | } 15 | 16 | if ('minimum' in valueSpec && value < valueSpec.minimum) { 17 | return [new ValidationError(key, value, '%s is less than the minimum value %s', value, valueSpec.minimum)]; 18 | } 19 | 20 | if ('maximum' in valueSpec && value > valueSpec.maximum) { 21 | return [new ValidationError(key, value, '%s is greater than the maximum value %s', value, valueSpec.maximum)]; 22 | } 23 | 24 | return []; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/validate/validate_object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var getType = require('../util/get_type'); 5 | var validate = require('./validate'); 6 | 7 | module.exports = function validateObject(options) { 8 | var key = options.key; 9 | var object = options.value; 10 | var valueSpec = options.valueSpec; 11 | var objectElementValidators = options.objectElementValidators || {}; 12 | var style = options.style; 13 | var styleSpec = options.styleSpec; 14 | var errors = []; 15 | 16 | var type = getType(object); 17 | if (type !== 'object') { 18 | return [new ValidationError(key, object, 'object expected, %s found', type)]; 19 | } 20 | 21 | for (var objectKey in object) { 22 | var valueSpecKey = objectKey.split('.')[0]; // treat 'paint.*' as 'paint' 23 | var objectElementSpec = valueSpec && (valueSpec[valueSpecKey] || valueSpec['*']); 24 | var objectElementValidator = objectElementValidators[valueSpecKey] || objectElementValidators['*']; 25 | 26 | if (objectElementSpec || objectElementValidator) { 27 | errors = errors.concat((objectElementValidator || validate)({ 28 | key: (key ? key + '.' : key) + objectKey, 29 | value: object[objectKey], 30 | valueSpec: objectElementSpec, 31 | style: style, 32 | styleSpec: styleSpec, 33 | object: object, 34 | objectKey: objectKey 35 | })); 36 | 37 | // tolerate root-level extra keys & arbitrary layer properties 38 | // TODO remove this layer-specific logic 39 | } else if (key !== '' && key.split('.').length !== 1) { 40 | errors.push(new ValidationError(key, object[objectKey], 'unknown property "%s"', objectKey)); 41 | } 42 | } 43 | 44 | for (valueSpecKey in valueSpec) { 45 | if (valueSpec[valueSpecKey].required && valueSpec[valueSpecKey]['default'] === undefined && object[valueSpecKey] === undefined) { 46 | errors.push(new ValidationError(key, object, 'missing required property "%s"', valueSpecKey)); 47 | } 48 | } 49 | 50 | return errors; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/validate/validate_paint_property.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validate = require('./validate'); 4 | var ValidationError = require('../error/validation_error'); 5 | 6 | module.exports = function validatePaintProperty(options) { 7 | var key = options.key; 8 | var style = options.style; 9 | var styleSpec = options.styleSpec; 10 | var value = options.value; 11 | var propertyKey = options.objectKey; 12 | var layerSpec = styleSpec['paint_' + options.layerType]; 13 | 14 | if (!layerSpec) return []; 15 | 16 | var transitionMatch = propertyKey.match(/^(.*)-transition$/); 17 | 18 | if (transitionMatch && layerSpec[transitionMatch[1]] && layerSpec[transitionMatch[1]].transition) { 19 | return validate({ 20 | key: key, 21 | value: value, 22 | valueSpec: styleSpec.transition, 23 | style: style, 24 | styleSpec: styleSpec 25 | }); 26 | 27 | } else if (options.valueSpec || layerSpec[propertyKey]) { 28 | return validate({ 29 | key: options.key, 30 | value: value, 31 | valueSpec: options.valueSpec || layerSpec[propertyKey], 32 | style: style, 33 | styleSpec: styleSpec 34 | }); 35 | 36 | } else { 37 | return [new ValidationError(key, value, 'unknown property "%s"', propertyKey)]; 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /lib/validate/validate_source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ValidationError = require('../error/validation_error'); 4 | var unbundle = require('../util/unbundle_jsonlint'); 5 | var validateObject = require('./validate_object'); 6 | var validateEnum = require('./validate_enum'); 7 | 8 | module.exports = function validateSource(options) { 9 | var value = options.value; 10 | var key = options.key; 11 | var styleSpec = options.styleSpec; 12 | var style = options.style; 13 | 14 | if (!value.type) { 15 | return [new ValidationError(key, value, '"type" is required')]; 16 | } 17 | 18 | var type = unbundle(value.type); 19 | switch (type) { 20 | case 'vector': 21 | case 'raster': 22 | var errors = []; 23 | errors = errors.concat(validateObject({ 24 | key: key, 25 | value: value, 26 | valueSpec: styleSpec.source_tile, 27 | style: options.style, 28 | styleSpec: styleSpec 29 | })); 30 | if ('url' in value) { 31 | for (var prop in value) { 32 | if (['type', 'url', 'tileSize'].indexOf(prop) < 0) { 33 | errors.push(new ValidationError(key + '.' + prop, value[prop], 'a source with a "url" property may not include a "%s" property', prop)); 34 | } 35 | } 36 | } 37 | return errors; 38 | 39 | case 'geojson': 40 | return validateObject({ 41 | key: key, 42 | value: value, 43 | valueSpec: styleSpec.source_geojson, 44 | style: style, 45 | styleSpec: styleSpec 46 | }); 47 | 48 | case 'video': 49 | return validateObject({ 50 | key: key, 51 | value: value, 52 | valueSpec: styleSpec.source_video, 53 | style: style, 54 | styleSpec: styleSpec 55 | }); 56 | 57 | case 'image': 58 | return validateObject({ 59 | key: key, 60 | value: value, 61 | valueSpec: styleSpec.source_image, 62 | style: style, 63 | styleSpec: styleSpec 64 | }); 65 | 66 | default: 67 | return validateEnum({ 68 | key: key + '.type', 69 | value: value.type, 70 | valueSpec: {values: ['vector', 'raster', 'geojson', 'video', 'image']}, 71 | style: style, 72 | styleSpec: styleSpec 73 | }); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/validate/validate_string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getType = require('../util/get_type'); 4 | var ValidationError = require('../error/validation_error'); 5 | 6 | module.exports = function validateString(options) { 7 | var value = options.value; 8 | var key = options.key; 9 | var type = getType(value); 10 | 11 | if (type !== 'string') { 12 | return [new ValidationError(key, value, 'string expected, %s found', type)]; 13 | } 14 | 15 | return []; 16 | }; 17 | -------------------------------------------------------------------------------- /lib/validate_style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validateStyleMin = require('./validate_style.min'); 4 | var ParsingError = require('./error/parsing_error'); 5 | var jsonlint = require('jsonlint-lines-primitives'); 6 | 7 | /** 8 | * Validate a Mapbox GL style against the style specification. 9 | * 10 | * @alias validate 11 | * @param {Object|String|Buffer} style The style to be validated. If a `String` 12 | * or `Buffer` is provided, the returned errors will contain line numbers. 13 | * @param {Object} [styleSpec] The style specification to validate against. 14 | * If omitted, the spec version is inferred from the stylesheet. 15 | * @returns {Array} 16 | * @example 17 | * var validate = require('mapbox-gl-style-spec').validate; 18 | * var style = fs.readFileSync('./style.json', 'utf8'); 19 | * var errors = validate(style); 20 | */ 21 | 22 | module.exports = function validateStyle(style, styleSpec) { 23 | var index = require('../'); 24 | 25 | if (style instanceof String || typeof style === 'string' || style instanceof Buffer) { 26 | try { 27 | style = jsonlint.parse(style.toString()); 28 | } catch (e) { 29 | return [new ParsingError(e)]; 30 | } 31 | } 32 | 33 | styleSpec = styleSpec || index['v' + style.version]; 34 | 35 | return validateStyleMin(style, styleSpec); 36 | }; 37 | 38 | exports.source = validateStyleMin.source; 39 | exports.light = validateStyleMin.light; 40 | exports.layer = validateStyleMin.layer; 41 | exports.filter = validateStyleMin.filter; 42 | exports.paintProperty = validateStyleMin.paintProperty; 43 | exports.layoutProperty = validateStyleMin.layoutProperty; 44 | -------------------------------------------------------------------------------- /lib/validate_style.min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validateConstants = require('./validate/validate_constants'); 4 | var validate = require('./validate/validate'); 5 | var latestStyleSpec = require('../reference/latest.min'); 6 | var validateGlyphsURL = require('./validate/validate_glyphs_url'); 7 | 8 | /** 9 | * Validate a Mapbox GL style against the style specification. This entrypoint, 10 | * `mapbox-gl-style-spec/lib/validate_style.min`, is designed to produce as 11 | * small a browserify bundle as possible by omitting unnecessary functionality 12 | * and legacy style specifications. 13 | * 14 | * @param {Object} style The style to be validated. 15 | * @param {Object} [styleSpec] The style specification to validate against. 16 | * If omitted, the latest style spec is used. 17 | * @returns {Array} 18 | * @example 19 | * var validate = require('mapbox-gl-style-spec/lib/validate_style.min'); 20 | * var errors = validate(style); 21 | */ 22 | function validateStyleMin(style, styleSpec) { 23 | styleSpec = styleSpec || latestStyleSpec; 24 | 25 | var errors = []; 26 | 27 | errors = errors.concat(validate({ 28 | key: '', 29 | value: style, 30 | valueSpec: styleSpec.$root, 31 | styleSpec: styleSpec, 32 | style: style, 33 | objectElementValidators: { 34 | glyphs: validateGlyphsURL 35 | } 36 | })); 37 | 38 | if (styleSpec.$version > 7 && style.constants) { 39 | errors = errors.concat(validateConstants({ 40 | key: 'constants', 41 | value: style.constants, 42 | style: style, 43 | styleSpec: styleSpec 44 | })); 45 | } 46 | 47 | return sortErrors(errors); 48 | } 49 | 50 | validateStyleMin.source = wrapCleanErrors(require('./validate/validate_source')); 51 | validateStyleMin.light = wrapCleanErrors(require('./validate/validate_light')); 52 | validateStyleMin.layer = wrapCleanErrors(require('./validate/validate_layer')); 53 | validateStyleMin.filter = wrapCleanErrors(require('./validate/validate_filter')); 54 | validateStyleMin.paintProperty = wrapCleanErrors(require('./validate/validate_paint_property')); 55 | validateStyleMin.layoutProperty = wrapCleanErrors(require('./validate/validate_layout_property')); 56 | 57 | function sortErrors(errors) { 58 | return [].concat(errors).sort(function (a, b) { 59 | return a.line - b.line; 60 | }); 61 | } 62 | 63 | function wrapCleanErrors(inner) { 64 | return function() { 65 | return sortErrors(inner.apply(this, arguments)); 66 | }; 67 | } 68 | 69 | module.exports = validateStyleMin; 70 | -------------------------------------------------------------------------------- /migrations/v7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ref = require('../reference/v7'); 4 | 5 | function eachLayer(layer, callback) { 6 | for (var k in layer.layers) { 7 | callback(layer.layers[k]); 8 | eachLayer(layer.layers[k], callback); 9 | } 10 | } 11 | 12 | function eachPaint(layer, callback) { 13 | for (var k in layer) { 14 | if (k.indexOf('paint') === 0) { 15 | callback(layer[k], k); 16 | } 17 | } 18 | } 19 | 20 | 21 | // dash migrations are only safe to run once per style 22 | var MIGRATE_DASHES = false; 23 | 24 | var vec2props = { 25 | "fill-translate": true, 26 | "line-translate": true, 27 | "icon-offset": true, 28 | "text-offset": true, 29 | "icon-translate": true, 30 | "text-translate": true 31 | }; 32 | 33 | 34 | module.exports = function(style) { 35 | style.version = 7; 36 | 37 | var processedConstants = {}; 38 | 39 | eachLayer(style, function(layer) { 40 | 41 | var round = layer.layout && layer.layout['line-cap'] === 'round'; 42 | 43 | eachPaint(layer, function(paint) { 44 | 45 | 46 | // split raster brightness 47 | if (paint['raster-brightness']) { 48 | var bval = paint['raster-brightness']; 49 | if (typeof bval === 'string') bval = style.constants[bval]; 50 | paint['raster-brightness-min'] = typeof bval[0] === 'string' ? style.constants[bval[0]] : bval[0]; 51 | paint['raster-brightness-max'] = typeof bval[1] === 'string' ? style.constants[bval[1]] : bval[1]; 52 | delete paint['raster-brightness']; 53 | } 54 | 55 | 56 | 57 | // Migrate vec2 prop functions 58 | for (var vec2prop in vec2props) { 59 | var val = paint[vec2prop]; 60 | if (val && Array.isArray(val)) { 61 | var s = val[0]; 62 | var t = val[1]; 63 | 64 | if (typeof s === 'string') { 65 | s = style.constants[s]; 66 | } 67 | if (typeof t === 'string') { 68 | t = style.constants[t]; 69 | } 70 | 71 | // not functions, nothing to migrate 72 | if (s === undefined || t === undefined) continue; 73 | if (!s.stops && !t.stops) continue; 74 | 75 | var stopZooms = []; 76 | var base; 77 | if (s.stops) { 78 | for (var i = 0; i < s.stops.length; i++) { 79 | stopZooms.push(s.stops[i][0]); 80 | } 81 | base = s.base; 82 | } 83 | if (t.stops) { 84 | for (var k = 0; k < t.stops.length; k++) { 85 | stopZooms.push(t.stops[k][0]); 86 | } 87 | base = base || t.base; 88 | } 89 | stopZooms.sort(); 90 | 91 | var fn = parseNumberArray([s, t]); 92 | 93 | var newStops = []; 94 | for (var h = 0; h < stopZooms.length; h++) { 95 | var z = stopZooms[h]; 96 | if (z === stopZooms[h - 1]) continue; 97 | newStops.push([z, fn(z)]); 98 | } 99 | 100 | paint[vec2prop] = { stops: newStops }; 101 | if (base) { 102 | paint[vec2prop].base = base; 103 | } 104 | } 105 | } 106 | 107 | 108 | 109 | if (paint['line-dasharray'] && MIGRATE_DASHES) { 110 | var w = paint['line-width'] ? paint['line-width'] : ref.class_line['line-width'].default; 111 | if (typeof w === 'string') w = style.constants[w]; 112 | 113 | var dasharray = paint['line-dasharray']; 114 | if (typeof dasharray === 'string') { 115 | // don't process a constant more than once 116 | if (processedConstants[dasharray]) return; 117 | processedConstants[dasharray] = true; 118 | 119 | dasharray = style.constants[dasharray]; 120 | } 121 | 122 | if (typeof dasharray[0] === 'string') { 123 | dasharray[0] = style.constants[dasharray[0]]; 124 | } 125 | if (typeof dasharray[1] === 'string') { 126 | dasharray[1] = style.constants[dasharray[1]]; 127 | } 128 | 129 | var widthFn = parseNumber(w); 130 | var dashFn = parseNumberArray(dasharray); 131 | 132 | // since there is no perfect way to convert old functions, 133 | // just use the values at z17 to make the new value. 134 | var zoom = 17; 135 | 136 | var width = typeof widthFn === 'function' ? widthFn(zoom) : widthFn; 137 | var dash = dashFn(zoom); 138 | 139 | dash[0] /= width; 140 | dash[1] /= width; 141 | 142 | if (round) { 143 | dash[0] -= 1; 144 | dash[1] += 1; 145 | } 146 | 147 | if (typeof paint['line-dasharray'] === 'string') { 148 | style.constants[paint['line-dasharray']] = dash; 149 | } else { 150 | paint['line-dasharray'] = dash; 151 | } 152 | } 153 | }); 154 | }); 155 | 156 | style.layers = style.layers.filter(function(layer) { 157 | return !layer.layers; 158 | }); 159 | 160 | return style; 161 | }; 162 | 163 | // from mapbox-gl-js/js/style/style_declaration.js 164 | 165 | function parseNumberArray(array) { 166 | var widths = array.map(parseNumber); 167 | 168 | return function(z) { 169 | var result = []; 170 | for (var i = 0; i < widths.length; i++) { 171 | result.push(typeof widths[i] === 'function' ? widths[i](z) : widths[i]); 172 | } 173 | return result; 174 | }; 175 | } 176 | 177 | 178 | function parseNumber(num) { 179 | if (num.stops) num = stopsFn(num); 180 | var value = +num; 181 | return !isNaN(value) ? value : num; 182 | } 183 | 184 | 185 | function stopsFn(params) { 186 | var stops = params.stops; 187 | var base = params.base || ref.function.base.default; 188 | 189 | return function(z) { 190 | 191 | // find the two stops which the current z is between 192 | var low, high; 193 | 194 | for (var i = 0; i < stops.length; i++) { 195 | var stop = stops[i]; 196 | if (stop[0] <= z) low = stop; 197 | if (stop[0] > z) { 198 | high = stop; 199 | break; 200 | } 201 | } 202 | 203 | if (low && high) { 204 | var zoomDiff = high[0] - low[0], 205 | zoomProgress = z - low[0], 206 | 207 | t = base === 1 ? 208 | zoomProgress / zoomDiff : 209 | (Math.pow(base, zoomProgress) - 1) / (Math.pow(base, zoomDiff) - 1); 210 | 211 | return interp(low[1], high[1], t); 212 | 213 | } else if (low) { 214 | return low[1]; 215 | 216 | } else if (high) { 217 | return high[1]; 218 | 219 | } else { 220 | return 1; 221 | } 222 | }; 223 | } 224 | 225 | function interp(a, b, t) { 226 | return (a * (1 - t)) + (b * t); 227 | } 228 | 229 | -------------------------------------------------------------------------------- /migrations/v8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Reference = require('../reference/v8'); 4 | var URL = require('url'); 5 | 6 | function getPropertyReference(propertyName) { 7 | for (var i = 0; i < Reference.layout.length; i++) { 8 | for (var key in Reference[Reference.layout[i]]) { 9 | if (key === propertyName) return Reference[Reference.layout[i]][key]; 10 | } 11 | } 12 | for (i = 0; i < Reference.paint.length; i++) { 13 | for (key in Reference[Reference.paint[i]]) { 14 | if (key === propertyName) return Reference[Reference.paint[i]][key]; 15 | } 16 | } 17 | } 18 | 19 | function eachSource(style, callback) { 20 | for (var k in style.sources) { 21 | callback(style.sources[k]); 22 | } 23 | } 24 | 25 | function eachLayer(style, callback) { 26 | for (var k in style.layers) { 27 | callback(style.layers[k]); 28 | eachLayer(style.layers[k], callback); 29 | } 30 | } 31 | 32 | function eachLayout(layer, callback) { 33 | for (var k in layer) { 34 | if (k.indexOf('layout') === 0) { 35 | callback(layer[k], k); 36 | } 37 | } 38 | } 39 | 40 | function eachPaint(layer, callback) { 41 | for (var k in layer) { 42 | if (k.indexOf('paint') === 0) { 43 | callback(layer[k], k); 44 | } 45 | } 46 | } 47 | 48 | function resolveConstant(style, value) { 49 | if (typeof value === 'string' && value[0] === '@') { 50 | return resolveConstant(style, style.constants[value]); 51 | } else { 52 | return value; 53 | } 54 | } 55 | 56 | function eachProperty(style, options, callback) { 57 | if (arguments.length === 2) { 58 | callback = options; 59 | options = {}; 60 | } 61 | 62 | options.layout = options.layout === undefined ? true : options.layout; 63 | options.paint = options.paint === undefined ? true : options.paint; 64 | 65 | function inner(layer, properties) { 66 | Object.keys(properties).forEach(function(key) { 67 | callback({ 68 | key: key, 69 | value: properties[key], 70 | reference: getPropertyReference(key), 71 | set: function(x) { 72 | properties[key] = x; 73 | } 74 | }); 75 | }); 76 | } 77 | 78 | eachLayer(style, function(layer) { 79 | if (options.paint) { 80 | eachPaint(layer, function(paint) { 81 | inner(layer, paint); 82 | }); 83 | } 84 | if (options.layout) { 85 | eachLayout(layer, function(layout) { 86 | inner(layer, layout); 87 | }); 88 | } 89 | }); 90 | } 91 | 92 | function isFunction(value) { 93 | return Array.isArray(value.stops); 94 | } 95 | 96 | function renameProperty(obj, from, to) { 97 | obj[to] = obj[from]; delete obj[from]; 98 | } 99 | 100 | module.exports = function(style) { 101 | style.version = 8; 102 | 103 | // Rename properties, reverse coordinates in source and layers 104 | eachSource(style, function(source) { 105 | if (source.type === 'video' && source.url !== undefined) { 106 | renameProperty(source, 'url', 'urls'); 107 | } 108 | if (source.type === 'video') { 109 | source.coordinates.forEach(function(coord) { 110 | return coord.reverse(); 111 | }); 112 | } 113 | }); 114 | 115 | eachLayer(style, function(layer) { 116 | eachLayout(layer, function(layout) { 117 | if (layout['symbol-min-distance'] !== undefined) { 118 | renameProperty(layout, 'symbol-min-distance', 'symbol-spacing'); 119 | } 120 | }); 121 | 122 | eachPaint(layer, function(paint) { 123 | if (paint['background-image'] !== undefined) { 124 | renameProperty(paint, 'background-image', 'background-pattern'); 125 | } 126 | if (paint['line-image'] !== undefined) { 127 | renameProperty(paint, 'line-image', 'line-pattern'); 128 | } 129 | if (paint['fill-image'] !== undefined) { 130 | renameProperty(paint, 'fill-image', 'fill-pattern'); 131 | } 132 | }); 133 | }); 134 | 135 | // Inline Constants 136 | eachProperty(style, function(property) { 137 | var value = resolveConstant(style, property.value); 138 | 139 | if (isFunction(value)) { 140 | value.stops.forEach(function(stop) { 141 | stop[1] = resolveConstant(style, stop[1]); 142 | }); 143 | } 144 | 145 | property.set(value); 146 | }); 147 | delete style.constants; 148 | 149 | eachLayer(style, function(layer) { 150 | // get rid of text-max-size, icon-max-size 151 | // turn text-size, icon-size into layout properties 152 | // https://github.com/mapbox/mapbox-gl-style-spec/issues/255 153 | 154 | eachLayout(layer, function(layout) { 155 | delete layout['text-max-size']; 156 | delete layout['icon-max-size']; 157 | }); 158 | 159 | eachPaint(layer, function(paint) { 160 | if (paint['text-size']) { 161 | if (!layer.layout) layer.layout = {}; 162 | layer.layout['text-size'] = paint['text-size']; 163 | delete paint['text-size']; 164 | } 165 | 166 | if (paint['icon-size']) { 167 | if (!layer.layout) layer.layout = {}; 168 | layer.layout['icon-size'] = paint['icon-size']; 169 | delete paint['icon-size']; 170 | } 171 | }); 172 | }); 173 | 174 | function migrateFontstackURL(input) { 175 | var inputParsed = URL.parse(input); 176 | var inputPathnameParts = inputParsed.pathname.split('/'); 177 | 178 | if (inputParsed.protocol !== 'mapbox:') { 179 | return input; 180 | 181 | } else if (inputParsed.hostname === 'fontstack') { 182 | assert(decodeURI(inputParsed.pathname) === '/{fontstack}/{range}.pbf'); 183 | return 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf'; 184 | 185 | } else if (inputParsed.hostname === 'fonts') { 186 | assert(inputPathnameParts[1] === 'v1'); 187 | assert(decodeURI(inputPathnameParts[3]) === '{fontstack}'); 188 | assert(decodeURI(inputPathnameParts[4]) === '{range}.pbf'); 189 | return 'mapbox://fonts/' + inputPathnameParts[2] + '/{fontstack}/{range}.pbf'; 190 | 191 | } else { 192 | assert(false); 193 | } 194 | 195 | function assert(predicate) { 196 | if (!predicate) { 197 | throw new Error('Invalid font url: "' + input + '"'); 198 | } 199 | } 200 | } 201 | 202 | if (style.glyphs) { 203 | style.glyphs = migrateFontstackURL(style.glyphs); 204 | } 205 | 206 | function migrateFontStack(font) { 207 | function splitAndTrim(string) { 208 | return string.split(',').map(function(s) { 209 | return s.trim(); 210 | }); 211 | } 212 | 213 | if (Array.isArray(font)) { 214 | // Assume it's a previously migrated font-array. 215 | return font; 216 | 217 | } else if (typeof font === 'string') { 218 | return splitAndTrim(font); 219 | 220 | } else if (typeof font === 'object') { 221 | font.stops.forEach(function(stop) { 222 | stop[1] = splitAndTrim(stop[1]); 223 | }); 224 | return font; 225 | 226 | } else { 227 | throw new Error("unexpected font value"); 228 | } 229 | } 230 | 231 | eachLayer(style, function(layer) { 232 | eachLayout(layer, function(layout) { 233 | if (layout['text-font']) { 234 | layout['text-font'] = migrateFontStack(layout['text-font']); 235 | } 236 | }); 237 | }); 238 | 239 | // Reverse order of symbol layers. This is an imperfect migration. 240 | // 241 | // The order of a symbol layer in the layers list affects two things: 242 | // - how it is drawn relative to other layers (like oneway arrows below bridges) 243 | // - the placement priority compared to other layers 244 | // 245 | // It's impossible to reverse the placement priority without breaking the draw order 246 | // in some cases. This migration only reverses the order of symbol layers that 247 | // are above all other types of layers. 248 | // 249 | // Symbol layers that are at the top of the map preserve their priority. 250 | // Symbol layers that are below another type (line, fill) of layer preserve their draw order. 251 | 252 | var firstSymbolLayer = 0; 253 | for (var i = style.layers.length - 1; i >= 0; i--) { 254 | var layer = style.layers[i]; 255 | if (layer.type !== 'symbol') { 256 | firstSymbolLayer = i + 1; 257 | break; 258 | } 259 | } 260 | 261 | var symbolLayers = style.layers.splice(firstSymbolLayer); 262 | symbolLayers.reverse(); 263 | style.layers = style.layers.concat(symbolLayers); 264 | 265 | return style; 266 | }; 267 | -------------------------------------------------------------------------------- /minify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | function replacer(k,v) { 6 | return (k === 'doc' || k === 'example' || k === 'sdk-support') ? undefined : v; 7 | } 8 | 9 | var glob = require('glob'), 10 | path = require('path'), 11 | rw = require('rw'); 12 | 13 | var files = glob.sync(path.join(__dirname, 'reference/*.json')); 14 | files.forEach(function(file) { 15 | if (file.match(/.min.json/i) !== null) return; 16 | rw.writeFileSync(file.replace(/.json/i, '.min.json'), 17 | JSON.stringify(JSON.parse(rw.readFileSync(file)), replacer, 0) 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mapbox-gl-style-spec", 3 | "description": "a specification for mapbox gl styles", 4 | "version": "8.9.0", 5 | "author": "Mapbox", 6 | "bin": { 7 | "gl-style-migrate": "bin/gl-style-migrate", 8 | "gl-style-validate": "bin/gl-style-validate", 9 | "gl-style-format": "bin/gl-style-format", 10 | "gl-style-composite": "bin/gl-style-composite" 11 | }, 12 | "dependencies": { 13 | "csscolorparser": "~1.0.2", 14 | "jsonlint-lines-primitives": "~1.6.0", 15 | "lodash.isequal": "^3.0.4", 16 | "minimist": "0.0.8", 17 | "rw": "^0.1.4", 18 | "sort-object": "^0.3.2" 19 | }, 20 | "devDependencies": { 21 | "coveralls": "^2.11.9", 22 | "coverify": "~1.0.7", 23 | "dox": "^0.6.1", 24 | "doxme": "^1.4.2", 25 | "eslint": "^0.16.2", 26 | "glob": "^7.0.3", 27 | "istanbul": "~0.2.11", 28 | "lodash": "^4.16.0", 29 | "remark": "^6.0.1", 30 | "remark-html": "^5.0.1", 31 | "tap-min": "^1.0.0", 32 | "tape": "^2.12.1" 33 | }, 34 | "keywords": [ 35 | "mapbox", 36 | "mapbox-gl", 37 | "mapbox-gl-js" 38 | ], 39 | "license": "ISC", 40 | "main": "index.js", 41 | "repository": { 42 | "type": "git", 43 | "url": "git@github.com:mapbox/mapbox-gl-style-spec.git" 44 | }, 45 | "scripts": { 46 | "build": "node minify.js && npm run docs", 47 | "cov": "istanbul cover ./node_modules/.bin/tape test/*.js test/migrations/*.js && coveralls < ./coverage/lcov.info", 48 | "docs": "cat lib/*.js lib/*/*.js | dox --raw --skipSingleStar | doxme > API.md && cd docs/_generate && node generate.js", 49 | "lint": "eslint lib/*.js lib/*/*.js migrations/*.js", 50 | "test": "npm run lint && tape test/*.js test/migrations/*.js | tap-min", 51 | "start": "npm run docs && jekyll serve -w" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /reference/latest.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./v8.json'); 2 | -------------------------------------------------------------------------------- /reference/latest.min.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./v8.min.json'); 2 | -------------------------------------------------------------------------------- /reference/v6.min.json: -------------------------------------------------------------------------------- 1 | {"$version":6,"$root":{"version":{"required":true,"type":"enum","values":[6]},"constants":{"type":"constants"},"sources":{"required":true,"type":"sources"},"sprite":{"type":"string"},"glyphs":{"type":"string"},"transition":{"type":"transition"},"layers":{"required":true,"type":"array","value":"layer"}},"constants":{"*":{"type":"*"}},"sources":{"*":{"type":"source"}},"source":["source_tile","source_geojson","source_video"],"source_tile":{"type":{"required":true,"type":"enum","values":["vector","raster"]},"url":{"type":"string"},"tiles":{"type":"array","value":"string"},"minzoom":{"type":"number","default":0},"maxzoom":{"type":"number","default":22},"tileSize":{"type":"number","default":512,"units":"pixels"},"*":{"type":"*"}},"source_geojson":{"type":{"required":true,"type":"enum","values":["geojson"]},"data":{"type":"*"}},"source_video":{"type":{"required":true,"type":"enum","values":["video"]},"url":{"required":true,"type":"array","value":"string"},"coordinates":{"required":true,"type":"array","length":4,"value":{"type":"array","length":2,"value":"number"}}},"layer":{"id":{"type":"string"},"type":{"type":"enum","values":["fill","line","symbol","raster","background"]},"ref":{"type":"string"},"source":{"type":"string"},"source-layer":{"type":"string"},"minzoom":{"type":"number","minimum":0,"maximum":22},"maxzoom":{"type":"number","minimum":0,"maximum":22},"interactive":{"type":"boolean","default":false},"filter":{"type":"filter"},"layers":{"type":"array","value":"layer"},"layout":{"type":"layout"},"paint":{"type":"paint"},"paint.*":{"type":"paint"}},"layout":["layout_fill","layout_line","layout_symbol","layout_raster","layout_background"],"layout_background":{},"layout_fill":{},"layout_line":{"line-cap":{"type":"enum","values":["butt","round","square"],"default":"butt"},"line-join":{"type":"enum","values":["bevel","round","miter"],"default":"miter"},"line-miter-limit":{"type":"number","default":2,"requires":[{"line-join":"miter"}]},"line-round-limit":{"type":"number","default":1,"requires":[{"line-join":"round"}]}},"layout_symbol":{"symbol-placement":{"type":"enum","values":["point","line"],"default":"point"},"symbol-min-distance":{"type":"number","default":250,"minimum":1,"units":"pixels","requires":[{"symbol-placement":"line"}]},"symbol-avoid-edges":{"type":"boolean","default":false},"icon-allow-overlap":{"type":"boolean","default":false,"requires":["icon-image"]},"icon-ignore-placement":{"type":"boolean","default":false,"requires":["icon-image"]},"icon-optional":{"type":"boolean","default":false,"requires":["icon-image","text-field"]},"icon-rotation-alignment":{"type":"enum","values":["map","viewport"],"default":"viewport","requires":["icon-image"]},"icon-max-size":{"type":"number","default":1,"minimum":0,"requires":["icon-image"]},"icon-image":{"type":"string","tokens":true},"icon-rotate":{"type":"number","default":0,"period":360,"units":"degrees","requires":["icon-image"]},"icon-padding":{"type":"number","default":2,"minimum":0,"units":"pixels","requires":["icon-image"]},"icon-keep-upright":{"type":"boolean","default":false,"requires":["icon-image",{"icon-rotation-alignment":"map"}]},"icon-offset":{"type":"array","value":"number","length":2,"default":[0,0],"requires":["icon-image"]},"text-rotation-alignment":{"type":"enum","values":["map","viewport"],"default":"viewport","requires":["text-field"]},"text-field":{"type":"string","default":"","tokens":true},"text-font":{"type":"string","default":"Open Sans Regular, Arial Unicode MS Regular","requires":["text-field"]},"text-max-size":{"type":"number","default":16,"minimum":0,"units":"pixels","requires":["text-field"]},"text-max-width":{"type":"number","default":15,"minimum":0,"units":"em","requires":["text-field"]},"text-line-height":{"type":"number","default":1.2,"units":"em","requires":["text-field"]},"text-letter-spacing":{"type":"number","default":0,"units":"em","requires":["text-field"]},"text-justify":{"type":"enum","values":["left","center","right"],"default":"center","requires":["text-field"]},"text-anchor":{"type":"enum","values":["center","left","right","top","bottom","top-left","top-right","bottom-left","bottom-right"],"default":"center","requires":["text-field"]},"text-max-angle":{"type":"number","default":45,"units":"degrees","requires":["text-field",{"symbol-placement":"line"}]},"text-rotate":{"type":"number","default":0,"period":360,"units":"degrees","requires":["text-field"]},"text-padding":{"type":"number","default":2,"minimum":0,"units":"pixels","requires":["text-field"]},"text-keep-upright":{"type":"boolean","default":true,"requires":["text-field",{"text-rotation-alignment":"map"}]},"text-transform":{"type":"enum","values":["none","uppercase","lowercase"],"default":"none","requires":["text-field"]},"text-offset":{"type":"array","value":"number","units":"ems","length":2,"default":[0,0],"requires":["text-field"]},"text-allow-overlap":{"type":"boolean","default":false,"requires":["text-field"]},"text-ignore-placement":{"type":"boolean","default":false,"requires":["text-field"]},"text-optional":{"type":"boolean","default":false,"requires":["text-field","icon-image"]}},"layout_raster":{"raster-size":{"type":"number","default":256,"minimum":0,"maximum":3855,"units":"pixels"},"raster-blur":{"type":"number","default":0,"minimum":0,"units":"pixels"}},"filter":{"type":"array","value":"*"},"filter_operator":{"type":"enum","values":["==","!=",">",">=","<","<=","in","!in","all","any","none"]},"geometry_type":{"type":"enum","values":["Point","LineString","Polygon"]},"function":{"stops":{"type":"array","required":true,"value":"function_stop"},"base":{"type":"number","default":1,"minimum":0}},"function_stop":{"type":"array","minimum":0,"maximum":22,"value":["number","color"],"length":2},"paint":["paint_fill","paint_line","paint_symbol","paint_raster","paint_background"],"paint_fill":{"fill-antialias":{"type":"boolean","default":true,"function":true},"fill-opacity":{"type":"number","function":true,"default":1,"minimum":0,"maximum":1,"transition":true},"fill-color":{"type":"color","default":"#000000","function":true,"transition":true,"requires":[{"!":"fill-image"}]},"fill-outline-color":{"type":"color","function":true,"transition":true,"requires":[{"!":"fill-image"},{"fill-antialias":true}]},"fill-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":true,"transition":true,"units":"pixels"},"fill-translate-anchor":{"type":"enum","values":["map","viewport"],"default":"map","requires":["fill-translate"]},"fill-image":{"type":"string"}},"paint_line":{"line-opacity":{"type":"number","function":true,"default":1,"minimum":0,"maximum":1,"transition":true},"line-color":{"type":"color","default":"#000000","function":true,"transition":true,"requires":[{"!":"line-image"}]},"line-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":true,"transition":true,"units":"pixels"},"line-translate-anchor":{"type":"enum","values":["map","viewport"],"default":"map","requires":["line-translate"]},"line-width":{"type":"number","default":1,"minimum":0,"function":true,"transition":true,"units":"pixels"},"line-gap-width":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels"},"line-blur":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels"},"line-dasharray":{"type":"array","value":"number","length":2,"default":[1,-1],"minimum":0,"function":true,"transition":true,"requires":[{"!":"line-image"}]},"line-image":{"type":"string"}},"paint_symbol":{"icon-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":true,"transition":true,"requires":["icon-image"]},"icon-size":{"type":"number","default":1,"function":true,"transition":true,"requires":["icon-image"]},"icon-color":{"type":"color","default":"#000000","function":true,"transition":true,"requires":["icon-image"]},"icon-halo-color":{"type":"color","default":"rgba(0, 0, 0, 0)","function":true,"transition":true,"requires":["icon-image"]},"icon-halo-width":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels","requires":["icon-image"]},"icon-halo-blur":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels","requires":["icon-image"]},"icon-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":true,"transition":true,"units":"pixels","requires":["icon-image"]},"icon-translate-anchor":{"type":"enum","values":["map","viewport"],"default":"map","requires":["icon-image","icon-translate"]},"text-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":true,"transition":true,"requires":["text-field"]},"text-size":{"type":"number","default":16,"minimum":0,"function":true,"transition":true,"units":"pixels","requires":["text-field"]},"text-color":{"type":"color","default":"#000000","function":true,"transition":true,"requires":["text-field"]},"text-halo-color":{"type":"color","default":"rgba(0, 0, 0, 0)","function":true,"transition":true,"requires":["text-field"]},"text-halo-width":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels","requires":["text-field"]},"text-halo-blur":{"type":"number","default":0,"minimum":0,"function":true,"transition":true,"units":"pixels","requires":["text-field"]},"text-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":true,"transition":true,"units":"pixels","requires":["text-field"]},"text-translate-anchor":{"type":"enum","values":["map","viewport"],"default":"map","requires":["text-field","text-translate"]}},"paint_raster":{"raster-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"transition":true},"raster-hue-rotate":{"type":"number","default":0,"period":360,"function":true,"transition":true,"units":"degrees"},"raster-brightness":{"type":"array","value":"number","length":2,"default":[0,1],"function":true,"transition":true},"raster-saturation":{"type":"number","default":0,"minimum":-1,"maximum":1,"function":true,"transition":true},"raster-contrast":{"type":"number","default":0,"minimum":-1,"maximum":1,"function":true,"transition":true},"raster-fade-duration":{"type":"number","default":300,"minimum":0,"function":true,"transition":true,"units":"milliseconds"}},"paint_background":{"background-color":{"type":"color","default":"#000000","function":true,"transition":true,"requires":[{"!":"background-image"}]},"background-image":{"type":"string"},"background-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":true,"transition":true}},"transition":{"duration":{"type":"number","default":300,"minimum":0,"units":"milliseconds"},"delay":{"type":"number","default":0,"minimum":0,"units":"milliseconds"}}} -------------------------------------------------------------------------------- /reference/v7.min.json: -------------------------------------------------------------------------------- 1 | {"$version":7,"$root":{"version":{"required":true,"type":"enum","values":[7]},"name":{"type":"string"},"constants":{"type":"constants"},"sources":{"required":true,"type":"sources"},"sprite":{"type":"string"},"glyphs":{"type":"string"},"transition":{"type":"transition"},"layers":{"required":true,"type":"array","value":"layer"}},"constants":{"*":{"type":"*"}},"sources":{"*":{"type":"source"}},"source":["source_tile","source_geojson","source_video"],"source_tile":{"type":{"required":true,"type":"enum","values":["vector","raster"]},"url":{"type":"string"},"tiles":{"type":"array","value":"string"},"minzoom":{"type":"number","default":0},"maxzoom":{"type":"number","default":22},"tileSize":{"type":"number","default":512,"units":"pixels"},"*":{"type":"*"}},"source_geojson":{"type":{"required":true,"type":"enum","values":["geojson"]},"data":{"type":"*"}},"source_video":{"type":{"required":true,"type":"enum","values":["video"]},"url":{"required":true,"type":"array","value":"string"},"coordinates":{"required":true,"type":"array","length":4,"value":{"type":"array","length":2,"value":"number"}}},"layer":{"id":{"type":"string"},"type":{"type":"enum","values":["fill","line","symbol","raster","background"]},"ref":{"type":"string"},"source":{"type":"string"},"source-layer":{"type":"string"},"minzoom":{"type":"number","minimum":0,"maximum":22},"maxzoom":{"type":"number","minimum":0,"maximum":22},"interactive":{"type":"boolean","default":false},"filter":{"type":"filter"},"layout":{"type":"layout"},"paint":{"type":"paint"},"paint.*":{"type":"paint"}},"layout":["layout_fill","layout_line","layout_symbol","layout_raster","layout_background"],"layout_background":{"visibility":{"type":"enum","function":"piecewise-constant","values":["visible","none"],"default":"visible"}},"layout_fill":{"visibility":{"type":"enum","function":"piecewise-constant","values":["visible","none"],"default":"visible"}},"layout_line":{"line-cap":{"type":"enum","function":"piecewise-constant","values":["butt","round","square"],"default":"butt"},"line-join":{"type":"enum","function":"piecewise-constant","values":["bevel","round","miter"],"default":"miter"},"line-miter-limit":{"type":"number","default":2,"function":"interpolated","requires":[{"line-join":"miter"}]},"line-round-limit":{"type":"number","default":1,"function":"interpolated","requires":[{"line-join":"round"}]},"visibility":{"type":"enum","function":"piecewise-constant","values":["visible","none"],"default":"visible"}},"layout_symbol":{"symbol-placement":{"type":"enum","function":"piecewise-constant","values":["point","line"],"default":"point"},"symbol-min-distance":{"type":"number","default":250,"minimum":1,"function":"interpolated","units":"pixels","requires":[{"symbol-placement":"line"}]},"symbol-avoid-edges":{"type":"boolean","function":"piecewise-constant","default":false},"icon-allow-overlap":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["icon-image"]},"icon-ignore-placement":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["icon-image"]},"icon-optional":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["icon-image","text-field"]},"icon-rotation-alignment":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"viewport","requires":["icon-image"]},"icon-max-size":{"type":"number","default":1,"minimum":0,"function":"interpolated","requires":["icon-image"]},"icon-image":{"type":"string","function":"piecewise-constant","tokens":true},"icon-rotate":{"type":"number","default":0,"period":360,"function":"interpolated","units":"degrees","requires":["icon-image"]},"icon-padding":{"type":"number","default":2,"minimum":0,"function":"interpolated","units":"pixels","requires":["icon-image"]},"icon-keep-upright":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["icon-image",{"icon-rotation-alignment":"map"}]},"icon-offset":{"type":"array","value":"number","length":2,"default":[0,0],"function":"interpolated","requires":["icon-image"]},"text-rotation-alignment":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"viewport","requires":["text-field"]},"text-field":{"type":"string","function":"piecewise-constant","default":"","tokens":true},"text-font":{"type":"string","function":"piecewise-constant","default":"Open Sans Regular, Arial Unicode MS Regular","requires":["text-field"]},"text-max-size":{"type":"number","default":16,"minimum":0,"units":"pixels","function":"interpolated","requires":["text-field"]},"text-max-width":{"type":"number","default":15,"minimum":0,"units":"em","function":"interpolated","requires":["text-field"]},"text-line-height":{"type":"number","default":1.2,"units":"em","function":"interpolated","requires":["text-field"]},"text-letter-spacing":{"type":"number","default":0,"units":"em","function":"interpolated","requires":["text-field"]},"text-justify":{"type":"enum","function":"piecewise-constant","values":["left","center","right"],"default":"center","requires":["text-field"]},"text-anchor":{"type":"enum","function":"piecewise-constant","values":["center","left","right","top","bottom","top-left","top-right","bottom-left","bottom-right"],"default":"center","requires":["text-field"]},"text-max-angle":{"type":"number","default":45,"units":"degrees","function":"interpolated","requires":["text-field",{"symbol-placement":"line"}]},"text-rotate":{"type":"number","default":0,"period":360,"units":"degrees","function":"interpolated","requires":["text-field"]},"text-padding":{"type":"number","default":2,"minimum":0,"units":"pixels","function":"interpolated","requires":["text-field"]},"text-keep-upright":{"type":"boolean","function":"piecewise-constant","default":true,"requires":["text-field",{"text-rotation-alignment":"map"}]},"text-transform":{"type":"enum","function":"piecewise-constant","values":["none","uppercase","lowercase"],"default":"none","requires":["text-field"]},"text-offset":{"type":"array","value":"number","units":"ems","function":"interpolated","length":2,"default":[0,0],"requires":["text-field"]},"text-allow-overlap":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["text-field"]},"text-ignore-placement":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["text-field"]},"text-optional":{"type":"boolean","function":"piecewise-constant","default":false,"requires":["text-field","icon-image"]},"visibility":{"type":"enum","function":"piecewise-constant","values":["visible","none"],"default":"visible"}},"layout_raster":{"visibility":{"type":"enum","function":"piecewise-constant","values":["visible","none"],"default":"visible"}},"filter":{"type":"array","value":"*"},"filter_operator":{"type":"enum","values":["==","!=",">",">=","<","<=","in","!in","all","any","none"]},"geometry_type":{"type":"enum","values":["Point","LineString","Polygon"]},"function":{"stops":{"type":"array","required":true,"value":"function_stop"},"base":{"type":"number","default":1,"minimum":0}},"function_stop":{"type":"array","minimum":0,"maximum":22,"value":["number","color"],"length":2},"paint":["paint_fill","paint_line","paint_symbol","paint_raster","paint_background"],"paint_fill":{"fill-antialias":{"type":"boolean","function":"piecewise-constant","default":true},"fill-opacity":{"type":"number","function":"interpolated","default":1,"minimum":0,"maximum":1,"transition":true},"fill-color":{"type":"color","default":"#000000","function":"interpolated","transition":true,"requires":[{"!":"fill-image"}]},"fill-outline-color":{"type":"color","function":"interpolated","transition":true,"requires":[{"!":"fill-image"},{"fill-antialias":true}]},"fill-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":"interpolated","transition":true,"units":"pixels"},"fill-translate-anchor":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"map","requires":["fill-translate"]},"fill-image":{"type":"string","function":"piecewise-constant","transition":true}},"paint_line":{"line-opacity":{"type":"number","function":"interpolated","default":1,"minimum":0,"maximum":1,"transition":true},"line-color":{"type":"color","default":"#000000","function":"interpolated","transition":true,"requires":[{"!":"line-image"}]},"line-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":"interpolated","transition":true,"units":"pixels"},"line-translate-anchor":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"map","requires":["line-translate"]},"line-width":{"type":"number","default":1,"minimum":0,"function":"interpolated","transition":true,"units":"pixels"},"line-gap-width":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels"},"line-blur":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels"},"line-dasharray":{"type":"array","function":"piecewise-constant","value":"number","minimum":0,"transition":true,"units":"line widths","requires":[{"!":"line-image"}]},"line-image":{"type":"string","function":"piecewise-constant","transition":true}},"paint_symbol":{"icon-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":"interpolated","transition":true,"requires":["icon-image"]},"icon-size":{"type":"number","default":1,"function":"interpolated","transition":true,"requires":["icon-image"]},"icon-color":{"type":"color","default":"#000000","function":"interpolated","transition":true,"requires":["icon-image"]},"icon-halo-color":{"type":"color","default":"rgba(0, 0, 0, 0)","function":"interpolated","transition":true,"requires":["icon-image"]},"icon-halo-width":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels","requires":["icon-image"]},"icon-halo-blur":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels","requires":["icon-image"]},"icon-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":"interpolated","transition":true,"units":"pixels","requires":["icon-image"]},"icon-translate-anchor":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"map","requires":["icon-image","icon-translate"]},"text-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":"interpolated","transition":true,"requires":["text-field"]},"text-size":{"type":"number","default":16,"minimum":0,"function":"interpolated","transition":true,"units":"pixels","requires":["text-field"]},"text-color":{"type":"color","default":"#000000","function":"interpolated","transition":true,"requires":["text-field"]},"text-halo-color":{"type":"color","default":"rgba(0, 0, 0, 0)","function":"interpolated","transition":true,"requires":["text-field"]},"text-halo-width":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels","requires":["text-field"]},"text-halo-blur":{"type":"number","default":0,"minimum":0,"function":"interpolated","transition":true,"units":"pixels","requires":["text-field"]},"text-translate":{"type":"array","value":"number","length":2,"default":[0,0],"function":"interpolated","transition":true,"units":"pixels","requires":["text-field"]},"text-translate-anchor":{"type":"enum","function":"piecewise-constant","values":["map","viewport"],"default":"map","requires":["text-field","text-translate"]}},"paint_raster":{"raster-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":"interpolated","transition":true},"raster-hue-rotate":{"type":"number","default":0,"period":360,"function":"interpolated","transition":true,"units":"degrees"},"raster-brightness-min":{"type":"number","function":"interpolated","default":0,"minimum":0,"maximum":1,"transition":true},"raster-brightness-max":{"type":"number","function":"interpolated","default":1,"minimum":0,"maximum":1,"transition":true},"raster-saturation":{"type":"number","default":0,"minimum":-1,"maximum":1,"function":"interpolated","transition":true},"raster-contrast":{"type":"number","default":0,"minimum":-1,"maximum":1,"function":"interpolated","transition":true},"raster-fade-duration":{"type":"number","default":300,"minimum":0,"function":"interpolated","transition":true,"units":"milliseconds"}},"paint_background":{"background-color":{"type":"color","default":"#000000","function":"interpolated","transition":true,"requires":[{"!":"background-image"}]},"background-image":{"type":"string","function":"piecewise-constant","transition":true},"background-opacity":{"type":"number","default":1,"minimum":0,"maximum":1,"function":"interpolated","transition":true}},"transition":{"duration":{"type":"number","default":300,"minimum":0,"units":"milliseconds"},"delay":{"type":"number","default":0,"minimum":0,"units":"milliseconds"}}} -------------------------------------------------------------------------------- /test/composite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var composite = require('../lib/composite'); 5 | 6 | test('composites Mapbox vector sources', function (t) { 7 | var result = composite({ 8 | "version": 7, 9 | "sources": { 10 | "mapbox-a": { 11 | "type": "vector", 12 | "url": "mapbox://a" 13 | }, 14 | "mapbox-b": { 15 | "type": "vector", 16 | "url": "mapbox://b" 17 | } 18 | }, 19 | "layers": [{ 20 | "id": "a", 21 | "type": "line", 22 | "source": "mapbox-a" 23 | }, { 24 | "id": "b", 25 | "type": "line", 26 | "source": "mapbox-b" 27 | }] 28 | }); 29 | 30 | t.deepEqual(result.sources, { 31 | "a,b": { 32 | "type": "vector", 33 | "url": "mapbox://a,b" 34 | } 35 | }); 36 | 37 | t.equal(result.layers[0].source, "a,b"); 38 | t.equal(result.layers[1].source, "a,b"); 39 | t.end(); 40 | }); 41 | 42 | test('does not composite vector + raster', function (t) { 43 | var result = composite({ 44 | "version": 7, 45 | "sources": { 46 | "a": { 47 | "type": "vector", 48 | "url": "mapbox://a" 49 | }, 50 | "b": { 51 | "type": "raster", 52 | "url": "mapbox://b" 53 | } 54 | }, 55 | "layers": [] 56 | }); 57 | 58 | t.deepEqual(Object.keys(result.sources), ["a", "b"]); 59 | t.end(); 60 | }); 61 | -------------------------------------------------------------------------------- /test/diff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var t = require('tape'), 4 | diffStyles = require('../lib/diff'); 5 | 6 | t('diff', function (t) { 7 | 8 | t.deepEqual(diffStyles({ 9 | layers: [{ id: 'a' }] 10 | }, { 11 | layers: [{ id: 'a' }] 12 | }), [], 'no changes'); 13 | 14 | t.deepEqual(diffStyles({ 15 | version: 7, 16 | layers: [{ id: 'a' }] 17 | }, { 18 | version: 8, 19 | layers: [{ id: 'a' }] 20 | }), [ 21 | { command: 'setStyle', args: [{ version: 8, layers: [{ id: 'a' }] }] } 22 | ], 'version change'); 23 | 24 | 25 | t.deepEqual(diffStyles({ 26 | layers: [{ id: 'a' }] 27 | }, { 28 | layers: [{ id: 'a' }, { id: 'b' }] 29 | }), [ 30 | { command: 'addLayer', args: [{ id: 'b' }, undefined] } 31 | ], 'add a layer'); 32 | 33 | t.deepEqual(diffStyles({ 34 | layers: [{ id: 'b' }] 35 | }, { 36 | layers: [{ id: 'a' }, { id: 'b' }] 37 | }), [ 38 | { command: 'addLayer', args: [{ id: 'a' }, 'b'] } 39 | ], 'add a layer before another'); 40 | 41 | t.deepEqual(diffStyles({ 42 | layers: [{ id: 'a' }, { id: 'b', source: 'foo', nested: [1] }] 43 | }, { 44 | layers: [{ id: 'a' }] 45 | }), [ 46 | { command: 'removeLayer', args: ['b'] } 47 | ], 'remove a layer'); 48 | 49 | t.deepEqual(diffStyles({ 50 | layers: [{ id: 'a' }, { id: 'b' }] 51 | }, { 52 | layers: [{ id: 'b' }, { id: 'a' }] 53 | }), [ 54 | { command: 'removeLayer', args: ['a'] }, 55 | { command: 'addLayer', args: [{ id: 'a' }, undefined] } 56 | ], 'move a layer'); 57 | 58 | t.deepEqual(diffStyles({ 59 | layers: [{ id: 'a', paint: { foo: 1 } }] 60 | }, { 61 | layers: [{ id: 'a', paint: { foo: 2 } }] 62 | }), [ 63 | { command: 'setPaintProperty', args: ['a', 'foo', 2, null] } 64 | ], 'update paint property'); 65 | 66 | t.deepEqual(diffStyles({ 67 | layers: [{ id: 'a', 'paint.light': { foo: 1 } }] 68 | }, { 69 | layers: [{ id: 'a', 'paint.light': { foo: 2 } }] 70 | }), [ 71 | { command: 'setPaintProperty', args: ['a', 'foo', 2, 'light'] } 72 | ], 'update paint property class'); 73 | 74 | t.deepEqual(diffStyles({ 75 | layers: [{ id: 'a', paint: { foo: { ramp: [1, 2] } } }] 76 | }, { 77 | layers: [{ id: 'a', paint: { foo: { ramp: [1] } } }] 78 | }), [ 79 | { command: 'setPaintProperty', args: ['a', 'foo', { ramp: [1] }, null] } 80 | ], 'nested style change'); 81 | 82 | t.deepEqual(diffStyles({ 83 | layers: [{ id: 'a', layout: { foo: 1 } }] 84 | }, { 85 | layers: [{ id: 'a', layout: { foo: 2 } }] 86 | }), [ 87 | { command: 'setLayoutProperty', args: ['a', 'foo', 2, null] } 88 | ], 'update layout property'); 89 | 90 | t.deepEqual(diffStyles({ 91 | layers: [{ id: 'a', filter: ['==', 'foo', 'bar'] }] 92 | }, { 93 | layers: [{ id: 'a', filter: ['==', 'foo', 'baz'] }] 94 | }), [ 95 | { command: 'setFilter', args: ['a', [ '==', 'foo', 'baz' ] ] } 96 | ], 'update a filter'); 97 | 98 | t.deepEqual(diffStyles({ 99 | sources: { foo: 1 } 100 | }, { 101 | sources: {} 102 | }), [ 103 | { command: 'removeSource', args: ['foo'] } 104 | ], 'remove a source'); 105 | 106 | t.deepEqual(diffStyles({ 107 | sources: {} 108 | }, { 109 | sources: { foo: 1 } 110 | }), [ 111 | { command: 'addSource', args: ['foo', 1] } 112 | ], 'add a source'); 113 | 114 | t.deepEqual(diffStyles({}, { 115 | metadata: { 'mapbox:author': 'nobody' } 116 | }), [], 'ignore style metadata'); 117 | 118 | t.deepEqual(diffStyles({ 119 | layers: [{ id: 'a', metadata: { 'mapbox:group': 'Group Name' } }] 120 | }, { 121 | layers: [{ id: 'a', metadata: { 'mapbox:group': 'Another Name' } }] 122 | }), [], 'ignore layer metadata'); 123 | 124 | t.deepEqual(diffStyles({ 125 | center: [0, 0] 126 | }, { 127 | center: [1, 1] 128 | }), [ 129 | { command: 'setCenter', args: [[1, 1]] } 130 | ], 'center change'); 131 | 132 | t.deepEqual(diffStyles({ 133 | zoom: 12 134 | }, { 135 | zoom: 15 136 | }), [ 137 | { command: 'setZoom', args: [15] } 138 | ], 'zoom change'); 139 | 140 | t.deepEqual(diffStyles({ 141 | bearing: 0 142 | }, { 143 | bearing: 180 144 | }), [ 145 | { command: 'setBearing', args: [180] } 146 | ], 'bearing change'); 147 | 148 | t.deepEqual(diffStyles({ 149 | pitch: 0 150 | }, { 151 | pitch: 1 152 | }), [ 153 | { command: 'setPitch', args: [1] } 154 | ], 'pitch change'); 155 | 156 | t.deepEqual(diffStyles({ 157 | light: { 158 | anchor: 'map', 159 | color: 'white', 160 | position: [0, 1, 0], 161 | intensity: 1 162 | } 163 | }, { 164 | light: { 165 | anchor: 'map', 166 | color: 'white', 167 | position: [0, 1, 0], 168 | intensity: 1 169 | } 170 | }), [ 171 | ], 'light no change'); 172 | 173 | t.deepEqual(diffStyles({ 174 | light: { anchor: 'map' } 175 | }, { 176 | light: { anchor: 'viewport' } 177 | }), [ 178 | { command: 'setLight', args: [{'anchor': 'viewport'}] } 179 | ], 'light anchor change'); 180 | 181 | t.deepEqual(diffStyles({ 182 | light: { color: 'white' } 183 | }, { 184 | light: { color: 'red' } 185 | }), [ 186 | { command: 'setLight', args: [{'color': 'red'}] } 187 | ], 'light color change'); 188 | 189 | t.deepEqual(diffStyles({ 190 | light: { position: [0, 1, 0] } 191 | }, { 192 | light: { position: [1, 0, 0] } 193 | }), [ 194 | { command: 'setLight', args: [{'position': [1, 0, 0]}] } 195 | ], 'light position change'); 196 | 197 | t.deepEqual(diffStyles({ 198 | light: { intensity: 1 } 199 | }, { 200 | light: { intensity: 10 } 201 | }), [ 202 | { command: 'setLight', args: [{'intensity': 10}] } 203 | ], 'light intensity change'); 204 | 205 | t.deepEqual(diffStyles({ 206 | light: { 207 | anchor: 'map', 208 | color: 'orange', 209 | position: [2, 80, 30], 210 | intensity: 1.0 211 | } 212 | }, { 213 | light: { 214 | anchor: 'map', 215 | color: 'red', 216 | position: [1, 40, 30], 217 | intensity: 1.0 218 | } 219 | }), [ 220 | { command: 'setLight', args: [{ 221 | anchor: 'map', 222 | color: 'red', 223 | position: [1, 40, 30], 224 | intensity: 1.0 225 | }] } 226 | ], 'multiple light properties change'); 227 | 228 | t.end(); 229 | }); 230 | -------------------------------------------------------------------------------- /test/fixture/bad-color.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "minimum", 12 | "type": "fill", 13 | "source": "vector", 14 | "source-layer": "layer", 15 | "paint": { 16 | "fill-color": [1,2,3], 17 | "fill-outline-color": "not a color" 18 | } 19 | }, 20 | { 21 | "id": "ops", 22 | "type": "fill", 23 | "source": "vector", 24 | "source-layer": "layer", 25 | "paint": { 26 | "fill-outline-color": ["darken", 10, "#FF0000"] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/fixture/bad-color.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].paint.fill-color: color expected, array found", 4 | "line": 16 5 | }, 6 | { 7 | "message": "layers[0].paint.fill-outline-color: color expected, \"not a color\" found", 8 | "line": 17 9 | }, 10 | { 11 | "message": "layers[1].paint.fill-outline-color: color expected, array found", 12 | "line": 26 13 | } 14 | ] -------------------------------------------------------------------------------- /test/fixture/constants-v7.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "constants": { 4 | "@valid": "#f00", 5 | "invalid": "#0f0" 6 | }, 7 | "sources": {}, 8 | "layers": [ 9 | { 10 | "id": "bad-constant", 11 | "type": "background", 12 | "paint": { 13 | "background-color": "@missing" 14 | } 15 | }, { 16 | "id": "good-constant", 17 | "type": "background", 18 | "paint": { 19 | "background-color": "@valid" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /test/fixture/constants-v7.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "constants.invalid: constants must start with \"@\"", 4 | "line": 5 5 | }, 6 | { 7 | "message": "layers[0].paint.background-color: constant \"@missing\" not found", 8 | "line": 13 9 | } 10 | ] -------------------------------------------------------------------------------- /test/fixture/constants-v8.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "constants": { 10 | "@mapbox": "#fff" 11 | }, 12 | "layers": [ 13 | { 14 | "id": "constant", 15 | "type": "fill", 16 | "source": "vector", 17 | "source-layer": "layer", 18 | "paint": { 19 | "fill-color": "#fff" 20 | } 21 | }, 22 | { 23 | "id": "color-op", 24 | "type": "fill", 25 | "source": "vector", 26 | "source-layer": "layer", 27 | "paint": { 28 | "fill-color": ["lighten", -20, "#000"] 29 | } 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/fixture/constants-v8.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "constants: constants have been deprecated as of v8", 4 | "line": 9 5 | }, 6 | { 7 | "message": "layers[1].paint.fill-color: color expected, array found", 8 | "line": 28 9 | } 10 | ] -------------------------------------------------------------------------------- /test/fixture/extrakeys.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "constants": {}, 4 | "sources": {}, 5 | "layers": [ 6 | { 7 | "id": "bad-constant", 8 | "type": "background", 9 | "paint": { 10 | "background-color": "#000" 11 | } 12 | } 13 | ], 14 | "extrakey": { 15 | "foo": "bar" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixture/extrakeys.output.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixture/filters.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "source": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "not-array", 12 | "type": "line", 13 | "source": "source", 14 | "source-layer": "source-layer", 15 | "filter": {} 16 | }, 17 | { 18 | "id": "zero-elements", 19 | "type": "line", 20 | "source": "source", 21 | "source-layer": "source-layer", 22 | "filter": [] 23 | }, 24 | { 25 | "id": "invalid-operator", 26 | "type": "line", 27 | "source": "source", 28 | "source-layer": "source-layer", 29 | "filter": [ 30 | "=", 31 | "key", 32 | "value" 33 | ] 34 | }, 35 | { 36 | "id": "missing-key", 37 | "type": "line", 38 | "source": "source", 39 | "source-layer": "source-layer", 40 | "filter": ["=="] 41 | }, 42 | { 43 | "id": "missing-value", 44 | "type": "line", 45 | "source": "source", 46 | "source-layer": "source-layer", 47 | "filter": [ 48 | "==", 49 | "key" 50 | ] 51 | }, 52 | { 53 | "id": "invalid-key", 54 | "type": "line", 55 | "source": "source", 56 | "source-layer": "source-layer", 57 | "filter": [ 58 | "==", 59 | 1, 60 | "value" 61 | ] 62 | }, 63 | { 64 | "id": "invalid-value", 65 | "type": "line", 66 | "source": "source", 67 | "source-layer": "source-layer", 68 | "filter": [ 69 | "==", 70 | "key", 71 | [] 72 | ] 73 | }, 74 | { 75 | "id": "invalid-type", 76 | "type": "line", 77 | "source": "source", 78 | "source-layer": "source-layer", 79 | "filter": [ 80 | "==", 81 | "$type", 82 | "value" 83 | ] 84 | }, 85 | { 86 | "id": "key-constant", 87 | "type": "line", 88 | "source": "source", 89 | "source-layer": "source-layer", 90 | "filter": [ 91 | "==", 92 | "@constant", 93 | "value" 94 | ] 95 | }, 96 | { 97 | "id": "value-constant", 98 | "type": "line", 99 | "source": "source", 100 | "source-layer": "source-layer", 101 | "filter": [ 102 | "==", 103 | "key", 104 | "@constant" 105 | ] 106 | }, 107 | { 108 | "id": "invalid-type-operator", 109 | "type": "line", 110 | "source": "source", 111 | "source-layer": "source-layer", 112 | "filter": [ 113 | ">", 114 | "$type", 115 | "value" 116 | ] 117 | }, 118 | { 119 | "id": "invalid-nested-filter", 120 | "type": "line", 121 | "source": "source", 122 | "source-layer": "source-layer", 123 | "filter": [ 124 | "any", 125 | [] 126 | ] 127 | }, 128 | { 129 | "id": "valid-has-filter", 130 | "type": "line", 131 | "source": "source", 132 | "source-layer": "source-layer", 133 | "filter": [ 134 | "has", 135 | "mapbox" 136 | ] 137 | }, 138 | { 139 | "id": "valid-!has-filter", 140 | "type": "line", 141 | "source": "source", 142 | "source-layer": "source-layer", 143 | "filter": [ 144 | "!has", 145 | "mapbox" 146 | ] 147 | }, 148 | { 149 | "id": "invalid-has-filter-length", 150 | "type": "line", 151 | "source": "source", 152 | "source-layer": "source-layer", 153 | "filter": [ 154 | "has", 155 | "mapbox", 156 | "bad" 157 | ] 158 | }, 159 | { 160 | "id": "invalid-has-filter-type", 161 | "type": "line", 162 | "source": "source", 163 | "source-layer": "source-layer", 164 | "filter": [ 165 | "has", 166 | {"mapbox": true} 167 | ] 168 | }, 169 | { 170 | "id": "invalid-has-filter-constant", 171 | "type": "line", 172 | "source": "source", 173 | "source-layer": "source-layer", 174 | "filter": [ 175 | "has", 176 | "@mapbox" 177 | ] 178 | } 179 | ] 180 | } 181 | -------------------------------------------------------------------------------- /test/fixture/filters.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].filter: array expected, object found", 4 | "line": 15 5 | }, 6 | { 7 | "message": "layers[1].filter: filter array must have at least 1 element", 8 | "line": 22 9 | }, 10 | { 11 | "message": "layers[2].filter[0]: expected one of [==, !=, >, >=, <, <=, in, !in, all, any, none, has, !has], = found", 12 | "line": 30 13 | }, 14 | { 15 | "message": "layers[3].filter: filter array for operator \"==\" must have 3 elements", 16 | "line": 40 17 | }, 18 | { 19 | "message": "layers[4].filter: filter array for operator \"==\" must have 3 elements", 20 | "line": 47 21 | }, 22 | { 23 | "message": "layers[5].filter[1]: string expected, number found", 24 | "line": 59 25 | }, 26 | { 27 | "message": "layers[6].filter[2]: string, number, or boolean expected, array found", 28 | "line": 71 29 | }, 30 | { 31 | "message": "layers[7].filter[2]: expected one of [Point, LineString, Polygon], value found", 32 | "line": 82 33 | }, 34 | { 35 | "message": "layers[8].filter[1]: filter key cannot be a constant", 36 | "line": 92 37 | }, 38 | { 39 | "message": "layers[9].filter[2]: filter value cannot be a constant", 40 | "line": 104 41 | }, 42 | { 43 | "message": "layers[10].filter: \"$type\" cannot be use with operator \">\"", 44 | "line": 112 45 | }, 46 | { 47 | "message": "layers[10].filter[2]: expected one of [Point, LineString, Polygon], value found", 48 | "line": 115 49 | }, 50 | { 51 | "message": "layers[11].filter[1]: filter array must have at least 1 element", 52 | "line": 125 53 | }, 54 | { 55 | "message": "layers[14].filter: filter array for \"has\" operator must have 2 elements", 56 | "line": 153 57 | }, 58 | { 59 | "message": "layers[15].filter[1]: string expected, object found", 60 | "line": 166 61 | }, 62 | { 63 | "message": "layers[16].filter[1]: filter key cannot be a constant", 64 | "line": 176 65 | } 66 | ] -------------------------------------------------------------------------------- /test/fixture/functions.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "source": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "bad-base", 12 | "type": "line", 13 | "source": "source", 14 | "source-layer": "layer", 15 | "paint": { 16 | "line-width": { 17 | "base": "5", 18 | "stops": [] 19 | } 20 | } 21 | }, 22 | { 23 | "id": "missing-stops", 24 | "type": "line", 25 | "source": "source", 26 | "source-layer": "layer", 27 | "paint": { 28 | "line-width": { 29 | } 30 | } 31 | }, 32 | { 33 | "id": "zero-stops", 34 | "type": "line", 35 | "source": "source", 36 | "source-layer": "layer", 37 | "paint": { 38 | "line-width": { 39 | "stops": [] 40 | } 41 | } 42 | }, 43 | { 44 | "id": "bad-stops", 45 | "type": "line", 46 | "source": "source", 47 | "source-layer": "layer", 48 | "paint": { 49 | "line-width": { 50 | "stops": {} 51 | } 52 | } 53 | }, 54 | { 55 | "id": "bad-stop-type", 56 | "type": "line", 57 | "source": "source", 58 | "source-layer": "layer", 59 | "paint": { 60 | "line-width": { 61 | "stops": ["1"] 62 | } 63 | } 64 | }, 65 | { 66 | "id": "bad-stop-length", 67 | "type": "line", 68 | "source": "source", 69 | "source-layer": "layer", 70 | "paint": { 71 | "line-width": { 72 | "stops": [[]] 73 | } 74 | } 75 | }, 76 | { 77 | "id": "bad-stop-zoom", 78 | "type": "line", 79 | "source": "source", 80 | "source-layer": "layer", 81 | "paint": { 82 | "line-width": { 83 | "stops": [ 84 | [ 85 | "1", 86 | 1 87 | ] 88 | ] 89 | } 90 | } 91 | }, 92 | { 93 | "id": "bad-stop-value", 94 | "type": "line", 95 | "source": "source", 96 | "source-layer": "layer", 97 | "paint": { 98 | "line-width": { 99 | "stops": [ 100 | [ 101 | 1, 102 | "#fff" 103 | ] 104 | ] 105 | } 106 | } 107 | }, 108 | { 109 | "id": "bad-stop-order", 110 | "type": "line", 111 | "source": "source", 112 | "source-layer": "layer", 113 | "paint": { 114 | "line-width": { 115 | "stops": [ 116 | [ 117 | 2, 118 | 0 119 | ], 120 | [ 121 | 1, 122 | 0 123 | ] 124 | ] 125 | } 126 | } 127 | }, 128 | { 129 | "id": "v6-array-function", 130 | "type": "fill", 131 | "source": "source", 132 | "source-layer": "layer", 133 | "paint": { 134 | "fill-translate": [ 135 | { 136 | "base": 1, 137 | "stops": [[15, 0], [16, -2]] 138 | }, 139 | { 140 | "base": 1, 141 | "stops": [[15, 0], [16, -2]] 142 | } 143 | ] 144 | } 145 | }, 146 | { 147 | "id": "v7-array-function", 148 | "type": "fill", 149 | "source": "source", 150 | "source-layer": "layer", 151 | "paint": { 152 | "fill-translate": { 153 | "base": 1, 154 | "stops": [[15, [0, 0]], [16, [-2, -2]]] 155 | } 156 | } 157 | }, 158 | { 159 | "id": "non-integer-discrete-zoom", 160 | "type": "fill", 161 | "source": "source", 162 | "source-layer": "layer", 163 | "paint": { 164 | "fill-antialias": { 165 | "stops": [[0.5, true]] 166 | } 167 | } 168 | }, 169 | { 170 | "id": "mixed-stop-key-types", 171 | "type": "fill", 172 | "source": "source", 173 | "source-layer": "layer", 174 | "paint": { 175 | "fill-antialias": { 176 | "property": "mapbox", 177 | "stops": [ 178 | [{ "zoom": 0, "value": 1 }, true], 179 | [0.5, true]] 180 | } 181 | } 182 | }, 183 | { 184 | "id": "missing stop zoom", 185 | "type": "fill", 186 | "source": "source", 187 | "source-layer": "layer", 188 | "paint": { 189 | "fill-antialias": { 190 | "property": "mapbox", 191 | "stops": [[{"value": "asdf"}, true]] 192 | } 193 | } 194 | }, 195 | { 196 | "id": "missing stop feature value", 197 | "type": "fill", 198 | "source": "source", 199 | "source-layer": "layer", 200 | "paint": { 201 | "fill-antialias": { 202 | "property": "mapbox", 203 | "stops": [[{"zoom": 1}, true]] 204 | } 205 | } 206 | }, 207 | { 208 | "id": "invalid stop zoom type", 209 | "type": "fill", 210 | "source": "source", 211 | "source-layer": "layer", 212 | "paint": { 213 | "fill-antialias": { 214 | "property": "mapbox", 215 | "stops": [[{"zoom": "1", "value": "asdf"}, true]] 216 | } 217 | } 218 | }, 219 | { 220 | "id": "invalid stop feature value type", 221 | "type": "fill", 222 | "source": "source", 223 | "source-layer": "layer", 224 | "paint": { 225 | "fill-antialias": { 226 | "property": "mapbox", 227 | "stops": [[{"zoom": 1, "value": { "asdf": true }}, true]] 228 | } 229 | } 230 | }, 231 | { 232 | "id": "invalid object-keyed string stop for zoom function", 233 | "type": "fill", 234 | "source": "source", 235 | "source-layer": "layer", 236 | "paint": { 237 | "fill-antialias": { 238 | "stops": [["bad", true]] 239 | } 240 | } 241 | }, 242 | { 243 | "id": "valid object-keyed string stop for property function", 244 | "type": "fill", 245 | "source": "source", 246 | "source-layer": "layer", 247 | "paint": { 248 | "fill-antialias": { 249 | "property": "mapbox", 250 | "stops": [["good", true]] 251 | } 252 | } 253 | }, 254 | { 255 | "id": "invalid use of property function", 256 | "type": "background", 257 | "source": "source", 258 | "source-layer": "layer", 259 | "paint": { 260 | "background-color": { 261 | "property": "mapbox", 262 | "stops": [["good", "green"]] 263 | } 264 | } 265 | }, 266 | { 267 | "id": "valid identity function", 268 | "type": "background", 269 | "source": "source", 270 | "source-layer": "layer", 271 | "paint": { 272 | "background-color": { 273 | "type": "identity" 274 | } 275 | } 276 | }, { 277 | "id": "invalid identity function with stops", 278 | "type": "background", 279 | "source": "source", 280 | "source-layer": "layer", 281 | "paint": { 282 | "background-color": { 283 | "type": "identity", 284 | "stops": [] 285 | } 286 | } 287 | } 288 | ] 289 | } 290 | -------------------------------------------------------------------------------- /test/fixture/functions.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].paint.line-width.base: number expected, string found", 4 | "line": 17 5 | }, 6 | { 7 | "message": "layers[0].paint.line-width.stops: array must have at least one stop", 8 | "line": 18 9 | }, 10 | { 11 | "message": "layers[1].paint.line-width: missing required property \"stops\"", 12 | "line": 28 13 | }, 14 | { 15 | "message": "layers[2].paint.line-width.stops: array must have at least one stop", 16 | "line": 39 17 | }, 18 | { 19 | "message": "layers[3].paint.line-width.stops: array expected, object found", 20 | "line": 50 21 | }, 22 | { 23 | "message": "layers[4].paint.line-width.stops[0]: array expected, string found", 24 | "line": 61 25 | }, 26 | { 27 | "message": "layers[5].paint.line-width.stops[0]: array length 2 expected, length 0 found", 28 | "line": 72 29 | }, 30 | { 31 | "message": "layers[6].paint.line-width.stops[0][0]: number expected, string found", 32 | "line": 85 33 | }, 34 | { 35 | "message": "layers[7].paint.line-width.stops[0][1]: number expected, string found", 36 | "line": 102 37 | }, 38 | { 39 | "message": "layers[8].paint.line-width.stops[1][0]: array stops must appear in ascending order", 40 | "line": 121 41 | }, 42 | { 43 | "message": "layers[9].paint.fill-translate[0]: number expected, object found", 44 | "line": 135 45 | }, 46 | { 47 | "message": "layers[9].paint.fill-translate[1]: number expected, object found", 48 | "line": 139 49 | }, 50 | { 51 | "message": "layers[11].paint.fill-antialias.stops[0][0]: zoom level for piecewise-constant functions must be an integer", 52 | "line": 165 53 | }, 54 | { 55 | "message": "layers[12].paint.fill-antialias.stops[1]: number stop key type must match previous stop key type object", 56 | "line": 179 57 | }, 58 | { 59 | "message": "layers[13].paint.fill-antialias.stops[0]: object stop key must have zoom", 60 | "line": 191 61 | }, 62 | { 63 | "message": "layers[14].paint.fill-antialias.stops[0]: object stop key must have value", 64 | "line": 203 65 | }, 66 | { 67 | "message": "layers[15].paint.fill-antialias.stops[0][0].zoom: number expected, string found", 68 | "line": 215 69 | }, 70 | { 71 | "message": "layers[16].paint.fill-antialias.stops[0][0].value: property value must be a number, string or array", 72 | "line": 227 73 | }, 74 | { 75 | "message": "layers[17].paint.fill-antialias.stops[0][0]: number expected, string found", 76 | "line": 238 77 | }, 78 | { 79 | "message": "layers[19].paint.background-color: property functions not supported", 80 | "line": 260 81 | }, 82 | { 83 | "message": "layers[21].paint.background-color.stops: identity function may not have a \"stops\" property", 84 | "line": 284 85 | } 86 | ] -------------------------------------------------------------------------------- /test/fixture/invalidjson.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1" 3 | "buckets": { } 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/invalidjson.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "error": {}, 4 | "message": "Parse error on line 2:\n... \"version\": \"1\" \"buckets\": { }}\n---------------------^\nExpecting 'EOF', '}', ':', ',', ']', got 'STRING'", 5 | "line": 2 6 | } 7 | ] -------------------------------------------------------------------------------- /test/fixture/layers.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | }, 8 | "raster": { 9 | "type": "raster", 10 | "url": "mapbox://mapbox.mapbox-satellite" 11 | } 12 | }, 13 | "layers": [ 14 | { 15 | "id": "no-ref-or-type", 16 | "source": "vector", 17 | "source-layer": "layer" 18 | }, 19 | { 20 | "type": "line", 21 | "source": "vector", 22 | "source-layer": "source-layer" 23 | }, 24 | { 25 | "id": "ref", 26 | "type": "line", 27 | "source": "vector", 28 | "source-layer": "source-layer" 29 | }, 30 | { 31 | "id": "ref-prohibited-properties", 32 | "ref": "ref", 33 | "type": "line", 34 | "source": "vector", 35 | "source-layer": "source-layer", 36 | "filter": ["any"], 37 | "layout": {} 38 | }, 39 | { 40 | "id": "ref-not-found", 41 | "ref": "not-found" 42 | }, 43 | { 44 | "id": "self-ref", 45 | "ref": "self-ref" 46 | }, 47 | { 48 | "id": "missing-source", 49 | "type": "line" 50 | }, 51 | { 52 | "id": "source-not-found", 53 | "type": "line", 54 | "source": "not-found" 55 | }, 56 | { 57 | "id": "vector-raster-mismatch", 58 | "type": "line", 59 | "source": "raster" 60 | }, 61 | { 62 | "id": "raster-vector-mismatch", 63 | "type": "raster", 64 | "source": "vector", 65 | "source-layer": "source-layer" 66 | }, 67 | { 68 | "id": "duplicate", 69 | "type": "line", 70 | "source": "vector", 71 | "source-layer": "source-layer" 72 | }, 73 | { 74 | "id": "duplicate", 75 | "type": "line", 76 | "source": "vector", 77 | "source-layer": "source-layer" 78 | }, 79 | { 80 | "id": "invalid-type", 81 | "type": "invalid", 82 | "source": "vector", 83 | "source-layer": "source-layer", 84 | "layout": { 85 | "invalid-size": 42 86 | }, 87 | "paint": { 88 | "invalid-color": "red" 89 | } 90 | }, 91 | { 92 | "id": "missing-source-layer", 93 | "type": "line", 94 | "source": "vector" 95 | } 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /test/fixture/layers.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0]: either \"type\" or \"ref\" is required", 4 | "line": 14 5 | }, 6 | { 7 | "message": "layers[1]: missing required property \"id\"", 8 | "line": 19 9 | }, 10 | { 11 | "message": "layers[3]: \"type\" is prohibited for ref layers", 12 | "line": 33 13 | }, 14 | { 15 | "message": "layers[3]: \"source\" is prohibited for ref layers", 16 | "line": 34 17 | }, 18 | { 19 | "message": "layers[3]: \"source-layer\" is prohibited for ref layers", 20 | "line": 35 21 | }, 22 | { 23 | "message": "layers[3]: \"filter\" is prohibited for ref layers", 24 | "line": 36 25 | }, 26 | { 27 | "message": "layers[3]: \"layout\" is prohibited for ref layers", 28 | "line": 37 29 | }, 30 | { 31 | "message": "layers[4]: ref layer \"not-found\" not found", 32 | "line": 41 33 | }, 34 | { 35 | "message": "layers[5]: ref cannot reference another ref layer", 36 | "line": 45 37 | }, 38 | { 39 | "message": "layers[6]: missing required property \"source\"", 40 | "line": 47 41 | }, 42 | { 43 | "message": "layers[7]: source \"not-found\" not found", 44 | "line": 54 45 | }, 46 | { 47 | "message": "layers[8]: layer \"vector-raster-mismatch\" requires a vector source", 48 | "line": 59 49 | }, 50 | { 51 | "message": "layers[9]: layer \"raster-vector-mismatch\" requires a raster source", 52 | "line": 64 53 | }, 54 | { 55 | "message": "layers[11]: duplicate layer id \"duplicate\", previously used at line 68", 56 | "line": 74 57 | }, 58 | { 59 | "message": "layers[12].type: expected one of [fill, line, symbol, circle, raster, background], invalid found", 60 | "line": 81 61 | }, 62 | { 63 | "message": "layers[13]: layer \"missing-source-layer\" must specify a \"source-layer\"", 64 | "line": 91 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /test/fixture/light.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "light": { 10 | "anchor": true, 11 | "position": [1, 2], 12 | "color": [255, 255, 255, 1], 13 | "intensity": 1.5 14 | }, 15 | "layers": [] 16 | } 17 | -------------------------------------------------------------------------------- /test/fixture/light.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "light.anchor: expected one of [map, viewport], true found", 4 | "line": 10 5 | }, 6 | { 7 | "message": "light.position: array length 3 expected, length 2 found", 8 | "line": 11 9 | }, 10 | { 11 | "message": "light.color: color expected, array found", 12 | "line": 12 13 | }, 14 | { 15 | "message": "light.intensity: 1.5 is greater than the maximum value 1", 16 | "line": 13 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /test/fixture/malformed-glyphs-type.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "glyphs": true, 4 | "sources": { 5 | "vector": { 6 | "type": "vector", 7 | "url": "mapbox://mapbox.mapbox-streets-v5" 8 | } 9 | }, 10 | "layers": [ 11 | { 12 | "id": "minimum", 13 | "type": "symbol", 14 | "source": "vector", 15 | "source-layer": "layer", 16 | "layout": { 17 | "text-font": ["Helvetica"], 18 | "text-field": "{foo}" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixture/malformed-glyphs-type.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "glyphs: string expected, boolean found", 4 | "line": 3 5 | } 6 | ] -------------------------------------------------------------------------------- /test/fixture/malformed-glyphs.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "glyphs": "https://example.com", 4 | "sources": { 5 | "vector": { 6 | "type": "vector", 7 | "url": "mapbox://mapbox.mapbox-streets-v5" 8 | } 9 | }, 10 | "layers": [ 11 | { 12 | "id": "minimum", 13 | "type": "symbol", 14 | "source": "vector", 15 | "source-layer": "layer", 16 | "layout": { 17 | "text-font": ["Helvetica"], 18 | "text-field": "{foo}" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixture/malformed-glyphs.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "glyphs: \"glyphs\" url must include a \"{fontstack}\" token", 4 | "line": 3 5 | }, 6 | { 7 | "message": "glyphs: \"glyphs\" url must include a \"{range}\" token", 8 | "line": 3 9 | } 10 | ] -------------------------------------------------------------------------------- /test/fixture/map-properties.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "zoom": 5, 4 | "center": [10, 10], 5 | "bearing": 20, 6 | "glyphs": "https://example.com/{fontstack}/{range}", 7 | "sources": { 8 | "vector": { 9 | "type": "vector", 10 | "url": "mapbox://mapbox.mapbox-streets-v5" 11 | } 12 | }, 13 | "layers": [ 14 | { 15 | "id": "minimum", 16 | "type": "symbol", 17 | "source": "vector", 18 | "source-layer": "layer", 19 | "layout": { 20 | "text-font": ["Helvetica"], 21 | "text-field": "{foo}" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /test/fixture/map-properties.output.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixture/metadata.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "metadata": { 4 | "mapbox:groups": { 5 | "group-0": { 6 | "name": "Group Name" 7 | } 8 | } 9 | }, 10 | "constants": {}, 11 | "sources": {}, 12 | "layers": [ 13 | { 14 | "id": "background", 15 | "type": "background", 16 | "metadata": { 17 | "mapbox:group": "group-0" 18 | }, 19 | "paint": { 20 | "background-color": "#000" 21 | } 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /test/fixture/metadata.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "constants: constants have been deprecated as of v8", 4 | "line": 10 5 | } 6 | ] -------------------------------------------------------------------------------- /test/fixture/missing-glyphs.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "minimum", 12 | "type": "symbol", 13 | "source": "vector", 14 | "source-layer": "layer", 15 | "layout": { 16 | "text-font": ["Helvetica"], 17 | "text-field": "{foo}" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /test/fixture/missing-glyphs.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].layout.text-field: use of \"text-field\" requires a style \"glyphs\" property", 4 | "line": 17 5 | } 6 | ] -------------------------------------------------------------------------------- /test/fixture/missing-sprite.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "minimum", 12 | "type": "symbol", 13 | "source": "vector", 14 | "source-layer": "layer", 15 | "layout": { 16 | "icon-image": "mapbox" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /test/fixture/missing-sprite.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].layout.icon-image: use of \"icon-image\" requires a style \"sprite\" property", 4 | "line": 16 5 | } 6 | ] -------------------------------------------------------------------------------- /test/fixture/no-sources.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "layers": [ 4 | { 5 | "id": "vector-raster-mismatch", 6 | "type": "line", 7 | "source": "raster" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/no-sources.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "missing required property \"sources\"", 4 | "line": 1 5 | }, 6 | { 7 | "message": "layers[0]: source \"raster\" not found", 8 | "line": 7 9 | } 10 | ] -------------------------------------------------------------------------------- /test/fixture/pitch.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "pitch": "45", 4 | "sources": {}, 5 | "layers": [] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixture/pitch.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "pitch: number expected, string found", 4 | "line": 3 5 | } 6 | ] -------------------------------------------------------------------------------- /test/fixture/properties.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "sources": { 4 | "vector": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "minimum", 12 | "type": "fill", 13 | "source": "vector", 14 | "source-layer": "layer", 15 | "paint": { 16 | "fill-opacity": -1, 17 | "fill-opacity-transition": { 18 | "duration": 500, 19 | "delay": -1, 20 | "bad": "value" 21 | }, 22 | "bad-transition": { 23 | "duration": 400 24 | }, 25 | "fill-antialias-transition": { 26 | "duration": 500 27 | } 28 | } 29 | }, 30 | { 31 | "id": "maximum", 32 | "type": "fill", 33 | "source": "vector", 34 | "source-layer": "layer", 35 | "paint": { 36 | "fill-opacity": 1.5 37 | } 38 | }, 39 | { 40 | "id": "null", 41 | "type": "fill", 42 | "source": "vector", 43 | "source-layer": "layer", 44 | "paint": { 45 | "fill-opacity": null 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /test/fixture/properties.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "layers[0].paint.fill-opacity: -1 is less than the minimum value 0", 4 | "line": 16 5 | }, 6 | { 7 | "message": "layers[0].paint.fill-opacity-transition.delay: -1 is less than the minimum value 0", 8 | "line": 19 9 | }, 10 | { 11 | "message": "layers[0].paint.fill-opacity-transition: unknown property \"bad\"", 12 | "line": 20 13 | }, 14 | { 15 | "message": "layers[0].paint.bad-transition: unknown property \"bad-transition\"", 16 | "line": 22 17 | }, 18 | { 19 | "message": "layers[0].paint.fill-antialias-transition: unknown property \"fill-antialias-transition\"", 20 | "line": 25 21 | }, 22 | { 23 | "message": "layers[1].paint.fill-opacity: 1.5 is greater than the maximum value 1", 24 | "line": 36 25 | }, 26 | { 27 | "message": "layers[2].paint.fill-opacity: number expected, null found" 28 | } 29 | ] -------------------------------------------------------------------------------- /test/fixture/required.input.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /test/fixture/required.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "missing required property \"version\"", 4 | "line": 1 5 | }, 6 | { 7 | "message": "missing required property \"sources\"", 8 | "line": 1 9 | }, 10 | { 11 | "message": "missing required property \"layers\"", 12 | "line": 1 13 | } 14 | ] -------------------------------------------------------------------------------- /test/fixture/root-properties.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "constants": [], 4 | "sources": [], 5 | "layers": {}, 6 | "created": "", 7 | "modified": "", 8 | "name": "Test", 9 | "owner": "peterqliu", 10 | "id": "peterqliu.test", 11 | "light": true 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/root-properties.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "version: expected one of [8], 1 found", 4 | "line": 2 5 | }, 6 | { 7 | "message": "constants: constants have been deprecated as of v8", 8 | "line": 3 9 | }, 10 | { 11 | "message": "sources: object expected, array found", 12 | "line": 4 13 | }, 14 | { 15 | "message": "layers: array expected, object found", 16 | "line": 5 17 | }, 18 | { 19 | "message": "light: object expected, boolean found", 20 | "line": 11 21 | } 22 | ] -------------------------------------------------------------------------------- /test/fixture/sources.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "sources": { 4 | "missing-type": { 5 | }, 6 | "invalid-type": { 7 | "type": "invalid" 8 | }, 9 | "no-tilejson-properties-with-url": { 10 | "type": "vector", 11 | "url": "http://example.com", 12 | "tiles": [] 13 | }, 14 | "no-unknown-properties-with-url": { 15 | "type": "vector", 16 | "url": "http://example.com", 17 | "foo": 1 18 | }, 19 | "video-valid": { 20 | "type": "video", 21 | "url": [], 22 | "coordinates": [ 23 | [1, 2], [3, 4], [5, 6], [7, 8] 24 | ] 25 | }, 26 | "video-missing-coordinates": { 27 | "type": "video", 28 | "url": [] 29 | }, 30 | "video-wrong-coordinates": { 31 | "type": "video", 32 | "url": [], 33 | "coordinates": [ 34 | 1, "2", [3, "4"], [] 35 | ] 36 | } 37 | }, 38 | "layers": [] 39 | } 40 | -------------------------------------------------------------------------------- /test/fixture/sources.output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "message": "sources.missing-type: \"type\" is required", 4 | "line": 4 5 | }, 6 | { 7 | "message": "sources.invalid-type.type: expected one of [vector, raster, geojson, video, image], invalid found", 8 | "line": 7 9 | }, 10 | { 11 | "message": "sources.no-tilejson-properties-with-url.tiles: a source with a \"url\" property may not include a \"tiles\" property", 12 | "line": 12 13 | }, 14 | { 15 | "message": "sources.no-unknown-properties-with-url.foo: a source with a \"url\" property may not include a \"foo\" property", 16 | "line": 17 17 | }, 18 | { 19 | "message": "sources.video-missing-coordinates: missing required property \"coordinates\"", 20 | "line": 26 21 | }, 22 | { 23 | "message": "sources.video-wrong-coordinates.coordinates[0]: array expected, number found", 24 | "line": 34 25 | }, 26 | { 27 | "message": "sources.video-wrong-coordinates.coordinates[1]: array expected, string found", 28 | "line": 34 29 | }, 30 | { 31 | "message": "sources.video-wrong-coordinates.coordinates[2][1]: number expected, string found", 32 | "line": 34 33 | }, 34 | { 35 | "message": "sources.video-wrong-coordinates.coordinates[3]: array length 2 expected, length 0 found", 36 | "line": 34 37 | } 38 | ] -------------------------------------------------------------------------------- /test/fixture/text-font.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "glyphs": "https://example.com/{fontstack}/{range}", 4 | "sources": { 5 | "vector": { 6 | "type": "vector", 7 | "url": "mapbox://mapbox.mapbox-streets-v5" 8 | } 9 | }, 10 | "layers": [ 11 | { 12 | "id": "minimum", 13 | "type": "symbol", 14 | "source": "vector", 15 | "source-layer": "layer", 16 | "layout": { 17 | "text-font": ["Helvetica"], 18 | "text-field": "{foo}" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixture/text-font.output.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixture/unknown-keys-nested.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "constants": { 4 | }, 5 | "sources": {}, 6 | "layers": [ 7 | { 8 | "id": "bad-constant", 9 | "type": "background", 10 | "foo": "bar", 11 | "paint": { } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/fixture/unknown-keys-nested.output.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixture/v6.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "sources": { 4 | "source": { 5 | "type": "vector", 6 | "url": "mapbox://mapbox.mapbox-streets-v5" 7 | } 8 | }, 9 | "layers": [ 10 | { 11 | "id": "array-function", 12 | "type": "fill", 13 | "source": "source", 14 | "source-layer": "layer", 15 | "paint": { 16 | "fill-translate": [ 17 | { 18 | "base": 1, 19 | "stops": [[15, 0], [16, -2]] 20 | }, 21 | { 22 | "base": 1, 23 | "stops": [[15, 0], [16, -2]] 24 | } 25 | ] 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/fixture/v6.output.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/fixture/v7-migrate/style-basic.input.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 7, 3 | "constants": { 4 | "@land": "#eee", 5 | "@water": "#999", 6 | "@park": "#bda", 7 | "@road": "#fefefe", 8 | "@border": "#6d90ab", 9 | "@building": "#ddd", 10 | "@building_outline": "#ccc", 11 | "@text": "#000000", 12 | "@road_blur": 1, 13 | "@stroke_width": 0.25 14 | }, 15 | "sources": { 16 | "mapbox.mapbox-streets-v5": { 17 | "type": "vector", 18 | "tiles": [] 19 | } 20 | }, 21 | "glyphs": "http://mapbox.s3.amazonaws.com/gl-glyphs-256/{fontstack}/{range}.pbf", 22 | "layers": [{ 23 | "id": "background", 24 | "type": "background", 25 | "paint": { 26 | "background-color": "@land", 27 | "background-color-transition": { 28 | "duration": 500, 29 | "delay": 0 30 | } 31 | } 32 | }, { 33 | "id": "park", 34 | "type": "fill", 35 | "source": "mapbox.mapbox-streets-v5", 36 | "source-layer": "landuse", 37 | "filter": ["==", "class", "park"], 38 | "paint": { 39 | "fill-color": "@park" 40 | } 41 | }, { 42 | "id": "water", 43 | "type": "fill", 44 | "source": "mapbox.mapbox-streets-v5", 45 | "source-layer": "water", 46 | "paint": { 47 | "fill-color": "#999" 48 | } 49 | }, { 50 | "id": "building", 51 | "type": "fill", 52 | "source": "mapbox.mapbox-streets-v5", 53 | "source-layer": "building", 54 | "paint": { 55 | "fill-color": "@building", 56 | "fill-opacity": { 57 | "base": 1.01, 58 | "stops": [[13, 0], [14, 1]] 59 | }, 60 | "fill-opacity-transition": { 61 | "duration": 500, 62 | "delay": 500 63 | }, 64 | "fill-outline-color": "@building_outline" 65 | } 66 | }, { 67 | "id": "borders", 68 | "type": "line", 69 | "source": "mapbox.mapbox-streets-v5", 70 | "source-layer": "admin", 71 | "paint": { 72 | "line-color": "rgba(0,0,0,0.3)", 73 | "line-width": 1 74 | } 75 | }, { 76 | "id": "poi", 77 | "type": "symbol", 78 | "source": "mapbox.mapbox-streets-v5", 79 | "source-layer": "poi_label", 80 | "layout": { 81 | "icon-max-size": 12, 82 | "icon-rotation-alignment": "viewport" 83 | } 84 | }, { 85 | "id": "country_label", 86 | "type": "symbol", 87 | "source": "mapbox.mapbox-streets-v5", 88 | "source-layer": "country_label", 89 | "filter": ["==", "$type", "Point"], 90 | "layout": { 91 | "text-field": "{name}", 92 | "text-font": "Open Sans Regular, Arial Unicode MS Regular", 93 | "text-max-size": 16, 94 | "text-padding": 10 95 | }, 96 | "paint": { 97 | "text-halo-color": "rgba(255,255,255,0.7)", 98 | "text-halo-width": "@stroke_width", 99 | "text-color": "@text" 100 | } 101 | }, { 102 | "id": "road_label", 103 | "type": "symbol", 104 | "source": "mapbox.mapbox-streets-v5", 105 | "source-layer": "road_label", 106 | "filter": ["==", "$type", "LineString"], 107 | "layout": { 108 | "text-field": "{name}", 109 | "text-font": "Open Sans Regular, Arial Unicode MS Regular", 110 | "text-max-size": 12, 111 | "text-max-angle": 59.59, 112 | "symbol-min-distance": 250 113 | }, 114 | "paint": { 115 | "text-color": "@text", 116 | "text-halo-color": "rgba(255,255,255,0.7)", 117 | "text-halo-width": "@stroke_width", 118 | "text-size": { 119 | "stops": [[4.770835839035499, 8.01], [15.477225251693334, 12]] 120 | } 121 | } 122 | }] 123 | } 124 | -------------------------------------------------------------------------------- /test/fixture/v7-migrate/style-basic.output.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "sources": { 4 | "mapbox.mapbox-streets-v5": { 5 | "type": "vector", 6 | "tiles": [] 7 | } 8 | }, 9 | "glyphs": "http://mapbox.s3.amazonaws.com/gl-glyphs-256/{fontstack}/{range}.pbf", 10 | "layers": [ 11 | { 12 | "id": "background", 13 | "type": "background", 14 | "paint": { 15 | "background-color": "#eee", 16 | "background-color-transition": { 17 | "duration": 500, 18 | "delay": 0 19 | } 20 | } 21 | }, 22 | { 23 | "id": "park", 24 | "type": "fill", 25 | "source": "mapbox.mapbox-streets-v5", 26 | "source-layer": "landuse", 27 | "filter": [ 28 | "==", 29 | "class", 30 | "park" 31 | ], 32 | "paint": { 33 | "fill-color": "#bda" 34 | } 35 | }, 36 | { 37 | "id": "water", 38 | "type": "fill", 39 | "source": "mapbox.mapbox-streets-v5", 40 | "source-layer": "water", 41 | "paint": { 42 | "fill-color": "#999" 43 | } 44 | }, 45 | { 46 | "id": "building", 47 | "type": "fill", 48 | "source": "mapbox.mapbox-streets-v5", 49 | "source-layer": "building", 50 | "paint": { 51 | "fill-color": "#ddd", 52 | "fill-opacity": { 53 | "base": 1.01, 54 | "stops": [ 55 | [ 56 | 13, 57 | 0 58 | ], 59 | [ 60 | 14, 61 | 1 62 | ] 63 | ] 64 | }, 65 | "fill-opacity-transition": { 66 | "duration": 500, 67 | "delay": 500 68 | }, 69 | "fill-outline-color": "#ccc" 70 | } 71 | }, 72 | { 73 | "id": "borders", 74 | "type": "line", 75 | "source": "mapbox.mapbox-streets-v5", 76 | "source-layer": "admin", 77 | "paint": { 78 | "line-color": "rgba(0,0,0,0.3)", 79 | "line-width": 1 80 | } 81 | }, 82 | { 83 | "id": "road_label", 84 | "type": "symbol", 85 | "source": "mapbox.mapbox-streets-v5", 86 | "source-layer": "road_label", 87 | "filter": [ 88 | "==", 89 | "$type", 90 | "LineString" 91 | ], 92 | "layout": { 93 | "text-field": "{name}", 94 | "text-font": [ 95 | "Open Sans Regular", 96 | "Arial Unicode MS Regular" 97 | ], 98 | "text-max-angle": 59.59, 99 | "symbol-spacing": 250, 100 | "text-size": { 101 | "stops": [ 102 | [ 103 | 4.770835839035499, 104 | 8.01 105 | ], 106 | [ 107 | 15.477225251693334, 108 | 12 109 | ] 110 | ] 111 | } 112 | }, 113 | "paint": { 114 | "text-color": "#000000", 115 | "text-halo-color": "rgba(255,255,255,0.7)", 116 | "text-halo-width": 0.25 117 | } 118 | }, 119 | { 120 | "id": "country_label", 121 | "type": "symbol", 122 | "source": "mapbox.mapbox-streets-v5", 123 | "source-layer": "country_label", 124 | "filter": [ 125 | "==", 126 | "$type", 127 | "Point" 128 | ], 129 | "layout": { 130 | "text-field": "{name}", 131 | "text-font": [ 132 | "Open Sans Regular", 133 | "Arial Unicode MS Regular" 134 | ], 135 | "text-padding": 10 136 | }, 137 | "paint": { 138 | "text-halo-color": "rgba(255,255,255,0.7)", 139 | "text-halo-width": 0.25, 140 | "text-color": "#000000" 141 | } 142 | }, 143 | { 144 | "id": "poi", 145 | "type": "symbol", 146 | "source": "mapbox.mapbox-streets-v5", 147 | "source-layer": "poi_label", 148 | "layout": { 149 | "icon-rotation-alignment": "viewport" 150 | } 151 | } 152 | ] 153 | } -------------------------------------------------------------------------------- /test/format.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var t = require('tape'), 4 | format = require('../lib/format'); 5 | 6 | function roundtrip(style) { 7 | return JSON.parse(format(style)); 8 | } 9 | 10 | t('orders top-level keys', function(t) { 11 | t.deepEqual(Object.keys(roundtrip({ 12 | "layers": [], 13 | "other": {}, 14 | "sources": {}, 15 | "glyphs": "", 16 | "sprite": "", 17 | "version": 6 18 | })), ['version', 'sources', 'sprite', 'glyphs', 'layers', 'other']); 19 | t.end(); 20 | }); 21 | 22 | t('orders layer keys', function(t) { 23 | t.deepEqual(Object.keys(roundtrip({ 24 | "layers": [{ 25 | "paint": {}, 26 | "layout": {}, 27 | "id": "id", 28 | "type": "type" 29 | }] 30 | }).layers[0]), ['id', 'type', 'layout', 'paint']); 31 | t.end(); 32 | }); 33 | -------------------------------------------------------------------------------- /test/migrate.js: -------------------------------------------------------------------------------- 1 | /* global process, __dirname */ 2 | 'use strict'; 3 | 4 | var t = require('tape'), 5 | fs = require('fs'), 6 | glob = require('glob'), 7 | spec = require('../'), 8 | path = require('path'), 9 | validate = require('../').validate, 10 | v8 = require('../reference/v8'), 11 | migrate = require('../').migrate; 12 | 13 | var UPDATE = !!process.env.UPDATE; 14 | 15 | t('migrates to latest version', function(t) { 16 | t.deepEqual(migrate({version: 7, layers: []}).version, spec.latest.$version); 17 | t.end(); 18 | }); 19 | 20 | glob.sync(__dirname + '/fixture/v7-migrate/*.input.json').forEach(function(file) { 21 | t(path.basename(file), function(t) { 22 | var outputfile = file.replace('.input', '.output'); 23 | var style = JSON.parse(fs.readFileSync(file)); 24 | var result = migrate(style); 25 | t.deepEqual(validate.parsed(result, v8), []); 26 | if (UPDATE) fs.writeFileSync(outputfile, JSON.stringify(result, null, 2)); 27 | var expect = JSON.parse(fs.readFileSync(outputfile)); 28 | t.deepEqual(result, expect); 29 | t.end(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/migrations/v7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var t = require('tape'), 4 | migrate = require('../../migrations/v7'); 5 | 6 | t('remove prerendered layer', function(t) { 7 | t.deepEqual(migrate({ 8 | "version": 6, 9 | "layers": [{ 10 | "type": "raster", 11 | "layers": [{}] 12 | }] 13 | }), { 14 | "version": 7, 15 | "layers": [] 16 | }); 17 | t.end(); 18 | }); 19 | -------------------------------------------------------------------------------- /test/migrations/v8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var t = require('tape'), 4 | migrate = require('../../migrations/v8'); 5 | 6 | t('split text-font', function (t) { 7 | var input = { 8 | "version": 7, 9 | "sources": { 10 | "vector": { 11 | "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5" 12 | } 13 | }, 14 | "layers": [ 15 | { 16 | "id": "minimum", 17 | "type": "symbol", 18 | "source": "vector", 19 | "source-layer": "layer", 20 | "layout": { 21 | "text-font": "Helvetica, Arial", 22 | "text-field": "{foo}" 23 | } 24 | } 25 | ] 26 | }; 27 | 28 | var output = { 29 | "version": 8, 30 | "sources": { 31 | "vector": { 32 | "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5" 33 | } 34 | }, 35 | "layers": [ 36 | { 37 | "id": "minimum", 38 | "type": "symbol", 39 | "source": "vector", 40 | "source-layer": "layer", 41 | "layout": { 42 | "text-font": ["Helvetica", "Arial"], 43 | "text-field": "{foo}" 44 | } 45 | } 46 | ] 47 | }; 48 | 49 | t.deepEqual(migrate(input), output, 'splits text-font'); 50 | t.end(); 51 | }); 52 | 53 | t('rename symbol-min-distance', function (t) { 54 | var input = { 55 | "version": 7, 56 | "sources": { 57 | "vector": { 58 | "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5" 59 | } 60 | }, 61 | "layers": [ 62 | { 63 | "id": "minimum", 64 | "type": "symbol", 65 | "source": "vector", 66 | "source-layer": "layer", 67 | "layout": { 68 | "symbol-min-distance": 2 69 | } 70 | } 71 | ] 72 | }; 73 | 74 | var output = { 75 | "version": 8, 76 | "sources": { 77 | "vector": { 78 | "type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5" 79 | } 80 | }, 81 | "layers": [ 82 | { 83 | "id": "minimum", 84 | "type": "symbol", 85 | "source": "vector", 86 | "source-layer": "layer", 87 | "layout": { 88 | "symbol-spacing": 2 89 | } 90 | } 91 | ] 92 | }; 93 | 94 | t.deepEqual(migrate(input), output, 'renames symbol-min-distance'); 95 | t.end(); 96 | }); 97 | 98 | t('renames urls', function (t) { 99 | var input = { 100 | "version": 7, 101 | "sources": { 102 | "vector": { 103 | "type": "video", "url": ["foo"], 104 | coordinates: [[1, 0], [1, 0], [1, 0], [1, 0]] 105 | } 106 | }, 107 | "layers": [] 108 | }; 109 | 110 | var output = { 111 | "version": 8, 112 | "sources": { 113 | "vector": { 114 | "type": "video", "urls": ["foo"], 115 | coordinates: [[0, 1], [0, 1], [0, 1], [0, 1]] 116 | } 117 | }, 118 | "layers": [] 119 | }; 120 | 121 | t.deepEqual(migrate(input), output, 'renames url and flips coordinates of of video'); 122 | t.end(); 123 | }); 124 | 125 | 126 | t('not migrate interpolated functions', function (t) { 127 | var input = { 128 | "version": 7, 129 | "sources": { 130 | "vector": { 131 | "type": "vector", 132 | "url": "mapbox://mapbox.mapbox-streets-v5" 133 | } 134 | }, 135 | "layers": [{ 136 | "id": "functions", 137 | "type": "symbol", 138 | "source": "vector", 139 | "source-layer": "layer", 140 | "layout": { 141 | "line-width": { 142 | base: 2, 143 | stops: [[1, 2], [3, 6]] 144 | } 145 | } 146 | }] 147 | }; 148 | 149 | var output = { 150 | "version": 8, 151 | "sources": { 152 | "vector": { 153 | "type": "vector", 154 | "url": "mapbox://mapbox.mapbox-streets-v5" 155 | } 156 | }, 157 | "layers": [{ 158 | "id": "functions", 159 | "type": "symbol", 160 | "source": "vector", 161 | "source-layer": "layer", 162 | "layout": { 163 | "line-width": { 164 | base: 2, 165 | stops: [[1, 2], [3, 6]] 166 | } 167 | } 168 | }] 169 | }; 170 | 171 | t.deepEqual(migrate(input), output); 172 | t.end(); 173 | }); 174 | 175 | t('not migrate piecewise-constant functions', function (t) { 176 | var input = { 177 | "version": 7, 178 | "sources": { 179 | "vector": { 180 | "type": "vector", 181 | "url": "mapbox://mapbox.mapbox-streets-v5" 182 | } 183 | }, 184 | "layers": [{ 185 | "id": "functions", 186 | "type": "symbol", 187 | "source": "vector", 188 | "source-layer": "layer", 189 | "layout": { 190 | "text-transform": { 191 | stops: [[1, "uppercase"], [3, "lowercase"]], 192 | } 193 | } 194 | }] 195 | }; 196 | 197 | var output = { 198 | "version": 8, 199 | "sources": { 200 | "vector": { 201 | "type": "vector", 202 | "url": "mapbox://mapbox.mapbox-streets-v5" 203 | } 204 | }, 205 | "layers": [{ 206 | "id": "functions", 207 | "type": "symbol", 208 | "source": "vector", 209 | "source-layer": "layer", 210 | "layout": { 211 | "text-transform": { 212 | stops: [[1, "uppercase"], [3, "lowercase"]], 213 | } 214 | } 215 | }] 216 | }; 217 | 218 | t.deepEqual(migrate(input), output); 219 | t.end(); 220 | }); 221 | 222 | t('inline constants', function (t) { 223 | var input = { 224 | "version": 7, 225 | "constants": { 226 | "@foo": 0.5 227 | }, 228 | "sources": { 229 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 230 | }, 231 | "layers": [ 232 | { 233 | "id": "minimum", 234 | "type": "fill", 235 | "source": "vector", 236 | "source-layer": "layer", 237 | "layout": { 238 | "fill-opacity": "@foo" 239 | } 240 | } 241 | ] 242 | }; 243 | 244 | var output = { 245 | "version": 8, 246 | "sources": { 247 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 248 | }, 249 | "layers": [ 250 | { 251 | "id": "minimum", 252 | "type": "fill", 253 | "source": "vector", 254 | "source-layer": "layer", 255 | "layout": { 256 | "fill-opacity": 0.5 257 | } 258 | } 259 | ] 260 | }; 261 | 262 | t.deepEqual(migrate(input), output, 'infers opacity type'); 263 | t.end(); 264 | }); 265 | 266 | t('migrate and inline fontstack constants', function (t) { 267 | var input = { 268 | "version": 7, 269 | "constants": { 270 | "@foo": "Arial Unicode,Foo Bar" 271 | }, 272 | "sources": { 273 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 274 | }, 275 | "layers": [ 276 | { 277 | "id": "minimum", 278 | "type": "symbol", 279 | "source": "vector", 280 | "source-layer": "layer", 281 | "layout": { 282 | "text-font": "@foo" 283 | } 284 | } 285 | ] 286 | }; 287 | 288 | var output = { 289 | "version": 8, 290 | "sources": { 291 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 292 | }, 293 | "layers": [ 294 | { 295 | "id": "minimum", 296 | "type": "symbol", 297 | "source": "vector", 298 | "source-layer": "layer", 299 | "layout": { 300 | "text-font": ["Arial Unicode", "Foo Bar"] 301 | } 302 | } 303 | ] 304 | }; 305 | 306 | t.deepEqual(migrate(input), output, 'infers opacity type'); 307 | t.end(); 308 | }); 309 | 310 | t('update fontstack function', function (t) { 311 | var input = { 312 | "version": 7, 313 | "sources": { 314 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 315 | }, 316 | "layers": [ 317 | { 318 | "id": "minimum", 319 | "type": "symbol", 320 | "source": "vector", 321 | "source-layer": "layer", 322 | "layout": { 323 | "text-font": { 324 | "base": 1, 325 | "stops": [ 326 | [ 327 | 0, 328 | "Open Sans Regular, Arial Unicode MS Regular" 329 | ], 330 | [ 331 | 6, 332 | "Open Sans Semibold, Arial Unicode MS Regular" 333 | ] 334 | ] 335 | } 336 | } 337 | } 338 | ] 339 | }; 340 | 341 | var output = { 342 | "version": 8, 343 | "sources": { 344 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 345 | }, 346 | "layers": [ 347 | { 348 | "id": "minimum", 349 | "type": "symbol", 350 | "source": "vector", 351 | "source-layer": "layer", 352 | "layout": { 353 | "text-font": { 354 | "base": 1, 355 | "stops": [ 356 | [0, ["Open Sans Regular", "Arial Unicode MS Regular"]], 357 | [6, ["Open Sans Semibold", "Arial Unicode MS Regular"]] 358 | ] 359 | } 360 | } 361 | } 362 | ] 363 | }; 364 | 365 | t.deepEqual(migrate(input), output); 366 | t.end(); 367 | }); 368 | 369 | t('inline and migrate fontstack constant function', function (t) { 370 | var input = { 371 | "version": 7, 372 | "constants": { 373 | "@function": { 374 | "base": 1, 375 | "stops": [ 376 | [ 377 | 0, 378 | "Open Sans Regular, Arial Unicode MS Regular" 379 | ], 380 | [ 381 | 6, 382 | "Open Sans Semibold, Arial Unicode MS Regular" 383 | ] 384 | ] 385 | } 386 | }, 387 | "sources": { 388 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 389 | }, 390 | "layers": [ 391 | { 392 | "id": "minimum", 393 | "type": "symbol", 394 | "source": "vector", 395 | "source-layer": "layer", 396 | "layout": { 397 | "text-font": "@function" 398 | } 399 | } 400 | ] 401 | }; 402 | 403 | var output = { 404 | "version": 8, 405 | "sources": { 406 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 407 | }, 408 | "layers": [ 409 | { 410 | "id": "minimum", 411 | "type": "symbol", 412 | "source": "vector", 413 | "source-layer": "layer", 414 | "layout": { 415 | "text-font": { 416 | "base": 1, 417 | "stops": [ 418 | [0, ["Open Sans Regular", "Arial Unicode MS Regular"]], 419 | [6, ["Open Sans Semibold", "Arial Unicode MS Regular"]] 420 | ] 421 | } 422 | } 423 | } 424 | ] 425 | }; 426 | 427 | t.deepEqual(migrate(input), output); 428 | t.end(); 429 | }); 430 | 431 | t('update fontstack function constant', function (t) { 432 | var input = { 433 | "version": 7, 434 | "constants": { 435 | "@font-stack-a": "Open Sans Regular, Arial Unicode MS Regular", 436 | "@font-stack-b": "Open Sans Semibold, Arial Unicode MS Regular" 437 | }, 438 | "sources": { 439 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 440 | }, 441 | "layers": [ 442 | { 443 | "id": "minimum", 444 | "type": "symbol", 445 | "source": "vector", 446 | "source-layer": "layer", 447 | "layout": { 448 | "text-font": { 449 | "base": 1, 450 | "stops": [ 451 | [0, "@font-stack-a"], 452 | [6, "@font-stack-b"] 453 | ] 454 | } 455 | } 456 | } 457 | ] 458 | }; 459 | 460 | var output = { 461 | "version": 8, 462 | "sources": { 463 | "vector": {"type": "vector", "url": "mapbox://mapbox.mapbox-streets-v5"} 464 | }, 465 | "layers": [ 466 | { 467 | "id": "minimum", 468 | "type": "symbol", 469 | "source": "vector", 470 | "source-layer": "layer", 471 | "layout": { 472 | "text-font": { 473 | "base": 1, 474 | "stops": [ 475 | [0, ["Open Sans Regular", "Arial Unicode MS Regular"]], 476 | [6, ["Open Sans Semibold", "Arial Unicode MS Regular"]] 477 | ] 478 | } 479 | } 480 | } 481 | ] 482 | }; 483 | 484 | t.deepEqual(migrate(input), output); 485 | t.end(); 486 | }); 487 | 488 | t('migrate UNversioned fontstack urls', function (t) { 489 | var input = { 490 | "version": 7, 491 | "glyphs": "mapbox://fontstack/{fontstack}/{range}.pbf", 492 | "layers": [] 493 | }; 494 | 495 | var output = { 496 | "version": 8, 497 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 498 | "layers": [] 499 | }; 500 | 501 | t.deepEqual(migrate(input), output); 502 | t.end(); 503 | }); 504 | 505 | t('migrate versioned fontstack urls', function (t) { 506 | var input = { 507 | "version": 7, 508 | "glyphs": "mapbox://fonts/v1/boxmap/{fontstack}/{range}.pbf", 509 | "layers": [] 510 | }; 511 | 512 | var output = { 513 | "version": 8, 514 | "glyphs": "mapbox://fonts/boxmap/{fontstack}/{range}.pbf", 515 | "layers": [] 516 | }; 517 | 518 | t.deepEqual(migrate(input), output); 519 | t.end(); 520 | }); 521 | -------------------------------------------------------------------------------- /test/spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var spec = require('../'); 5 | 6 | ['v6', 'v7', 'v8', 'latest'].forEach(function(version) { 7 | ['', 'min'].forEach(function(kind) { 8 | var v = version + kind; 9 | test(v, function(t) { 10 | for (var k in spec[v]) { 11 | // Exception for version. 12 | if (k === '$version') { 13 | t.equal(typeof spec[v].$version, 'number', '$version (number)'); 14 | } else { 15 | validSchema(k, t, spec[v][k], spec[v]); 16 | } 17 | } 18 | t.end(); 19 | }); 20 | }); 21 | }); 22 | 23 | function validSchema(k, t, obj, ref) { 24 | var minified = t.name.match(/min$/) !== null; 25 | var scalar = ['boolean', 'string', 'number']; 26 | var types = Object.keys(ref).concat(['boolean', 'string', 'number', 27 | 'array', 'enum', 'color', '*', 28 | // new in v8 29 | 'opacity', 'translate-array', 'dash-array', 'offset-array', 'font-array', 'field-template', 30 | // new enums in v8 31 | 'line-cap-enum', 32 | 'line-join-enum', 33 | 'symbol-placement-enum', 34 | 'rotation-alignment-enum', 35 | 'text-justify-enum', 36 | 'text-anchor-enum', 37 | 'text-transform-enum', 38 | 'visibility-enum' 39 | ]); 40 | var keys = [ 41 | 'default', 42 | 'doc', 43 | 'example', 44 | 'function', 45 | 'zoom-function', 46 | 'property-function', 47 | 'function-output', 48 | 'length', 49 | 'min-length', 50 | 'required', 51 | 'transition', 52 | 'type', 53 | 'value', 54 | 'units', 55 | 'tokens', 56 | 'values', 57 | 'maximum', 58 | 'minimum', 59 | 'period', 60 | 'requires', 61 | 'sdk-support' 62 | ]; 63 | 64 | // Schema object. 65 | if (Array.isArray(obj.type) || typeof obj.type === 'string') { 66 | // schema must have only known keys 67 | for (var attr in obj) { 68 | t.ok(keys.indexOf(attr) !== -1, k + '.' + attr, 'stray key'); 69 | } 70 | 71 | // schema type must be js native, 'color', or present in ref root object. 72 | t.ok(types.indexOf(obj.type) !== -1, k + '.type (' + obj.type + ')'); 73 | 74 | // schema type is an enum, it must have 'values' and they must be 75 | // objects (>=v8) or scalars (<=v7). If objects, check that doc key 76 | // (if present) is a string. 77 | if (obj.type === 'enum') { 78 | var values = (ref.$version >= 8 ? Object.keys(obj.values) : obj.values); 79 | t.ok(Array.isArray(values) && values.every(function(v) { 80 | return scalar.indexOf(typeof v) !== -1; 81 | }), k + '.values [' + values +']'); 82 | if (ref.$version >= 8) { 83 | for (var v in obj.values) { 84 | if (Array.isArray(obj.values) === false) { // skips $root.version 85 | if (obj.values[v].doc !== undefined) { 86 | t.equal('string', typeof obj.values[v].doc, k + '.doc (string)'); 87 | if (minified) t.fail('minified file should not have ' + k + '.doc'); 88 | } else { 89 | if (t.name === 'latest') t.fail('doc missing for ' + k); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | // schema type is array, it must have 'value' and it must be a type. 97 | if (obj.value !== undefined) { 98 | if (Array.isArray(obj.value)) { 99 | obj.value.forEach(function(i) { 100 | t.ok(types.indexOf(i) !== -1, k + '.value (' + i + ')'); 101 | }); 102 | } else if (typeof obj.value === 'object') { 103 | validSchema(k + '.value', t, obj.value, ref); 104 | } else { 105 | t.ok(types.indexOf(obj.value) !== -1, k + '.value (' + obj.value + ')'); 106 | } 107 | } 108 | 109 | // schema key doc checks 110 | if (obj.doc !== undefined) { 111 | t.equal('string', typeof obj.doc, k + '.doc (string)'); 112 | if (minified) t.fail('minified file should not have ' + k + '.doc'); 113 | } else { 114 | if (t.name === 'latest') t.fail('doc missing for ' + k); 115 | } 116 | 117 | // schema key example checks 118 | if (minified && obj.example !== undefined) { 119 | t.fail('minified file should not have ' + k + '.example'); 120 | } 121 | 122 | // schema key function checks 123 | if (obj.function !== undefined) { 124 | if (ref.$version >= 7) { 125 | t.equal(true, ['interpolated', 'piecewise-constant'].indexOf(obj.function) >= 0, 'function: ' + obj.function); 126 | } else { 127 | t.equal('boolean', typeof obj.function, k + '.required (boolean)'); 128 | } 129 | } 130 | 131 | // schema key required checks 132 | if (obj.required !== undefined) { 133 | t.equal('boolean', typeof obj.required, k + '.required (boolean)'); 134 | } 135 | 136 | // schema key transition checks 137 | if (obj.transition !== undefined) { 138 | t.equal('boolean', typeof obj.transition, k + '.transition (boolean)'); 139 | } 140 | 141 | // schema key requires checks 142 | if (obj.requires !== undefined) { 143 | t.equal(true, Array.isArray(obj.requires), k + '.requires (array)'); 144 | } 145 | } else if (Array.isArray(obj)) { 146 | obj.forEach(function(child, j) { 147 | if (typeof child === 'string' && scalar.indexOf(child) !== -1) return; 148 | validSchema(k + '[' + j + ']', t, typeof child === 'string' ? ref[child] : child, ref); 149 | }); 150 | // Container object. 151 | } else if (typeof obj === 'object') { 152 | for (var j in obj) validSchema(k + '.' + j, t, obj[j], ref); 153 | // Invalid ref object. 154 | } else { 155 | t.ok(false, 'Invalid: ' + k); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /test/validate.js: -------------------------------------------------------------------------------- 1 | /* global process, __dirname */ 2 | 'use strict'; 3 | 4 | var t = require('tape'), 5 | glob = require('glob'), 6 | fs = require('fs'), 7 | path = require('path'), 8 | validate = require('../').validate; 9 | 10 | var UPDATE = !!process.env.UPDATE; 11 | 12 | glob.sync(__dirname + '/fixture/*.input.json').forEach(function(file) { 13 | t(path.basename(file), function(t) { 14 | var outputfile = file.replace('.input', '.output'); 15 | var style = fs.readFileSync(file); 16 | var result = validate(style); 17 | if (UPDATE) fs.writeFileSync(outputfile, JSON.stringify(result, null, 2)); 18 | var expect = JSON.parse(fs.readFileSync(outputfile)); 19 | t.deepEqual(result, expect); 20 | t.end(); 21 | }); 22 | }); 23 | 24 | var fixtures = glob.sync(__dirname + '/fixture/*.input.json'); 25 | var style = JSON.parse(fs.readFileSync(fixtures[0])); 26 | var reference = require('../reference/latest.min'); 27 | 28 | t('validate.parsed exists', function(t) { 29 | t.equal(typeof validate.parsed, 'function'); 30 | t.end(); 31 | }); 32 | 33 | t('errors from validate.parsed do not contain line numbers', function(t) { 34 | var result = validate.parsed(style, reference); 35 | t.equal(result[0].line, undefined); 36 | t.end(); 37 | }); 38 | 39 | t('validate.latest exists', function(t) { 40 | t.equal(typeof validate.latest, 'function'); 41 | t.end(); 42 | }); 43 | 44 | t('errors from validate.latest do not contain line numbers', function(t) { 45 | var result = validate.latest(style); 46 | t.equal(result[0].line, undefined); 47 | t.end(); 48 | }); 49 | --------------------------------------------------------------------------------