├── .github
├── dependabot.yml
└── workflows
│ └── build.yml
├── .gitignore
├── CHANGELOG.md
├── ICONS.md
├── LICENSE.md
├── README.md
├── eslint.config.mjs
├── jest.config.js
├── lib
├── build.js
├── index.js
└── translations.js
├── package-lock.json
├── package.json
├── schemas
├── deprecated.json
├── discarded.json
├── field.json
├── preset.json
├── preset_category.json
└── preset_defaults.json
└── tests
└── schema-builder.test.js
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm"
7 | directory: "/"
8 | schedule:
9 | interval: "daily"
10 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [20]
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v1
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | - run: npm install
27 | - run: npm run lint
28 | - run: npm run test
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .esm-cache
3 | .vscode/
4 | .idea/
5 | /node_modules/
6 | /.tx/tmp/
7 | npm-debug.log
8 |
9 | /.coverage
10 | /tests/workspace
11 |
12 | transifex.auth
13 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | :warning: = Breaking change
2 |
3 |
9 |
10 | # 6.5.1
11 | ##### 2024-Mar-14
12 |
13 | * Also include category icons in `/interim/icons.json`
14 |
15 |
16 | # 6.5.0
17 | ##### 2024-Mar-14
18 |
19 | * Fix links to icons from the maki icon set ([#119], thanks [@Cj-Malone])
20 | * Write a list of used icons to the `/interim` directory, so they can be more timely pre-fetched/included by consumer applications like iD
21 |
22 | [#119]: https://github.com/ideditor/schema-builder/pull/119
23 | [@Cj-Malone]: https://github.com/Cj-Malone
24 |
25 | # 6.4.0
26 | ##### 2023-Aug-25
27 |
28 | * Improve documentation of: Field inheritance ([#104]), Icons ([#103]), TagInfo output ([#102]), tag deprecations ([#105]), thanks [@tordans]
29 | * Enhance taginfo output: mention used special characters in the project description which are used in tag descriptions, resolve labels of presets/fields which use cross-referenced strings, include discarded tags in taginfo output
30 |
31 | [#102]: https://github.com/ideditor/schema-builder/pull/102
32 | [#103]: https://github.com/ideditor/schema-builder/pull/103
33 | [#104]: https://github.com/ideditor/schema-builder/pull/104
34 | [#105]: https://github.com/ideditor/schema-builder/pull/105
35 |
36 |
37 | # 6.3.0
38 | ##### 2023-May-23
39 |
40 | * Allow to specify alternative keys for `text`, `number`, `tel`, `email` and `url` fields. ([#98])
41 | * Fix regression to make the project work on Windows ([#97], thanks [@k-yle])
42 |
43 | [#97]: https://github.com/ideditor/schema-builder/pull/97
44 | [#98]: https://github.com/ideditor/schema-builder/pull/98
45 | [@k-yle]: https://github.com/k-yle
46 |
47 | # 6.2.0
48 | ##### 2023-Mar-21
49 |
50 | * Produce transifex developer notes also for options of multi-key fields and fields with title/description strings ([#92])
51 | * Extend transifex developer notes for regional presets/fields ([#93])
52 |
53 | [#92]: https://github.com/ideditor/schema-builder/pull/92
54 | [#93]: https://github.com/ideditor/schema-builder/pull/93
55 |
56 | # 6.1.0
57 | ##### 2023-Mar-14
58 |
59 | * Update `teamki` URLs to new repository organization
60 | * Bump `glob` dependency to v9.3
61 | * Move documentation about icons to a separate page
62 |
63 | # 6.0.1
64 | ##### 2023-Jan-20
65 |
66 | * Fix bug in validation of `prerequisiteTag` values (v6.0.0 does falsely disallow requirements with only a `key` but neither `value` nor `valueNot`)
67 |
68 | # 6.0.0
69 | ##### 2023-Jan-20
70 |
71 | * :warning: Rename field type `cycleway` to `directionalCombo` ([#79], thanks [@tordans])
72 | * :warning: the tag keys of this field are now split into two separate parts: the `key` property contains the common (e.g. `*:both`) variant of the tag and the `keys` property is for the directional (e.g. `:left`/`:right`) subtags
73 | * Introduce new `date` field type ([#76])
74 | * Allow the `Röntgen` icon set to be used for icons ([#75])
75 | * Allow to specify icons for values of combo fields ([#56])
76 | * Fix JSON schema's type definition of `prerequisiteTag` ([#81], thanks [@tordans]) and `reference` property of fields
77 |
78 | [#56]: https://github.com/ideditor/schema-builder/issues/56
79 | [#75]: https://github.com/ideditor/schema-builder/issues/75
80 | [#76]: https://github.com/ideditor/schema-builder/issues/76
81 | [#79]: https://github.com/ideditor/schema-builder/issues/79
82 | [#81]: https://github.com/ideditor/schema-builder/pull/81
83 | [@tordans]: https://github.com/tordans
84 |
85 | # 5.3.0
86 | ##### 2022-Dec-09
87 |
88 | * Add requirement to json schema that either `key` or `keys` property must be present on (most) fields ([#78])
89 | * Upgrade dependency `@transifex/api` to v5
90 |
91 | [#78]: https://github.com/ideditor/schema-builder/pull/78
92 |
93 | # 5.2.2
94 | ##### 2022-Nov-28
95 |
96 | * fix test and build commands on Windows OS (regression in v5.0.0) ([id-tagging-schema#655])
97 |
98 | [id-tagging-schema#655]: https://github.com/openstreetmap/id-tagging-schema/issues/655
99 |
100 | # 5.2.1
101 | ##### 2022-Nov-18
102 |
103 | * fix clearing the `dist` directory when running `buildDist` (regression in v5.2.0)
104 |
105 | # 5.2.0
106 | ##### 2022-Nov-18
107 |
108 | * upgrade transifex API version to v3
109 | * don't clear translations when running `buildDist` without translation settings
110 |
111 | # 5.1.1
112 | ##### 2022-Sep-29
113 |
114 | * Fix a bug which caused a crash when fetching translations
115 |
116 | # 5.1.0
117 | ##### 2022-Sep-29
118 |
119 | * :warning: make `placeholder` property of fields referenceable like labels/terms/etc.
120 |
121 | # 5.0.0
122 | ##### 2022-Sep-29
123 |
124 | * :warning: add new `colour` field type ([#26])
125 | * :warning: add functionality to reference labels/strings from other fields/presets by using the referenced preset/field name in brackets, similar to how the fields/moreFields can be referenced between presets ([#42])
126 | * drop undocumented and unused `icon` property for fields ([#30])
127 | * refactor js code to be an ESM module ([#42])
128 | * improve documentation about usage of aliases and terms ([#57])
129 |
130 | [#26]: https://github.com/ideditor/schema-builder/issues/26
131 | [#30]: https://github.com/ideditor/schema-builder/issues/30
132 | [#42]: https://github.com/ideditor/schema-builder/issues/42
133 | [#57]: https://github.com/ideditor/schema-builder/pull/57
134 |
135 |
136 | # 4.0.8
137 | ##### 2022-Jun-17
138 |
139 | * Taginfo metadata output: Include short description about deprecated tags
140 |
141 | # 4.0.7
142 | ##### 2022-Jan-28
143 |
144 | * Fix fetching of translations after upgrading `js-yaml` library to v4
145 |
146 | # 4.0.6
147 | ##### 2022-Jan-18
148 |
149 | * Replace the broken `color` dependency with `chalk`
150 | * Use pipe separators instead of newlines for name translation comments
151 | * Filter out preset name from aliases and preset aliases from terms
152 |
153 | # 4.0.4
154 | ##### 2020-Dec-10
155 |
156 | * Don't add incorrect option comments for fields with `keys`
157 |
158 | # 4.0.3
159 | ##### 2020-Dec-10
160 |
161 | * Fix issue with generating source_strings.yaml when there are string keys with whitespace
162 |
163 | # 4.0.2
164 | ##### 2020-Dec-10
165 |
166 | * Fix issue where only the first character of translated preset names would be saved
167 |
168 | # 4.0.1
169 | ##### 2020-Dec-10
170 |
171 | * Use commas instead of pluses to separate tags in the field label Transifex comments
172 |
173 | # 4.0.0
174 | ##### 2020-Dec-10
175 |
176 | * :warning: Separate `aliases` with newlines (\n) instead of commas
177 | * :warning: Don't include empty `terms` properties in the English locale
178 | * Make all `terms` lower case
179 | * Remove whitespace between `terms`
180 | * Collapse duplicate `terms`
181 |
182 | # 3.1.0
183 | ##### 2020-Dec-09
184 |
185 | * Add `aliases` preset property for listing `name` synonyms ([#3])
186 | * Fix an issue with generating some TagInfo field value descriptions
187 |
188 | [#3]: https://github.com/ideditor/schema-builder/issues/3
189 |
190 | # 3.0.0
191 | ##### 2020-Dec-08
192 |
193 | * :warning: Don't include English strings redundantly in built data files that are already in translation files
194 | * :warning: Rename `fetchTranslations` options:
195 | * `credentials` -> `translCredentials`
196 | * `organizationId` -> `translOrgId`
197 | * `projectId` -> `translProjectId`
198 | * `resourceIds` -> `translResourceIds`
199 | * `reviewedOnly` -> `translReviewedOnly`
200 | * Accept translation options in the `buildDist` function in order to run `fetchTranslations` at the same time
201 | * Add `autoSuggestions` combo field property to control whether TagInfo dropdown options should be loaded
202 | * Add `customValues` combo field property to specify if freeform text values are allowed
203 | * Add optional `listReusedIcons` diagnostic option to find overused icons
204 |
205 | # 2.1.0
206 | ##### 2020-Nov-30
207 |
208 | * Build both minified and non-minified translation files ([#2])
209 | * Discard `terms` preset and field properties with no values
210 |
211 | [#2]: https://github.com/ideditor/schema-builder/issues/2
212 |
213 | # 2.0.0
214 | ##### 2020-Nov-25
215 |
216 | * :warning: Rename `build` endpoint to `buildDist`
217 | * :warning: Replace `countryCodes` and `notCountryCodes` preset and field properties with `locationSet`
218 | * :warning: Rename `maxspeed` field type to `roadspeed`
219 | * Add `roadheight` field type
220 | * Rename and relocate translations source file from `dist/translations/en.yaml` to `interim/source_strings.yaml`
221 | * Add `buildDev` endpoint for compiling development-only files (e.g. `interim/source_strings.yaml`)
222 | * Add `validate` endpoint for checking data errors without compiling any files
223 | * Add `fetchTranslations` endpoint for downloading translation files from Transifex
224 | * Add `sourceLocale` option for using a data language other than English
225 | * Include unminifed JSON files in the `dist` directory
226 | * Minify the source locale file (e.g. `dist/translations/en.json`) for consistency and space savings
227 | * Make `lib/index.js` the main module file
228 | * Enable code tests, es-lint, Travis CI, and Dependabot
229 |
--------------------------------------------------------------------------------
/ICONS.md:
--------------------------------------------------------------------------------
1 | # Icons
2 |
3 | For presets and some fields, icons can be specified. They provide an additional hint to the user about what the respective OSM tags are about. Additionaly, icons can help to make presets more universally understood, as icons are even present when presets are not translated into all languages.
4 |
5 | # Where do the icons come from?
6 |
7 | Icons from the below listed sources can be used. When specifying an icon, use the prefixed version of the name, for example `"icon": "maki-park"`.
8 |
9 | * [Maki](https://labs.mapbox.com/maki-icons/) (`maki-`), map-specific icons from Mapbox
10 | * [Temaki](https://rapideditor.github.io/temaki/docs/) (`temaki-`), an expansion pack for Maki
11 | * [Röntgen](https://github.com/enzet/map-machine#r%C3%B6ntgen-icon-set) ([available icons](https://github.com/openstreetmap/iD/tree/develop/svg/roentgen)) (`roentgen-`), part of the Map Machine project
12 | * [Font Awesome](https://fontawesome.com/icons?d=gallery&m=free), thousands of general-purpose icons
13 | * There is a free and pro tier. You can use any icon from the free tier in the following styles:
14 | * [Solid](https://fontawesome.com/search?o=r&ic=free&s=solid) (`fas-`)
15 | * [Regular](https://fontawesome.com/search?o=r&ic=free&s=regular) (`far-`)
16 | * [Brands](https://fontawesome.com/search?o=r&ic=free&ip=brands) (`fab-`)
17 | * [iD's presets-icons](https://github.com/openstreetmap/iD/tree/develop/svg/iD-sprite/presets), [iD's fields-icons](https://github.com/openstreetmap/iD/tree/develop/svg/iD-sprite/fields) (`iD-`)
18 |
19 | ## How can I add new icons?
20 |
21 | A good place to submit a PR for a new icon is the [Temaki](https://github.com/rapideditor/temaki#readme) project. This is because Temaki was specifically created for icons for tagging presets and has therefore relatively low acceptance criteria and short release cycles. But in principle, you could propose icons to be added to any of the listed sources above, if you prefer to do so.
22 |
23 | ## Guidelines
24 |
25 | In addition to the [design](https://github.com/rapideditor/temaki#design-guidelines) [guidelines](https://labs.mapbox.com/maki-icons/guidelines/), the following points should be considered when selecting or designing icons:
26 |
27 | * icons should be as specific as possible for a given preset and not reused too much between presets
28 | * icons should be universal and generic (i.e. not specific to a single region)
29 | * icons are not images, i.e. don't need to be an exact depiction of the preset's features
30 |
31 | ## `imageURL`
32 |
33 | For presets, it is possible to define an icon via the `imageURL` property in addition to an `icon`. The referenced image _might_ be displayed by an editor instead of the `icon`. For example, iD displays icons from `imageURL` only when users have not disabled to _show third party icons_.
34 |
35 | `imageURL` is extensively used to specify the logos of brand presets by the [name-suggestion-index](https://github.com/osmlab/name-suggestion-index) project.
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## ISC License
2 |
3 | Copyright (c) 2017, iD Contributors
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
11 | AND 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 | [](https://github.com/ideditor/schema-builder/actions?query=workflow%3A%22build%22)
2 | [](https://badge.fury.io/js/%40ideditor%2Fschema-builder)
3 |
4 | # schema-builder
5 |
6 | This package lets you define and compile OpenStreetMap presets, fields, and other tagging
7 | info into the format expected by the [iD editor](https://github.com/openstreetmap/iD).
8 |
9 | The [iD Tagging Schema](https://github.com/openstreetmap/id-tagging-schema) project uses
10 | this to manage iD's tags. You can use it to create a custom schema for your own iD instance.
11 |
12 | ## Usage
13 |
14 | ### Building Distribution Data
15 |
16 | To validate your source data and compile output files for iD (i.e. when releasing a new schema version):
17 |
18 | ```JS
19 | const schemaBuilder = require('@ideditor/schema-builder');
20 | schemaBuilder.buildDist({
21 | inDirectory: 'data',
22 | interimDirectory: 'interim',
23 | outDirectory: 'dist',
24 | sourceLocale: 'en',
25 | taginfoProjectInfo: {
26 | name: 'IntrepiD',
27 | description: 'iD editor, but adventurous.',
28 | project_url: 'https://example.com/IntrepiD',
29 | contact_name: 'J. Maintainer',
30 | contact_email: 'maintainer@example.com'
31 | }
32 | });
33 | ```
34 |
35 | The following options are optional:
36 |
37 | - `inDirectory`: `string`, The relative directory of the source data files. Defaults to `data`.
38 | - `interimDirectory`: `string`, The relative directory of files needed during development but not for distribution. Be aware that
39 | everything in this directory will be overwritten when building. Defaults to `interim`.
40 | - `outDirectory`: `string`, The relative directory of the built data files intended for distribution. Be aware that
41 | everything in this directory will be overwritten when building. Defaults to `dist`.
42 | - `sourceLocale`: `string`, The code of the language/locale used for the translatable strings in the data files. Defaults to `en`.
43 | - `taginfoProjectInfo`: `object`, Project metadata required by TagInfo ([Wiki](https://wiki.openstreetmap.org/wiki/Taginfo/Projects)). If this info is not provided, the `taginfo.json` file will not be built. See the [schema](https://github.com/taginfo/taginfo-projects/blob/master/taginfo-project-schema.json) for more details. The generated taginfo.json will use the following mnemonics to give context to the generated [description on taginfo](https://taginfo.openstreetmap.org/projects/id_editor#tags):
44 | - 🄿: [preset](https://github.com/openstreetmap/id-tagging-schema/tree/main/data/presets)
45 | - 🄵: [field](https://github.com/openstreetmap/id-tagging-schema/tree/main/data/fields)
46 | - 🄵🅅: field value
47 | - 🄳: [deprecated tag](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/deprecated.json)
48 | - 🄳🄳: [discarded tag](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/discarded.json)
49 | - `processPresets`: `function(presets)`, An opportunity to edit the built presets.
50 | - `processFields`: `function(fields)`, An opportunity to edit the built fields.
51 | - `processCategories`: `function(categories)`, An opportunity to edit the built preset categories.
52 | - `listReusedIcons`: `boolean` or `number`, If true, icons used by multiple searchable presets will be listed. If a number, icons used more than that number of times are listed. Defaults to `false`.
53 |
54 | You can also include options from `schemaBuilder.fetchTranslations()` in order to
55 | download translation files at the same time as compiling data.
56 |
57 | ### Building Development Data
58 |
59 | To validate your source data and compile files needed during development:
60 |
61 | ```JS
62 | const schemaBuilder = require('@ideditor/schema-builder');
63 | schemaBuilder.buildDev({
64 | inDirectory: 'data',
65 | interimDirectory: 'interim',
66 | sourceLocale: 'en'
67 | });
68 | ```
69 |
70 | The following options are identical to those for `schemaBuilder.buildDist()`:
71 | - `inDirectory`
72 | - `interimDirectory`
73 | - `sourceLocale`
74 | - `processPresets`
75 | - `processFields`
76 | - `processCategories`
77 | - `listReusedIcons`
78 |
79 | ### Validating Data
80 |
81 | To validate your source data without compiling anything:
82 |
83 | ```JS
84 | const schemaBuilder = require('@ideditor/schema-builder');
85 | schemaBuilder.validate({
86 | inDirectory: 'data'
87 | });
88 | ```
89 |
90 | The following options are identical to those for `schemaBuilder.buildDist()`:
91 | - `inDirectory`
92 | - `processPresets`
93 | - `processFields`
94 | - `processCategories`
95 | - `listReusedIcons`
96 |
97 | ### Fetching Translations
98 |
99 | To download locale files from Transfiex:
100 |
101 | ```JS
102 | const schemaBuilder = require('@ideditor/schema-builder');
103 | schemaBuilder.fetchTranslations({
104 | outDirectory: 'dist',
105 | sourceLocale: 'en',
106 | translOrgId: 'openstreetmap',
107 | translProjectId: 'intrepid',
108 | translResourceIds: ['presets'],
109 | translReviewedOnly: ['de', 'es']
110 | });
111 | ```
112 |
113 | The following options are required:
114 |
115 | - `translOrgId`: `string`, The ID of the Transfiex organization where the translation project is hosted.
116 | - `translProjectId`: `string`, The ID of the Transfiex project within the organization where the schema resource is translated.
117 |
118 | The following options are optional:
119 |
120 | - `translResourceIds`: `[string]`, The IDs of the resources to download. Defaults to `['presets']`.
121 | - `translCredentials`: `{ user: string, password: string }`, Your Transifex API credentials.
122 | Defaults to those stored as JSON in a `transifex.auth` file in your working directory.
123 | - `translReviewedOnly`: `boolean` or `[string]`, If `true`, only reviewed translations are included.
124 | If `false`, all translations are included. If an array of locale codes, only reviewed
125 | translations are included for those specified locale codes, while all translations are included
126 | for the remaining locales.
127 | - `outDirectory`: `string`, Same as the `outDirectory` option for `schemaBuilder.buildDist()`.
128 | - `sourceLocale`: `string`, Same as the `sourceLocale` option for `schemaBuilder.buildDist()`.
129 |
130 | ## Source Files
131 |
132 | Your `inDirectory` folder (`data` by default) should contain your source files with this structure:
133 |
134 | ```
135 | data/
136 | categories/
137 | category1.json
138 | category2.json
139 | ...
140 | fields/
141 | field1.json
142 | field2.json
143 | ...
144 | presets/
145 | preset1.json
146 | preset2.json
147 | ...
148 | defaults.json
149 | deprecated.json
150 | discarded.json
151 | ```
152 |
153 | The format for each file is defined in the [`schemas`](schemas) directory.
154 |
155 | ### Presets
156 |
157 | A [preset](https://wiki.openstreetmap.org/wiki/Preset) represents a specific type of
158 | [map feature](https://wiki.openstreetmap.org/wiki/Map_features). For example, presets
159 | can exist for parks, restaurants, drinking water fountains, buildings, railway tracks,
160 | and many more feature types.
161 |
162 | iD editor preset and field types are defined in [JSON](http://en.wikipedia.org/wiki/JSON)
163 | files located under the `data/presets` folder.
164 |
165 | #### Preset Files
166 |
167 | Presets are defined in JSON files located under `data/presets`. They're organized in a
168 | directory hierarchy based on OSM key/value pairs. For example, the preset that matches
169 | the tag `leisure=park` is in the file `data/presets/leisure/park.json`.
170 |
171 | #### Preset Schema
172 |
173 | A basic preset is of the form:
174 |
175 | ```javascript
176 | {
177 | // Display name for this feature type in the `sourceLocale` language.
178 | "name": "Produce Stand",
179 | // Aliases are synonyms of the preset's name - this is for alternative
180 | // names a preset might also be known as
181 | "aliases": [
182 | "Farm Shop",
183 | "Farm Stand"
184 | ]
185 | // Terms are additional search terms for the preset - these are added to
186 | // fuel the search functionality. searching for 'vegetables' will bring
187 | // up this 'farm shop' preset
188 | "terms": [
189 | "fresh food",
190 | "fruits",
191 | "greengrocer",
192 | "orchard",
193 | "organics",
194 | "vegetables"
195 | ],
196 | // Tags that are added to the feature when selecting the preset,
197 | // and also used to match the preset against existing features.
198 | // You can use the value "*" to match any value.
199 | "tags": {
200 | "shop": "farm"
201 | },
202 | // The geometry types for which this preset is valid.
203 | // options are point, area, line, and vertex.
204 | // vertices are points that are parts of lines, like the nodes in a road
205 | // lines are unclosed ways, and areas are closed ways
206 | "geometry": [
207 | "point", "area"
208 | ]
209 | // The icon in iD which represents this feature.
210 | "icon": "maki-shop",
211 | // The names of fields that will appear by default in the editor sidebar.
212 | // See the fields documentation for details of what's valid here.
213 | "fields": [
214 | "{shop}",
215 | "organic"
216 | ],
217 | // The names of fields that the user can add manually. These will also
218 | // appear if the corresponding tags are present.
219 | "moreFields": [
220 | "produce"
221 | ]
222 | }
223 | ```
224 | The complete JSON schema for presets can be found in [`schemas/preset.json`](schemas/preset.json)
225 |
226 |
227 | #### Preset Properties
228 |
229 | ##### `name`
230 |
231 | The primary name of the feature type.
232 |
233 | Upon merging into the `main` branch, this is sent to Transifex for translating to other localizations. Changing the name of an existing preset will require it to be re-translated to all localizations.
234 |
235 | A preset can optionally reference the label of another by using that preset's name contained in brackets, like `{presetId}` or `{folder/presetId}`. In which case the presets's _terms_ and _aliases_ are also automatically sourced from that other field. This is for example useful for regional presets which should get the same labels as the preset they are based on. The `presetId` is the same as the filename but ignoring the underscore convention for unsearchable presets. So for a preset at `folder/_name` the reference would be `{folder/name}`.
236 |
237 | This property is required. There is no default.
238 |
239 | ##### `aliases`
240 |
241 | A list of synonyms for the preset's `name`. These are alternative terms a preset might _also be known as_. For example, _Port_ could be added as an alias to the _Harbor_ preset. Terms which describe a specific sub-type of a preset should not be added as an alias (e.g. _Barber Shop_ should not be added as an alias to the _Hairdresser_ preset).
242 |
243 | ##### `terms`
244 |
245 | A list of additional search terms or keywords for the preset. These might be names which describe a subset of the preset's features, or simply related terms a user might enter when searching for the preset.
246 |
247 | ##### `geometry`
248 |
249 | An array of possible geometry types that a feature must have in order to match this preset.
250 |
251 | * `point`: an OSM node that is not a member of any way
252 | * `vertex`: an OSM node that is a member of one or more ways
253 | * `line`: an OSM way that is not an area
254 | * `area`: an OSM way that is closed/circular (the first and last nodes are the same) or a `type=multipolygon` relation
255 | * `relation`: an OSM relation
256 |
257 | Closed ways can be treated as both `line` or `area` geometry. If a preset allows both, iD will add an additional `area=yes` tag when choosing the preset for an area feature.
258 |
259 | The geometry types should be listed in order of preference. For example, the preset for `leisure=swimming_pool` lists `area` before `point`.
260 |
261 | This property is required. There is no default.
262 |
263 | ##### `tags`
264 |
265 | An object with the `"key": "value"` tags a feature must have to match this preset. A `"*"` wildcard value can be set to have this preset match any value for that key.
266 |
267 | A feature can only match one preset even if its tags and geometry could technically match more than one. iD will pick the best match based on `matchScore`, the number of tags, and the use of wildcard values.
268 |
269 | This property is required. There is no default.
270 |
271 | ##### `addTags`
272 |
273 | The tags that are added to the feature when selecting this preset. Defaults to `tags`. If needed, this property will typically be a superset of `tags`.
274 |
275 | iD's validator will recommend that users add missing tags from `addTags` to matching features. For example, the Bridge preset has these properties:
276 |
277 | ```
278 | "tags": {
279 | "man_made": "bridge"
280 | },
281 | "addTags": {
282 | "man_made": "bridge",
283 | "layer": "1"
284 | },
285 | ```
286 |
287 | When adding a feature with this preset, it will be given the tags `man_made=bridge` and `layer=1`. The user could then change `layer` to `3`, for instance, and the feature would still match the preset because it still has `man_made=bridge`. If the user removes the `layer` tag altogether, iD will recommend adding it back with a value of `1`.
288 |
289 | ##### `removeTags`
290 |
291 | The tags that are removed from the feature when deselecting this preset. Defaults to `addTags` or if this is also not defined, to `tags`.
292 |
293 | ##### `fields`/`moreFields`
294 |
295 | Both these properties are arrays of field paths (e.g. `description` or `generator/type`).
296 | `fields` are shown by default and `moreFields` are shown if manually added by the
297 | user or if a matching tag is present. Note that some fields have a [`prerequisiteTag`](#prerequisitetag)
298 | property that limits when they will be shown.
299 |
300 | A preset can reference the fields of another by using that preset's name contained in
301 | brackets, like `{preset}`. For example, `{shop}` in `presets/shop/books.json` references and extends the fields
302 | of `presets/shop.json`. When subfolders are used, the format is `{shop/books}` to reference the properties of the `shop/books.json`.
303 |
304 | ```javascript
305 | "fields": [
306 | "{shop}",
307 | "internet_access"
308 | ],
309 | "moreFields": [
310 | "{shop}",
311 | "internet_access/fee",
312 | "internet_access/ssid"
313 | ],
314 | "tags": {
315 | "shop": "books"
316 | }
317 | ```
318 |
319 | If `fields` or `moreFields` are not defined, the values of the preset's "parent"
320 | preset are used. For example, `shop/convenience` automatically uses the same
321 | fields as `shop`.
322 |
323 | In both explicit and implicit inheritance, fields for keys that define the
324 | preset via `tags` are generally not inherited, even when specified by the parent explicity.
325 | E.g. the `shop` field is not inherited by `shop/…` presets.
326 | This can be overwritten by adding the field explicitly like `"fields": [ "shop", "{shop}" ],`
327 |
328 | ##### `icon`
329 |
330 | An icon representing a preset, e.g. `"icon": "temaki-power_tower"` ([Example](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/presets/power/tower.json)). More information about available icon sets and usage of icons can be found on the [icons subpage](ICONS.md).
331 |
332 | ##### `imageURL`
333 |
334 | The URL of a remote image file. This does not fully replace `icon`—both may be shown in the UI.
335 |
336 | For example, `imageURL` is used to specify the logos of brand presets from the [name-suggestion-index](https://github.com/osmlab/name-suggestion-index).
337 |
338 | Bitmap images should be at least 100×100 px² to look good on high-resolution screens.
339 |
340 | ##### `searchable`
341 |
342 | Deprecated or generic presets can include the property `"searchable": false`.
343 | This means that they will be recognized by iD when editing existing data,
344 | but will not be available as an option when adding new features.
345 |
346 | By convention, unsearchable presets have filenames that begin with an underscore
347 | (e.g. `data/presets/landuse/_farm.json`). However, when using the preset name as reference,
348 | the underscore is omitted (e.g. `{landuse/farm}`).
349 |
350 | ##### `matchScore`
351 |
352 | A number that ranks this preset against others that match the feature.
353 |
354 | For example, a feature with `amenity=cafe` and `building=commercial` will match the Cafe preset instead of the Commercial Building preset because Commercial Building has a lower `matchScore`.
355 |
356 | The default is `1.0`.
357 |
358 | ##### `locationSet`
359 |
360 | An object with the identifiers of regions where this preset should or shouldn't be shown. By default, presets are available everywhere.
361 |
362 | See the [location-conflation](https://github.com/ideditor/location-conflation) package for details.
363 |
364 | ```js
365 | "locationSet": {
366 | "include": ["US"],
367 | "exclude": ["PR", "VI"]
368 | }
369 | ```
370 |
371 | ##### `replacement`
372 |
373 | The ID of a preset that is preferable to this one. iD's validator will flag features matching this preset and recommend that the user upgrade the tags.
374 |
375 | When possible, use [`deprecated.json`](#deprecations) instead to specify upgrade paths for old tags. This property is meant for special cases, such as upgrades with geometry requirements.
376 |
377 | ##### `reference`
378 |
379 | A key and optionally a value to link to the wiki documentation for this preset. Only necessary if the preset consists of several tags.
380 |
381 | For example,
382 | ```javascript
383 | "reference": {
384 | "key": "tower:type",
385 | "value": "communication"
386 | }
387 | ```
388 |
389 | ### Fields
390 |
391 | Fields are reusable form elements that can be associated with presets.
392 |
393 | #### Field Files
394 |
395 | Fields are defined in JSON files located under `data/fields`.
396 |
397 | The field files are typically named according to their associated OSM key.
398 | For example, the field for the tag `sport=*` is stored in the file
399 | `data/fields/sport.json`. When a field has multiple versions that
400 | depend on which preset is active, we add a suffix to the filename:
401 | (`sport.json`, `sport_ice.json`, `sport_racing_motor.json`).
402 |
403 | Some keys in OSM are namespaced using colons (':'). Namespaced fields
404 | are nested in folders according to their tag.
405 | For example, the field for the tag `piste:difficulty=*` is stored in the file
406 | `data/fields/piste/difficulty.json`.
407 |
408 |
409 | #### Field Schema
410 |
411 | ```js
412 | {
413 | "key": "cuisine",
414 | "type": "combo",
415 | "label": "Cuisine"
416 | }
417 | ```
418 | The complete JSON schema for fields can be found in [`schemas/field.json`](schemas/field.json)
419 |
420 | #### Field Properties
421 |
422 | ##### `label`
423 |
424 | A sort desciption or caption of the field.
425 |
426 | A field can optionally reference the label of another by using that field's name contained in brackets, like `{field}`. In which case the field's _terms_ are also automatically sourced from that other field. This is for example useful when there are multiple variants of fields for the same tag, which should all have the same labels.
427 |
428 | ##### `type`
429 |
430 | A string specifying the UI and behavior of the field. Must be one of the following values.
431 |
432 | ###### Text fields
433 |
434 | * `text` - Basic single line text field
435 | * `number` - Text field with up/down buttons for entering numbers (e.g. `width=*`)
436 | * `localized` - Text field with localization abilities (e.g. `name=*`, `name:es=*`, etc.)
437 | * `tel` - Text field for entering phone numbers (localized for editing location)
438 | * `email` - Text field for entering email addresses
439 | * `url` - Text field for entering URLs
440 | * `identifier` - Text field for foreign IDs (e.g. `gnis:feature_id`)
441 | * `colour` - Text field for entering colours
442 | * `schedule` - Field for entering a recurring schedule (e.g., `opening_hours=*`, `service_times=*`)
443 | * `textarea` - Multi-line text area (e.g. `description=*`)
444 | * `date` - Text field for entering dates in ISO 8601 format.
445 |
446 | ###### Combo/Dropdown fields
447 |
448 | * `combo` - Dropdown field for picking one option out of many (e.g. `surface=*`)
449 | * `typeCombo` - Dropdown field picking a specific type from a generic category key
450 | (e.g. `waterway=*`. If unset, tag will be `waterway=yes`, but dropdown contains options like `stream`, `ditch`, `river`)
451 | * `multiCombo` - Dropdown field for adding `yes` values to multiple keys with the same prefix (a common multikey)
452 | (e.g. `recycling:*` -> `recycling:glass=yes`, `recycling:paper=yes`, etc.)
453 | * `manyCombo` - Dropdown field for adding `yes` values to many different keys
454 | (e.g. `bus`, `tram`, `train` -> `bus=yes`, `tram=yes`, etc.)
455 | * `networkCombo` - Dropdown field that helps users pick a route `network` tag (localized for editing location)
456 | * `semiCombo` - Dropdown field for adding multiple values to a semicolon-delimited list
457 | (e.g. `sport=*` -> `soccer;lacrosse;athletics;field_hockey`)
458 | * `directionalCombo` - Block of dropdowns for adding directional (e.g. `*:left`/`*:right` or `*:forward`/`*:backward`) tags on a linear way. This field was named `cycleway` until [`v5.3.0`](CHANGELOG.md#540). This field type requires that both the `keys` and `key` properties are specified (`key` for the _common_ (e.g. `:both`) subtag of this field and `keys` for the _directional_ (e.g. `:left`/`:right`) subtags).
459 |
460 |
461 | ###### Checkboxes
462 |
463 | * `check` - 3-state checkbox: `yes`, `no`, unknown (no tag)
464 | * `defaultCheck` - 2-state checkbox where checked produces `yes` and unchecked produces no tag
465 | * `onewayCheck` - 3-state checkbox for `oneway` fields, with extra button for direction switching
466 |
467 | ###### Radio Buttons
468 |
469 | * `radio` - Multiple choice radio button field
470 | * `structureRadio` - Multiple choice structure radio button field, with extra input for bridge/tunnel level
471 |
472 | ###### Special
473 |
474 | * `access` - Block of dropdowns for defining the `access=*` tags on a highway
475 | * `address` - Block of text and dropdown fields for entering address information (localized for editing location)
476 | * `roadspeed` - Numeric text field for speed and dropdown for "mph" / "km/h", defaulting to the speed unit used for roads in the feature's region
477 | * `roadheight` - Numeric text field for height and dropdowns for "m" / "ft" and "in", defaulting to the height unit used for roads in the feature's region
478 | * `restrictions` - Graphical field for editing turn restrictions
479 | * `wikidata` - Search field for selecting a Wikidata entity
480 | * `wikipedia` - Block of fields for selecting a wiki language and Wikipedia page
481 |
482 | ##### `usage`
483 |
484 | A string specifying how iD uses the field. Must be one of the following values.
485 |
486 | * `preset` - The field is listed in one or more preset files (default and most common value)
487 | * `changeset` - The field is only used for changeset tags when uploading, e.g. `comment`
488 | * `group` - The field is only used within another field such as `structure`, e.g. `cutting`
489 | * `manual` - The field is only added by iD programmatically as needed, e.g. `restrictions`
490 |
491 | ##### `key`/`keys`
492 |
493 | The `key` property names the OSM tag key that the field will edit. Some fields, like the `address` field, operate on more than one tag: These expect an array of keys in the `keys` property. The following table lists which field types accept which properties:
494 |
495 | field type | `key` | `keys` | description | example
496 | ---------- | ----- | ------ | ----------- | -------
497 | `text`, `number`, `email`, `url`, `tel` | :heavy_check_mark: | optional | Optionally, these fields can match multiple tag `keys` of an OSM object: which is useful to support OSM tags which have more than one established tag key like `phone` and `contact:phone`.[^1] | `"key": "phone", "keys": ["phone", "contact:phone"]`
498 | `address` | :heavy_check_mark: | :heavy_check_mark: | `keys` must contains all possible subtags to be used in the address field and `key` must contain the tag key prefix (e.g. `addr`). | `"key": "addr", "keys": ["addr:city", "addr:street", …]`
499 | `wikipedia`, `wikidata` | :heavy_check_mark: | :heavy_check_mark: | As the values of these two fields should be updated in sync by the editor, the `keys` should always contain both the respective wikipedia and wikidata keys. | `"key": "flag:wikidata", "keys": ["flag:wikidata", "flag:wikipedia"]`
500 | `directionalCombo` | :heavy_check_mark: | :heavy_check_mark: | For directional fields, the `key` is the tag to use when the OSM feature has the same attributes in both directions, while the `keys` are the two tags for the individual directions. iD considers `key` with and without the `:both` suffix (for example, `cycleway` and `cycleway:both`). | `"key": "cycleway", "keys": ["cycleway:right", "cycleway:left"]`
501 | `access` | :x: | :heavy_check_mark: | `keys` lists all access tags to consider in the field. | `"keys": ["access", "foot", "bicycle", …]`
502 | `localized` | :heavy_check_mark: | :x: | `key` specified the main tag, which will also be used as the tag key prefix for localized versions of the tag (i.e. the `name` field will also display contents of the tags `name:*`). | `"key": "name"`
503 | `multiCombo` | :heavy_check_mark: | :x: | This field allows to toggle multiple `yes/no` subtags which share a common tag prefix specified in the field's `key`. | `"key": "recycling:"`
504 | `manyCombo` | :x: | :heavy_check_mark: | Similar to the `multiCombo` field, but here the `keys` property contains the full list of OSM tag keys which the options of the field should correspond to. | `"keys": ["hiking", "bicycle", …]`
505 | `structureRadio` | :x: | :heavy_check_mark: | Like the `radio` field, but operates on multiple tags: Selecting an option will remove the tag for the previously active option. | `"keys": ["bridge", "tunnel", …]`
506 | `restrictions` | :x: | :x: | A special field which does not operate on tags, therefore does not need `key` or `keys`. |
507 | all other fields | :heavy_check_mark: | :x: | A regular field which only operates on a single tag. | `"key": "oneway"`
508 |
509 | [^1]: The intended behaviour of a field with alternative `keys` is the following: If an OSM feature does not yet have a tag of the given `keys`, the supplied `key` will be used; if a feature has a single tag which matches a key from the `keys`, it should be used by the field; if a feature has multiple tags matching a key from the `keys` alternatives, the field should update them simultaneously and display a _multiple/conflicting values_ message if necessary.
510 |
511 | ##### `universal`
512 |
513 | If a field definition contains the property `"universal": true`, this field will
514 | appear in the "Add Field" list for all presets
515 |
516 | ##### `geometry`
517 |
518 | If specified, only show the field for this kind of geometry. Should contain
519 | one of `point`, `vertex`, `line`, `area`.
520 |
521 | ##### `default`
522 |
523 | The default value for the field. For example, the `building_area.json` field
524 | will automatically add the tag `building=yes` to certain presets that are
525 | associated with building features (but only if drawn as a closed area).
526 |
527 | ```js
528 | {
529 | "key": "building",
530 | "type": "combo",
531 | "default": "yes",
532 | "geometry": "area",
533 | "label": "Building"
534 | }
535 | ```
536 |
537 | ##### `placeholder`
538 |
539 | The text which should be shown in a field's input box when no value has been entered yet. This text is shown as a grayed-out text and can be used to give the user some examples of what to enter in the respective field.
540 |
541 | A field can optionally reference the placeholder text of another by using that field's name contained in brackets, like `{field}`. In which case the field's _terms_ are also automatically sourced from that other field. This is for example useful when there are multiple variants of fields for the same tag, which should all have the same labels.
542 |
543 | ##### `options`
544 |
545 | Combo field types can provide dropdown values in an `options` array.
546 | The user can pick from any of the options, or type their own value.
547 |
548 | ```js
549 | {
550 | "key": "diaper",
551 | "type": "combo",
552 | "label": "Diaper Changing Available",
553 | "options": ["yes", "no", "room", "1", "2", "3", "4", "5"]
554 | }
555 | ```
556 |
557 | ##### `strings`
558 |
559 | The `strings` object contains values that the field wants to be translated on Transifex.
560 |
561 | [Combo field types](#combodropdown-fields) can accept key-label pairs in the `options` value of the `strings` property.
562 | These values populate the `options` property if it isn't otherwise specified.
563 | If `autoSuggestions` is `true` (as per default), then raw and labeled values might be mixed
564 | in the dropdown suggestions.
565 |
566 | ```js
567 | {
568 | "key": "smoothness",
569 | "type": "combo",
570 | "label": "Smoothness",
571 | "placeholder": "Thin Rollers, Wheels, Off-Road...",
572 | "strings": {
573 | "options": {
574 | "excellent": "Thin Rollers: rollerblade, skateboard",
575 | "good": "Thin Wheels: racing bike",
576 | "intermediate": "Wheels: city bike, wheelchair, scooter",
577 | "bad": "Robust Wheels: trekking bike, car, rickshaw",
578 | "very_bad": "High Clearance: light duty off-road vehicle",
579 | "horrible": "Off-Road: heavy duty off-road vehicle",
580 | "very_horrible": "Specialized off-road: tractor, ATV",
581 | "impassable": "Impassable / No wheeled vehicle"
582 | }
583 | }
584 | }
585 | ```
586 |
587 | [Checkbox field tyes](#checkboxes) use the options keys to specify the values of the OSM tag corresponding
588 | to the different states of the checkbox input element, in the following order:
589 | 1. fields of type `check`: _unset state_ (must use the option `undefined`), _checked state_,
590 | _unchecked state_ ([example](https://github.com/openstreetmap/id-tagging-schema/blob/2375a6b/data/fields/parcel_pickup.json))
591 | 2. fields of type `defaultCheck`: _unchecked state_ (must use the option `undefined`), _checked state_ ([example](https://github.com/openstreetmap/id-tagging-schema/blob/2375a6b/data/fields/crossing_raised.json))
592 |
593 | ##### `stringsCrossReference`
594 |
595 | An optional property to reference to the strings of another field, indicated by using that field's name contained in brackets, like `{field}`. This is for example useful when there are multiple variants of fields for the same tag, which should all use the same strings.
596 |
597 | ##### `autoSuggestions`
598 |
599 | For combo fields, the most common tag values will be fetched from TagInfo and shown
600 | in the dropdown list if `autoSuggestions` is `true`. The default is `true`.
601 |
602 | ##### `customValues`
603 |
604 | For combo fields, the user can type a custom value in addition to choosing any shown
605 | in the dropdown list if `customValues` is `true`. The default is `true`.
606 |
607 | ##### `snake_case`
608 |
609 | For combo fields, spaces are replaced with underscores in the tag value if `snake_case` is `true`. The default is `true`.
610 |
611 | ##### `caseSensitive`
612 |
613 | For combo fields, case-sensitive field values are allowed if `caseSensitive` is `true`. The default is `false`.
614 |
615 | ##### `allowDuplicates`
616 |
617 | For semiCombo fields, duplicate values are allowed if `allowDuplicates` is `true`. The default is `false`.
618 |
619 | ##### `minValue`
620 |
621 | For number fields, the lowest valid value. There is no default.
622 |
623 | ##### `maxValue`
624 |
625 | For number fields, the greatest valid value. There is no default.
626 |
627 | ##### `increment`
628 |
629 | For number fields, the amount the stepper control increases or decreases the value. The default is `1`.
630 |
631 | ##### `prerequisiteTag`
632 |
633 | An object defining the tags the feature needs before this field will be displayed. It may have this property:
634 |
635 | - `key`: The key for the required tag.
636 |
637 | And may optionally be combined with one of these properties:
638 |
639 | - `value`: The value that the key must have.
640 | - `valueNot`: The value that the key must not have.
641 |
642 | Alternatively, the object may contain a single property:
643 |
644 | - `keyNot`: The key that must not be present.
645 |
646 | For example, this is how we show the Internet Access Fee field only if the feature has an `internet_access` tag not equal to `no`.
647 |
648 | ```js
649 | "prerequisiteTag": {
650 | "key": "internet_access",
651 | "valueNot": "no"
652 | }
653 | ```
654 |
655 | If a feature has a value for this field's `key` or `keys`, it will display regardless of the `prerequisiteTag` property.
656 |
657 | ##### `locationSet`
658 |
659 | An object with the identifiers of regions where this field should or shouldn't be shown. By default, fields are available everywhere.
660 |
661 | See the [location-conflation](https://github.com/ideditor/location-conflation) package for details.
662 |
663 | ```js
664 | "locationSet": {
665 | "include": ["US"],
666 | "exclude": ["PR", "VI"]
667 | }
668 | ```
669 |
670 | ##### `urlFormat`
671 |
672 | For `identifier` fields, the permalink URL of the external record. It must contain a `{value}` placeholder where the tag value will be inserted. For example:
673 |
674 | ```js
675 | "urlFormat": "https://geonames.usgs.gov/apex/f?p=gnispq:3:::NO::P3_FID:{value}"
676 | ```
677 |
678 | ##### `pattern`
679 |
680 | For `identifier` fields, the regular expression that valid values are expected to match to be linkable.
681 |
682 | ##### `icons`
683 |
684 | For combo fields, the `icons` object might contain the name of icons which represent the different values of the field. More information about available icon sets and usage of icons can be found on the [icons subpage](ICONS.md).
685 |
686 | Combo field types can accept key-label pairs in the `options` value of the `strings` property.
687 |
688 | ```js
689 | {
690 | "key": "crossing:markings",
691 | "type": "combo",
692 | "label": "Crossing Markings",
693 | "icons": {
694 | "zebra": "iD-crossing_markings-zebra",
695 | "lines": "iD-crossing_markings-lines",
696 | …
697 | }
698 | }
699 | ```
700 |
701 | ##### `iconsCrossReference`
702 |
703 | An optional property to reference to the icons of another field, indicated by using that field's name contained in brackets, like `{field}`. This is for example useful when there are multiple variants of fields for the same tag, which should all use the same icons.
704 |
705 | ### Deprecations
706 |
707 | Use `deprecated.json` ([Example](https://github.com/openstreetmap/id-tagging-schema/blob/main/data/deprecated.json), [Schema](https://github.com/ideditor/schema-builder/blob/main/schemas/deprecated.json)) to specify tag deprecations.
708 |
709 | Usage example: iD Editor will show an information panel that informs users about deprecated tags and an update-tag-action.
710 |
711 | **Example: Default Case**
712 |
713 | To update a specific tag to a specific new tag
714 |
715 | ```
716 | {
717 | "old": {"foo": "value"},
718 | "replace": {"bar": "value"}
719 | },
720 | ```
721 |
722 | **Example: Change the key, keep the value**
723 |
724 | ```
725 | {
726 | "old": {"foo": "*"},
727 | "replace": {"bar": "$1"}
728 | },
729 | ```
730 |
731 | **Example: Delete a tag**
732 |
733 | ```
734 | {
735 | "old": {"content": "unknown"}
736 | },
737 | ```
738 |
739 | ## Contributing
740 |
741 | iD's [code of conduct](https://github.com/openstreetmap/iD/blob/release/CODE_OF_CONDUCT.md) and
742 | [privacy policy](https://github.com/openstreetmap/iD/blob/release/PRIVACY.md) also apply to this project.
743 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import js from '@eslint/js';
3 | import globals from 'globals';
4 |
5 | export default [
6 | js.configs.recommended,
7 | {
8 | files: ['**/*.js', '**/*.mjs'],
9 | languageOptions: {
10 | ecmaVersion: 'latest',
11 | sourceType: 'module',
12 | globals: {
13 | ...globals.browser,
14 | ...globals.node,
15 | }
16 | },
17 | rules: {
18 | 'accessor-pairs': 'error',
19 | 'array-callback-return': 'warn',
20 | 'arrow-spacing': 'warn',
21 | 'block-scoped-var': 'error',
22 | 'class-methods-use-this': 'error',
23 | 'complexity': ['warn', 50],
24 | 'curly': ['warn', 'multi-line', 'consistent'],
25 | 'default-case-last': 'error',
26 | 'default-param-last': 'error',
27 | 'dot-notation': 'error',
28 | 'eqeqeq': ['error', 'smart'],
29 | 'grouped-accessor-pairs': 'error',
30 | 'indent': ['off', 4],
31 | 'keyword-spacing': 'error',
32 | 'linebreak-style': ['error', 'unix'],
33 | 'no-await-in-loop': 'error',
34 | 'no-caller': 'error',
35 | 'no-catch-shadow': 'error',
36 | 'no-console': 'warn',
37 | 'no-constructor-return': 'error',
38 | 'no-div-regex': 'error',
39 | 'no-duplicate-imports': 'warn',
40 | 'no-eq-null': 'error',
41 | 'no-eval': 'error',
42 | 'no-extend-native': 'error',
43 | 'no-extra-bind': 'error',
44 | 'no-extra-label': 'error',
45 | 'no-floating-decimal': 'error',
46 | 'no-global-assign': 'error',
47 | 'no-implicit-coercion': ['warn', { 'boolean': false, 'number': false }],
48 | 'no-implied-eval': 'error',
49 | 'no-invalid-this': 'off',
50 | 'no-iterator': 'error',
51 | 'no-labels': 'error',
52 | 'no-label-var': 'error',
53 | 'no-lone-blocks': 'error',
54 | 'no-loop-func': 'error',
55 | 'no-loss-of-precision': 'error',
56 | 'no-multi-str': 'error',
57 | 'no-new': 'error',
58 | 'no-new-func': 'error',
59 | 'no-new-wrappers': 'error',
60 | 'no-octal': 'error',
61 | 'no-octal-escape': 'error',
62 | 'no-process-env': 'error',
63 | 'no-promise-executor-return': 'error',
64 | 'no-proto': 'error',
65 | 'no-prototype-builtins': 'off',
66 | 'no-restricted-properties': 'error',
67 | 'no-return-assign': 'off',
68 | 'no-return-await': 'error',
69 | 'no-script-url': 'error',
70 | 'no-self-compare': 'error',
71 | 'no-sequences': 'error',
72 | 'no-shadow': 'off',
73 | 'no-shadow-restricted-names': 'error',
74 | 'no-template-curly-in-string': 'warn',
75 | 'no-throw-literal': 'error',
76 | 'no-trailing-spaces': 'warn',
77 | 'no-undef': 'error',
78 | 'no-undef-init': 'warn',
79 | 'no-unexpected-multiline': 'error',
80 | 'no-unneeded-ternary': 'error',
81 | 'no-unmodified-loop-condition': 'error',
82 | 'no-unreachable': 'warn',
83 | 'no-unreachable-loop': 'warn',
84 | 'no-unused-expressions': 'error',
85 | 'no-unused-vars': 'warn',
86 | 'no-use-before-define': ['off', 'nofunc'],
87 | 'no-useless-backreference': 'warn',
88 | 'no-useless-call': 'warn',
89 | 'no-useless-computed-key': 'warn',
90 | 'no-useless-concat': 'warn',
91 | 'no-useless-constructor': 'warn',
92 | 'no-useless-escape': 'off',
93 | 'no-useless-rename': 'warn',
94 | 'no-var': 'warn',
95 | 'no-void': 'error',
96 | 'no-warning-comments': 'warn',
97 | 'no-whitespace-before-property': 'warn',
98 | 'no-with': 'error',
99 | 'quotes': ['error', 'single'],
100 | 'radix': ['error', 'always'],
101 | 'require-atomic-updates': 'error',
102 | 'require-await': 'error',
103 | 'semi': ['error', 'always'],
104 | 'semi-spacing': 'error',
105 | 'space-unary-ops': 'error',
106 | 'wrap-regex': 'off'
107 | }
108 | }
109 | ];
110 |
111 |
112 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | collectCoverage: true,
3 | collectCoverageFrom: ['lib/*.js'],
4 | coverageDirectory: '/.coverage',
5 | verbose: true,
6 | transform: {},
7 | moduleNameMapper: {
8 | "#ansi-styles": "/node_modules/chalk/source/vendor/ansi-styles/index.js",
9 | "#supports-color": "/node_modules/chalk/source/vendor/supports-color/index.js"
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/lib/build.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import fs from 'fs';
3 | import { globSync } from 'glob';
4 | import jsonschema from 'jsonschema';
5 | import path from 'path';
6 | import shell from 'shelljs';
7 | import YAML from 'js-yaml';
8 | import marky from 'marky';
9 | import { createRequire } from 'module';
10 |
11 | import fetchTranslations from './translations.js';
12 |
13 | const require = createRequire(import.meta.url);
14 |
15 | const fieldSchema = require('../schemas/field.json');
16 | const presetSchema = require('../schemas/preset.json');
17 | const categorySchema = require('../schemas/preset_category.json');
18 | const defaultsSchema = require('../schemas/preset_defaults.json');
19 | const deprecatedSchema = require('../schemas/deprecated.json');
20 | const discardedSchema = require('../schemas/discarded.json');
21 |
22 | let _currBuild = null;
23 |
24 | function validateData(options) {
25 | const START = '🔬 ' + chalk.yellow('Validating schema...');
26 | const END = '👍 ' + chalk.green('schema okay');
27 |
28 | process.stdout.write('\n');
29 | process.stdout.write(START + '\n');
30 | marky.mark(END);
31 |
32 | processData(options, 'validate');
33 |
34 | marky.stop(END);
35 | process.stdout.write('\n');
36 | }
37 |
38 | function buildDev(options) {
39 |
40 | if (_currBuild) return _currBuild;
41 |
42 | const START = '🏗 ' + chalk.yellow('Validating and building for development...');
43 | const END = '👍 ' + chalk.green('built for development');
44 |
45 | process.stdout.write('\n');
46 | process.stdout.write(START + '\n');
47 | marky.mark(END);
48 |
49 | processData(options, 'build-interim');
50 |
51 | marky.stop(END);
52 | process.stdout.write('\n');
53 | }
54 |
55 | function buildDist(options) {
56 |
57 | if (_currBuild) return _currBuild;
58 |
59 | const START = '🏗 ' + chalk.yellow('Validating and building dist files...');
60 | const END = '👍 ' + chalk.green('dist files built');
61 |
62 | process.stdout.write('\n');
63 | process.stdout.write(START + '\n');
64 | marky.mark(END);
65 |
66 | return _currBuild = processData(options, 'build-dist')
67 | .then(() => {
68 | marky.stop(END);
69 | process.stdout.write('\n');
70 | _currBuild = null;
71 | })
72 | .catch((err) => {
73 | process.stderr.write(err);
74 | process.stdout.write('\n');
75 | _currBuild = null;
76 | process.exit(1);
77 | });
78 | }
79 |
80 | function processData(options, type) {
81 | if (!options) options = {};
82 | options = Object.assign({
83 | inDirectory: 'data',
84 | interimDirectory: 'interim',
85 | outDirectory: 'dist',
86 | sourceLocale: 'en',
87 | taginfoProjectInfo: {},
88 | processCategories: null,
89 | processFields: null,
90 | processPresets: null,
91 | listReusedIcons: false
92 | }, options);
93 |
94 | const dataDir = './' + options.inDirectory;
95 |
96 | // Translation strings
97 | let tstrings = {
98 | categories: {},
99 | fields: {},
100 | presets: {}
101 | };
102 |
103 | // all fields searchable under "add field"
104 | let searchableFieldIDs = {};
105 |
106 | const deprecated = read(dataDir + '/deprecated.json');
107 | if (deprecated) {
108 | validateSchema(dataDir + '/deprecated.json', deprecated, deprecatedSchema);
109 | }
110 |
111 | const discarded = read(dataDir + '/discarded.json');
112 | if (discarded) {
113 | validateSchema(dataDir + '/discarded.json', discarded, discardedSchema);
114 | }
115 |
116 | let categories = generateCategories(dataDir, tstrings);
117 | if (options.processCategories) options.processCategories(categories);
118 |
119 | let fields = generateFields(dataDir, tstrings, searchableFieldIDs);
120 | if (options.processFields) options.processFields(fields);
121 |
122 | let presets = generatePresets(dataDir, tstrings, searchableFieldIDs, options.listReusedIcons);
123 | if (options.processPresets) options.processPresets(presets);
124 |
125 | // Additional consistency checks
126 | validateCategoryPresets(categories, presets);
127 | validatePresetFields(presets, fields);
128 |
129 | const defaults = read(dataDir + '/preset_defaults.json');
130 | if (defaults) {
131 | validateSchema(dataDir + '/preset_defaults.json', defaults, defaultsSchema);
132 | validateDefaults(defaults, categories, presets);
133 | }
134 |
135 | if (type.indexOf('build') !== 0) return;
136 |
137 | const sourceLocale = options.sourceLocale;
138 |
139 | const interimDir = './' + options.interimDirectory;
140 | if (!fs.existsSync(interimDir)) fs.mkdirSync(interimDir);
141 | shell.rm('-f', [interimDir + '/*']); // clean directory
142 |
143 | let translations = generateTranslations(fields, presets, tstrings, searchableFieldIDs);
144 |
145 | let translationsForYaml = {};
146 | translationsForYaml[sourceLocale] = { presets: translations };
147 | fs.writeFileSync(interimDir + '/source_strings.yaml', translationsToYAML(translationsForYaml));
148 |
149 | let icons = generateIconsList(presets, fields, categories);
150 | fs.writeFileSync(interimDir + '/icons.json', JSON.stringify(icons, null, 4));
151 |
152 | if (type !== 'build-dist') return;
153 |
154 | const doFetchTranslations = options.translOrgId && options.translProjectId;
155 |
156 | const distDir = './' + options.outDirectory;
157 | if (!fs.existsSync(distDir)) fs.mkdirSync(distDir);
158 | // clean directory
159 | shell.rm('-f', [distDir + '/*.*']);
160 | if (doFetchTranslations) {
161 | shell.rm('-rf', [distDir + '/translations']);
162 | }
163 |
164 | fs.writeFileSync(distDir + '/preset_categories.json', JSON.stringify(categories, null, 4));
165 | fs.writeFileSync(distDir + '/fields.json', JSON.stringify(fields, null, 4));
166 | fs.writeFileSync(distDir + '/presets.json', JSON.stringify(presets, null, 4));
167 |
168 | let taginfo = generateTaginfo(presets, fields, deprecated, discarded, tstrings, options.taginfoProjectInfo);
169 | if (taginfo) fs.writeFileSync(distDir + '/taginfo.json', JSON.stringify(taginfo, null, 4));
170 |
171 | if (defaults) fs.writeFileSync(distDir + '/preset_defaults.json', JSON.stringify(defaults, null, 4));
172 | if (deprecated) fs.writeFileSync(distDir + '/deprecated.json', JSON.stringify(deprecated, null, 4));
173 | if (discarded) fs.writeFileSync(distDir + '/discarded.json', JSON.stringify(discarded, null, 4));
174 |
175 | let translationsForJson = {};
176 | translationsForJson[sourceLocale] = { presets: tstrings };
177 |
178 | if (!fs.existsSync(distDir + '/translations')) fs.mkdirSync(distDir + '/translations');
179 | fs.writeFileSync(distDir + '/translations/' + sourceLocale + '.json', JSON.stringify(translationsForJson, null, 4));
180 |
181 | const tasks = [
182 | // Minify files
183 | minifyJSON(distDir + '/preset_categories.json', distDir + '/preset_categories.min.json'),
184 | minifyJSON(distDir + '/fields.json', distDir + '/fields.min.json'),
185 | minifyJSON(distDir + '/presets.json', distDir + '/presets.min.json'),
186 | minifyJSON(distDir + '/taginfo.json', distDir + '/taginfo.min.json'),
187 | minifyJSON(distDir + '/preset_defaults.json', distDir + '/preset_defaults.min.json'),
188 | minifyJSON(distDir + '/deprecated.json', distDir + '/deprecated.min.json'),
189 | minifyJSON(distDir + '/discarded.json', distDir + '/discarded.min.json'),
190 | minifyJSON(distDir + '/translations/' + sourceLocale + '.json', distDir + '/translations/' + sourceLocale + '.min.json')
191 | ];
192 |
193 | if (doFetchTranslations) {
194 | tasks.push(fetchTranslations(options));
195 | }
196 | return Promise.all(tasks);
197 | }
198 |
199 |
200 | function read(f) {
201 | return fs.existsSync(f) && JSON.parse(fs.readFileSync(f, 'utf8'));
202 | }
203 |
204 |
205 | function validateSchema(file, instance, schema) {
206 | let validationErrors = jsonschema.validate(instance, schema).errors;
207 |
208 | if (validationErrors.length) {
209 | process.stderr.write(`${file}: \n`);
210 | validationErrors.forEach(error => {
211 | if (error.property) {
212 | process.stderr.write(error.property + ' ' + error.message);
213 | } else {
214 | process.stderr.write(error + '\n');
215 | }
216 | });
217 | process.stdout.write('\n');
218 | process.exit(1);
219 | }
220 | }
221 |
222 |
223 | function generateCategories(dataDir, tstrings) {
224 | let categories = {};
225 |
226 | globSync(dataDir + '/preset_categories/*.json', {
227 | posix: true,
228 | }).forEach(file => {
229 | let category = read(file);
230 | validateSchema(file, category, categorySchema);
231 |
232 | let id = 'category-' + path.basename(file, '.json');
233 | tstrings.categories[id] = { name: category.name };
234 | delete category.name;
235 |
236 | categories[id] = category;
237 | });
238 |
239 | return categories;
240 | }
241 |
242 |
243 | function generateFields(dataDir, tstrings, searchableFieldIDs) {
244 | let fields = {};
245 |
246 | globSync(dataDir + '/fields/**/*.json', {
247 | posix: true,
248 | }).forEach(file => {
249 | let field = read(file);
250 | let id = stripLeadingUnderscores(file.match(/fields\/([^.]*)\.json/)[1]);
251 |
252 | validateSchema(file, field, fieldSchema);
253 |
254 | let t = tstrings.fields[id] = {};
255 |
256 | const label = field.label;
257 |
258 | if (!label.startsWith('{')) {
259 | t.label = label;
260 | delete field.label;
261 | }
262 |
263 | tstrings.fields[id].terms = Array.from(new Set(
264 | (field.terms || [])
265 | .map(t => t.toLowerCase().trim())
266 | .filter(Boolean)
267 | )).join(',');
268 | delete field.terms;
269 |
270 | if (field.universal) {
271 | searchableFieldIDs[id] = true;
272 | }
273 |
274 | if (field.placeholder && !field.placeholder.startsWith('{')) {
275 | t.placeholder = field.placeholder;
276 | delete field.placeholder;
277 | }
278 |
279 | if (field.strings) {
280 | for (let key in field.strings) {
281 | t[key] = field.strings[key];
282 | }
283 | if (!field.options && field.strings.options) {
284 | field.options = Object.keys(field.strings.options);
285 | }
286 | delete field.strings;
287 | }
288 |
289 | fields[id] = field;
290 | });
291 |
292 | return fields;
293 | }
294 |
295 |
296 | function stripLeadingUnderscores(str) {
297 | return str.split('/')
298 | .map(s => s.replace(/^_/,''))
299 | .join('/');
300 | }
301 |
302 |
303 | function generatePresets(dataDir, tstrings, searchableFieldIDs, listReusedIcons) {
304 | let presets = {};
305 |
306 | let icons = {};
307 |
308 | globSync(dataDir + '/presets/**/*.json', {
309 | posix: true,
310 | }).forEach(file => {
311 | let preset = read(file);
312 | let id = stripLeadingUnderscores(file.match(/presets\/([^.]*)\.json/)[1]);
313 |
314 | if (presets[id] !== undefined) {
315 | process.stderr.write(`Preset with id "${id}" defined multiple times\n`);
316 | process.stdout.write('\n');
317 | process.exit(1);
318 | }
319 |
320 | validateSchema(file, preset, presetSchema);
321 |
322 | let names = new Set([]);
323 | tstrings.presets[id] = {};
324 |
325 | if (!preset.name.startsWith('{')) {
326 | tstrings.presets[id].name = preset.name;
327 | names.add(preset.name.toLowerCase());
328 | // don't include localized strings in the presets dist file since they're already in the locale file
329 | delete preset.name;
330 | }
331 |
332 | preset.aliases = Array.from(new Set(
333 | (preset.aliases || [])
334 | .map(t => t.trim())
335 | .filter(Boolean)
336 | .filter(t => !names.has(t.toLowerCase()))
337 | ));
338 | preset.aliases.forEach(a => names.add(a.toLowerCase()));
339 |
340 | preset.terms = Array.from(new Set(
341 | (preset.terms || [])
342 | .map(t => t.toLowerCase().trim())
343 | .filter(Boolean)
344 | .filter(t => !names.has(t))
345 | ));
346 |
347 | if (preset.aliases.length) tstrings.presets[id].aliases = preset.aliases.join('\n');
348 | if (preset.terms.length) tstrings.presets[id].terms = preset.terms.join(',');
349 |
350 | // don't include localized strings in the presets dist file since they're already in the locale file
351 | delete preset.aliases;
352 | delete preset.terms;
353 |
354 | if (preset.moreFields) {
355 | preset.moreFields.forEach(fieldID => { searchableFieldIDs[fieldID] = true; });
356 | }
357 |
358 | presets[id] = preset;
359 |
360 | if (preset.searchable !== false) {
361 | let icon = preset.icon || '(none)';
362 | if (!icons[icon]) icons[icon] = [];
363 | icons[icon].push(id);
364 | }
365 | });
366 |
367 | if (listReusedIcons) {
368 | const reuseLimit = typeof listReusedIcons === 'number' && listReusedIcons > 0 ? listReusedIcons : 1;
369 |
370 | let reusedIconPresetCount = 0;
371 | const reusedIcons = Object.keys(icons).filter(function(iconID) {
372 | const presetIDs = icons[iconID];
373 | if (presetIDs.length > reuseLimit) {
374 | reusedIconPresetCount += presetIDs.length;
375 | return true;
376 | }
377 | return false;
378 | });
379 |
380 | if (reusedIcons.length > 0) {
381 | process.stdout.write(reusedIcons.length + ' icon(s), including (none), are each used more than ' + reuseLimit + ' time(s), affecting ' + reusedIconPresetCount + ' presets\n');
382 |
383 | reusedIcons.sort(function(iconID1, iconID2) {
384 | return icons[iconID2].length - icons[iconID1].length;
385 |
386 | }).forEach(function(iconID) {
387 | const presetIDs = icons[iconID];
388 | process.stdout.write(iconID + ', ' + presetIDs.length + '\n');
389 | for (let i in presetIDs) {
390 | process.stdout.write('-' + presetIDs[i] + '\n');
391 | }
392 | process.stdout.write('\n');
393 | });
394 | } else {
395 | process.stdout.write(chalk.green('No icon is used more than ' + reuseLimit + ' time(s) across all searchable presets\n'));
396 | }
397 | }
398 |
399 | return presets;
400 | }
401 |
402 |
403 | function generateTranslations(fields, presets, tstrings, searchableFieldIDs) {
404 | let yamlStrings = JSON.parse(JSON.stringify(tstrings)); // deep clone
405 |
406 | for (let fieldId in yamlStrings.fields) {
407 | let yamlField = yamlStrings.fields[fieldId];
408 | let field = fields[fieldId];
409 | let options = yamlField.options || {};
410 | let optkeys = Object.keys(options);
411 |
412 | if (field.keys) {
413 | yamlField['#label'] = field.keys.map(k => `${k}=*`).join(', ');
414 | } else if (field.key) {
415 | yamlField['#label'] = `${field.key}=*`;
416 | }
417 | optkeys.forEach(k => {
418 | if (typeof options[k] === 'string'){
419 | options['#' + k] = field.key ? `${field.key}=${k}` : `field "${fieldId}" with value "${k}"`;
420 | } else {
421 | options[k]['#description'] = `description for ${field.key}=${k}`;
422 | options[k]['#title'] = `title for ${field.key}=${k}`;
423 | }
424 | });
425 |
426 | if (field.locationSet?.include) {
427 | yamlField['#label'] += ` | Local preset for countries ${field.locationSet.include.map(country => `"${country.toUpperCase()}"`).join(', ')}`;
428 | }
429 |
430 | if (yamlField.placeholder) {
431 | yamlField['#placeholder'] = `${fieldId} field placeholder`;
432 | }
433 |
434 | if (searchableFieldIDs[fieldId]) {
435 | if (yamlField.terms) {
436 | yamlField['#terms'] = 'terms: ' + yamlField.terms;
437 | } else {
438 | delete tstrings.fields[fieldId].terms;
439 | }
440 | if (yamlField.label) {
441 | yamlField.terms = `[translate with synonyms or related terms for '${yamlField.label}', separated by commas]`;
442 | } else {
443 | delete yamlField.terms;
444 | }
445 | } else {
446 | delete tstrings.fields[fieldId].terms;
447 | delete yamlField.terms;
448 | }
449 | }
450 |
451 | for (let presetId in yamlStrings.presets) {
452 | let yamlPreset = yamlStrings.presets[presetId];
453 | let preset = presets[presetId];
454 | let tags = preset.tags || {};
455 | let keys = Object.keys(tags);
456 |
457 | if (keys.length) {
458 | yamlPreset['#name'] = keys.map(k => `${k}=${tags[k]}`).join(' + ');
459 | if (yamlPreset.aliases) {
460 | yamlPreset['#name'] += ' | ' + yamlPreset.aliases.split('\n').join(', ');
461 | }
462 | yamlPreset['#name'] += ' | Translate the primary name. Optionally, add equivalent synonyms on newlines in order of preference (press the Return key).';
463 | }
464 |
465 | if (preset.locationSet?.include) {
466 | yamlPreset['#name'] += ` | Local preset for countries ${preset.locationSet.include.map(country => `"${country.toUpperCase()}"`).join(', ')}`;
467 | }
468 |
469 | if (preset.searchable !== false) {
470 | if (yamlPreset.terms) {
471 | yamlPreset['#terms'] = 'terms: ' + yamlPreset.terms;
472 | } else {
473 | delete yamlPreset.terms;
474 | }
475 | if (yamlPreset.name) {
476 | yamlPreset.terms = ``;
477 | }
478 | } else {
479 | delete tstrings.presets[presetId].terms;
480 | delete yamlPreset.terms;
481 | }
482 | delete yamlPreset.aliases;
483 | }
484 |
485 | return yamlStrings;
486 | }
487 |
488 |
489 | function generateTaginfo(presets, fields, deprecated, discarded, tstrings, projectInfo) {
490 |
491 | const packageInfo = JSON.parse(fs.readFileSync('./package.json'));
492 |
493 | if (!projectInfo.name) projectInfo.name = packageInfo.name;
494 | if (!projectInfo.description) projectInfo.description = packageInfo.description;
495 |
496 | projectInfo.description +=
497 | ' The following mnemonics are used in individual tag descriptions to annotate in which context the respective tag is supported:' +
498 | ' 🄿: preset, 🄵 field, 🄵🅅: field value, 🄳: deprecated tag, 🄳🄳: discarded tag';
499 | const requiredProps = ['name', 'description', 'project_url', 'contact_name', 'contact_email'];
500 | for (let i in requiredProps) {
501 | if (!(requiredProps[i] in projectInfo)) {
502 | process.stdout.write(chalk.yellow('Cannot compile taginfo.json: missing required project property `' + requiredProps[i] + '`') + '\n');
503 | return null;
504 | }
505 | }
506 |
507 | let taginfo = {
508 | data_format: 1,
509 | project: projectInfo,
510 | tags: []
511 | };
512 |
513 | Object.keys(presets).forEach(id => {
514 | let preset = presets[id];
515 | if (preset.suggestion) return;
516 | if (id.startsWith('@')) return;
517 |
518 | let keys = Object.keys(preset.tags);
519 | let last = keys[keys.length - 1];
520 | let tag = { key: last };
521 |
522 | if (!last) return;
523 |
524 | if (preset.tags[last] !== '*') {
525 | tag.value = preset.tags[last];
526 | }
527 |
528 | let name = tstrings.presets[id].name;
529 | if (!name && preset.name.startsWith('{')) {
530 | name = tstrings.presets[preset.name.slice(1, -1)].name;
531 | }
532 | let legacy = (preset.searchable === false) ? ' (unsearchable)' : '';
533 | tag.description = [ `🄿 ${name}${legacy}` ];
534 |
535 | if (preset.geometry) {
536 | setObjectType(tag, preset);
537 | }
538 |
539 | // add icon
540 | if (/^maki-/.test(preset.icon)) {
541 | tag.icon_url = 'https://cdn.jsdelivr.net/gh/mapbox/maki/icons/' +
542 | preset.icon.replace(/^maki-/, '') + '.svg';
543 | } else if (/^temaki-/.test(preset.icon)) {
544 | tag.icon_url = 'https://cdn.jsdelivr.net/gh/rapideditor/temaki/icons/' +
545 | preset.icon.replace(/^temaki-/, '') + '.svg';
546 | } else if (/^fa[srb]-/.test(preset.icon)) {
547 | tag.icon_url = 'https://cdn.jsdelivr.net/gh/openstreetmap/iD@develop/svg/fontawesome/' +
548 | preset.icon + '.svg';
549 | } else if (/^iD-/.test(preset.icon)) {
550 | tag.icon_url = 'https://cdn.jsdelivr.net/gh/openstreetmap/iD@develop/svg/iD-sprite/presets/' +
551 | preset.icon.replace(/^iD-/, '') + '.svg';
552 | }
553 |
554 | coalesceTags(taginfo, tag);
555 | });
556 |
557 | Object.keys(fields).forEach(id => {
558 | const field = fields[id];
559 | const keys = field.keys || (field.key && [field.key]) || [];
560 | const isRadio = (field.type === 'radio' || field.type === 'structureRadio');
561 |
562 | keys.forEach(key => {
563 | let tag = { key: key };
564 |
565 | let label = tstrings.fields[id].label;
566 | if (!label && field.label.startsWith('{')) {
567 | label = tstrings.fields[field.label.slice(1, -1)].label;
568 | }
569 | tag.description = [ `🄵 ${label}` ];
570 |
571 | coalesceTags(taginfo, tag);
572 |
573 | if (field.options && !isRadio && field.type !== 'manyCombo') {
574 | field.options.forEach(value => {
575 | if (value === 'undefined' || value === '*' || value === '') return;
576 | let tag;
577 | if (field.type === 'multiCombo') {
578 | tag = { key: key + value };
579 | } else {
580 | tag = { key: key, value: value };
581 | }
582 | let valueLabel = tstrings.fields[id].options?.[value];
583 | if (!valueLabel && field.stringsCrossReference) {
584 | valueLabel = tstrings.fields[field.stringsCrossReference.slice(1, -1)].options?.[value];
585 | }
586 | if (valueLabel && typeof valueLabel === 'string') {
587 | tag.description = [ `🄵🅅 ${label}: ${valueLabel}` ];
588 | } else {
589 | tag.description = [ `🄵🅅 ${label}: \`${value}\`` ];
590 | }
591 | coalesceTags(taginfo, tag);
592 | });
593 | }
594 | });
595 | });
596 |
597 | if (!deprecated) deprecated = [];
598 | deprecated.forEach(elem => {
599 | let old = elem.old;
600 | let oldKeys = Object.keys(old);
601 | if (oldKeys.length === 1) {
602 | let oldKey = oldKeys[0];
603 | let tag = { key: oldKey };
604 |
605 | let oldValue = old[oldKey];
606 | if (oldValue !== '*') tag.value = oldValue;
607 | let replacementStrings = [];
608 | for (let replaceKey in elem.replace) {
609 | let replaceValue = elem.replace[replaceKey];
610 | if (replaceValue === '$1') replaceValue = '*';
611 | replacementStrings.push(`${replaceKey}=${replaceValue}`);
612 | }
613 | let description = '🄳 (deprecated tag)';
614 | if (replacementStrings.length > 0) {
615 | description += ' ➜ ' + replacementStrings.join(' + ');
616 | }
617 | tag.description = [description];
618 | coalesceTags(taginfo, tag);
619 | }
620 | });
621 |
622 | if (!discarded) discarded = {};
623 | Object.keys(discarded).forEach(key => {
624 | let description = '🄳🄳 (discarded tag)';
625 | let tag = {
626 | key,
627 | description: [description]
628 | };
629 | coalesceTags(taginfo, tag);
630 | });
631 |
632 | taginfo.tags.forEach(elem => {
633 | if (elem.description) {
634 | elem.description = elem.description.join(', ');
635 | }
636 | });
637 |
638 |
639 | function coalesceTags(taginfo, tag) {
640 | if (!tag.key) return;
641 |
642 | let currentTaginfoEntries = taginfo.tags
643 | .filter(t => (t.key === tag.key && t.value === tag.value));
644 |
645 | if (currentTaginfoEntries.length === 0) {
646 | taginfo.tags.push(tag);
647 | return;
648 | }
649 |
650 | if (!tag.description) return;
651 |
652 | if (!currentTaginfoEntries[0].description) {
653 | currentTaginfoEntries[0].description = tag.description;
654 | return;
655 | }
656 |
657 | let isNewDescription = currentTaginfoEntries[0].description
658 | .indexOf(tag.description[0]) === -1;
659 |
660 | if (isNewDescription) {
661 | currentTaginfoEntries[0].description.push(tag.description[0]);
662 | }
663 | }
664 |
665 |
666 | function setObjectType(tag, input) {
667 | tag.object_types = [];
668 | const mapping = {
669 | 'point' : 'node',
670 | 'vertex' : 'node',
671 | 'line' : 'way',
672 | 'relation' : 'relation',
673 | 'area' : 'area'
674 | };
675 |
676 | input.geometry.forEach(geom => {
677 | if (tag.object_types.indexOf(mapping[geom]) === -1) {
678 | tag.object_types.push(mapping[geom]);
679 | }
680 | });
681 | }
682 |
683 | return taginfo;
684 | }
685 |
686 | function generateIconsList(presets, fields, categories) {
687 | const icons = {};
688 | [
689 | ...Object.values(presets).map(p => p.icon).filter(Boolean),
690 | ...Object.values(categories).map(c => c.icon).filter(Boolean),
691 | ...Object.values(fields).flatMap(f => Object.values(f.icons || {}))
692 | ].forEach(icon => icons[icon] = true);
693 | return Object.keys(icons).sort();
694 | }
695 |
696 |
697 | function validateCategoryPresets(categories, presets) {
698 | Object.keys(categories).forEach(id => {
699 | const category = categories[id];
700 | if (!category.members) return;
701 | category.members.forEach(preset => {
702 | if (presets[preset] === undefined) {
703 | process.stderr.write('Unknown preset: ' + preset + ' in category ' + category.name + '\n');
704 | process.stdout.write('\n');
705 | process.exit(1);
706 | }
707 | });
708 | });
709 | }
710 |
711 | function validatePresetFields(presets, fields) {
712 | const betweenBracketsRegex = /([^{]*?)(?=\})/;
713 | const maxFieldsBeforeError = 10;
714 |
715 | let usedFieldIDs = new Set();
716 |
717 | for (let presetID in presets) {
718 | let preset = presets[presetID];
719 |
720 | if (preset.replacement) {
721 | let replacementPreset = presets[preset.replacement];
722 | let p1geometry = preset.geometry.slice().sort.toString();
723 | let p2geometry = replacementPreset.geometry.slice().sort.toString();
724 | if (replacementPreset === undefined) {
725 | process.stderr.write('Unknown preset "' + preset.replacement + '" referenced as replacement of preset "' + presetID + '" (' + preset.name + ')\n');
726 | process.stdout.write('\n');
727 | process.exit(1);
728 | } else if (p1geometry !== p2geometry) {
729 | process.stderr.write('The preset "' + presetID + '" has different geometry than its replacement preset, "' + preset.replacement + '". They must match for tag upgrades to work.\n');
730 | process.stdout.write('\n');
731 | process.exit(1);
732 | }
733 | }
734 |
735 | // the keys for properties that contain arrays of field ids
736 | let fieldKeys = ['fields', 'moreFields'];
737 | for (let fieldsKeyIndex in fieldKeys) {
738 | let fieldsKey = fieldKeys[fieldsKeyIndex];
739 | if (!preset[fieldsKey]) continue; // no fields are referenced, okay
740 |
741 | for (let fieldIndex in preset[fieldsKey]) {
742 | let fieldID = preset[fieldsKey][fieldIndex];
743 | usedFieldIDs.add(fieldID);
744 | let field = fields[fieldID];
745 | if (field) {
746 | if (field.geometry) {
747 | let sharedGeometry = field.geometry.filter(value => preset.geometry.includes(value));
748 | if (!sharedGeometry.length) {
749 | process.stderr.write('The preset "' + presetID + '" (' + preset.name + ') will never display the field "' + fieldID + '" since they don\'t share geometry types.\n');
750 | process.stdout.write('\n');
751 | process.exit(1);
752 | }
753 | }
754 |
755 | } else {
756 | // no field found with this ID...
757 |
758 | let regexResult = betweenBracketsRegex.exec(fieldID);
759 | if (regexResult) {
760 | let foreignPresetID = regexResult[0];
761 | if (presets[foreignPresetID] === undefined) {
762 | process.stderr.write('Unknown preset "' + foreignPresetID + '" referenced in "' + fieldsKey + '" array of preset "' + presetID + '" (' + preset.name + ')\n');
763 | process.stdout.write('\n');
764 | process.exit(1);
765 | }
766 | } else {
767 | process.stderr.write('Unknown preset field "' + fieldID + '" in "' + fieldsKey + '" array of preset "' + presetID + '" (' + preset.name + ')\n');
768 | process.stdout.write('\n');
769 | process.exit(1);
770 | }
771 | }
772 |
773 |
774 | }
775 | }
776 |
777 | if (preset.fields) {
778 | // since `moreFields` is available, check that `fields` doesn't get too cluttered
779 | let fieldCount = preset.fields.length;
780 |
781 | if (fieldCount > maxFieldsBeforeError) {
782 | // Fields with `prerequisiteTag` or `geometry` may not always be shown,
783 | // so don't count them against the limits.
784 | const alwaysShownFields = preset.fields.filter(fieldID => {
785 | if (fields[fieldID]?.prerequisiteTag || fields[fieldID]?.geometry) return false;
786 | return true;
787 | });
788 | fieldCount = alwaysShownFields.length;
789 | }
790 | if (fieldCount > maxFieldsBeforeError) {
791 | process.stderr.write(fieldCount + ' values in "fields" of "' + preset.name + '" (' + presetID + '). Limit: ' + maxFieldsBeforeError + '. Please move lower-priority fields to "moreFields".\n');
792 | process.stdout.write('\n');
793 | process.exit(1);
794 | }
795 | }
796 | }
797 |
798 | for (let fieldID in fields) {
799 | if (!usedFieldIDs.has(fieldID) &&
800 | fields[fieldID].universal !== true &&
801 | (fields[fieldID].usage || 'preset') === 'preset') {
802 | process.stdout.write('Field "' + fields[fieldID].label + '" (' + fieldID + ') isn\'t used by any presets.\n');
803 | }
804 | }
805 |
806 | }
807 |
808 | function validateDefaults(defaults, categories, presets) {
809 | Object.keys(defaults).forEach(name => {
810 | const members = defaults[name];
811 | members.forEach(id => {
812 | if (!presets[id] && !categories[id]) {
813 | process.stderr.write(`Unknown category or preset: ${id} in default ${name}\n`);
814 | process.stdout.write('\n');
815 | process.exit(1);
816 | }
817 | });
818 | });
819 | }
820 |
821 | function translationsToYAML(translations) {
822 | // comment keys start with '#' and should sort immediately before their related key.
823 | function commentFirst(a, b) {
824 | if (a === '#' + b) return -1;
825 | if (b === '#' + a) return 1;
826 | if (a[0] !== b[0]) {
827 | if (a[0] === '#') a = a.substr(1);
828 | if (b[0] === '#') b = b.substr(1);
829 | }
830 | return (a > b ? 1 : a < b ? -1 : 0);
831 | }
832 |
833 | return YAML.dump(translations, { sortKeys: commentFirst, lineWidth: -1 })
834 | .replace(/'?#.*?'?:/g, '#');
835 | }
836 |
837 |
838 | function minifyJSON(inPath, outPath) {
839 | return new Promise((resolve, reject) => {
840 | if (!fs.existsSync(inPath)) {
841 | resolve();
842 | return;
843 | }
844 | fs.readFile(inPath, 'utf8', (err, data) => {
845 | if (err) return reject(err);
846 |
847 | const minified = JSON.stringify(JSON.parse(data));
848 | fs.writeFile(outPath, minified, (err) => {
849 | if (err) return reject(err);
850 | resolve();
851 | });
852 |
853 | });
854 | });
855 | }
856 |
857 | export {
858 | buildDev,
859 | buildDist,
860 | validateData as validate
861 | };
862 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import { buildDev, buildDist, validate } from './build.js';
2 | import fetchTranslations from './translations.js';
3 |
4 | export default {
5 | buildDev, buildDist, validate,
6 | fetchTranslations
7 | };
8 |
--------------------------------------------------------------------------------
/lib/translations.js:
--------------------------------------------------------------------------------
1 | /* Downloads the latest translations from Transifex */
2 | import fs from 'fs';
3 | import fetch from 'node-fetch';
4 | import YAML from 'js-yaml';
5 | import { transifexApi } from '@transifex/api';
6 |
7 |
8 | function fetchTranslations(options) {
9 |
10 | // Transifex doesn't allow anonymous downloading
11 | /* eslint-disable no-process-env */
12 | if (process.env.transifex_password) {
13 | // Deployment scripts may prefer environment variables
14 | transifexApi.setup({ auth: process.env.transifex_password });
15 | } else {
16 | // Credentials can be stored in transifex.auth as a json object. This file is gitignored.
17 | // You must use an API token for authentication: You can generate one at https://www.transifex.com/user/settings/api/.
18 | // {
19 | // "password": ""
20 | // }
21 | transifexApi.setup({ auth: JSON.parse(fs.readFileSync('./transifex.auth', 'utf8')).password });
22 | }
23 | /* eslint-enable no-process-env */
24 |
25 | if (!options) options = {};
26 | options = Object.assign({
27 | translOrgId: '',
28 | translProjectId: '',
29 | translResourceIds: ['presets'],
30 | translReviewedOnly: false,
31 | outDirectory: 'dist',
32 | sourceLocale: 'en'
33 | }, options);
34 |
35 | const outDir = `./${options.outDirectory}/translations`;
36 |
37 | if (!fs.existsSync(outDir)) {
38 | fs.mkdirSync(outDir);
39 | }
40 |
41 | const translResourceIds = options.translResourceIds;
42 | return new Promise(function(resolve) {
43 | asyncMap(translResourceIds, getResourceInfo, function(err, results) {
44 | gotResourceInfo(err, results);
45 | asyncMap(translResourceIds, getResource, function(err, results) {
46 | gotResource(err, results);
47 | resolve();
48 | });
49 | });
50 | });
51 |
52 |
53 | async function getResourceInfo(resourceId, callback) {
54 | try {
55 | const result = [];
56 | for await (const stat of transifexApi.ResourceLanguageStats.filter({
57 | project: `o:${options.translOrgId}:p:${options.translProjectId}`,
58 | resource: `o:${options.translOrgId}:p:${options.translProjectId}:r:${resourceId}`
59 | }).all()) {
60 | result.push(stat);
61 | }
62 | process.stdout.write(`got resource language stats collection for ${resourceId}\n`);
63 | callback(null, result);
64 | } catch (err) {
65 | process.stderr.write(`error while getting resource language stats collection for ${resourceId}\n`, err);
66 | callback(err);
67 | }
68 | }
69 |
70 | function gotResourceInfo(err, results) {
71 | if (err) return process.stderr.write(err + '\n');
72 |
73 | let coverageByLocaleCode = {};
74 | results.forEach(function(info) {
75 | info.forEach(stat => {
76 | let code = stat.relationships.language.data.id.substr(2).replace(/_/g, '-');
77 | let type = 'translated_strings';
78 | if (options.translReviewedOnly && (
79 | !Array.isArray(options.translReviewedOnly)
80 | || options.translReviewedOnly.indexOf(code) !== -1)) {
81 | type = 'reviewed_strings';
82 | }
83 | let coveragePart = (stat.attributes[type] / stat.attributes.total_strings) / results.length;
84 |
85 | if (coverageByLocaleCode[code] === undefined) coverageByLocaleCode[code] = 0;
86 | coverageByLocaleCode[code] += coveragePart;
87 | });
88 | });
89 | let dataLocales = {};
90 | // explicitly list the source locale as having 100% coverage
91 | dataLocales[options.sourceLocale] = { pct: 1 };
92 |
93 | for (let code in coverageByLocaleCode) {
94 | let coverage = coverageByLocaleCode[code];
95 | // we don't need high precision here, but we need to know if it's exactly 100% or not
96 | coverage = Math.floor(coverage * 100) / 100;
97 | dataLocales[code] = {
98 | pct: coverage
99 | };
100 | }
101 |
102 | const keys = Object.keys(dataLocales).sort();
103 | let sortedLocales = {};
104 | keys.forEach(k => sortedLocales[k] = dataLocales[k]);
105 | fs.writeFileSync(`${outDir}/index.json`, JSON.stringify(sortedLocales));
106 | fs.writeFileSync(`${outDir}/index.min.json`, JSON.stringify(sortedLocales, null, 4));
107 | }
108 |
109 | function getResource(resourceId, callback) {
110 | getLanguages((err, codes) => {
111 | if (err) return callback(err);
112 |
113 | asyncMap(codes, getLanguage(resourceId), (err, results) => {
114 | if (err) return callback(err);
115 |
116 | let locale = {};
117 | results.forEach((result, i) => {
118 | let presets = (result.presets && result.presets.presets) || {};
119 | for (const key of Object.keys(presets)) {
120 | let preset = presets[key];
121 |
122 | if (preset.name) {
123 | let names = preset.name.split('\n').map(s => s.trim()).filter(Boolean);
124 | preset.name = names[0];
125 | if (names.length > 1) {
126 | preset.aliases = names.slice(1).join('\n');
127 | }
128 | }
129 |
130 | if (!preset.terms) continue;
131 |
132 | // remove duplicates
133 | preset.terms = Array.from(new Set(
134 | // remove translation message if it was included somehow
135 | preset.terms.replace(/<.*>/, '')
136 | // convert to an array
137 | .split(',')
138 | // make everything lowercase and remove whitespace
139 | .map(s => s.toLowerCase().trim())
140 | // remove empty strings
141 | .filter(Boolean)
142 | ))
143 | // convert back to a concatenated string
144 | .join(',');
145 |
146 | if (!preset.terms) {
147 | // no need to include empty terms
148 | delete preset.terms;
149 | if (!Object.keys(preset).length) {
150 | delete presets[key];
151 | }
152 | }
153 | }
154 |
155 | let fields = (result.presets && result.presets.fields) || {};
156 | for (const key of Object.keys(fields)) {
157 | let field = fields[key];
158 | if (!field.terms) continue;
159 |
160 | // remove duplicates
161 | field.terms = Array.from(new Set(
162 | // remove translation message if it was included somehow
163 | field.terms.replace(/\[.*\]/, '')
164 | // convert to an array
165 | .split(',')
166 | // make everything lowercase and remove whitespace
167 | .map(s => s.toLowerCase().trim())
168 | // remove empty strings
169 | .filter(Boolean)
170 | ))
171 | // convert back to a concatenated string
172 | .join(',');
173 |
174 | if (!field.terms) {
175 | delete field.terms;
176 | if (!Object.keys(field).length) {
177 | delete fields[key];
178 | }
179 | }
180 | }
181 |
182 | locale[codes[i]] = result;
183 | });
184 |
185 | callback(null, locale);
186 | });
187 | });
188 | }
189 |
190 |
191 | function gotResource(err, results) {
192 | if (err) return process.stderr.write(err + '\n');
193 |
194 | // merge in strings fetched from transifex
195 | let allStrings = {};
196 | results.forEach(resourceStrings => {
197 | Object.keys(resourceStrings).forEach(code => {
198 | if (!allStrings[code]) allStrings[code] = {};
199 | let source = resourceStrings[code];
200 | let target = allStrings[code];
201 | Object.keys(source).forEach(k => target[k] = source[k]);
202 | });
203 | });
204 |
205 | for (let code in allStrings) {
206 | let obj = {};
207 | obj[code] = allStrings[code] || {};
208 | fs.writeFileSync(`${outDir}/${code}.json`, JSON.stringify(obj, null, 4));
209 | fs.writeFileSync(`${outDir}/${code}.min.json`, JSON.stringify(obj));
210 | }
211 | }
212 |
213 | function getLanguage(resourceId) {
214 | return async (code, callback) => {
215 | try {
216 | code = code.replace(/-/g, '_');
217 | let reviewedOnly = options.translReviewedOnly && (
218 | !Array.isArray(options.translReviewedOnly)
219 | || options.translReviewedOnly.indexOf(code) !== -1);
220 | const url = await transifexApi.ResourceTranslationsAsyncDownload.download({
221 | resource: {data:{type:'resources', id:`o:${options.translOrgId}:p:${options.translProjectId}:r:${resourceId}`}},
222 | language: {data:{type:'languages', id:`l:${code}`}},
223 | // fetch only reviewed strings for some languages
224 | mode: reviewedOnly ? 'reviewed' : 'default'
225 | });
226 | const data = await fetch(url).then(d => d.text());
227 | process.stdout.write(`got translations for ${resourceId}, language ${code}\n`);
228 | callback(null, YAML.load(data)[code]);
229 | } catch (err) {
230 | process.stderr.write(`error while getting translations for ${resourceId}, language ${code}\n`);
231 | callback(err);
232 | }
233 | };
234 | }
235 |
236 |
237 | async function getLanguages(callback) {
238 | try {
239 | const result = [];
240 | const project = await transifexApi.Project.get({
241 | organization: `o:${options.translOrgId}`,
242 | slug: options.translProjectId
243 | });
244 | const lngs = await project.fetch('languages');
245 | for await (const lng of lngs.all()) {
246 | if (lng.attributes.code === 'en') continue;
247 | result.push(lng.attributes.code.replace(/_/g, '-'));
248 | }
249 | process.stdout.write('got project languages\n');
250 | callback(null, result);
251 | } catch (err) {
252 | process.stderr.write('error while getting project languages\n');
253 | callback(err);
254 | }
255 | }
256 | }
257 |
258 |
259 | function asyncMap(inputs, func, callback) {
260 | let index = 0;
261 | let remaining = inputs.length;
262 | let results = [];
263 | let error;
264 |
265 | next();
266 |
267 | function next() {
268 | callFunc(index++);
269 | if (index < inputs.length) {
270 | setTimeout(next, 50);
271 | }
272 | }
273 |
274 | function callFunc(i) {
275 | let d = inputs[i];
276 | func(d, (err, data) => {
277 | if (err) error = err;
278 | results[i] = data;
279 | remaining--;
280 | if (!remaining && callback) callback(error, results);
281 | });
282 | }
283 | }
284 |
285 | export default fetchTranslations;
286 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "name": "@ideditor/schema-builder",
4 | "version": "7.0.0-dev",
5 | "description": "Framework for defining iD-compatible tagging models",
6 | "homepage": "https://github.com/ideditor/schema-builder#readme",
7 | "bugs": "https://github.com/ideditor/schema-builder/issues",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/ideditor/schema-builder.git"
11 | },
12 | "license": "ISC",
13 | "exports": "./lib/index.js",
14 | "dependencies": {
15 | "@transifex/api": "^7.1.0",
16 | "chalk": "^5.0.1",
17 | "glob": "^11.0.2",
18 | "js-yaml": "^4.0.0",
19 | "jsonschema": "^1.1.0",
20 | "marky": "^1.2.4",
21 | "node-fetch": "^3.2.10",
22 | "shelljs": "^0.9.2"
23 | },
24 | "devDependencies": {
25 | "eslint": "^9.0.0",
26 | "jest": "^29.1.1"
27 | },
28 | "engines": {
29 | "node": ">=20"
30 | },
31 | "scripts": {
32 | "lint": "eslint lib",
33 | "lint:fix": "eslint lib --fix",
34 | "test": "NODE_OPTIONS=--experimental-vm-modules jest schema-builder.test.js"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/schemas/deprecated.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/deprecated.json",
4 | "title": "Deprecated tags",
5 | "description": "An ordered list of old tags mapped to new tags.",
6 | "type": "array",
7 | "items": {
8 | "type": "object",
9 | "properties": {
10 | "old": {
11 | "type": "object",
12 | "additionalProperties": {
13 | "type": "string"
14 | }
15 | },
16 | "replace": {
17 | "type": "object",
18 | "additionalProperties": {
19 | "type": "string"
20 | }
21 | }
22 | },
23 | "additionalProperties": false,
24 | "required": ["old"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/schemas/discarded.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/discarded.json",
4 | "title": "Discarded tags",
5 | "description": "Keys of tags to be deleted when editing features.",
6 | "type": "object",
7 | "additionalProperties": {
8 | "type": "boolean"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/schemas/field.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/field.json",
4 | "title": "Field",
5 | "description": "A reusable form element for presets",
6 | "type": "object",
7 | "properties": {
8 | "key": {
9 | "description": "Tag key whose value is to be displayed",
10 | "type": "string"
11 | },
12 | "keys": {
13 | "description": "Tag keys whose value is to be displayed",
14 | "type": "array",
15 | "items": {
16 | "type": "string"
17 | }
18 | },
19 | "reference": {
20 | "description": "Taginfo documentation parameters (to be used when a field manages multiple tags)",
21 | "anyOf": [
22 | {
23 | "type": "object",
24 | "properties": {
25 | "key": {
26 | "description": "For documentation of a key",
27 | "type": "string"
28 | },
29 | "value": {
30 | "description": "For documentation of a tag (key and value)",
31 | "type": "string"
32 | }
33 | },
34 | "additionalProperties": false,
35 | "required": ["key"]
36 | },
37 | {
38 | "type": "object",
39 | "properties": {
40 | "rtype": {
41 | "description": "For documentation of a relation type",
42 | "type": "string"
43 | }
44 | },
45 | "additionalProperties": false
46 | }
47 | ]
48 | },
49 | "type": {
50 | "description": "Type of field",
51 | "type": "string",
52 | "enum": [
53 | "access",
54 | "address",
55 | "check",
56 | "colour",
57 | "combo",
58 | "date",
59 | "defaultCheck",
60 | "directionalCombo",
61 | "email",
62 | "identifier",
63 | "lanes",
64 | "localized",
65 | "manyCombo",
66 | "multiCombo",
67 | "networkCombo",
68 | "number",
69 | "onewayCheck",
70 | "radio",
71 | "restrictions",
72 | "roadheight",
73 | "roadspeed",
74 | "schedule",
75 | "semiCombo",
76 | "structureRadio",
77 | "tel",
78 | "text",
79 | "textarea",
80 | "typeCombo",
81 | "url",
82 | "wikidata",
83 | "wikipedia"
84 | ]
85 | },
86 | "label": {
87 | "description": "English label for the field caption. A field can reference the label of another by using that field's identifier contained in brackets (e.g. {field}), in which case also the field's terms will be referenced from that field.",
88 | "type": "string"
89 | },
90 | "geometry": {
91 | "description": "If specified, only show the field for these kinds of geometry",
92 | "type": "array",
93 | "minItems": 1,
94 | "uniqueItems": true,
95 | "items": {
96 | "type": "string",
97 | "enum": ["point", "vertex", "line", "area", "relation"]
98 | }
99 | },
100 | "default": {
101 | "description": "The default value for this field",
102 | "type": "string"
103 | },
104 | "options": {
105 | "description": "List of untranslatable string suggestions (combo fields)",
106 | "type": "array",
107 | "items": {
108 | "type": "string"
109 | }
110 | },
111 | "autoSuggestions": {
112 | "description": "If true, the top values from TagInfo will be suggested in the dropdown (combo fields only)",
113 | "type": "boolean",
114 | "default": true
115 | },
116 | "customValues": {
117 | "description": "If true, the user can type their own value in addition to any listed in `options` or `strings.options` (combo fields only)",
118 | "type": "boolean",
119 | "default": true
120 | },
121 | "universal": {
122 | "description": "If true, this field will appear in the Add Field list for all presets",
123 | "type": "boolean",
124 | "default": false
125 | },
126 | "placeholder": {
127 | "description": "Placeholder text for this field. A field can reference the placeholder text of another by using that field's identifier contained in brackets, like {field}.",
128 | "type": "string"
129 | },
130 | "strings": {
131 | "description": "Strings sent to transifex for translation",
132 | "type": "object",
133 | "properties": {
134 | "options": {
135 | "description": "Translatable options (combo fields).",
136 | "type": "object"
137 | }
138 | },
139 | "additionalProperties": {
140 | "description": "Specialized fields can request translation of arbitrary strings",
141 | "type": "object"
142 | }
143 | },
144 | "stringsCrossReference": {
145 | "description": "A field can reference strings of another by using that field's identifier contained in brackets, like {field}.",
146 | "type": "string"
147 | },
148 | "snake_case": {
149 | "description": "If true, replace spaces with underscores in the tag value (combo fields only)",
150 | "type": "boolean",
151 | "default": true
152 | },
153 | "caseSensitive": {
154 | "description": "If true, allow case sensitive field values (combo fields only)",
155 | "type": "boolean",
156 | "default": false
157 | },
158 | "allowDuplicates": {
159 | "description": "If true, duplicate values are allowed (semiCombo fields only)",
160 | "type": "boolean",
161 | "default": false
162 | },
163 | "minValue": {
164 | "description": "Minimum field value (number fields only)",
165 | "type": "integer"
166 | },
167 | "maxValue": {
168 | "description": "Maximum field value (number fields only)",
169 | "type": "integer"
170 | },
171 | "increment": {
172 | "description": "The amount the stepper control should add or subtract (number fields only)",
173 | "minimum": 1,
174 | "type": "integer"
175 | },
176 | "prerequisiteTag": {
177 | "description": "Tagging constraint for showing this field in the editor",
178 | "oneOf": [
179 | {
180 | "$id": "requires-key-any-value",
181 | "type": "object",
182 | "properties": {
183 | "key": {
184 | "description": "The key of the required tag",
185 | "type": "string"
186 | }
187 | },
188 | "additionalProperties": false,
189 | "required": ["key"]
190 | },
191 | {
192 | "$id": "requires-key-equals-value",
193 | "type": "object",
194 | "properties": {
195 | "key": {
196 | "description": "The key of the required tag",
197 | "type": "string"
198 | },
199 | "value": {
200 | "description": "The value that the tag must have. (alternative to 'valueNot')",
201 | "type": "string"
202 | }
203 | },
204 | "additionalProperties": false,
205 | "required": ["key", "value"]
206 | },
207 | {
208 | "$id": "requires-key-not-value",
209 | "type": "object",
210 | "properties": {
211 | "key": {
212 | "description": "The key of the required tag",
213 | "type": "string"
214 | },
215 | "valueNot": {
216 | "description": "The value that the tag cannot have. (alternative to 'value')",
217 | "type": "string"
218 | }
219 | },
220 | "additionalProperties": false,
221 | "required": ["key", "valueNot"]
222 | },
223 | {
224 | "$id": "requires-not-key",
225 | "type": "object",
226 | "properties": {
227 | "keyNot": {
228 | "description": "A key that must not be present",
229 | "type": "string"
230 | }
231 | },
232 | "additionalProperties": false,
233 | "required": ["keyNot"]
234 | }
235 | ]
236 | },
237 | "terms": {
238 | "description": "English synonyms or related search terms",
239 | "type": "array",
240 | "items": {
241 | "type": "string"
242 | }
243 | },
244 | "locationSet": {
245 | "description": "An object specifying the IDs of regions where this field is or isn't valid. See: https://github.com/ideditor/location-conflation",
246 | "type": "object",
247 | "properties": {
248 | "include": {
249 | "type": "array",
250 | "uniqueItems": true,
251 | "items": {
252 | "type": "string"
253 | }
254 | },
255 | "exclude": {
256 | "type": "array",
257 | "uniqueItems": true,
258 | "items": {
259 | "type": "string"
260 | }
261 | }
262 | },
263 | "additionalProperties": false
264 | },
265 | "urlFormat": {
266 | "description": "Permalink URL for `identifier` fields. Must contain a {value} placeholder",
267 | "type": "string"
268 | },
269 | "pattern": {
270 | "description": "Regular expression that a valid `identifier` value is expected to match",
271 | "type": "string"
272 | },
273 | "usage": {
274 | "description": "The manner and context in which the field is used",
275 | "type": "string",
276 | "enum": ["preset", "changeset", "manual", "group"]
277 | },
278 | "icons": {
279 | "description": "For combo fields: Name of icons which represents different values of this field",
280 | "type": "object"
281 | },
282 | "iconsCrossReference": {
283 | "description": "A field can reference icons of another by using that field's identifier contained in brackets, like {field}.",
284 | "type": "string"
285 | }
286 | },
287 | "additionalProperties": false,
288 | "required": ["type", "label"],
289 | "anyOf": [
290 | { "$id": "field-type-without-key-nor-keys", "properties": { "type": { "enum": ["restrictions"] } }, "allOf": [
291 | { "not": { "required": ["key"] }},
292 | { "not": { "required": ["keys"] }}
293 | ]},
294 | { "$id": "field-type-with-key-optional-keys", "properties": { "type": { "enum": ["email", "url", "tel", "text", "number"] } }, "required": ["key"] },
295 | { "$id": "field-type-with-key-and-keys", "properties": { "type": { "enum": ["address", "wikipedia", "wikidata", "directionalCombo"] } }, "required": ["key", "keys"] },
296 | { "$id": "field-type-with-key-or-keys", "allOf": [
297 | { "not": { "properties": { "type": { "enum": ["restriction", "email", "url", "tel", "text", "number", "address", "wikipedia", "wikidata", "directionalCombo"] } } } },
298 | { "oneOf": [
299 | { "required": ["key"] },
300 | { "required": ["keys"] }
301 | ]}
302 | ]}
303 | ]
304 | }
305 |
--------------------------------------------------------------------------------
/schemas/preset.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/preset.json",
4 | "title": "Preset",
5 | "description": "Associates an icon, form fields, and other UI with a set of OSM tags",
6 | "type": "object",
7 | "properties": {
8 | "name": {
9 | "description": "The English name for the feature. A preset can reference the label of another by using that preset's identifier contained in brackets (e.g. {preset}), in which case also the preset's aliases and terms will also be referenced from that preset.",
10 | "type": "string"
11 | },
12 | "geometry": {
13 | "description": "Valid geometry types for the feature, in order of preference",
14 | "type": "array",
15 | "minItems": 1,
16 | "uniqueItems": true,
17 | "items": {
18 | "type": "string",
19 | "enum": ["point", "vertex", "line", "area", "relation"]
20 | }
21 | },
22 | "tags": {
23 | "description": "Tags that must be present for the preset to match",
24 | "type": "object",
25 | "additionalProperties": {
26 | "type": "string"
27 | }
28 | },
29 | "addTags": {
30 | "description": "Tags that are added when changing to the preset (default is the same value as 'tags')",
31 | "type": "object",
32 | "additionalProperties": {
33 | "type": "string"
34 | }
35 | },
36 | "removeTags": {
37 | "description": "Tags that are removed when changing to another preset (default is the same value as 'addTags' which in turn defaults to 'tags')",
38 | "type": "object",
39 | "additionalProperties": {
40 | "type": "string"
41 | }
42 | },
43 | "fields": {
44 | "description": "Default form fields that are displayed for the preset. A preset can reference the fields of another by using that preset's identifier contained in brackets, like {preset}.",
45 | "type": "array",
46 | "items": {
47 | "type": "string"
48 | }
49 | },
50 | "moreFields": {
51 | "description": "Additional form fields that can be attached with the 'Add field' dropdown. A preset can reference the \"moreFields\" of another by using that preset's identifier contained in brackets, like {preset}.",
52 | "type": "array",
53 | "items": {
54 | "type": "string"
55 | }
56 | },
57 | "icon": {
58 | "description": "Name of preset icon which represents this preset",
59 | "type": "string"
60 | },
61 | "imageURL": {
62 | "description": "The URL of a remote image that is more specific than 'icon'",
63 | "type": "string"
64 | },
65 | "terms": {
66 | "description": "English search terms or related keywords",
67 | "type": "array",
68 | "items": {
69 | "type": "string"
70 | }
71 | },
72 | "aliases": {
73 | "description": "Display-ready English synonyms for the `name`",
74 | "type": "array",
75 | "items": {
76 | "type": "string"
77 | }
78 | },
79 | "searchable": {
80 | "description": "Whether or not the preset will be suggested via search",
81 | "type": "boolean",
82 | "default": true
83 | },
84 | "matchScore": {
85 | "description": "The quality score this preset will receive when being compared with other matches (higher is better)",
86 | "type": "number",
87 | "default": 1.0
88 | },
89 | "reference": {
90 | "description": "Taginfo documentation parameters (to be used when a preset manages multiple tags)",
91 | "type": "object",
92 | "properties": {
93 | "key": {
94 | "description": "For documentation of a key",
95 | "type": "string"
96 | },
97 | "value": {
98 | "description": "For documentation of a tag (key and value)",
99 | "type": "string"
100 | }
101 | },
102 | "additionalProperties": false,
103 | "required": ["key"]
104 | },
105 | "replacement": {
106 | "description": "The ID of a preset that is preferable to this one",
107 | "type": "string"
108 | },
109 | "locationSet": {
110 | "description": "An object specifying the IDs of regions where this preset is or isn't valid. See: https://github.com/ideditor/location-conflation",
111 | "type": "object",
112 | "properties": {
113 | "include": {
114 | "type": "array",
115 | "uniqueItems": true,
116 | "items": {
117 | "type": "string"
118 | }
119 | },
120 | "exclude": {
121 | "type": "array",
122 | "uniqueItems": true,
123 | "items": {
124 | "type": "string"
125 | }
126 | }
127 | },
128 | "additionalProperties": false
129 | }
130 | },
131 | "additionalProperties": false,
132 | "required": ["name", "geometry", "tags"]
133 | }
134 |
--------------------------------------------------------------------------------
/schemas/preset_category.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/preset_category.json",
4 | "title": "Preset category",
5 | "description": "An ordered grouping of presets",
6 | "type": "object",
7 | "properties": {
8 | "name": {
9 | "description": "The title of this category in US English",
10 | "type": "string",
11 | "required": true
12 | },
13 | "icon": {
14 | "description": "Name of preset icon which represents this preset",
15 | "type": "string",
16 | "required": true
17 | },
18 | "members": {
19 | "description": "The IDs of the presets in this category",
20 | "type": "array",
21 | "minItems": 1,
22 | "uniqueItems": true,
23 | "items": {
24 | "type": "string"
25 | },
26 | "required": true
27 | }
28 | },
29 | "additionalProperties": false
30 | }
31 |
--------------------------------------------------------------------------------
/schemas/preset_defaults.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "$id": "https://github.com/ideditor/schema-builder/raw/main/schemas/preset_defaults.json",
4 | "title": "Default presets",
5 | "description": "Directory of the IDs of the presets or categories to be shown in the default list for each geometry type.",
6 | "type": "object",
7 | "properties": {
8 | "point": {
9 | "type": "array",
10 | "items": {
11 | "type": "string"
12 | }
13 | },
14 | "vertex": {
15 | "type": "array",
16 | "items": {
17 | "type": "string"
18 | }
19 | },
20 | "line": {
21 | "type": "array",
22 | "items": {
23 | "type": "string"
24 | }
25 | },
26 | "area": {
27 | "type": "array",
28 | "items": {
29 | "type": "string"
30 | }
31 | },
32 | "relation": {
33 | "type": "array",
34 | "items": {
35 | "type": "string"
36 | }
37 | }
38 | },
39 | "additionalProperties": false
40 | }
41 |
--------------------------------------------------------------------------------
/tests/schema-builder.test.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import shell from 'shelljs';
3 |
4 | import schemaBuilder from '../lib/index.js';
5 |
6 | const _workspace = 'tests/workspace';
7 |
8 | beforeAll(() => {
9 | if (!fs.existsSync(_workspace)) {
10 | fs.mkdirSync(_workspace);
11 | }
12 | });
13 |
14 | afterAll(() => {
15 | shell.rm('-rf', [
16 | _workspace
17 | ]);
18 | });
19 |
20 | beforeEach(() => {
21 | shell.rm('-rf', [
22 | _workspace + '/*'
23 | ]);
24 | });
25 |
26 | function writeSourceData(data) {
27 | for (let key in data) {
28 | let path = '';
29 | let pathComponents = key.split('/');
30 | pathComponents.pop();
31 | while (pathComponents.length) {
32 | path += '/' + pathComponents.shift();
33 | if (!fs.existsSync(_workspace + path)) {
34 | fs.mkdirSync(_workspace + path);
35 | }
36 | }
37 | fs.writeFileSync(_workspace + '/' + key, JSON.stringify(data[key], null, 4));
38 | }
39 | }
40 |
41 |
42 | describe('schema-builder', () => {
43 | it('accesses modules without error', () => {
44 | expect(schemaBuilder && schemaBuilder.buildDist).not.toBeUndefined();
45 | expect(schemaBuilder && schemaBuilder.buildDev).not.toBeUndefined();
46 | expect(schemaBuilder && schemaBuilder.validate).not.toBeUndefined();
47 | expect(schemaBuilder && schemaBuilder.fetchTranslations).not.toBeUndefined();
48 | });
49 |
50 | it('runs validate', () => {
51 | writeSourceData({
52 | 'data/presets/natural.json': {
53 | tags: {
54 | natural: '*'
55 | },
56 | geometry: ['point', 'vertex', 'line', 'area', 'relation'],
57 | name: 'Natural Feature'
58 | }
59 | });
60 | schemaBuilder.validate({
61 | inDirectory: _workspace + '/data'
62 | });
63 | expect(fs.existsSync(_workspace + '/interim')).toBe(false);
64 | expect(fs.existsSync(_workspace + '/dist')).toBe(false);
65 | });
66 |
67 | it('runs buildDev', () => {
68 | writeSourceData({
69 | 'data/presets/natural.json': {
70 | tags: {
71 | natural: '*'
72 | },
73 | geometry: ['point', 'vertex', 'line', 'area', 'relation'],
74 | name: 'Natural Feature'
75 | }
76 | });
77 | schemaBuilder.buildDev({
78 | inDirectory: _workspace + '/data',
79 | interimDirectory: _workspace + '/interim'
80 | });
81 | expect(fs.existsSync(_workspace + '/interim/source_strings.yaml')).toBe(true);
82 | expect(fs.existsSync(_workspace + '/dist')).toBe(false);
83 | });
84 |
85 | it('runs buildDist', (done) => {
86 | writeSourceData({
87 | 'data/preset_categories/water.json': {
88 | icon: 'temaki-water',
89 | name: 'Water Features',
90 | members: [
91 | 'natural/water',
92 | 'natural/water/pond'
93 | ]
94 | },
95 | 'data/fields/natural.json': {
96 | key: 'natural',
97 | type: 'typeCombo',
98 | label: 'Natural Type',
99 | placeholder: 'water, tree, wood…'
100 | },
101 | 'data/fields/description.json': {
102 | key: 'description',
103 | type: 'textarea',
104 | label: 'Description',
105 | universal: true
106 | },
107 | 'data/fields/water_quality.json': {
108 | key: 'water_quality',
109 | type: 'combo',
110 | label: 'Water Quality',
111 | strings: {
112 | options: {
113 | terrible: 'Terrible',
114 | bad: 'Bad',
115 | okay: 'Okay',
116 | good: 'Good',
117 | excellent: 'Excellent',
118 | 'super fantastic': 'Super Fantastic'
119 | }
120 | },
121 | terms: [
122 | 'water health'
123 | ]
124 | },
125 | 'data/fields/water_quality_multi.json': {
126 | key: 'water_quality',
127 | type: 'semiCombo',
128 | label: '{water_quality}',
129 | stringsCrossReference: '{water_quality}'
130 | },
131 | 'data/fields/swimming.json': {
132 | key: 'swimming',
133 | type: 'combo',
134 | label: 'Swimming',
135 | strings: {
136 | options: {
137 | yes: 'Yes',
138 | no: 'No',
139 | seasonal: 'Seasonal'
140 | }
141 | },
142 | autoSuggestions: false
143 | },
144 | 'data/fields/salt.json': {
145 | key: 'salt',
146 | type: 'combo',
147 | label: 'Salt',
148 | strings: {
149 | options: {
150 | yes: {
151 | title: 'Yes',
152 | description: 'Notable salinity'
153 | },
154 | no: {
155 | title: 'No',
156 | description: 'No notable salinity'
157 | }
158 | }
159 | },
160 | terms: [
161 | 'Saline', 'salinity ', ' saline', 'nitrates'
162 | ]
163 | },
164 | 'data/fields/color_water.json': {
165 | key: 'color',
166 | type: 'colour',
167 | label: 'Colors',
168 | options: [
169 | 'azure',
170 | 'teal',
171 | 'sky',
172 | 'aquamarine',
173 | 'pearl',
174 | 'turquoise',
175 | 'mud'
176 | ],
177 | autoSuggestions: false
178 | },
179 | 'data/presets/_natural.json': {
180 | fields: [
181 | 'natural'
182 | ],
183 | tags: {
184 | natural: '*'
185 | },
186 | geometry: ['point', 'vertex', 'line', 'area', 'relation'],
187 | searchable: false,
188 | name: 'Natural Feature'
189 | },
190 | 'data/presets/natural/water.json': {
191 | fields: [
192 | 'water_quality',
193 | 'color_water'
194 | ],
195 | moreFields: [
196 | 'salt',
197 | 'swimming'
198 | ],
199 | tags: {
200 | natural: 'water'
201 | },
202 | geometry: ['point', 'area'],
203 | terms: ['pond', 'lake', ' POOL', 'reservoir ', 'Lake', 'water', 'WATER Body'],
204 | name: 'Water',
205 | aliases: [
206 | 'Water',
207 | 'Water Body'
208 | ]
209 | },
210 | 'data/presets/natural/water/pond.json': {
211 | tags: {
212 | natural: 'water',
213 | water: 'pond'
214 | },
215 | geometry: ['point', 'area'],
216 | terms: ['frogs', 'vernal pool ', 'guppies', 'puddle'],
217 | name: 'Pond',
218 | aliases: ['Vernal Pool', 'Puddle']
219 | },
220 | 'data/presets/natural/water/lake.json': {
221 | fields: [
222 | 'water_quality_multi',
223 | 'color_water'
224 | ],
225 | tags: {
226 | natural: 'water',
227 | water: 'lake'
228 | },
229 | geometry: ['point', 'area'],
230 | name: 'Lake'
231 | }
232 | });
233 | schemaBuilder.buildDist({
234 | inDirectory: _workspace + '/data',
235 | interimDirectory: _workspace + '/interim',
236 | outDirectory: _workspace + '/dist',
237 | taginfoProjectInfo: {
238 | name: 'IntrepiD',
239 | description: 'iD editor, but adventurous.',
240 | project_url: 'https://example.com/IntrepiD',
241 | contact_name: 'J. Maintainer',
242 | contact_email: 'maintainer@example.com'
243 | },
244 | listReusedIcons: true
245 | }).then(function() {
246 | expect(fs.existsSync(_workspace + '/interim/source_strings.yaml')).toBe(true);
247 | expect(fs.existsSync(_workspace + '/dist')).toBe(true);
248 | done();
249 | });
250 | });
251 | });
252 |
--------------------------------------------------------------------------------