Refactoring: refactoring of all Conversions

This commit is contained in:
Pieter Vander Vennet 2023-10-11 04:16:52 +02:00
parent 4e8dfc0026
commit f2863cdf17
38 changed files with 1177 additions and 1269 deletions

View file

@ -1169,15 +1169,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1196,7 +1196,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -1365,15 +1365,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1392,7 +1392,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -1156,15 +1156,15 @@ export default {
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1183,7 +1183,7 @@ export default {
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -1351,15 +1351,15 @@ export default {
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1378,7 +1378,7 @@ export default {
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -1076,15 +1076,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1103,7 +1103,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -1272,15 +1272,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1299,7 +1299,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -1063,15 +1063,15 @@ export default {
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1090,7 +1090,7 @@ export default {
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -1258,15 +1258,15 @@ export default {
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1285,7 +1285,7 @@ export default {
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -21,15 +21,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -48,7 +48,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -21,15 +21,15 @@ export default {
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -48,7 +48,7 @@ export default {
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },

View file

@ -13,7 +13,9 @@ export default abstract class Script {
ScriptUtils.fixUtils() ScriptUtils.fixUtils()
const args = [...process.argv] const args = [...process.argv]
args.splice(0, 2) args.splice(0, 2)
this.main(args).then((_) => console.log("All done")) this.main(args)
.then((_) => console.log("All done"))
.catch((e) => console.log("ERROR:", e))
} }
public printHelp() { public printHelp() {

View file

@ -14,13 +14,18 @@ import {
import { Translation } from "../src/UI/i18n/Translation" import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme" import { PrepareTheme } from "../src/Models/ThemeConfig/Conversion/PrepareTheme"
import { DesugaringContext } from "../src/Models/ThemeConfig/Conversion/Conversion" import {
ConversionContext,
DesugaringContext,
} from "../src/Models/ThemeConfig/Conversion/Conversion"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import Script from "./Script" import Script from "./Script"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import { parse as parse_html } from "node-html-parser" import { parse as parse_html } from "node-html-parser"
import { ExtraFunctions } from "../src/Logic/ExtraFunctions" import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files. // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them
@ -307,7 +312,7 @@ class LayerOverviewUtils extends Script {
layers: ScriptUtils.getLayerFiles().map((f) => f.parsed), layers: ScriptUtils.getLayerFiles().map((f) => f.parsed),
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed), themes: ScriptUtils.getThemeFiles().map((f) => f.parsed),
}, },
"GenerateLayerOverview:" ConversionContext.construct([], [])
) )
if (AllSharedLayers.getSharedLayersConfigs().size == 0) { if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
@ -329,8 +334,13 @@ class LayerOverviewUtils extends Script {
} catch (e) { } catch (e) {
throw "Could not parse or read file " + sharedLayerPath throw "Could not parse or read file " + sharedLayerPath
} }
const context = "While building builtin layer " + sharedLayerPath if (parsed === undefined) {
const fixed = prepLayer.convertStrict(parsed, context) throw "File " + sharedLayerPath + " yielded undefined"
}
const fixed = prepLayer.convertStrict(
parsed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
if (!fixed.source) { if (!fixed.source) {
console.error(sharedLayerPath, "has no source configured:", fixed) console.error(sharedLayerPath, "has no source configured:", fixed)
@ -346,7 +356,10 @@ class LayerOverviewUtils extends Script {
} }
const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist) const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist)
validator.convertStrict(fixed, context) validator.convertStrict(
fixed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
return fixed return fixed
} }
@ -386,12 +399,35 @@ class LayerOverviewUtils extends Script {
const fixed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath) const fixed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath)
if (sharedLayers.has(fixed.id)) { if (sharedLayers.has(fixed.id)) {
throw "There are multiple layers with the id " + fixed.id throw "There are multiple layers with the id " + fixed.id + ", " + sharedLayerPath
} }
sharedLayers.set(fixed.id, fixed) sharedLayers.set(fixed.id, fixed)
recompiledLayers.push(fixed.id) recompiledLayers.push(fixed.id)
{
// Add a summary of the icon
const layerConfig = new LayerConfig(fixed, "generating_icon")
const pointRendering: PointRenderingConfig = layerConfig.mapRendering.find((pr) =>
pr.location.has("point")
)
const defaultTags = layerConfig.GetBaseTags()
fixed["_layerIcon"] = Utils.NoNull(
(pointRendering?.marker ?? []).map((i) => {
const icon = i.icon?.GetRenderValue(defaultTags)?.txt
if (!icon) {
return undefined
}
const result = { icon }
const c = i.color?.GetRenderValue(defaultTags)?.txt
if (c) {
result["color"] = c
}
return result
})
)
}
this.writeLayer(fixed) this.writeLayer(fixed)
} }
@ -594,16 +630,25 @@ class LayerOverviewUtils extends Script {
recompiledThemes.push(themeFile.id) recompiledThemes.push(themeFile.id)
new PrevalidateTheme().convertStrict(themeFile, themePath) new PrevalidateTheme().convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
try { try {
themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath) themeFile = new PrepareTheme(convertState).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
new ValidateThemeAndLayers( new ValidateThemeAndLayers(
new DoesImageExist(licensePaths, existsSync, knownTagRenderings), new DoesImageExist(licensePaths, existsSync, knownTagRenderings),
themePath, themePath,
true, true,
knownTagRenderings knownTagRenderings
).convertStrict(themeFile, themePath) ).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
if (themeFile.icon.endsWith(".svg")) { if (themeFile.icon.endsWith(".svg")) {
try { try {

View file

@ -19,3 +19,10 @@ report.mapcomplete.org {
to http://127.0.0.1:2600 to http://127.0.0.1:2600
} }
} }
studio.mapcomplete.org {
reverse_proxy {
to http://127.0.0.1:1235
}
}

View file

@ -1,7 +1,6 @@
import * as fs from "node:fs" import * as fs from "node:fs"
import * as http from "node:http" import * as http from "node:http"
import * as path from "node:path" import * as path from "node:path"
import { ReadStream } from "fs"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
const PORT = 1235 const PORT = 1235
@ -26,15 +25,10 @@ async function prepareFile(url: string): Promise<string> {
const paths = [STATIC_PATH, url] const paths = [STATIC_PATH, url]
if (url.endsWith("/")) paths.push("index.html") if (url.endsWith("/")) paths.push("index.html")
const filePath = path.join(...paths) const filePath = path.join(...paths)
const exists = fs.existsSync(filePath) if (fs.existsSync(filePath)) {
console.log("Checking", filePath, exists) return fs.readFileSync(filePath, "utf8")
const found = exists
if (!found) {
return null
} }
const streamPath = filePath return null
const ext = path.extname(streamPath).substring(1).toLowerCase()
return fs.readFileSync(streamPath, "utf8")
} }
http.createServer(async (req, res) => { http.createServer(async (req, res) => {
@ -61,7 +55,7 @@ http.createServer(async (req, res) => {
fs.mkdirSync(dir) fs.mkdirSync(dir)
} }
} }
req.pipe(fs.createWriteStream(STATIC_PATH + paths.join("/") + ".new.json")) req.pipe(fs.createWriteStream(STATIC_PATH + paths.join("/")))
res.writeHead(200, { "Content-Type": MIME_TYPES.html }) res.writeHead(200, { "Content-Type": MIME_TYPES.html })
res.write("<html><body>OK</body></html>", "utf8") res.write("<html><body>OK</body></html>", "utf8")
res.end() res.end()

View file

@ -24,12 +24,9 @@ import {
ValidateThemeAndLayers, ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation" } from "../Models/ThemeConfig/Conversion/Validation"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion" import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
MinimalTagRenderingConfigJson,
TagRenderingConfigJson
} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
import Hash from "./Web/Hash" import Hash from "./Web/Hash"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"; import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
export default class DetermineLayout { export default class DetermineLayout {
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
@ -168,8 +165,12 @@ export default class DetermineLayout {
private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig { private static prepCustomTheme(json: any, sourceUrl?: string, forceId?: string): LayoutConfig {
if (json.layers === undefined && json.tagRenderings !== undefined) { if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme // We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json const layerConfig = <LayerConfigJson>json
const iconTr: string | TagRenderingConfigJson = layerConfig.pointRendering.map((mr) => mr.marker.find(icon => icon.icon !== undefined).icon).find((i) => i !== undefined) const iconTr: string | TagRenderingConfigJson = <any>(
layerConfig.pointRendering
.map((mr) => mr.marker.find((icon) => icon.icon !== undefined).icon)
.find((i) => i !== undefined)
)
const icon = new TagRenderingConfig(iconTr).render.txt const icon = new TagRenderingConfig(iconTr).render.txt
json = { json = {
id: json.id, id: json.id,
@ -193,34 +194,25 @@ export default class DetermineLayout {
sharedLayers: knownLayersDict, sharedLayers: knownLayersDict,
publicLayers: new Set<string>(), publicLayers: new Set<string>(),
} }
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme") json = new FixLegacyTheme().convertStrict(json)
const raw = json const raw = json
json = new FixImages(DetermineLayout._knownImages).convertStrict( json = new FixImages(DetermineLayout._knownImages).convertStrict(json)
json,
"While fixing the images"
)
json.enableNoteImports = json.enableNoteImports ?? false json.enableNoteImports = json.enableNoteImports ?? false
json = new PrepareTheme(convertState).convertStrict(json, "While preparing a dynamic theme") json = new PrepareTheme(convertState).convertStrict(json)
console.log("The layoutconfig is ", json) console.log("The layoutconfig is ", json)
json.id = forceId ?? json.id json.id = forceId ?? json.id
{ {
let { errors } = new PrevalidateTheme().convert(json, "validation") new PrevalidateTheme().convertStrict(json)
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")
}
} }
{ {
let { errors } = new ValidateThemeAndLayers( new ValidateThemeAndLayers(
new DoesImageExist(new Set<string>(), (_) => true), new DoesImageExist(new Set<string>(), (_) => true),
"", "",
false false
).convert(json, "validation") ).convertStrict(json)
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")
}
} }
return new LayoutConfig(json, false, { return new LayoutConfig(json, false, {
definitionRaw: JSON.stringify(raw, null, " "), definitionRaw: JSON.stringify(raw, null, " "),

View file

@ -308,9 +308,6 @@ export class RegexTag extends TagsFilter {
if (typeof this.value === "string") { if (typeof this.value === "string") {
return [{ k: this.key, v: this.value }] return [{ k: this.key, v: this.value }]
} }
if (this.value.toString() != "/^..*$/" || this.value.toString() != ".+") {
console.warn("Regex value in tag; using wildcard:", this.key, this.value)
}
return [{ k: this.key, v: undefined }] return [{ k: this.key, v: undefined }]
} }
console.error("Cannot export regex tag to asChange; ", this.key, this.value) console.error("Cannot export regex tag to asChange; ", this.key, this.value)

View file

@ -1,4 +1,4 @@
import { DesugaringStep } from "./Conversion" import { ConversionContext, DesugaringStep } from "./Conversion"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
@ -117,15 +117,12 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* rewritten // => theme * rewritten // => theme
* *
*/ */
convert( convert(json: T, context: ConversionContext): T {
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json["#dont-translate"] === "*") { if (json["#dont-translate"] === "*") {
return { result: json } return json
} }
const result = Utils.WalkJson( return Utils.WalkJson(
json, json,
(leaf, path) => { (leaf, path) => {
if (leaf === undefined || leaf === null) { if (leaf === undefined || leaf === null) {
@ -149,9 +146,5 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
}, },
(obj) => obj === undefined || obj === null || Translations.isProbablyATranslation(obj) (obj) => obj === undefined || obj === null || Translations.isProbablyATranslation(obj)
) )
return {
result,
}
} }
} }

View file

@ -1,4 +1,3 @@
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
@ -9,6 +8,91 @@ export interface DesugaringContext {
publicLayers?: Set<string> publicLayers?: Set<string>
} }
export class ConversionContext {
readonly path: ReadonlyArray<string | number>
readonly operation: ReadonlyArray<string>
readonly messages: ConversionMessage[] = []
private constructor(path: ReadonlyArray<string | number>, operation?: ReadonlyArray<string>) {
this.path = path
this.operation = operation ?? []
}
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([...path], [...operation])
}
static print(msg: ConversionMessage) {
if (msg.level === "error") {
console.error(
ConversionContext.red("ERR "),
msg.context.path.join("."),
ConversionContext.red(msg.message),
msg.context.operation.join(".")
)
} else if (msg.level === "warning") {
console.warn(
ConversionContext.red("<!> "),
msg.context.path.join("."),
ConversionContext.yellow(msg.message),
msg.context.operation.join(".")
)
} else {
console.log(
" ",
msg.context.path.join("."),
msg.message,
msg.context.operation.join(".")
)
}
}
private static yellow(s) {
return "\x1b[33m" + s + "\x1b[0m"
}
private static red(s) {
return "\x1b[31m" + s + "\x1b[0m"
}
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
return new ConversionContext([...this.path, key], this.operation)
}
return new ConversionContext([...this.path, ...key], this.operation)
}
public enters(...key: (string | number)[]) {
return this.enter(key)
}
public inOperation(key: string) {
return new ConversionContext(this.path, [...this.operation, key])
}
warn(message: string) {
this.messages.push({ context: this, level: "warning", message })
}
err(message: string) {
this.messages.push({ context: this, level: "error", message })
}
info(message: string) {
this.messages.push({ context: this, level: "information", message })
}
public hasErrors() {
return this.messages?.find((m) => m.level === "error") !== undefined
}
}
export interface ConversionMessage {
context: ConversionContext
message: string
level: "debug" | "information" | "warning" | "error"
}
export abstract class Conversion<TIn, TOut> { export abstract class Conversion<TIn, TOut> {
public readonly modifiedAttributes: string[] public readonly modifiedAttributes: string[]
public readonly name: string public readonly name: string
@ -20,52 +104,24 @@ export abstract class Conversion<TIn, TOut> {
this.name = name this.name = name
} }
public static strict<T>(fixed: { public convertStrict(json: TIn, context?: ConversionContext): TOut {
errors?: string[] context ??= ConversionContext.construct([], [])
warnings?: string[] context = context.enter(this.name)
information?: string[] const fixed = this.convert(json, context)
result?: T for (const msg of context.messages) {
}): T { ConversionContext.print(msg)
fixed.information?.forEach((i) => console.log(" ", i)) }
const yellow = (s) => "\x1b[33m" + s + "\x1b[0m" if (context.hasErrors()) {
const red = (s) => "\x1b[31m" + s + "\x1b[0m"
fixed.warnings?.forEach((w) => console.warn(red(`<!> `), yellow(w)))
if (fixed?.errors !== undefined && fixed?.errors?.length > 0) {
fixed.errors?.forEach((e) => console.error(red(`ERR ` + e)))
throw "Detected one or more errors, stopping now" throw "Detected one or more errors, stopping now"
} }
return fixed
return fixed.result
}
public convertStrict(json: TIn, context: string): TOut {
const fixed = this.convert(json, context)
return DesugaringStep.strict(fixed)
}
public convertJoin(
json: TIn,
context: string,
errors: string[],
warnings?: string[],
information?: string[]
): TOut {
const fixed = this.convert(json, context)
errors?.push(...(fixed.errors ?? []))
warnings?.push(...(fixed.warnings ?? []))
information?.push(...(fixed.information ?? []))
return fixed.result
} }
public andThenF<X>(f: (tout: TOut) => X): Conversion<TIn, X> { public andThenF<X>(f: (tout: TOut) => X): Conversion<TIn, X> {
return new Pipe(this, new Pure(f)) return new Pipe(this, new Pure(f))
} }
abstract convert( public abstract convert(json: TIn, context: ConversionContext): TOut
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] }
} }
export abstract class DesugaringStep<T> extends Conversion<T, T> {} export abstract class DesugaringStep<T> extends Conversion<T, T> {}
@ -80,29 +136,12 @@ class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
this._step1 = step1 this._step1 = step1
} }
convert( convert(json: TIn, context: ConversionContext): TOut {
json: TIn, const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
context: string if (context.hasErrors()) {
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } { return undefined
const r0 = this._step0.convert(json, context)
const { result, errors, information, warnings } = r0
if (result === undefined && errors.length > 0) {
return {
...r0,
result: undefined,
}
}
const r = this._step1.convert(result, context)
Utils.PushList(errors, r.errors)
Utils.PushList(warnings, r.warnings)
Utils.PushList(information, r.information)
return {
result: r.result,
errors,
warnings,
information,
} }
return this._step1.convert(r0, context.inOperation(this._step1.name))
} }
} }
@ -114,11 +153,8 @@ class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
this._f = f this._f = f
} }
convert( convert(json: TIn, context: ConversionContext): TOut {
json: TIn, return this._f(json)
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
return { result: this._f(json) }
} }
} }
@ -134,31 +170,19 @@ export class Each<X, Y> extends Conversion<X[], Y[]> {
this._step = step this._step = step
} }
convert( convert(values: X[], context: ConversionContext): Y[] {
values: X[],
context: string
): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (values === undefined || values === null) { if (values === undefined || values === null) {
return { result: undefined } return <undefined | null>values
} }
const information: string[] = []
const warnings: string[] = []
const errors: string[] = []
const step = this._step const step = this._step
const result: Y[] = [] const result: Y[] = []
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const r = step.convert(values[i], context + "[" + i + "]") const context_ = context.enter(i).inOperation("each")
Utils.PushList(information, r.information) const r = step.convert(values[i], context_)
Utils.PushList(warnings, r.warnings) result.push(r)
Utils.PushList(errors, r.errors)
result.push(r.result)
}
return {
information,
errors,
warnings,
result,
} }
return result
} }
} }
@ -180,23 +204,17 @@ export class On<P, T> extends DesugaringStep<T> {
this.key = key this.key = key
} }
convert( convert(json: T, context: ConversionContext): T {
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
json = { ...json } json = { ...json }
const step = this.step(json) const step = this.step(json)
const key = this.key const key = this.key
const value: P = json[key] const value: P = json[key]
if (value === undefined || value === null) { if (value === undefined || value === null) {
return { result: json } return undefined
}
const r = step.convert(value, context + "." + key)
json[key] = r.result
return {
...r,
result: json,
} }
json[key] = step.convert(value, context.enter(key).inOperation("on[" + key + "]"))
return json
} }
} }
@ -205,13 +223,8 @@ export class Pass<T> extends Conversion<T, T> {
super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass") super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass")
} }
convert( convert(json: T, context: ConversionContext): T {
json: T, return json
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
return {
result: json,
}
} }
} }
@ -227,25 +240,13 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
this._step = step this._step = step
} }
convert( convert(values: X[], context: ConversionContext): T[] {
values: X[],
context: string
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (values === undefined || values === null) { if (values === undefined || values === null) {
// Move on - nothing to see here! // Move on - nothing to see here!
return { return <undefined | null>values
result: undefined,
}
}
const r = new Each(this._step).convert(values, context)
const vals: T[][] = r.result
const flattened: T[] = [].concat(...vals)
return {
...r,
result: flattened,
} }
const vals: T[][] = new Each(this._step).convert(values, context.inOperation("concat"))
return [].concat(...vals)
} }
} }
@ -261,15 +262,12 @@ export class FirstOf<T, X> extends Conversion<T, X> {
this._conversion = conversion this._conversion = conversion
} }
convert( convert(json: T, context: ConversionContext): X {
json: T, const values = this._conversion.convert(json, context.inOperation("firstOf"))
context: string if (values.length === 0) {
): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } { return undefined
const reslt = this._conversion.convert(json, context)
return {
...reslt,
result: reslt.result[0],
} }
return values[0]
} }
} }
@ -287,38 +285,24 @@ export class Fuse<T> extends DesugaringStep<T> {
this.steps = Utils.NoNull(steps) this.steps = Utils.NoNull(steps)
} }
convert( convert(json: T, context: ConversionContext): T {
json: T,
context: string
): { result: T; errors: string[]; warnings: string[]; information: string[] } {
const errors = []
const warnings = []
const information = []
for (let i = 0; i < this.steps.length; i++) { for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i] const step = this.steps[i]
try { try {
let r = step.convert(json, "While running step " + step.name + ": " + context) const r = step.convert(json, context.inOperation(step.name))
if (r.result["tagRenderings"]?.some((tr) => tr === undefined)) { if (r === undefined) {
throw step.name + " introduced an undefined tagRendering"
}
errors.push(...(r.errors ?? []))
warnings.push(...(r.warnings ?? []))
information.push(...(r.information ?? []))
json = r.result
if (errors.length > 0) {
break break
} }
if (context.hasErrors()) {
break
}
json = r
} catch (e) { } catch (e) {
console.error("Step " + step.name + " failed due to ", e, e.stack) console.error("Step " + step.name + " failed due to ", e, e.stack)
throw e throw e
} }
} }
return { return json
result: json,
errors,
warnings,
information,
}
} }
} }
@ -334,14 +318,15 @@ export class SetDefault<T> extends DesugaringStep<T> {
this._overrideEmptyString = overrideEmptyString this._overrideEmptyString = overrideEmptyString
} }
convert(json: T, context: string): { result: T } { convert(json: T, context: ConversionContext): T {
if (json === undefined) {
return undefined
}
if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) {
json = { ...json } json = { ...json }
json[this.key] = this.value json[this.key] = this.value
} }
return { return json
result: json,
}
} }
} }

View file

@ -1,4 +1,4 @@
import { Conversion } from "./Conversion" import { Conversion, ConversionContext } from "./Conversion"
import LayerConfig from "../LayerConfig" import LayerConfig from "../LayerConfig"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
@ -23,7 +23,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
this._includeClosedNotesDays = includeClosedNotesDays this._includeClosedNotesDays = includeClosedNotesDays
} }
convert(layerJson: LayerConfigJson, context: string): { result: LayerConfigJson } { convert(layerJson: LayerConfigJson, context: ConversionContext): LayerConfigJson {
const t = Translations.t.importLayer const t = Translations.t.importLayer
/** /**
@ -78,7 +78,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
return { ...translation.Subs(subs).translations, _context: translation.context } return { ...translation.Subs(subs).translations, _context: translation.context }
} }
const result: LayerConfigJson = { return {
id: "note_import_" + layer.id, id: "note_import_" + layer.id,
// By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations, // By disabling the name, the import-layers won't pollute the filter view "name": t.layerName.Subs({title: layer.title.render}).translations,
description: trs(t.description, { title: layer.title.render }), description: trs(t.description, { title: layer.title.render }),
@ -204,9 +204,5 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
}, },
], ],
} }
return {
result,
}
} }
} }

View file

@ -1,4 +1,4 @@
import { Conversion, DesugaringStep } from "./Conversion" import { Conversion, ConversionContext, DesugaringStep } from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson" import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import metapaths from "../../../assets/schemas/layoutconfigmeta.json" import metapaths from "../../../assets/schemas/layoutconfigmeta.json"
@ -6,13 +6,11 @@ import tagrenderingmetapaths from "../../../assets/schemas/questionabletagrender
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
import { parse as parse_html } from "node-html-parser" import { parse as parse_html } from "node-html-parser"
export class ExtractImages extends Conversion< export class ExtractImages extends Conversion<
LayoutConfigJson, LayoutConfigJson,
{ path: string; context: string }[] { path: string; context: string }[]
> { > {
private _isOfficial: boolean
private _sharedTagRenderings: Set<string>
private static readonly layoutMetaPaths = metapaths.filter((mp) => { private static readonly layoutMetaPaths = metapaths.filter((mp) => {
const typeHint = mp.hints.typehint const typeHint = mp.hints.typehint
return ( return (
@ -25,6 +23,8 @@ export class ExtractImages extends Conversion<
) )
}) })
private static readonly tagRenderingMetaPaths = tagrenderingmetapaths private static readonly tagRenderingMetaPaths = tagrenderingmetapaths
private _isOfficial: boolean
private _sharedTagRenderings: Set<string>
constructor(isOfficial: boolean, sharedTagRenderings: Set<string>) { constructor(isOfficial: boolean, sharedTagRenderings: Set<string>) {
super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages") super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages")
@ -89,11 +89,9 @@ export class ExtractImages extends Conversion<
*/ */
convert( convert(
json: LayoutConfigJson, json: LayoutConfigJson,
context: string context: ConversionContext
): { result: { path: string; context: string }[]; errors: string[]; warnings: string[] } { ): { path: string; context: string }[] {
const allFoundImages: { path: string; context: string }[] = [] const allFoundImages: { path: string; context: string }[] = []
const errors = []
const warnings = []
for (const metapath of ExtractImages.layoutMetaPaths) { for (const metapath of ExtractImages.layoutMetaPaths) {
const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath) const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath)
const allRenderedValuesAreImages = const allRenderedValuesAreImages =
@ -110,7 +108,7 @@ export class ExtractImages extends Conversion<
} }
if (foundImage == "") { if (foundImage == "") {
warnings.push(context + "." + path.join(".") + " Found an empty image") context.warn(context + "." + path.join(".") + " Found an empty image")
} }
if (this._sharedTagRenderings?.has(foundImage)) { if (this._sharedTagRenderings?.has(foundImage)) {
@ -135,17 +133,15 @@ export class ExtractImages extends Conversion<
if (allRenderedValuesAreImages && isRendered) { if (allRenderedValuesAreImages && isRendered) {
// What we found is an image // What we found is an image
if (img.leaf === "" || img.leaf["path"] == "") { if (img.leaf === "" || img.leaf["path"] == "") {
warnings.push( context
context + .enter(path)
[...path, ...img.path].join(".") + .enter(img.path)
": Found an empty image at " .warn("Found an emtpy image")
)
} else if (typeof img.leaf !== "string") { } else if (typeof img.leaf !== "string") {
;(this._isOfficial ? errors : warnings).push( const c = context.enter(img.path)
context + const w = this._isOfficial ? c.err : c.warn
"." + w(
img.path.join(".") + "found an image path that is not a string: " +
": found an image path that is not a string: " +
JSON.stringify(img.leaf) JSON.stringify(img.leaf)
) )
} else { } else {
@ -176,9 +172,8 @@ export class ExtractImages extends Conversion<
} else { } else {
for (const foundElement of found) { for (const foundElement of found) {
if (foundElement.leaf === "") { if (foundElement.leaf === "") {
warnings.push( context.enter(foundElement.path).warn("Found an empty image")
context + "." + foundElement.path.join(".") + " Found an empty image"
)
continue continue
} }
if (typeof foundElement.leaf !== "string") { if (typeof foundElement.leaf !== "string") {
@ -215,7 +210,7 @@ export class ExtractImages extends Conversion<
} }
} }
return { result: cleanedImages, errors, warnings } return cleanedImages
} }
} }
@ -265,26 +260,22 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg" * fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg" * fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
*/ */
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; warnings?: string[] } {
let url: URL let url: URL
try { try {
url = new URL(json.id) url = new URL(json.id)
} catch (e) { } catch (e) {
// Not a URL, we don't rewrite // Not a URL, we don't rewrite
return { result: json } return json
} }
const warnings: string[] = []
const absolute = url.protocol + "//" + url.host const absolute = url.protocol + "//" + url.host
let relative = url.protocol + "//" + url.host + url.pathname let relative = url.protocol + "//" + url.host + url.pathname
relative = relative.substring(0, relative.lastIndexOf("/")) relative = relative.substring(0, relative.lastIndexOf("/"))
const self = this const self = this
if (relative.endsWith("assets/generated/themes")) { if (relative.endsWith("assets/generated/themes")) {
warnings.push( context.warn(
"Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative" "Detected 'assets/generated/themes' as relative URL. I'm assuming that you are loading your file for the MC-repository, so I'm rewriting all image links as if they were absolute instead of relative"
) )
relative = absolute relative = absolute
@ -296,7 +287,7 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
} }
if (typeof leaf !== "string") { if (typeof leaf !== "string") {
warnings.push( context.warn(
"Found a non-string object while replacing images: " + JSON.stringify(leaf) "Found a non-string object while replacing images: " + JSON.stringify(leaf)
) )
return leaf return leaf
@ -318,7 +309,7 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
continue continue
} }
const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath) const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath)
Utils.WalkPath(metapath.path, json, (leaf, path) => { Utils.WalkPath(metapath.path, json, (leaf) => {
if (typeof leaf === "string") { if (typeof leaf === "string") {
return replaceString(leaf) return replaceString(leaf)
} }
@ -340,9 +331,6 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
}) })
} }
return { return json
warnings,
result: json,
}
} }
} }

View file

@ -2,7 +2,7 @@ import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { DesugaringStep, Each, Fuse, On } from "./Conversion" import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
export class UpdateLegacyLayer extends DesugaringStep< export class UpdateLegacyLayer extends DesugaringStep<
@ -16,15 +16,12 @@ export class UpdateLegacyLayer extends DesugaringStep<
) )
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors: string[]; warnings: string[] } {
const warnings = []
if (typeof json === "string" || json["builtin"] !== undefined) { if (typeof json === "string" || json["builtin"] !== undefined) {
// Reuse of an already existing layer; return as-is // Reuse of an already existing layer; return as-is
return { result: json, errors: [], warnings: [] } return json
} }
context = context.enter(json.id)
let config = { ...json } let config = { ...json }
if (config["overpassTags"]) { if (config["overpassTags"]) {
@ -141,7 +138,7 @@ export class UpdateLegacyLayer extends DesugaringStep<
} }
for (const overlay of mapRenderingElement["iconBadges"] ?? []) { for (const overlay of mapRenderingElement["iconBadges"] ?? []) {
if (overlay["badge"] !== true) { if (overlay["badge"] !== true) {
warnings.push("Warning: non-overlay element for ", config.id) context.enters("iconBadges", "badge").warn("Non-overlay element")
} }
delete overlay["badge"] delete overlay["badge"]
} }
@ -229,11 +226,7 @@ export class UpdateLegacyLayer extends DesugaringStep<
} }
} }
return { return config
result: config,
errors: [],
warnings,
}
} }
} }
@ -242,10 +235,7 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme") super("Small fixes in the theme config", ["roamingRenderings"], "UpdateLegacyTheme")
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const oldThemeConfig = { ...json } const oldThemeConfig = { ...json }
if (oldThemeConfig.socialImage === "") { if (oldThemeConfig.socialImage === "") {
@ -260,14 +250,8 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
if (oldThemeConfig["roamingRenderings"].length == 0) { if (oldThemeConfig["roamingRenderings"].length == 0) {
delete oldThemeConfig["roamingRenderings"] delete oldThemeConfig["roamingRenderings"]
} else { } else {
return { context.err("The theme contains roamingRenderings. These are not supported anymore")
result: null, return null
errors: [
context +
": The theme contains roamingRenderings. These are not supported anymore",
],
warnings: [],
}
} }
} }
@ -292,11 +276,7 @@ class UpdateLegacyTheme extends DesugaringStep<LayoutConfigJson> {
} }
} }
return { return oldThemeConfig
errors: [],
warnings: [],
result: oldThemeConfig,
}
} }
} }

View file

@ -1,6 +1,7 @@
import { import {
Concat, Concat,
Conversion, Conversion,
ConversionContext,
DesugaringContext, DesugaringContext,
DesugaringStep, DesugaringStep,
Each, Each,
@ -48,20 +49,16 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
return filters return filters
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson, if (json?.filter === undefined || json?.filter === null) {
context: string return json // Nothing to change here
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json.filter === undefined || json.filter === null) {
return { result: json } // Nothing to change here
} }
if (json.filter["sameAs"] !== undefined) { if (json.filter["sameAs"] !== undefined) {
return { result: json } // Nothing to change here return json // Nothing to change here
} }
const newFilters: FilterConfigJson[] = [] const newFilters: FilterConfigJson[] = []
const errors: string[] = []
for (const filter of <(FilterConfigJson | string)[]>json.filter) { for (const filter of <(FilterConfigJson | string)[]>json.filter) {
if (typeof filter !== "string") { if (typeof filter !== "string") {
newFilters.push(filter) newFilters.push(filter)
@ -71,16 +68,13 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
if (this._state.sharedLayers.size > 0) { if (this._state.sharedLayers.size > 0) {
const split = filter.split(".") const split = filter.split(".")
if (split.length > 2) { if (split.length > 2) {
errors.push( context.err(
context + "invalid filter name: " + filter + ", expected `layername.filterid`"
": invalid filter name: " +
filter +
", expected `layername.filterid`"
) )
} }
const layer = this._state.sharedLayers.get(split[0]) const layer = this._state.sharedLayers.get(split[0])
if (layer === undefined) { if (layer === undefined) {
errors.push(context + ": layer '" + split[0] + "' not found") context.err("Layer '" + split[0] + "' not found")
} }
const expectedId = split[1] const expectedId = split[1]
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find( const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
@ -100,28 +94,28 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
Array.from(ExpandFilter.predefinedFilters.keys()), Array.from(ExpandFilter.predefinedFilters.keys()),
(t) => t (t) => t
) )
const err = context
context + .enter(filter)
".filter: while searching for predifined filter " + .err(
filter + "While searching for predefined filter " +
": this filter is not found. Perhaps you meant one of: " + filter +
suggestions ": this filter is not found. Perhaps you meant one of: " +
errors.push(err) suggestions
)
} }
newFilters.push(found) newFilters.push(found)
} }
return { return { ...json, filter: newFilters }
result: {
...json,
filter: newFilters,
},
errors,
}
} }
} }
class ExpandTagRendering extends Conversion< class ExpandTagRendering extends Conversion<
string | TagRenderingConfigJson | { builtin: string | string[]; override: any }, | string
| TagRenderingConfigJson
| {
builtin: string | string[]
override: any
},
TagRenderingConfigJson[] TagRenderingConfigJson[]
> { > {
private readonly _state: DesugaringContext private readonly _state: DesugaringContext
@ -137,7 +131,10 @@ class ExpandTagRendering extends Conversion<
constructor( constructor(
state: DesugaringContext, state: DesugaringContext,
self: LayerConfigJson, self: LayerConfigJson,
options?: { applyCondition?: true | boolean; noHardcodedStrings?: false | boolean } options?: {
applyCondition?: true | boolean
noHardcodedStrings?: false | boolean
}
) { ) {
super( super(
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question", "Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question",
@ -160,23 +157,6 @@ class ExpandTagRendering extends Conversion<
} }
} }
convert(
json:
| string
| QuestionableTagRenderingConfigJson
| { builtin: string | string[]; override: any },
context: string
): { result: QuestionableTagRenderingConfigJson[]; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
return {
result: this.convertUntilStable(json, warnings, errors, context),
errors,
warnings,
}
}
private lookup(name: string): TagRenderingConfigJson[] | undefined { private lookup(name: string): TagRenderingConfigJson[] | undefined {
const direct = this.directLookup(name) const direct = this.directLookup(name)
@ -261,7 +241,13 @@ class ExpandTagRendering extends Conversion<
} }
} }
found = contextWriter.convertStrict(found, layer.id + ".tagRenderings." + found["id"]) found = contextWriter.convertStrict(
found,
ConversionContext.construct(
[layer.id, "tagRenderings", found["id"]],
["AddContextToTranslations"]
)
)
matchingTrs[i] = found matchingTrs[i] = found
} }
@ -271,12 +257,7 @@ class ExpandTagRendering extends Conversion<
return undefined return undefined
} }
private convertOnce( private convertOnce(tr: string | any, ctx: ConversionContext): TagRenderingConfigJson[] {
tr: string | any,
warnings: string[],
errors: string[],
ctx: string
): TagRenderingConfigJson[] {
const state = this._state const state = this._state
if (typeof tr === "string") { if (typeof tr === "string") {
@ -285,19 +266,17 @@ class ExpandTagRendering extends Conversion<
lookup = this.lookup(tr) lookup = this.lookup(tr)
} }
if (lookup === undefined) { if (lookup === undefined) {
const isTagRendering = ctx.indexOf("On(mapRendering") < 0 if (this._state.sharedLayers?.size > 0) {
if (isTagRendering && this._state.sharedLayers?.size > 0) { ctx.warn(
warnings.push( `A literal rendering was detected: ${tr}
`${ctx}: A literal rendering was detected: ${tr} Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
Array.from(state.sharedLayers.keys()).join(", ") Array.from(state.sharedLayers.keys()).join(", ")
) )
} }
if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) { if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) {
errors.push( ctx.err(
ctx + "Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
"Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
tr + tr +
" \n Did you perhaps forget to add the layer as prefix, such as `icons." + " \n Did you perhaps forget to add the layer as prefix, such as `icons." +
tr + tr +
@ -334,10 +313,8 @@ class ExpandTagRendering extends Conversion<
) { ) {
continue continue
} }
errors.push( ctx.err(
"At " + "An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
ctx +
": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
key + key +
"` was found. This won't be picked up! The full object is: " + "` was found. This won't be picked up! The full object is: " +
JSON.stringify(tr) JSON.stringify(tr)
@ -362,18 +339,16 @@ class ExpandTagRendering extends Conversion<
(s) => s (s) => s
) )
if (state.sharedLayers.size === 0) { if (state.sharedLayers.size === 0) {
warnings.push( ctx.warn(
ctx + "BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
": BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
name + name +
": layer " + ": layer " +
layerName + layerName +
" not found for now, but ignoring as this is a bootstrapping run. " " not found for now, but ignoring as this is a bootstrapping run. "
) )
} else { } else {
errors.push( ctx.err(
ctx + ": While reusing tagrendering: " +
": While reusing tagrendering: " +
name + name +
": layer " + ": layer " +
layerName + layerName +
@ -388,9 +363,8 @@ class ExpandTagRendering extends Conversion<
) )
} }
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i) candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
errors.push( ctx.err(
ctx + "The tagRendering with identifier " +
": The tagRendering with identifier " +
name + name +
" was not found.\n\tDid you mean one of " + " was not found.\n\tDid you mean one of " +
candidates.join(", ") + candidates.join(", ") +
@ -413,23 +387,16 @@ class ExpandTagRendering extends Conversion<
return [tr] return [tr]
} }
private convertUntilStable( public convert(
spec: string | any, spec: string | any,
warnings: string[], ctx: ConversionContext
errors: string[],
ctx: string
): QuestionableTagRenderingConfigJson[] { ): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, warnings, errors, ctx) const trs = this.convertOnce(spec, ctx)
const result = [] const result = []
for (const tr of trs) { for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) { if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convertUntilStable( const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
tr,
warnings,
errors,
ctx + "(RECURSIVE RESOLVE)"
)
result.push(...stable) result.push(...stable)
} else { } else {
result.push(tr) result.push(tr)
@ -451,15 +418,10 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
convert( convert(
json: QuestionableTagRenderingConfigJson, json: QuestionableTagRenderingConfigJson,
context: string context: ConversionContext
): { ): QuestionableTagRenderingConfigJson {
result: QuestionableTagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (json.freeform === undefined) { if (json.freeform === undefined) {
return { result: json } return json
} }
let spec: Record<string, string> let spec: Record<string, string>
if (typeof json.render === "string") { if (typeof json.render === "string") {
@ -467,40 +429,33 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
} else { } else {
spec = <Record<string, string>>json.render spec = <Record<string, string>>json.render
} }
const errors: string[] = []
for (const key in spec) { for (const key in spec) {
if (spec[key].indexOf("<a ") >= 0) { if (spec[key].indexOf("<a ") >= 0) {
// We have a link element, it probably contains something that needs to be substituted... // We have a link element, it probably contains something that needs to be substituted...
// Let's play this safe and not inline it // Let's play this safe and not inline it
return { result: json } return json
} }
const fullSpecification = SpecialVisualizations.constructSpecification(spec[key]) const fullSpecification = SpecialVisualizations.constructSpecification(spec[key])
if (fullSpecification.length > 1) { if (fullSpecification.length > 1) {
// We found a special rendering! // We found a special rendering!
if (json.freeform.inline === true) { if (json.freeform.inline === true) {
errors.push( context.err(
"At " + "'inline' is set, but the rendering contains a special visualisation...\n " +
context +
": 'inline' is set, but the rendering contains a special visualisation...\n " +
spec[key] spec[key]
) )
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
json.freeform.inline = false json.freeform.inline = false
return { result: json, errors } return json
} }
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
if (typeof json.freeform === "string") { if (typeof json.freeform === "string") {
errors.push("At " + context + ": 'freeform' is a string, but should be an object") context.err("'freeform' is a string, but should be an object")
return { result: json, errors } return json
} }
try { json.freeform.inline ??= true
json.freeform.inline ??= true return json
} catch (e) {
errors.push("At " + context + ": " + e.message)
}
return { result: json, errors }
} }
} }
@ -513,15 +468,12 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
) )
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if ( if (
json.tagRenderings === undefined || json.tagRenderings === undefined ||
json.tagRenderings.some((tr) => tr["id"] === "leftover-questions") json.tagRenderings.some((tr) => tr["id"] === "leftover-questions")
) { ) {
return { result: json } return json
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
const allSpecials: Exclude<RenderingSpecification, string>[] = [] const allSpecials: Exclude<RenderingSpecification, string>[] = []
@ -537,13 +489,9 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
(sp) => sp.args.length === 0 || sp.args[0].trim() === "" (sp) => sp.args.length === 0 || sp.args[0].trim() === ""
) )
const errors: string[] = []
const warnings: string[] = []
if (noLabels.length > 1) { if (noLabels.length > 1) {
errors.push( context.err(
"At " + "Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
context +
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
) )
} }
@ -569,10 +517,8 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
?.map((a) => a.trim()) ?.map((a) => a.trim())
?.filter((s) => s != "") ?.filter((s) => s != "")
if (blacklisted?.length > 0 && used?.length > 0) { if (blacklisted?.length > 0 && used?.length > 0) {
errors.push( context.err(
"At " + "The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
context +
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
"\n Whitelisted: " + "\n Whitelisted: " +
used.join(", ") + used.join(", ") +
"\n Blacklisted: " + "\n Blacklisted: " +
@ -581,10 +527,8 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
} }
for (const usedLabel of used) { for (const usedLabel of used) {
if (!allLabels.has(usedLabel)) { if (!allLabels.has(usedLabel)) {
errors.push( context.err(
"At " + "This layers specifies a special question element for label `" +
context +
": this layers specifies a special question element for label `" +
usedLabel + usedLabel +
"`, but this label doesn't exist.\n" + "`, but this label doesn't exist.\n" +
" Available labels are " + " Available labels are " +
@ -607,11 +551,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
} }
json.tagRenderings.push(question) json.tagRenderings.push(question)
} }
return { return json
result: json,
errors,
warnings,
}
} }
} }
@ -627,12 +567,9 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
this._desugaring = desugaring this._desugaring = desugaring
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors?: string[]; warnings?: string[]; information?: string[] } {
if (this._desugaring.tagRenderings === null) { if (this._desugaring.tagRenderings === null) {
return { result: json } return json
} }
json = JSON.parse(JSON.stringify(json)) json = JSON.parse(JSON.stringify(json))
@ -693,7 +630,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
json.tagRenderings?.push(trc) json.tagRenderings?.push(trc)
} }
return { result: json } return json
} }
} }
@ -798,21 +735,16 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* ] * ]
* new ExpandRewrite().convertStrict(spec, "test") // => expected * new ExpandRewrite().convertStrict(spec, "test") // => expected
*/ */
convert( convert(json: T | RewritableConfigJson<T>, context: ConversionContext): T[] {
json: T | RewritableConfigJson<T>,
context: string
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
if (json === null || json === undefined) { if (json === null || json === undefined) {
return { result: [] } return []
} }
if (json["rewrite"] === undefined) { if (json["rewrite"] === undefined) {
// not a rewrite // not a rewrite
return { result: [<T>json] } return [<T>json]
} }
console.log("Rewriting at", context)
const rewrite = <RewritableConfigJson<T>>json const rewrite = <RewritableConfigJson<T>>json
const keysToRewrite = rewrite.rewrite const keysToRewrite = rewrite.rewrite
const ts: T[] = [] const ts: T[] = []
@ -824,7 +756,9 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) { for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) {
const toRewrite = keysToRewrite.sourceString[j] const toRewrite = keysToRewrite.sourceString[j]
if (toRewrite.indexOf(guard) >= 0) { if (toRewrite.indexOf(guard) >= 0) {
throw `${context} Error in rewrite: sourcestring[${i}] is a substring of sourcestring[${j}]: ${guard} will be substituted away before ${toRewrite} is reached.` context.err(
`sourcestring[${i}] is a substring of sourcestring[${j}]: ${guard} will be substituted away before ${toRewrite} is reached.`
)
} }
} }
} }
@ -835,7 +769,11 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
for (let i = 0; i < rewrite.rewrite.into.length; i++) { for (let i = 0; i < rewrite.rewrite.into.length; i++) {
const into = keysToRewrite.into[i] const into = keysToRewrite.into[i]
if (into.length !== rewrite.rewrite.sourceString.length) { if (into.length !== rewrite.rewrite.sourceString.length) {
throw `${context}.into.${i} Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values` context
.enters("into", i)
.err(
`Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values`
)
} }
} }
} }
@ -850,7 +788,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
ts.push(t) ts.push(t)
} }
return { result: ts } return ts
} }
} }
@ -925,7 +863,13 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* errors // => [] * errors // => []
*/ */
private static convertIfNeeded( private static convertIfNeeded(
input: (object & { special: { type: string } }) | any, input:
| (object & {
special: {
type: string
}
})
| any,
errors: string[], errors: string[],
context: string context: string
): any { ): any {
@ -1090,15 +1034,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}} * const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
* result // => expected * result // => expected
*/ */
convert( convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors = [] const errors = []
json = Utils.Clone(json) json = Utils.Clone(json)
const paths: ConfigMeta[] = tagrenderingconfigmeta const paths: ConfigMeta[] = tagrenderingconfigmeta
@ -1111,10 +1047,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
) )
} }
return { return json
result: json,
errors,
}
} }
} }
@ -1126,51 +1059,42 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
this._expand = new ExpandTagRendering(state, layer) this._expand = new ExpandTagRendering(state, layer)
} }
convert( convert(json: PointRenderingConfigJson, context: ConversionContext): PointRenderingConfigJson {
json: PointRenderingConfigJson,
context: string
): {
result: PointRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (!json["iconBadges"]) { if (!json["iconBadges"]) {
return { result: json } return json
} }
const badgesJson = json.iconBadges const badgesJson = json.iconBadges
const iconBadges: { if: TagConfigJson; then: string | TagRenderingConfigJson }[] = [] const iconBadges: {
if: TagConfigJson
then: string | TagRenderingConfigJson
}[] = []
const errs: string[] = [] const errs: string[] = []
const warns: string[] = [] const warns: string[] = []
for (let i = 0; i < badgesJson.length; i++) { for (let i = 0; i < badgesJson.length; i++) {
const iconBadge: { if: TagConfigJson; then: string | TagRenderingConfigJson } = const iconBadge: {
badgesJson[i] if: TagConfigJson
const { errors, result, warnings } = this._expand.convert( then: string | TagRenderingConfigJson
} = badgesJson[i]
const expanded = this._expand.convert(
<QuestionableTagRenderingConfigJson>iconBadge.then, <QuestionableTagRenderingConfigJson>iconBadge.then,
context + ".iconBadges[" + i + "]" context.enters("iconBadges", i)
) )
errs.push(...errors) if (expanded === undefined) {
warns.push(...warnings)
if (result === undefined) {
iconBadges.push(iconBadge) iconBadges.push(iconBadge)
continue continue
} }
iconBadges.push( iconBadges.push(
...result.map((resolved) => ({ ...expanded.map((resolved) => ({
if: iconBadge.if, if: iconBadge.if,
then: resolved, then: resolved,
})) }))
) )
} }
return { return { ...json, iconBadges }
result: { ...json, iconBadges },
errors: errs,
warnings: warns,
}
} }
} }
@ -1196,15 +1120,7 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
) )
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson,
context: string
): {
result: LayerConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const needsSpecial = const needsSpecial =
json.tagRenderings?.some((tr) => { json.tagRenderings?.some((tr) => {
if (typeof tr === "string") { if (typeof tr === "string") {
@ -1214,12 +1130,10 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
return specs?.some((sp) => sp.needsNodeDatabase) return specs?.some((sp) => sp.needsNodeDatabase)
}) ?? false }) ?? false
if (!needsSpecial) { if (!needsSpecial) {
return { result: json } return json
}
return {
result: { ...json, fullNodeDatabase: true },
information: ["Layer " + json.id + " needs the fullNodeDatabase"],
} }
context.info("Layer " + json.id + " needs the fullNodeDatabase")
return { ...json, fullNodeDatabase: true }
} }
} }
@ -1235,9 +1149,9 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
this._state = state this._state = state
} }
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } { convert(layerConfig: LayerConfigJson, context: ConversionContext): LayerConfigJson {
if (!layerConfig.tagRenderings || layerConfig.source === "special") { if (!layerConfig.tagRenderings || layerConfig.source === "special") {
return { result: layerConfig } return layerConfig
} }
const state = this._state const state = this._state
const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap") const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap")
@ -1254,9 +1168,7 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
} }
} }
return { return layerConfig
result: layerConfig,
}
} }
} }
@ -1274,30 +1186,22 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
this._state = state this._state = state
} }
convert( convert(json: IconConfigJson, context: ConversionContext): IconConfigJson {
json: IconConfigJson,
context: string
): {
result: IconConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const expander = new ExpandTagRendering(this._state, this._layer) const expander = new ExpandTagRendering(this._state, this._layer)
const result: IconConfigJson = { icon: undefined, color: undefined } const result: IconConfigJson = { icon: undefined, color: undefined }
const errors: string[] = [] const errors: string[] = []
const warnings: string[] = [] const warnings: string[] = []
if (json.icon && json.icon["builtin"]) { if (json.icon && json.icon["builtin"]) {
result.icon = expander.convertJoin(<any>json.icon, context, errors, warnings)[0] result.icon = expander.convert(<any>json.icon, context.enter("icon"))[0]
} else { } else {
result.icon = json.icon result.icon = json.icon
} }
if (json.color && json.color["builtin"]) { if (json.color && json.color["builtin"]) {
result.color = expander.convertJoin(<any>json.color, context, errors, warnings)[0] result.color = expander.convert(<any>json.color, context.enter("color"))[0]
} else { } else {
result.color = json.color result.color = json.color
} }
return { result, errors, warnings } return result
} }
} }

View file

@ -1,6 +1,7 @@
import { import {
Concat, Concat,
Conversion, Conversion,
ConversionContext,
DesugaringContext, DesugaringContext,
DesugaringStep, DesugaringStep,
Each, Each,
@ -33,12 +34,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
this._state = state this._state = state
} }
convert( convert(json: string | LayerConfigJson, context: ConversionContext): LayerConfigJson[] {
json: string | LayerConfigJson,
context: string
): { result: LayerConfigJson[]; errors: string[]; information?: string[] } {
const errors = []
const information = []
const state = this._state const state = this._state
function reportNotFound(name: string) { function reportNotFound(name: string) {
@ -50,7 +46,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
withDistance.sort((a, b) => a[1] - b[1]) withDistance.sort((a, b) => a[1] - b[1])
const ids = withDistance.map((n) => n[0]) const ids = withDistance.map((n) => n[0])
// Known builtin layers are "+.join(",")+"\n For more information, see " // Known builtin layers are "+.join(",")+"\n For more information, see "
errors.push(`${context}: The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}? context.err(`The layer with name ${name} was not found as a builtin layer. Perhaps you meant ${ids[0]}, ${ids[1]} or ${ids[2]}?
For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`) For an overview of all available layers, refer to https://github.com/pietervdvn/MapComplete/blob/develop/Docs/BuiltinLayers.md`)
} }
@ -58,119 +54,101 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
const found = state.sharedLayers.get(json) const found = state.sharedLayers.get(json)
if (found === undefined) { if (found === undefined) {
reportNotFound(json) reportNotFound(json)
return { return null
result: null,
errors,
}
}
return {
result: [found],
errors,
} }
return [found]
} }
if (json["builtin"] !== undefined) { if (json["builtin"] === undefined) {
let names = json["builtin"] return [json]
if (typeof names === "string") { }
names = [names]
let names = json["builtin"]
if (typeof names === "string") {
names = [names]
}
const layers = []
for (const name of names) {
const found = Utils.Clone(state.sharedLayers.get(name))
if (found === undefined) {
reportNotFound(name)
continue
}
if (
json["override"]["tagRenderings"] !== undefined &&
(found["tagRenderings"] ?? []).length > 0
) {
context.err(
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
)
}
try {
Utils.Merge(json["override"], found)
layers.push(found)
} catch (e) {
context.err(
`Could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"]
)}`
)
} }
const layers = []
for (const name of names) { if (json["hideTagRenderingsWithLabels"]) {
const found = Utils.Clone(state.sharedLayers.get(name)) const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
if (found === undefined) { // These labels caused at least one deletion
reportNotFound(name) const usedLabels: Set<string> = new Set<string>()
continue const filtered = []
} for (const tr of found.tagRenderings) {
if ( const labels = tr["labels"]
json["override"]["tagRenderings"] !== undefined && if (labels !== undefined) {
(found["tagRenderings"] ?? []).length > 0 const forbiddenLabel = labels.findIndex((l) => hideLabels.has(l))
) { if (forbiddenLabel >= 0) {
errors.push( usedLabels.add(labels[forbiddenLabel])
`At ${context}: when overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.` context.info(
) "Dropping tagRendering " +
}
try {
Utils.Merge(json["override"], found)
layers.push(found)
} catch (e) {
errors.push(
`At ${context}: could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"]
)}`
)
}
if (json["hideTagRenderingsWithLabels"]) {
const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
// These labels caused at least one deletion
const usedLabels: Set<string> = new Set<string>()
const filtered = []
for (const tr of found.tagRenderings) {
const labels = tr["labels"]
if (labels !== undefined) {
const forbiddenLabel = labels.findIndex((l) => hideLabels.has(l))
if (forbiddenLabel >= 0) {
usedLabels.add(labels[forbiddenLabel])
information.push(
context +
": Dropping tagRendering " +
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel]
)
continue
}
}
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
information.push(
context +
": Dropping tagRendering " +
tr["id"] + tr["id"] +
" as its id is a forbidden label" " as it has a forbidden label: " +
labels[forbiddenLabel]
) )
continue continue
} }
if (hideLabels.has(tr["group"])) {
usedLabels.add(tr["group"])
information.push(
context +
": Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
)
continue
}
filtered.push(tr)
} }
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
if (unused.length > 0) { if (hideLabels.has(tr["id"])) {
errors.push( usedLabels.add(tr["id"])
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " + context.info(
unused.join(", ") + "Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
) )
continue
} }
found.tagRenderings = filtered
}
}
return {
result: layers,
errors,
information,
}
}
return { if (hideLabels.has(tr["group"])) {
result: [json], usedLabels.add(tr["group"])
errors, context.info(
"Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
)
continue
}
filtered.push(tr)
}
const unused = Array.from(hideLabels).filter((l) => !usedLabels.has(l))
if (unused.length > 0) {
context.err(
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
)
}
found.tagRenderings = filtered
}
} }
return layers
} }
} }
@ -186,12 +164,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
this._state = state this._state = state
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
const state = this._state const state = this._state
json.layers = [...json.layers] json.layers = [...json.layers]
const alreadyLoaded = new Set(json.layers.map((l) => l["id"])) const alreadyLoaded = new Set(json.layers.map((l) => l["id"]))
@ -199,11 +172,11 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
for (const layerName of Constants.added_by_default) { for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName) const v = state.sharedLayers.get(layerName)
if (v === undefined) { if (v === undefined) {
errors.push("Default layer " + layerName + " not found") context.err("Default layer " + layerName + " not found")
continue continue
} }
if (alreadyLoaded.has(v.id)) { if (alreadyLoaded.has(v.id)) {
warnings.push( context.warn(
"Layout " + "Layout " +
context + context +
" already has a layer with name " + " already has a layer with name " +
@ -215,11 +188,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
json.layers.push(v) json.layers.push(v)
} }
return { return json
result: json,
errors,
warnings,
}
} }
} }
@ -232,21 +201,13 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
) )
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
if (!(json.enableNoteImports ?? true)) { if (!(json.enableNoteImports ?? true)) {
return { context.info(
warnings: [ "Not creating a note import layers for theme " + json.id + " as they are disabled"
"Not creating a note import layers for theme " + )
json.id + return json
" as they are disabled",
],
result: json,
}
} }
const errors = []
json = { ...json } json = { ...json }
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers
@ -278,20 +239,17 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
try { try {
const importLayerResult = creator.convert( const importLayerResult = creator.convert(
layer, layer,
context + ".(noteimportlayer)[" + i1 + "]" context.inOperation(this.name).enter(i1)
) )
if (importLayerResult.result !== undefined) { if (importLayerResult !== undefined) {
json.layers.push(importLayerResult.result) json.layers.push(importLayerResult)
} }
} catch (e) { } catch (e) {
errors.push("Could not generate an import-layer for " + layer.id + " due to " + e) context.err("Could not generate an import-layer for " + layer.id + " due to " + e)
} }
} }
return { return json
errors,
result: json,
}
} }
} }
@ -304,17 +262,9 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
) )
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:") const conversion = new AddContextToTranslations<LayoutConfigJson>("themes:")
return conversion.convert(json, json.id) return conversion.convert(json, context)
} }
} }
@ -327,13 +277,10 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
) )
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const overrideAll = json.overrideAll const overrideAll = json.overrideAll
if (overrideAll === undefined) { if (overrideAll === undefined) {
return { result: json, warnings: [], errors: [] } return json
} }
json = { ...json } json = { ...json }
@ -346,8 +293,7 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
newLayers.push(layer) newLayers.push(layer)
} }
json.layers = newLayers json.layers = newLayers
return json
return { result: json, warnings: [], errors: [] }
} }
} }
@ -458,18 +404,14 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
return dependenciesToAdd return dependenciesToAdd
} }
convert( convert(theme: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
theme: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; information: string[] } {
const state = this._state const state = this._state
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings
const information = []
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers // Layers should be expanded at this point const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers // Layers should be expanded at this point
knownTagRenderings.forEach((value, key) => { knownTagRenderings.forEach((value, key) => {
value.id = key value["id"] = key
}) })
const dependencies = AddDependencyLayersToTheme.CalculateDependencies( const dependencies = AddDependencyLayersToTheme.CalculateDependencies(
@ -481,23 +423,16 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
} }
if (dependencies.length > 0) { if (dependencies.length > 0) {
for (const dependency of dependencies) { for (const dependency of dependencies) {
information.push( context.info(
context + "Added " + dependency.config.id + " to the theme. " + dependency.reason
": added " +
dependency.config.id +
" to the theme. " +
dependency.reason
) )
} }
} }
layers.unshift(...dependencies.map((l) => l.config)) layers.unshift(...dependencies.map((l) => l.config))
return { return {
result: { ...theme,
...theme, layers: layers,
layers: layers,
},
information,
} }
} }
} }
@ -510,17 +445,9 @@ class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
this._state = state this._state = state
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (json.id !== "personal") { if (json.id !== "personal") {
return { result: json } return json
} }
// The only thing this _really_ does, is adding the layer-ids into 'layers' // The only thing this _really_ does, is adding the layer-ids into 'layers'
@ -529,10 +456,8 @@ class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
json.layers = Array.from(this._state.sharedLayers.keys()) json.layers = Array.from(this._state.sharedLayers.keys())
.filter((l) => this._state.sharedLayers.get(l).source !== null) .filter((l) => this._state.sharedLayers.get(l).source !== null)
.filter((l) => this._state.publicLayers.has(l)) .filter((l) => this._state.publicLayers.has(l))
return { context.info("The personal theme has " + json.layers.length + " public layers")
result: json, return json
information: ["The personal theme has " + json.layers.length + " public layers"],
}
} }
} }
@ -545,19 +470,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
) )
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (json.hideFromOverview === true) { if (json.hideFromOverview === true) {
return { result: json } return json
} }
const warnings = []
for (const layer of json.layers) { for (const layer of json.layers) {
if (typeof layer === "string") { if (typeof layer === "string") {
continue continue
@ -570,18 +486,15 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
continue continue
} }
const wrn = context.warn(
"The theme " + "The theme " +
json.id + json.id +
" has an inline layer: " + " has an inline layer: " +
layer["id"] + layer["id"] +
". This is discouraged." ". This is discouraged."
warnings.push(wrn) )
}
return {
result: json,
warnings,
} }
return json
} }
} }
@ -616,29 +529,25 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
this.state = state this.state = state
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
const result = super.convert(json, context) const result = super.convert(json, context)
if (this.state.publicLayers.size === 0) { if (this.state.publicLayers.size === 0) {
// THis is a bootstrapping run, no need to already set this flag // THis is a bootstrapping run, no need to already set this flag
return result return result
} }
const needsNodeDatabase = result.result.layers?.some((l: LayerConfigJson) => const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr: TagRenderingConfigJson) => l.tagRenderings?.some((tr) =>
ValidationUtils.getSpecialVisualisations(tr)?.some( ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
(special) => special.needsNodeDatabase (special) => special.needsNodeDatabase
) )
) )
) )
if (needsNodeDatabase) { if (needsNodeDatabase) {
result.information.push( context.info(
context + "Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
": setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
) )
result.result.enableNodeDatabase = true result.enableNodeDatabase = true
} }
return result return result

View file

@ -1,4 +1,4 @@
import { DesugaringStep, Each, Fuse, On } from "./Conversion" import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig" import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
@ -33,12 +33,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
this._languages = languages ?? ["en"] this._languages = languages ?? ["en"]
} }
convert( convert(obj: any, context: ConversionContext): LayerConfig {
obj: any,
context: string
): { result: LayerConfig; errors: string[]; warnings: string[] } {
const errors = []
const warnings: string[] = []
const translations = Translation.ExtractAllTranslationsFrom(obj) const translations = Translation.ExtractAllTranslationsFrom(obj)
for (const neededLanguage of this._languages) { for (const neededLanguage of this._languages) {
translations translations
@ -48,23 +43,20 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
t.tr.translations["*"] === undefined t.tr.translations["*"] === undefined
) )
.forEach((missing) => { .forEach((missing) => {
errors.push( context
context + .enter(missing.context.split("."))
"A theme should be translation-complete for " + .err(
neededLanguage + `The theme ${obj.id} should be translation-complete for ` +
", but it lacks a translation for " + neededLanguage +
missing.context + ", but it lacks a translation for " +
".\n\tThe known translation is " + missing.context +
missing.tr.textFor("en") ".\n\tThe known translation is " +
) missing.tr.textFor("en")
)
}) })
} }
return { return obj
result: obj,
errors,
warnings,
}
} }
} }
@ -84,58 +76,47 @@ export class DoesImageExist extends DesugaringStep<string> {
this.doesPathExist = checkExistsSync this.doesPathExist = checkExistsSync
} }
convert( convert(image: string, context: ConversionContext): string {
image: string,
context: string
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
if (this._ignore?.has(image)) { if (this._ignore?.has(image)) {
return { result: image } return image
} }
const errors = []
const warnings = []
const information = []
if (image.indexOf("{") >= 0) { if (image.indexOf("{") >= 0) {
information.push("Ignoring image with { in the path: " + image) context.info("Ignoring image with { in the path: " + image)
return { result: image } return image
} }
if (image === "assets/SocialImage.png") { if (image === "assets/SocialImage.png") {
return { result: image } return image
} }
if (image.match(/[a-z]*/)) { if (image.match(/[a-z]*/)) {
if (Svg.All[image + ".svg"] !== undefined) { if (Svg.All[image + ".svg"] !== undefined) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair' // This is a builtin img, e.g. 'checkmark' or 'crosshair'
return { result: image } return image
} }
} }
if (image.startsWith("<") && image.endsWith(">")) { if (image.startsWith("<") && image.endsWith(">")) {
// This is probably HTML, you're on your own here // This is probably HTML, you're on your own here
return { result: image } return image
} }
if (!this._knownImagePaths.has(image)) { if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) { if (this.doesPathExist === undefined) {
errors.push( context.err(
`Image with path ${image} not found or not attributed; it is used in ${context}` `Image with path ${image} not found or not attributed; it is used in ${context}`
) )
} else if (!this.doesPathExist(image)) { } else if (!this.doesPathExist(image)) {
errors.push( context.err(
`Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.` `Image with path ${image} does not exist; it is used in ${context}.\n Check for typo's and missing directories in the path.`
) )
} else { } else {
errors.push( context.err(
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
) )
} }
} }
return { return image
result: image,
errors,
warnings,
information,
}
} }
} }
@ -165,28 +146,20 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
} }
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
const theme = new LayoutConfig(json, this._isBuiltin) const theme = new LayoutConfig(json, this._isBuiltin)
{ {
// Legacy format checks // Legacy format checks
if (this._isBuiltin) { if (this._isBuiltin) {
if (json["units"] !== undefined) { if (json["units"] !== undefined) {
errors.push( context.err(
"The theme " + "The theme " +
json.id + json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
) )
} }
if (json["roamingRenderings"] !== undefined) { if (json["roamingRenderings"] !== undefined) {
errors.push( context.err(
"Theme " + "Theme " +
json.id + json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead" " contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
@ -196,10 +169,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
} }
if (this._isBuiltin && this._extractImages !== undefined) { if (this._isBuiltin && this._extractImages !== undefined) {
// Check images: are they local, are the licenses there, is the theme icon square, ... // Check images: are they local, are the licenses there, is the theme icon square, ...
const images = this._extractImages.convertStrict(json, "validation") const images = this._extractImages.convert(json, context.inOperation("ValidateTheme"))
const remoteImages = images.filter((img) => img.path.indexOf("http") == 0) const remoteImages = images.filter((img) => img.path.indexOf("http") == 0)
for (const remoteImage of remoteImages) { for (const remoteImage of remoteImages) {
errors.push( context.err(
"Found a remote image: " + "Found a remote image: " +
remoteImage + remoteImage +
" in theme " + " in theme " +
@ -208,20 +181,14 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
) )
} }
for (const image of images) { for (const image of images) {
this._validateImage.convertJoin( this._validateImage.convert(image.path, context.enters(image.context))
image.path,
context === undefined ? "" : ` in the theme ${context} at ${image.context}`,
errors,
warnings,
information
)
} }
} }
try { try {
if (this._isBuiltin) { if (this._isBuiltin) {
if (theme.id !== theme.id.toLowerCase()) { if (theme.id !== theme.id.toLowerCase()) {
errors.push("Theme ids should be in lowercase, but it is " + theme.id) context.err("Theme ids should be in lowercase, but it is " + theme.id)
} }
const filename = this._path.substring( const filename = this._path.substring(
@ -229,7 +196,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
this._path.length - 5 this._path.length - 5
) )
if (theme.id !== filename) { if (theme.id !== filename) {
errors.push( context.err(
"Theme ids should be the same as the name.json, but we got id: " + "Theme ids should be the same as the name.json, but we got id: " +
theme.id + theme.id +
" and filename " + " and filename " +
@ -239,54 +206,41 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
")" ")"
) )
} }
this._validateImage.convertJoin( this._validateImage.convert(theme.icon, context.enter("icon"))
theme.icon,
context + ".icon",
errors,
warnings,
information
)
} }
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) { if (dups.length > 0) {
errors.push( context.err(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
) )
} }
if (json["mustHaveLanguage"] !== undefined) { if (json["mustHaveLanguage"] !== undefined) {
const checked = new ValidateLanguageCompleteness( new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
...json["mustHaveLanguage"] theme,
).convert(theme, theme.id) context
)
errors.push(...checked.errors)
} }
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) { if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language // The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
const targetLanguage = theme.title.SupportedLanguages()[0] const targetLanguage = theme.title.SupportedLanguages()[0]
if (targetLanguage !== "en") { if (targetLanguage !== "en") {
warnings.push( context.err(
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key` `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`
) )
} }
// Official, public themes must have a full english translation // Official, public themes must have a full english translation
const checked = new ValidateLanguageCompleteness("en").convert(theme, theme.id) new ValidateLanguageCompleteness("en").convert(theme, context)
errors.push(...checked.errors)
} }
} catch (e) { } catch (e) {
errors.push(e) context.err(e)
} }
if (theme.id !== "personal") { if (theme.id !== "personal") {
new DetectDuplicatePresets().convertJoin(theme, context, errors, warnings, information) new DetectDuplicatePresets().convert(theme, context)
} }
return { return json
result: json,
errors,
warnings,
information,
}
} }
} }
@ -314,16 +268,12 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
) )
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
_: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
const overrideAll = json.overrideAll const overrideAll = json.overrideAll
if (overrideAll === undefined) { if (overrideAll === undefined) {
return { result: json } return json
} }
const errors = []
const withOverride = json.layers.filter((l) => l["override"] !== undefined) const withOverride = json.layers.filter((l) => l["override"] !== undefined)
for (const layer of withOverride) { for (const layer of withOverride) {
@ -342,12 +292,12 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
" has a shadowed property: " + " has a shadowed property: " +
key + key +
" is overriden by overrideAll of the theme" " is overriden by overrideAll of the theme"
errors.push(w) context.err(w)
} }
} }
} }
return { result: json, errors } return json
} }
} }
@ -356,28 +306,14 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
super("Miscelleanous checks on the theme", [], "MiscThemesChecks") super("Miscelleanous checks on the theme", [], "MiscThemesChecks")
} }
convert( convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const warnings = []
const errors = []
if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) { if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) {
errors.push("The theme " + json.id + " has no 'layers' defined (" + context + ")") context.err("The theme " + json.id + " has no 'layers' defined")
} }
if (json.socialImage === "") { if (json.socialImage === "") {
warnings.push("Social image for theme " + json.id + " is the emtpy string") context.warn("Social image for theme " + json.id + " is the emtpy string")
}
return {
result: json,
warnings,
errors,
} }
return json
} }
} }
@ -400,17 +336,9 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
) )
} }
convert( convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (!(json.mappings?.length > 0)) { if (!(json.mappings?.length > 0)) {
return { result: json } return json
} }
const tagRendering = new TagRenderingConfig(json) const tagRendering = new TagRenderingConfig(json)
@ -438,10 +366,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
} }
} }
return { return json
result: json,
errors,
}
} }
} }
@ -504,14 +429,9 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* r.errors.length // => 1 * r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true * r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
*/ */
convert( convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
json: TagRenderingConfigJson,
context: string
): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
const errors = []
const warnings = []
if (json.mappings === undefined || json.mappings.length === 0) { if (json.mappings === undefined || json.mappings.length === 0) {
return { result: json } return json
} }
const defaultProperties = {} const defaultProperties = {}
for (const calculatedTagName of this._calculatedTagNames) { for (const calculatedTagName of this._calculatedTagNames) {
@ -547,12 +467,12 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
json.mappings[j]["hideInAnswer"] === true && json.mappings[j]["hideInAnswer"] === true &&
json.mappings[i]["hideInAnswer"] !== true json.mappings[i]["hideInAnswer"] !== true
) { ) {
warnings.push( context.warn(
`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.` `Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
) )
} else if (doesMatch) { } else if (doesMatch) {
// The current mapping is shadowed! // The current mapping is shadowed!
errors.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j} and will thus never be shown: context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
The mapping ${parsedConditions[i].asHumanString( The mapping ${parsedConditions[i].asHumanString(
false, false,
false, false,
@ -573,11 +493,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
} }
} }
return { return json
errors,
warnings,
result: json,
}
} }
} }
@ -613,56 +529,40 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* r.errors.length > 0 // => true * r.errors.length > 0 // => true
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true * r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/ */
convert( convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
if (json.mappings === undefined || json.mappings.length === 0) { if (json.mappings === undefined || json.mappings.length === 0) {
return { result: json } return json
} }
const ignoreToken = "ignore-image-in-then" const ignoreToken = "ignore-image-in-then"
for (let i = 0; i < json.mappings.length; i++) { for (let i = 0; i < json.mappings.length; i++) {
const mapping = json.mappings[i] const mapping = json.mappings[i]
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0 const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? []) const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? [])
const ctx = `${context}.mappings[${i}]` const ctx = context.enters("mappings", i)
if (images.length > 0) { if (images.length > 0) {
if (!ignore) { if (!ignore) {
errors.push( ctx.err(
`${ctx}: A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join( `A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
", " ", "
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged` )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`
) )
} else { } else {
information.push( ctx.info(
`${ctx}: Ignored image ${images.join( `Ignored image ${images.join(
", " ", "
)} in 'then'-clause of a mapping as this check has been disabled` )} in 'then'-clause of a mapping as this check has been disabled`
) )
for (const image of images) { for (const image of images) {
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information) this._doesImageExist.convert(image, ctx)
} }
} }
} else if (ignore) { } else if (ignore) {
warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`) ctx.warn(`Unused '${ignoreToken}' - please remove this`)
} }
} }
return { return json
errors,
warnings,
information,
result: json,
}
} }
} }
@ -701,20 +601,12 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
convert( convert(
json: string | Record<string, string>, json: string | Record<string, string>,
context: string context: ConversionContext
): { ): string | Record<string, string> {
result: string | Record<string, string>
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors = []
if (typeof json === "string") { if (typeof json === "string") {
if (this.isTabnabbingProne(json)) { if (this.isTabnabbingProne(json)) {
errors.push( context.err(
"At " + "The string " +
context +
": the string " +
json + json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
) )
@ -722,16 +614,13 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
} else { } else {
for (const k in json) { for (const k in json) {
if (this.isTabnabbingProne(json[k])) { if (this.isTabnabbingProne(json[k])) {
errors.push( context.err(
`At ${context}: the translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping` `The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`
) )
} }
} }
} }
return { return json
errors,
result: json,
}
} }
} }
@ -745,50 +634,31 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
convert( convert(
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson, json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
context: string context: ConversionContext
): { ): TagRenderingConfigJson {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const warnings = []
const errors = []
if (json["special"] !== undefined) { if (json["special"] !== undefined) {
errors.push( context.err(
"At " + 'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
context +
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
) )
} }
if (json["group"]) { if (json["group"]) {
errors.push( context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead')
"At " +
context +
': groups are deprecated, use `"label": ["' +
json["group"] +
'"]` instead'
)
} }
const freeformType = json["freeform"]?.["type"] const freeformType = json["freeform"]?.["type"]
if (freeformType) { if (freeformType) {
if (Validators.availableTypes.indexOf(freeformType) < 0) { if (Validators.availableTypes.indexOf(freeformType) < 0) {
throw ( context
"At " + .enters("freeform", "type")
context + .err(
".freeform.type is an unknown type: " + "Unknown type: " +
freeformType + freeformType +
"; try one of " + "; try one of " +
Validators.availableTypes.join(", ") Validators.availableTypes.join(", ")
) )
} }
} }
return { return json
result: json,
errors,
warnings,
}
} }
} }
@ -828,24 +698,21 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
this._doesImageExist = doesImageExist this._doesImageExist = doesImageExist
} }
convert( convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
json: LayerConfigJson, context = context.inOperation(this.name)
context: string
): { result: LayerConfigJson; errors: string[]; warnings?: string[]; information?: string[] } {
const errors = []
const warnings = []
const information = []
context = "While validating a layer: " + context
if (typeof json === "string") { if (typeof json === "string") {
errors.push(context + ": This layer hasn't been expanded: " + json) context.err("This layer hasn't been expanded: " + json)
return { return null
result: null,
errors,
}
} }
const layerConfig = new LayerConfig(json, "validation", true) let layerConfig: LayerConfig
for (const [attribute, code, isStrict] of layerConfig.calculatedTags ?? []) { try {
layerConfig = new LayerConfig(json, "validation", true)
} catch (e) {
context.err(e)
return undefined
}
for (const [_, code, __] of layerConfig.calculatedTags ?? []) {
try { try {
new Function("feat", "return " + code + ";") new Function("feat", "return " + code + ";")
} catch (e) { } catch (e) {
@ -855,9 +722,8 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.source === "special") { if (json.source === "special") {
if (!Constants.priviliged_layers.find((x) => x == json.id)) { if (!Constants.priviliged_layers.find((x) => x == json.id)) {
errors.push( context.err(
context + "Layer " +
": layer " +
json.id + json.id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer" " uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
) )
@ -866,30 +732,27 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) { if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
if (json.title === undefined && json.source !== "special:library") { if (json.title === undefined && json.source !== "special:library") {
errors.push( context.err(
context + "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
) )
} }
if (json.title === null) { if (json.title === null) {
information.push( context.info(
context + "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
) )
} }
} }
if (json["builtin"] !== undefined) { if (json["builtin"] !== undefined) {
errors.push(context + ": This layer hasn't been expanded: " + json) context.err("This layer hasn't been expanded: " + json)
return { return null
result: null,
errors,
}
} }
if (json.minzoom > Constants.minZoomLevelToAddNewPoint) { if (json.minzoom > Constants.minZoomLevelToAddNewPoint) {
;(json.presets?.length > 0 ? errors : warnings).push( const c = context.enter("minzoom")
`At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates` const w = json.presets?.length > 0 ? c.err : c.warn
w(
`Minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`
) )
} }
{ {
@ -898,19 +761,17 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
) )
if (duplicates.length > 0) { if (duplicates.length > 0) {
console.log(json.tagRenderings) context
errors.push( .enter("tagRenderings")
"At " + .err("Some tagrenderings have a duplicate id: " + duplicates.join(", "))
context +
": some tagrenderings have a duplicate id: " +
duplicates.join(", ")
)
} }
} }
if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) { if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) {
if (json.deletion.softDeletionTags === undefined) { if (json.deletion.softDeletionTags === undefined) {
warnings.push("No soft-deletion tags in deletion block for layer " + json.id) context
.enter("deletion")
.warn("No soft-deletion tags in deletion block for layer " + json.id)
} }
} }
@ -919,7 +780,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
// Some checks for legacy elements // Some checks for legacy elements
if (json["overpassTags"] !== undefined) { if (json["overpassTags"] !== undefined) {
errors.push( context.err(
"Layer " + "Layer " +
json.id + json.id +
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)' 'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
@ -938,18 +799,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
] ]
for (const forbiddenKey of forbiddenTopLevel) { for (const forbiddenKey of forbiddenTopLevel) {
if (json[forbiddenKey] !== undefined) if (json[forbiddenKey] !== undefined)
errors.push( context.err(
context + "Layer " + json.id + " still has a forbidden key " + forbiddenKey
": layer " +
json.id +
" still has a forbidden key " +
forbiddenKey
) )
} }
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) { if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
errors.push( context.err(
context + "Layer " +
": layer " +
json.id + json.id +
" contains an old 'hideUnderlayingFeaturesMinPercentage'" " contains an old 'hideUnderlayingFeaturesMinPercentage'"
) )
@ -959,14 +815,14 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
json.isShown !== undefined && json.isShown !== undefined &&
(json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined) (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
) { ) {
warnings.push(context + " has a tagRendering as `isShown`") context.warn("Has a tagRendering as `isShown`")
} }
} }
if (this._isBuiltin) { if (this._isBuiltin) {
// Check location of layer file // Check location of layer file
const expected: string = `assets/layers/${json.id}/${json.id}.json` const expected: string = `assets/layers/${json.id}/${json.id}.json`
if (this._path != undefined && this._path.indexOf(expected) < 0) { if (this._path != undefined && this._path.indexOf(expected) < 0) {
errors.push( context.err(
"Layer is in an incorrect place. The path is " + "Layer is in an incorrect place. The path is " +
this._path + this._path +
", but expected " + ", but expected " +
@ -984,11 +840,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
emptyIndexes.push(i) emptyIndexes.push(i)
} }
} }
errors.push( context
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join( .enter(["tagRenderings", ...emptyIndexes])
"," .err(
)}])` `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
) ","
)}])`
)
} }
const duplicateIds = Utils.Duplicates( const duplicateIds = Utils.Duplicates(
@ -997,29 +855,26 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
.filter((id) => id !== "questions") .filter((id) => id !== "questions")
) )
if (duplicateIds.length > 0 && !Utils.runningFromConsole) { if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
errors.push( context
`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)` .enter("tagRenderings")
) .err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
} }
if (json.description === undefined) { if (json.description === undefined) {
if (typeof json.source === null) { if (typeof json.source === null) {
errors.push(context + ": A priviliged layer must have a description") context.err("A priviliged layer must have a description")
} else { } else {
warnings.push(context + ": A builtin layer should have a description") context.warn("A builtin layer should have a description")
} }
} }
} }
if (json.filter) { if (json.filter) {
const r = new On("filter", new Each(new ValidateFilter())).convert(json, context) new On("filter", new Each(new ValidateFilter())).convert(json, context)
warnings.push(...(r.warnings ?? []))
errors.push(...(r.errors ?? []))
information.push(...(r.information ?? []))
} }
if (json.tagRenderings !== undefined) { if (json.tagRenderings !== undefined) {
const r = new On( new On(
"tagRenderings", "tagRenderings",
new Each( new Each(
new ValidateTagRenderings(json, this._doesImageExist, { new ValidateTagRenderings(json, this._doesImageExist, {
@ -1027,9 +882,6 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}) })
) )
).convert(json, context) ).convert(json, context)
warnings.push(...(r.warnings ?? []))
errors.push(...(r.errors ?? []))
information.push(...(r.information ?? []))
} }
{ {
@ -1037,10 +889,8 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
(mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined (mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined
) )
if (hasCondition?.length > 0) { if (hasCondition?.length > 0) {
errors.push( context.err(
"At " + "One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
context +
":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
JSON.stringify(hasCondition, null, " ") JSON.stringify(hasCondition, null, " ")
) )
} }
@ -1048,7 +898,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.presets !== undefined) { if (json.presets !== undefined) {
if (typeof json.source === "string") { if (typeof json.source === "string") {
throw "A special layer cannot have presets" context.err("A special layer cannot have presets")
} }
// Check that a preset will be picked up by the layer itself // Check that a preset will be picked up by the layer itself
const baseTags = TagUtils.Tag(json.source["osmTags"]) const baseTags = TagUtils.Tag(json.source["osmTags"])
@ -1063,28 +913,22 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
} }
const doMatch = baseTags.matchesProperties(properties) const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) { if (!doMatch) {
errors.push( context
context + .enters("presets", i)
".presets[" + .err(
i + "This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
"]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + JSON.stringify(properties) +
JSON.stringify(properties) + "\n The required tags are: " +
"\n The required tags are: " + baseTags.asHumanString(false, false, {})
baseTags.asHumanString(false, false, {}) )
)
} }
} }
} }
} catch (e) { } catch (e) {
errors.push(e) context.err(e)
} }
return { return json
result: json,
errors,
warnings,
information,
}
} }
} }
@ -1093,33 +937,27 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
super("Detect common errors in the filters", [], "ValidateFilter") super("Detect common errors in the filters", [], "ValidateFilter")
} }
convert( convert(filter: FilterConfigJson, context: ConversionContext): FilterConfigJson {
filter: FilterConfigJson,
context: string
): {
result: FilterConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (typeof filter === "string") { if (typeof filter === "string") {
// Calling another filter, we skip // Calling another filter, we skip
return { result: filter } return filter
} }
const errors = []
for (const option of filter.options) { for (const option of filter.options) {
for (let i = 0; i < option.fields?.length ?? 0; i++) { for (let i = 0; i < option.fields?.length ?? 0; i++) {
const field = option.fields[i] const field = option.fields[i]
const type = field.type ?? "string" const type = field.type ?? "string"
if (Validators.availableTypes.find((t) => t === type) === undefined) { if (Validators.availableTypes.find((t) => t === type) === undefined) {
const err = `Invalid filter: ${type} is not a valid textfield type (at ${context}.fields[${i}])\n\tTry one of ${Array.from( context
Validators.availableTypes .enters("fields", i)
).join(",")}` .err(
errors.push(err) `Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
Validators.availableTypes
).join(",")}`
)
} }
} }
} }
return { result: filter, errors } return filter
} }
} }
@ -1137,17 +975,8 @@ export class DetectDuplicateFilters extends DesugaringStep<{
convert( convert(
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
__: string context: ConversionContext
): { ): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } {
result: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
const { layers, themes } = json const { layers, themes } = json
const perOsmTag = new Map< const perOsmTag = new Map<
string, string,
@ -1191,15 +1020,10 @@ export class DetectDuplicateFilters extends DesugaringStep<{
} }
msg += `\n - ${id}${layer.id}.${filter.id}` msg += `\n - ${id}${layer.id}.${filter.id}`
} }
warnings.push(msg) context.warn(msg)
}) })
return { return json
result: json,
errors,
warnings,
information,
}
} }
/** /**
@ -1258,18 +1082,10 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
"DetectDuplicatePresets" "DetectDuplicatePresets"
) )
} }
convert(
json: LayoutConfig, convert(json: LayoutConfig, context: ConversionContext): LayoutConfig {
context: string
): {
result: LayoutConfig
errors?: string[]
warnings?: string[]
information?: string[]
} {
const presets: PresetConfig[] = [].concat(...json.layers.map((l) => l.presets)) const presets: PresetConfig[] = [].concat(...json.layers.map((l) => l.presets))
const errors = []
const enNames = presets.map((p) => p.title.textFor("en")) const enNames = presets.map((p) => p.title.textFor("en"))
if (new Set(enNames).size != enNames.length) { if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames) const dups = Utils.Duplicates(enNames)
@ -1277,8 +1093,8 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
) )
const layerIds = layersWithDup.map((l) => l.id) const layerIds = layersWithDup.map((l) => l.id)
errors.push( context.err(
`At ${context}: this themes has multiple presets which are named:${dups}, namely layers ${layerIds.join( `This themes has multiple presets which are named:${dups}, namely layers ${layerIds.join(
", " ", "
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets` )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
) )
@ -1298,8 +1114,8 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
presetB.preciseInput.snapToLayers presetB.preciseInput.snapToLayers
) )
) { ) {
errors.push( context.err(
`At ${context}: this themes has multiple presets with the same tags: ${presetATags.asHumanString( `This themes has multiple presets with the same tags: ${presetATags.asHumanString(
false, false,
false, false,
{} {}
@ -1311,6 +1127,6 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
} }
} }
return { errors, result: json } return json
} }
} }

View file

@ -197,7 +197,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
*/ */
freeform?: { freeform?: {
/** /**
* question What is the name of the attribute that should be written to? * question: What is the name of the attribute that should be written to?
* ifunset: do not offer a freeform textfield as answer option * ifunset: do not offer a freeform textfield as answer option
*/ */
key: string key: string
@ -206,11 +206,14 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* question: What is the input type? * question: What is the input type?
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',... * The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
* ifunset: use an unconstrained <b>string</b> as input (default)
* suggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: "value="+type.name, then: "<b>"+type.name+"</b> "+type.explanation.split("\n")[0]})) * suggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: "value="+type.name, then: "<b>"+type.name+"</b> "+type.explanation.split("\n")[0]}))
*/ */
type?: string type?: string
/** /**
* question: What placeholder text should be shown in the input-element if there is no input?
* A (translated) text that is shown (as gray text) within the textfield * A (translated) text that is shown (as gray text) within the textfield
* type: translation
*/ */
placeholder?: string | any placeholder?: string | any
@ -236,8 +239,9 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
inline?: boolean inline?: boolean
/** /**
* default value to enter if no previous tagging is present. * question: What value should be entered in the text field if no value is set?
* Normally undefined (aka do not enter anything) * This can help people to quickly enter the most common option
* ifunset: do not prefill the textfield
*/ */
default?: string default?: string
} }

View file

@ -239,7 +239,9 @@ export default class LayerConfig extends WithContextLoader {
throw ( throw (
"Layer " + "Layer " +
this.id + this.id +
" defines a maxSnapDistance, but does not include a `snapToLayer`" " defines a maxSnapDistance, but does not include a `snapToLayer` (at " +
context +
")"
) )
} }

View file

@ -9,6 +9,7 @@ import { Utils } from "../../Utils"
import LanguageUtils from "../../Utils/LanguageUtils" import LanguageUtils from "../../Utils/LanguageUtils"
import { RasterLayerProperties } from "../RasterLayerProperties" import { RasterLayerProperties } from "../RasterLayerProperties"
import { ConversionContext } from "./Conversion/Conversion"
/** /**
* Minimal information about a theme * Minimal information about a theme
@ -97,10 +98,7 @@ export default class LayoutConfig implements LayoutInformation {
this.language = json.mustHaveLanguage ?? Object.keys(json.title) this.language = json.mustHaveLanguage ?? Object.keys(json.title)
this.usedImages = Array.from( this.usedImages = Array.from(
new ExtractImages(official, undefined) new ExtractImages(official, undefined)
.convertStrict( .convertStrict(json, ConversionContext.construct([json.id], ["ExtractImages"]))
json,
"while extracting the images of " + json.id + " " + context ?? ""
)
.map((i) => i.path) .map((i) => i.path)
).sort() ).sort()
{ {

View file

@ -14,6 +14,7 @@ import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import SvelteUIElement from "../../UI/Base/SvelteUIElement" import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Marker from "../../UI/Map/Marker.svelte" import Marker from "../../UI/Map/Marker.svelte"
import DynamicMarker from "../../UI/Map/DynamicMarker.svelte"
export class IconConfig extends WithContextLoader { export class IconConfig extends WithContextLoader {
public readonly icon: TagRenderingConfig public readonly icon: TagRenderingConfig
@ -45,8 +46,7 @@ export default class PointRenderingConfig extends WithContextLoader {
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
> >
// public readonly icon?: TagRenderingConfig public readonly marker: IconConfig[]
private readonly marker: IconConfig[]
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[] public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]
public readonly iconSize: TagRenderingConfig public readonly iconSize: TagRenderingConfig
public readonly anchor: TagRenderingConfig public readonly anchor: TagRenderingConfig
@ -192,7 +192,7 @@ export default class PointRenderingConfig extends WithContextLoader {
} }
public GetBaseIcon(tags?: Record<string, string>): BaseUIElement { public GetBaseIcon(tags?: Record<string, string>): BaseUIElement {
return new SvelteUIElement(Marker, { config: this, tags: new ImmutableStore(tags) }) return new SvelteUIElement(DynamicMarker, { config: this, tags: new ImmutableStore(tags) })
} }
public RenderIcon( public RenderIcon(
tags: Store<Record<string, string>>, tags: Store<Record<string, string>>,
@ -244,7 +244,9 @@ export default class PointRenderingConfig extends WithContextLoader {
anchorH = -iconH / 2 anchorH = -iconH / 2
} }
const icon = new SvelteUIElement(Marker, { config: this, tags }).SetClass("w-full h-full") const icon = new SvelteUIElement(DynamicMarker, { config: this, tags }).SetClass(
"w-full h-full"
)
let badges = undefined let badges = undefined
if (options?.includeBadges ?? true) { if (options?.includeBadges ?? true) {
badges = this.GetBadges(tags) badges = this.GetBadges(tags)

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
import { Store } from "../../Logic/UIEventSource";
import Pin from "../../assets/svg/Pin.svelte";
import Square from "../../assets/svg/Square.svelte";
import Circle from "../../assets/svg/Circle.svelte";
import Checkmark from "../../assets/svg/Checkmark.svelte";
import Clock from "../../assets/svg/Clock.svelte";
import Close from "../../assets/svg/Close.svelte";
import Crosshair from "../../assets/svg/Crosshair.svelte";
import Help from "../../assets/svg/Help.svelte";
import Home from "../../assets/svg/Home.svelte";
import Invalid from "../../assets/svg/Invalid.svelte";
import Location from "../../assets/svg/Location.svelte";
import Location_empty from "../../assets/svg/Location_empty.svelte";
import Location_locked from "../../assets/svg/Location_locked.svelte";
import Note from "../../assets/svg/Note.svelte";
import Resolved from "../../assets/svg/Resolved.svelte";
import Ring from "../../assets/svg/Ring.svelte";
import Scissors from "../../assets/svg/Scissors.svelte";
import Teardrop from "../../assets/svg/Teardrop.svelte";
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte";
import Triangle from "../../assets/svg/Triangle.svelte";
import Icon from "./Icon.svelte";
/**
* Renders a single icon.
*
* Icons -placed on top of each other- form a 'Marker' together
*/
export let icon: IconConfig;
export let tags: Store<Record<string, string>>;
let iconItem = icon.icon?.GetRenderValue(tags)?.txt;
$: iconItem = icon.icon?.GetRenderValue($tags)?.txt;
let color = icon.color?.GetRenderValue(tags)?.txt ?? "#000000";
$: color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000";
</script>
<Icon icon={iconItem} {color}/>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
import { Store } from "../../Logic/UIEventSource";
import DynamicIcon from "./DynamicIcon.svelte";
/**
* Renders a 'marker', which consists of multiple 'icons'
*/
export let config: PointRenderingConfig;
let icons: IconConfig[] = config.marker;
export let tags: Store<Record<string, string>>;
</script>
{#if config !== undefined}
<div class="relative w-full h-full">
{#each icons as icon}
<DynamicIcon {icon} {tags} />
{/each}
</div>
{/if}

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
import { Store } from "../../Logic/UIEventSource";
import Pin from "../../assets/svg/Pin.svelte"; import Pin from "../../assets/svg/Pin.svelte";
import Square from "../../assets/svg/Square.svelte"; import Square from "../../assets/svg/Square.svelte";
import Circle from "../../assets/svg/Circle.svelte"; import Circle from "../../assets/svg/Circle.svelte";
@ -27,60 +25,56 @@
* *
* Icons -placed on top of each other- form a 'Marker' together * Icons -placed on top of each other- form a 'Marker' together
*/ */
export let icon: IconConfig;
export let tags: Store<Record<string, string>>;
let iconItem = icon.icon?.GetRenderValue(tags)?.txt; export let icon: string | undefined;
$: iconItem = icon.icon?.GetRenderValue($tags)?.txt; export let color: string | undefined;
let color = icon.color?.GetRenderValue(tags)?.txt ?? "#000000";
$: color = icon.color?.GetRenderValue($tags)?.txt ?? "#000000";
</script> </script>
{#if iconItem} {#if icon}
<div class="absolute top-0 left-0 w-full h-full"> <div class="absolute top-0 left-0 w-full h-full">
{#if iconItem === "pin"} {#if icon === "pin"}
<Pin {color} /> <Pin {color} />
{:else if iconItem === "square"} {:else if icon === "square"}
<Square {color} /> <Square {color} />
{:else if iconItem === "circle"} {:else if icon === "circle"}
<Circle {color} /> <Circle {color} />
{:else if iconItem === "checkmark"} {:else if icon === "checkmark"}
<Checkmark {color} /> <Checkmark {color} />
{:else if iconItem === "clock"} {:else if icon === "clock"}
<Clock {color} /> <Clock {color} />
{:else if iconItem === "close"} {:else if icon === "close"}
<Close {color} /> <Close {color} />
{:else if iconItem === "crosshair"} {:else if icon === "crosshair"}
<Crosshair {color} /> <Crosshair {color} />
{:else if iconItem === "help"} {:else if icon === "help"}
<Help {color} /> <Help {color} />
{:else if iconItem === "home"} {:else if icon === "home"}
<Home {color} /> <Home {color} />
{:else if iconItem === "invalid"} {:else if icon === "invalid"}
<Invalid {color} /> <Invalid {color} />
{:else if iconItem === "location"} {:else if icon === "location"}
<Location {color} /> <Location {color} />
{:else if iconItem === "location_empty"} {:else if icon === "location_empty"}
<Location_empty {color} /> <Location_empty {color} />
{:else if iconItem === "location_locked"} {:else if icon === "location_locked"}
<Location_locked {color} /> <Location_locked {color} />
{:else if iconItem === "note"} {:else if icon === "note"}
<Note {color} /> <Note {color} />
{:else if iconItem === "resolved"} {:else if icon === "resolved"}
<Resolved {color} /> <Resolved {color} />
{:else if iconItem === "ring"} {:else if icon === "ring"}
<Ring {color} /> <Ring {color} />
{:else if iconItem === "scissors"} {:else if icon === "scissors"}
<Scissors {color} /> <Scissors {color} />
{:else if iconItem === "teardrop"} {:else if icon === "teardrop"}
<Teardrop {color} /> <Teardrop {color} />
{:else if iconItem === "teardrop_with_hole_green"} {:else if icon === "teardrop_with_hole_green"}
<Teardrop_with_hole_green {color} /> <Teardrop_with_hole_green {color} />
{:else if iconItem === "triangle"} {:else if icon === "triangle"}
<Triangle {color} /> <Triangle {color} />
{:else} {:else}
<img class="w-full h-full" src={iconItem} /> <img class="w-full h-full" src={icon} />
{/if} {/if}
</div> </div>
{/if} {/if}

View file

@ -1,21 +1,17 @@
<script lang="ts"> <script lang="ts">
import PointRenderingConfig, { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
import Icon from "./Icon.svelte"; import Icon from "./Icon.svelte";
import { Store } from "../../Logic/UIEventSource";
/** /**
* Renders a 'marker', which consists of multiple 'icons' * Renders a 'marker', which consists of multiple 'icons'
*/ */
export let config: PointRenderingConfig; export let icons: { icon: string, color: string }[]
let icons: IconConfig[] = config.marker;
export let tags: Store<Record<string, string>>;
</script> </script>
{#if config !== undefined} {#if icons !== undefined && icons.length > 0}
<div class="relative w-full h-full"> <div class="relative w-full h-full">
{#each icons as icon} {#each icons as icon}
<Icon {icon} {tags} /> <Icon icon={icon.icon} color={icon.color} />
{/each} {/each}
</div> </div>
{/if} {/if}

View file

@ -235,6 +235,7 @@
bind:group={selectedMapping} bind:group={selectedMapping}
name={"mappings-radio-" + config.id} name={"mappings-radio-" + config.id}
value={i} value={i}
on:keypress={e => {console.log(e) ; if(e.key === "Enter") onSave()}}
/> />
</TagRenderingMappingInput> </TagRenderingMappingInput>
{/each} {/each}

View file

@ -19,7 +19,7 @@
* Blacklist of regions for the general area tab * Blacklist of regions for the general area tab
* These are regions which are handled by a different tab * These are regions which are handled by a different tab
*/ */
const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title"]; const regionBlacklist = ["hidden", undefined, "infobox", "tagrenderings", "maprendering", "editing", "title","linerendering","pointrendering"];
const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group)); const allNames = Utils.Dedup(layerSchema.map(meta => meta.hints.group));
const perRegion: Record<string, ConfigMeta[]> = {}; const perRegion: Record<string, ConfigMeta[]> = {};
@ -27,7 +27,7 @@
perRegion[region] = layerSchema.filter(meta => meta.hints.group === region); perRegion[region] = layerSchema.filter(meta => meta.hints.group === region);
} }
const baselayerRegions: string[] = ["Basic", "presets", "filters", "advanced", "expert"]; const baselayerRegions: string[] = ["Basic", "presets", "filters"];
for (const baselayerRegion of baselayerRegions) { for (const baselayerRegion of baselayerRegions) {
if (perRegion[baselayerRegion] === undefined) { if (perRegion[baselayerRegion] === undefined) {
console.error("BaseLayerRegions in editLayer: no items have group '" + baselayerRegion + "\""); console.error("BaseLayerRegions in editLayer: no items have group '" + baselayerRegion + "\"");
@ -38,8 +38,6 @@
</script> </script>
<h3>Editing layer {$title}</h3> <h3>Editing layer {$title}</h3>
<h4>Leftover regions</h4>
{leftoverRegions.join("; ")}
<div class="m4"> <div class="m4">
<TabbedGroup tab={new UIEventSource(2)}> <TabbedGroup tab={new UIEventSource(2)}>
<div slot="title0">General properties</div> <div slot="title0">General properties</div>
@ -47,9 +45,6 @@
{#each baselayerRegions as region} {#each baselayerRegions as region}
<Region {state} configs={perRegion[region]} title={region} /> <Region {state} configs={perRegion[region]} title={region} />
{/each} {/each}
{#each leftoverRegions as region}
<Region {state} configs={perRegion[region]} title={region} />
{/each}
</div> </div>
<div slot="title1">Information panel (questions and answers)</div> <div slot="title1">Information panel (questions and answers)</div>
<div slot="content1"> <div slot="content1">
@ -63,8 +58,14 @@
<Region configs={perRegion["linerendering"]} {state} /> <Region configs={perRegion["linerendering"]} {state} />
<Region configs={perRegion["pointrendering"]} {state} /> <Region configs={perRegion["pointrendering"]} {state} />
</div> </div>
<div slot="title3">Configuration file</div>
<div slot="title3">Advanced functionality</div>
<div slot="content3"> <div slot="content3">
<Region configs={perRegion["advanced"]} {state} />
<Region configs={perRegion["expert"]} {state} />
</div>
<div slot="title4">Configuration file</div>
<div slot="content4">
<div> <div>
Below, you'll find the raw configuration file in `.json`-format. Below, you'll find the raw configuration file in `.json`-format.
This is mostly for debugging purposes This is mostly for debugging purposes

View file

@ -65,8 +65,13 @@ console.log("For ", schema.path, "got subparts", subparts)
} }
function del(value) { function del(value) {
values.data.splice(values.data.indexOf(value)); const index = values.data.indexOf(value)
console.log("Deleting",value, index)
values.data.splice(index, 1);
const store = <UIEventSource<[]>>state.getStoreFor(path);
store.data.splice(index, 1)
values.ping(); values.ping();
store.ping()
} }
</script> </script>

View file

@ -96,13 +96,13 @@
err = path.join(".") + " " + e err = path.join(".") + " " + e
} }
let startValue = state.getCurrentValueFor(path) let startValue = state.getCurrentValueFor(path)
if (typeof startValue !== "string") {
startValue = JSON.stringify(startValue)
}
const tags = new UIEventSource<Record<string, string>>({value: startValue ?? ""}) const tags = new UIEventSource<Record<string, string>>({value: startValue ?? ""})
try { try {
onDestroy(state.register(path, tags.map(tgs => { onDestroy(state.register(path, tags.map(tgs => {
const v = tgs["value"]; const v = tgs["value"];
if(typeof v !== "string"){
return v
}
if (schema.type === "boolan") { if (schema.type === "boolan") {
return v === "true" || v === "yes" || v === "1" return v === "true" || v === "yes" || v === "1"
} }
@ -135,7 +135,6 @@
<span class="alert">{err}</span> <span class="alert">{err}</span>
{:else} {:else}
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
<span class="subtle">{path.join(".")}</span>
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/> <TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
</div> </div>
{/if} {/if}

View file

@ -214,5 +214,4 @@
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput> path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each} {/each}
{/if} {/if}
{chosenOption}
</div> </div>

View file

@ -0,0 +1,38 @@
import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
export default class StudioServer {
private _url: string
constructor(url: string) {
this._url = url
}
public async fetchLayerOverview(): Promise<Set<string>> {
const { allFiles } = <{ allFiles: string[] }>(
await Utils.downloadJson(this._url + "/overview")
)
const layers = allFiles
.filter((f) => f.startsWith("layers/"))
.map((l) => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length))
.filter((layerId) => Constants.priviliged_layers.indexOf(<any>layerId) < 0)
return new Set<string>(layers)
}
async fetchLayer(layerId: string, checkNew: boolean = false): Promise<LayerConfigJson> {
try {
return await Utils.downloadJson(
this._url +
"/layers/" +
layerId +
"/" +
layerId +
".json" +
(checkNew ? ".new.json" : "")
)
} catch (e) {
return undefined
}
}
}

View file

@ -2,35 +2,32 @@
import NextButton from "./Base/NextButton.svelte"; import NextButton from "./Base/NextButton.svelte";
import { Utils } from "../Utils";
import { UIEventSource } from "../Logic/UIEventSource"; import { UIEventSource } from "../Logic/UIEventSource";
import Constants from "../Models/Constants";
import ValidatedInput from "./InputElement/ValidatedInput.svelte"; import ValidatedInput from "./InputElement/ValidatedInput.svelte";
import EditLayerState from "./Studio/EditLayerState"; import EditLayerState from "./Studio/EditLayerState";
import EditLayer from "./Studio/EditLayer.svelte"; import EditLayer from "./Studio/EditLayer.svelte";
import Loading from "../assets/svg/Loading.svelte"; import Loading from "../assets/svg/Loading.svelte";
import Marker from "./Map/Marker.svelte";
import { AllSharedLayers } from "../Customizations/AllSharedLayers";
import StudioServer from "./Studio/StudioServer";
import LoginToggle from "./Base/LoginToggle.svelte";
import { OsmConnection } from "../Logic/Osm/OsmConnection";
import { QueryParameters } from "../Logic/Web/QueryParameters";
export let studioUrl = "http://127.0.0.1:1235"; export let studioUrl = "http://127.0.0.1:1235";
let overview = UIEventSource.FromPromise<{ allFiles: string[] }>(Utils.downloadJson(studioUrl + "/overview")); const studio = new StudioServer(studioUrl);
let layers = overview.map(overview => { let layers = UIEventSource.FromPromise(studio.fetchLayerOverview());
if (!overview) {
return [];
}
return overview.allFiles.filter(f => f.startsWith("layers/")
).map(l => l.substring(l.lastIndexOf("/") + 1, l.length - ".json".length))
.filter(layerId => Constants.priviliged_layers.indexOf(layerId) < 0);
});
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined; let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
let initialLayerConfig: undefined; let initialLayerConfig: { id: string };
let newLayerId = new UIEventSource<string>(""); let newLayerId = new UIEventSource<string>("");
let layerIdFeedback = new UIEventSource<string>(undefined); let layerIdFeedback = new UIEventSource<string>(undefined);
newLayerId.addCallbackD(layerId => { newLayerId.addCallbackD(layerId => {
if (layerId === "") { if (layerId === "") {
return; return;
} }
if (layers.data.indexOf(layerId) >= 0) { if (layers.data.has(layerId)) {
layerIdFeedback.setData("This id is already used"); layerIdFeedback.setData("This id is already used");
} }
}, [layers]); }, [layers]);
@ -38,7 +35,28 @@
let editLayerState = new EditLayerState(); let editLayerState = new EditLayerState();
function fetchIconDescription(layerId): any {
const icon = AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
console.log(icon);
return icon;
}
let osmConnection = new OsmConnection( new OsmConnection({
oauth_token: QueryParameters.GetQueryParameter(
"oauth_token",
undefined,
"Used to complete the login"
),
}))
</script> </script>
<LoginToggle state={{osmConnection}}>
<div slot="not-logged-in" >
<NextButton clss="primary">
Please log in to use MapComplete Studio
</NextButton>
</div>
{#if state === undefined} {#if state === undefined}
<h1>MapComplete Studio</h1> <h1>MapComplete Studio</h1>
<div class="w-full flex flex-col"> <div class="w-full flex flex-col">
@ -58,13 +76,16 @@
</div> </div>
{:else if state === "edit_layer"} {:else if state === "edit_layer"}
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{#each $layers as layerId} {#each Array.from($layers) as layerId}
<NextButton clss="small" on:click={async () => { <NextButton clss="small" on:click={async () => {
console.log("Editing layer",layerId) console.log("Editing layer",layerId)
state = "loading" state = "loading"
initialLayerConfig = await Utils.downloadJson(studioUrl+"/layers/"+layerId+"/"+layerId+".json") initialLayerConfig = await studio.fetchLayer(layerId)
state = "editing_layer" state = "editing_layer"
}}> }}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layerId)} />
</div>
{layerId} {layerId}
</NextButton> </NextButton>
{/each} {/each}
@ -76,12 +97,22 @@
{$layerIdFeedback} {$layerIdFeedback}
</div> </div>
{:else } {:else }
<NextButton on:click={() => {initialLayerConfig = ({id: newLayerId.data}); state = "editing_layer"}}> <NextButton on:click={async () => {
state = "loading"
const id = newLayerId.data
const createdBy = osmConnection.userDetails.data.name
const loaded = await studio.fetchLayer(id, true)
initialLayerConfig = loaded ?? {id, credits: createdBy};
state = "editing_layer"}}>
Create this layer Create this layer
</NextButton> </NextButton>
{/if} {/if}
{:else if state === "loading"} {:else if state === "loading"}
<Loading /> <div class="w-8 h-8">
<Loading />
</div>
{:else if state === "editing_layer"} {:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} /> <EditLayer {initialLayerConfig} />
{/if} {/if}
</LoginToggle>

View file

@ -11745,6 +11745,10 @@
"if": "value=gps_track", "if": "value=gps_track",
"then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track." "then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track."
}, },
{
"if": "value=guidepost",
"then": "guidepost - Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
},
{ {
"if": "value=hackerspace", "if": "value=hackerspace",
"then": "hackerspace - Hackerspace" "then": "hackerspace - Hackerspace"
@ -12102,15 +12106,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -12129,7 +12133,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -12816,10 +12820,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -12830,6 +12835,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -12902,6 +12908,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -12915,7 +12925,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -12962,9 +12975,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -13837,10 +13853,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -13852,6 +13869,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -13924,6 +13942,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -13938,7 +13960,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -13989,9 +14014,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -14888,10 +14916,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -14903,6 +14932,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -14975,6 +15005,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -14989,7 +15023,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -15040,9 +15077,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -15951,10 +15991,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -15967,6 +16008,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -16039,6 +16081,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -16054,7 +16100,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -16109,9 +16158,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [

View file

@ -663,15 +663,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -690,7 +690,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -13206,6 +13206,10 @@
"if": "value=gps_track", "if": "value=gps_track",
"then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track." "then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track."
}, },
{
"if": "value=guidepost",
"then": "guidepost - Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
},
{ {
"if": "value=hackerspace", "if": "value=hackerspace",
"then": "hackerspace - Hackerspace" "then": "hackerspace - Hackerspace"
@ -13565,15 +13569,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -13592,7 +13596,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -14300,10 +14304,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -14315,6 +14320,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -14387,6 +14393,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -14401,7 +14411,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -14452,9 +14465,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -15363,10 +15379,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -15379,6 +15396,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -15451,6 +15469,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -15466,7 +15488,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -15521,9 +15546,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -16457,10 +16485,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -16473,6 +16502,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -16545,6 +16575,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -16560,7 +16594,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -16615,9 +16652,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -17562,10 +17602,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -17579,6 +17620,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -17651,6 +17693,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -17667,7 +17713,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -17726,9 +17775,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -31420,6 +31472,10 @@
"if": "value=gps_track", "if": "value=gps_track",
"then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track." "then": "gps_track - Meta layer showing the previous locations of the user as single line with controls, e.g. to erase, upload or download this track. Add this to your theme and override the maprendering to change the appearance of the travelled track."
}, },
{
"if": "value=guidepost",
"then": "guidepost - Guideposts (also known as fingerposts or finger posts) are often found along official hiking/cycling/riding/skiing routes to indicate the directions to different destinations"
},
{ {
"if": "value=hackerspace", "if": "value=hackerspace",
"then": "hackerspace - Hackerspace" "then": "hackerspace - Hackerspace"
@ -31781,15 +31837,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"key": { "key": {
"description": "question What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option", "description": "question: What is the name of the attribute that should be written to?\nifunset: do not offer a freeform textfield as answer option",
"type": "string" "type": "string"
}, },
"type": { "type": {
"description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))", "description": "question: What is the input type?\nThe type of the text-field, e.g. 'string', 'nat', 'float', 'date',...\nSee Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values\nifunset: use an unconstrained <b>string</b> as input (default)\nsuggestions: return validators.AllValidators.filter(type => !type.isMeta).map((type) => ({if: \"value=\"+type.name, then: \"<b>\"+type.name+\"</b> \"+type.explanation.split(\"\\n\")[0]}))",
"type": "string" "type": "string"
}, },
"placeholder": { "placeholder": {
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "question: What placeholder text should be shown in the input-element if there is no input?\nA (translated) text that is shown (as gray text) within the textfield\ntype: translation"
}, },
"helperArgs": { "helperArgs": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'", "description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -31808,7 +31864,7 @@
"type": "boolean" "type": "boolean"
}, },
"default": { "default": {
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)", "description": "question: What value should be entered in the text field if no value is set?\nThis can help people to quickly enter the most common option\nifunset: do not prefill the textfield",
"type": "string" "type": "string"
} }
}, },
@ -32537,10 +32593,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -32553,6 +32610,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -32625,6 +32683,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -32640,7 +32702,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -32695,9 +32760,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -33642,10 +33710,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -33659,6 +33728,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -33731,6 +33801,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -33747,7 +33821,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -33806,9 +33883,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -34779,10 +34859,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -34796,6 +34877,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -34868,6 +34950,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -34884,7 +34970,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -34943,9 +35032,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [
@ -35926,10 +36018,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -35944,6 +36037,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -36016,6 +36110,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -36033,7 +36131,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -36096,9 +36197,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [

View file

@ -473,10 +473,11 @@
], ],
"required": true, "required": true,
"hints": { "hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option" "ifunset": "do not offer a freeform textfield as answer option"
}, },
"type": "string", "type": "string",
"description": "question What is the name of the attribute that should be written to?" "description": ""
}, },
{ {
"path": [ "path": [
@ -486,6 +487,7 @@
"required": false, "required": false,
"hints": { "hints": {
"question": "What is the input type?", "question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [ "suggestions": [
{ {
"if": "value=string", "if": "value=string",
@ -558,6 +560,10 @@
{ {
"if": "value=fediverse", "if": "value=fediverse",
"then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format" "then": "<b>fediverse</b> Validates fediverse addresses and normalizes them into `@username@server`-format"
},
{
"if": "value=id",
"then": "<b>id</b> Checks for valid identifiers for layers, will automatically replace spaces and uppercase"
} }
] ]
}, },
@ -570,7 +576,10 @@
"placeholder" "placeholder"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"typehint": "translation",
"question": "What placeholder text should be shown in the input-element if there is no input?"
},
"description": "A (translated) text that is shown (as gray text) within the textfield" "description": "A (translated) text that is shown (as gray text) within the textfield"
}, },
{ {
@ -613,9 +622,12 @@
"default" "default"
], ],
"required": false, "required": false,
"hints": {}, "hints": {
"question": "What value should be entered in the text field if no value is set?",
"ifunset": "do not prefill the textfield"
},
"type": "string", "type": "string",
"description": "default value to enter if no previous tagging is present.\nNormally undefined (aka do not enter anything)" "description": "This can help people to quickly enter the most common option"
}, },
{ {
"path": [ "path": [