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",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1196,7 +1196,7 @@
"type": "boolean"
},
"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"
}
},
@ -1365,15 +1365,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1392,7 +1392,7 @@
"type": "boolean"
},
"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"
}
},

View file

@ -1156,15 +1156,15 @@ export default {
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1183,7 +1183,7 @@ export default {
"type": "boolean"
},
"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"
}
},
@ -1351,15 +1351,15 @@ export default {
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1378,7 +1378,7 @@ export default {
"type": "boolean"
},
"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"
}
},

View file

@ -1076,15 +1076,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1103,7 +1103,7 @@
"type": "boolean"
},
"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"
}
},
@ -1272,15 +1272,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1299,7 +1299,7 @@
"type": "boolean"
},
"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"
}
},

View file

@ -1063,15 +1063,15 @@ export default {
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1090,7 +1090,7 @@ export default {
"type": "boolean"
},
"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"
}
},
@ -1258,15 +1258,15 @@ export default {
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -1285,7 +1285,7 @@ export default {
"type": "boolean"
},
"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"
}
},

View file

@ -21,15 +21,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -48,7 +48,7 @@
"type": "boolean"
},
"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"
}
},

View file

@ -21,15 +21,15 @@ export default {
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -48,7 +48,7 @@ export default {
"type": "boolean"
},
"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"
}
},

View file

@ -13,7 +13,9 @@ export default abstract class Script {
ScriptUtils.fixUtils()
const args = [...process.argv]
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() {

View file

@ -14,13 +14,18 @@ import {
import { Translation } from "../src/UI/i18n/Translation"
import { PrepareLayer } from "../src/Models/ThemeConfig/Conversion/PrepareLayer"
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 Script from "./Script"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import { parse as parse_html } from "node-html-parser"
import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
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.
// 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),
themes: ScriptUtils.getThemeFiles().map((f) => f.parsed),
},
"GenerateLayerOverview:"
ConversionContext.construct([], [])
)
if (AllSharedLayers.getSharedLayersConfigs().size == 0) {
@ -329,8 +334,13 @@ class LayerOverviewUtils extends Script {
} catch (e) {
throw "Could not parse or read file " + sharedLayerPath
}
const context = "While building builtin layer " + sharedLayerPath
const fixed = prepLayer.convertStrict(parsed, context)
if (parsed === undefined) {
throw "File " + sharedLayerPath + " yielded undefined"
}
const fixed = prepLayer.convertStrict(
parsed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
if (!fixed.source) {
console.error(sharedLayerPath, "has no source configured:", fixed)
@ -346,7 +356,10 @@ class LayerOverviewUtils extends Script {
}
const validator = new ValidateLayer(sharedLayerPath, true, doesImageExist)
validator.convertStrict(fixed, context)
validator.convertStrict(
fixed,
ConversionContext.construct([sharedLayerPath], ["PrepareLayer"])
)
return fixed
}
@ -386,12 +399,35 @@ class LayerOverviewUtils extends Script {
const fixed = this.parseLayer(doesImageExist, prepLayer, sharedLayerPath)
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)
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)
}
@ -594,16 +630,25 @@ class LayerOverviewUtils extends Script {
recompiledThemes.push(themeFile.id)
new PrevalidateTheme().convertStrict(themeFile, themePath)
new PrevalidateTheme().convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
try {
themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath)
themeFile = new PrepareTheme(convertState).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
new ValidateThemeAndLayers(
new DoesImageExist(licensePaths, existsSync, knownTagRenderings),
themePath,
true,
knownTagRenderings
).convertStrict(themeFile, themePath)
).convertStrict(
themeFile,
ConversionContext.construct([themePath], ["PrepareLayer"])
)
if (themeFile.icon.endsWith(".svg")) {
try {

View file

@ -19,3 +19,10 @@ report.mapcomplete.org {
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 http from "node:http"
import * as path from "node:path"
import { ReadStream } from "fs"
import ScriptUtils from "./ScriptUtils"
const PORT = 1235
@ -26,15 +25,10 @@ async function prepareFile(url: string): Promise<string> {
const paths = [STATIC_PATH, url]
if (url.endsWith("/")) paths.push("index.html")
const filePath = path.join(...paths)
const exists = fs.existsSync(filePath)
console.log("Checking", filePath, exists)
const found = exists
if (!found) {
return null
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf8")
}
const streamPath = filePath
const ext = path.extname(streamPath).substring(1).toLowerCase()
return fs.readFileSync(streamPath, "utf8")
return null
}
http.createServer(async (req, res) => {
@ -61,7 +55,7 @@ http.createServer(async (req, res) => {
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.write("<html><body>OK</body></html>", "utf8")
res.end()

View file

@ -24,12 +24,9 @@ import {
ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson
} from "../Models/ThemeConfig/Json/TagRenderingConfigJson";
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import Hash from "./Web/Hash"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
export default class DetermineLayout {
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 {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
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 layerConfig = <LayerConfigJson>json
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
json = {
id: json.id,
@ -193,34 +194,25 @@ export default class DetermineLayout {
sharedLayers: knownLayersDict,
publicLayers: new Set<string>(),
}
json = new FixLegacyTheme().convertStrict(json, "While loading a dynamic theme")
json = new FixLegacyTheme().convertStrict(json)
const raw = json
json = new FixImages(DetermineLayout._knownImages).convertStrict(
json,
"While fixing the images"
)
json = new FixImages(DetermineLayout._knownImages).convertStrict(json)
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)
json.id = forceId ?? json.id
{
let { errors } = new PrevalidateTheme().convert(json, "validation")
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")
}
new PrevalidateTheme().convertStrict(json)
}
{
let { errors } = new ValidateThemeAndLayers(
new ValidateThemeAndLayers(
new DoesImageExist(new Set<string>(), (_) => true),
"",
false
).convert(json, "validation")
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")
}
).convertStrict(json)
}
return new LayoutConfig(json, false, {
definitionRaw: JSON.stringify(raw, null, " "),

View file

@ -308,9 +308,6 @@ export class RegexTag extends TagsFilter {
if (typeof this.value === "string") {
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 }]
}
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 Translations from "../../../UI/i18n/Translations"
@ -117,15 +117,12 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* rewritten // => theme
*
*/
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(json: T, context: ConversionContext): T {
if (json["#dont-translate"] === "*") {
return { result: json }
return json
}
const result = Utils.WalkJson(
return Utils.WalkJson(
json,
(leaf, path) => {
if (leaf === undefined || leaf === null) {
@ -149,9 +146,5 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
},
(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 { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
@ -9,6 +8,91 @@ export interface DesugaringContext {
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> {
public readonly modifiedAttributes: string[]
public readonly name: string
@ -20,52 +104,24 @@ export abstract class Conversion<TIn, TOut> {
this.name = name
}
public static strict<T>(fixed: {
errors?: string[]
warnings?: string[]
information?: string[]
result?: T
}): T {
fixed.information?.forEach((i) => console.log(" ", i))
const yellow = (s) => "\x1b[33m" + s + "\x1b[0m"
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)))
public convertStrict(json: TIn, context?: ConversionContext): TOut {
context ??= ConversionContext.construct([], [])
context = context.enter(this.name)
const fixed = this.convert(json, context)
for (const msg of context.messages) {
ConversionContext.print(msg)
}
if (context.hasErrors()) {
throw "Detected one or more errors, stopping now"
}
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
return fixed
}
public andThenF<X>(f: (tout: TOut) => X): Conversion<TIn, X> {
return new Pipe(this, new Pure(f))
}
abstract convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] }
public abstract convert(json: TIn, context: ConversionContext): TOut
}
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
}
convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
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,
convert(json: TIn, context: ConversionContext): TOut {
const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
if (context.hasErrors()) {
return undefined
}
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
}
convert(
json: TIn,
context: string
): { result: TOut; errors?: string[]; warnings?: string[]; information?: string[] } {
return { result: this._f(json) }
convert(json: TIn, context: ConversionContext): TOut {
return this._f(json)
}
}
@ -134,31 +170,19 @@ export class Each<X, Y> extends Conversion<X[], Y[]> {
this._step = step
}
convert(
values: X[],
context: string
): { result: Y[]; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(values: X[], context: ConversionContext): Y[] {
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 result: Y[] = []
for (let i = 0; i < values.length; i++) {
const r = step.convert(values[i], context + "[" + i + "]")
Utils.PushList(information, r.information)
Utils.PushList(warnings, r.warnings)
Utils.PushList(errors, r.errors)
result.push(r.result)
}
return {
information,
errors,
warnings,
result,
const context_ = context.enter(i).inOperation("each")
const r = step.convert(values[i], context_)
result.push(r)
}
return result
}
}
@ -180,23 +204,17 @@ export class On<P, T> extends DesugaringStep<T> {
this.key = key
}
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(json: T, context: ConversionContext): T {
json = { ...json }
const step = this.step(json)
const key = this.key
const value: P = json[key]
if (value === undefined || value === null) {
return { result: json }
}
const r = step.convert(value, context + "." + key)
json[key] = r.result
return {
...r,
result: json,
return undefined
}
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")
}
convert(
json: T,
context: string
): { result: T; errors?: string[]; warnings?: string[]; information?: string[] } {
return {
result: json,
}
convert(json: T, context: ConversionContext): T {
return json
}
}
@ -227,25 +240,13 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
this._step = step
}
convert(
values: X[],
context: string
): { result: T[]; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(values: X[], context: ConversionContext): T[] {
if (values === undefined || values === null) {
// Move on - nothing to see here!
return {
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,
return <undefined | null>values
}
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
}
convert(
json: T,
context: string
): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
const reslt = this._conversion.convert(json, context)
return {
...reslt,
result: reslt.result[0],
convert(json: T, context: ConversionContext): X {
const values = this._conversion.convert(json, context.inOperation("firstOf"))
if (values.length === 0) {
return undefined
}
return values[0]
}
}
@ -287,38 +285,24 @@ export class Fuse<T> extends DesugaringStep<T> {
this.steps = Utils.NoNull(steps)
}
convert(
json: T,
context: string
): { result: T; errors: string[]; warnings: string[]; information: string[] } {
const errors = []
const warnings = []
const information = []
convert(json: T, context: ConversionContext): T {
for (let i = 0; i < this.steps.length; i++) {
const step = this.steps[i]
try {
let r = step.convert(json, "While running step " + step.name + ": " + context)
if (r.result["tagRenderings"]?.some((tr) => tr === 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) {
const r = step.convert(json, context.inOperation(step.name))
if (r === undefined) {
break
}
if (context.hasErrors()) {
break
}
json = r
} catch (e) {
console.error("Step " + step.name + " failed due to ", e, e.stack)
throw e
}
}
return {
result: json,
errors,
warnings,
information,
}
return json
}
}
@ -334,14 +318,15 @@ export class SetDefault<T> extends DesugaringStep<T> {
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)) {
json = { ...json }
json[this.key] = this.value
}
return {
result: json,
}
return json
}
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import {
Concat,
Conversion,
ConversionContext,
DesugaringContext,
DesugaringStep,
Each,
@ -33,12 +34,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
this._state = state
}
convert(
json: string | LayerConfigJson,
context: string
): { result: LayerConfigJson[]; errors: string[]; information?: string[] } {
const errors = []
const information = []
convert(json: string | LayerConfigJson, context: ConversionContext): LayerConfigJson[] {
const state = this._state
function reportNotFound(name: string) {
@ -50,7 +46,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
withDistance.sort((a, b) => a[1] - b[1])
const ids = withDistance.map((n) => n[0])
// 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`)
}
@ -58,119 +54,101 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
const found = state.sharedLayers.get(json)
if (found === undefined) {
reportNotFound(json)
return {
result: null,
errors,
}
}
return {
result: [found],
errors,
return null
}
return [found]
}
if (json["builtin"] !== undefined) {
let names = json["builtin"]
if (typeof names === "string") {
names = [names]
if (json["builtin"] === undefined) {
return [json]
}
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) {
const found = Utils.Clone(state.sharedLayers.get(name))
if (found === undefined) {
reportNotFound(name)
continue
}
if (
json["override"]["tagRenderings"] !== undefined &&
(found["tagRenderings"] ?? []).length > 0
) {
errors.push(
`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.`
)
}
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 " +
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])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as its id is a forbidden label"
" as it has a forbidden label: " +
labels[forbiddenLabel]
)
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) {
errors.push(
"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"
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
context.info(
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
)
continue
}
found.tagRenderings = filtered
}
}
return {
result: layers,
errors,
information,
}
}
return {
result: [json],
errors,
if (hideLabels.has(tr["group"])) {
usedLabels.add(tr["group"])
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
}
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
const errors = []
const warnings = []
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const state = this._state
json.layers = [...json.layers]
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) {
const v = state.sharedLayers.get(layerName)
if (v === undefined) {
errors.push("Default layer " + layerName + " not found")
context.err("Default layer " + layerName + " not found")
continue
}
if (alreadyLoaded.has(v.id)) {
warnings.push(
context.warn(
"Layout " +
context +
" already has a layer with name " +
@ -215,11 +188,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
json.layers.push(v)
}
return {
result: json,
errors,
warnings,
}
return json
}
}
@ -232,21 +201,13 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
)
}
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
if (!(json.enableNoteImports ?? true)) {
return {
warnings: [
"Not creating a note import layers for theme " +
json.id +
" as they are disabled",
],
result: json,
}
context.info(
"Not creating a note import layers for theme " + json.id + " as they are disabled"
)
return json
}
const errors = []
json = { ...json }
const allLayers: LayerConfigJson[] = <LayerConfigJson[]>json.layers
@ -278,20 +239,17 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
try {
const importLayerResult = creator.convert(
layer,
context + ".(noteimportlayer)[" + i1 + "]"
context.inOperation(this.name).enter(i1)
)
if (importLayerResult.result !== undefined) {
json.layers.push(importLayerResult.result)
if (importLayerResult !== undefined) {
json.layers.push(importLayerResult)
}
} 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 {
errors,
result: json,
}
return json
}
}
@ -304,17 +262,9 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
)
}
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
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(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[] } {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const overrideAll = json.overrideAll
if (overrideAll === undefined) {
return { result: json, warnings: [], errors: [] }
return json
}
json = { ...json }
@ -346,8 +293,7 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
newLayers.push(layer)
}
json.layers = newLayers
return { result: json, warnings: [], errors: [] }
return json
}
}
@ -458,18 +404,14 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
return dependenciesToAdd
}
convert(
theme: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; information: string[] } {
convert(theme: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const state = this._state
const allKnownLayers: Map<string, LayerConfigJson> = state.sharedLayers
const knownTagRenderings: Map<string, TagRenderingConfigJson> = state.tagRenderings
const information = []
const layers: LayerConfigJson[] = <LayerConfigJson[]>theme.layers // Layers should be expanded at this point
knownTagRenderings.forEach((value, key) => {
value.id = key
value["id"] = key
})
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(
@ -481,23 +423,16 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
}
if (dependencies.length > 0) {
for (const dependency of dependencies) {
information.push(
context +
": added " +
dependency.config.id +
" to the theme. " +
dependency.reason
context.info(
"Added " + dependency.config.id + " to the theme. " + dependency.reason
)
}
}
layers.unshift(...dependencies.map((l) => l.config))
return {
result: {
...theme,
layers: layers,
},
information,
...theme,
layers: layers,
}
}
}
@ -510,17 +445,9 @@ class PreparePersonalTheme extends DesugaringStep<LayoutConfigJson> {
this._state = state
}
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
if (json.id !== "personal") {
return { result: json }
return json
}
// 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())
.filter((l) => this._state.sharedLayers.get(l).source !== null)
.filter((l) => this._state.publicLayers.has(l))
return {
result: json,
information: ["The personal theme has " + json.layers.length + " public layers"],
}
context.info("The personal theme has " + json.layers.length + " public layers")
return json
}
}
@ -545,19 +470,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
)
}
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
if (json.hideFromOverview === true) {
return { result: json }
return json
}
const warnings = []
for (const layer of json.layers) {
if (typeof layer === "string") {
continue
@ -570,18 +486,15 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
continue
}
const wrn =
context.warn(
"The theme " +
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
warnings.push(wrn)
}
return {
result: json,
warnings,
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
)
}
return json
}
}
@ -616,29 +529,25 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
this.state = state
}
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const result = super.convert(json, context)
if (this.state.publicLayers.size === 0) {
// THis is a bootstrapping run, no need to already set this flag
return result
}
const needsNodeDatabase = result.result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr: TagRenderingConfigJson) =>
ValidationUtils.getSpecialVisualisations(tr)?.some(
const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr) =>
ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
(special) => special.needsNodeDatabase
)
)
)
if (needsNodeDatabase) {
result.information.push(
context +
": setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
context.info(
"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

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 LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
@ -33,12 +33,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
this._languages = languages ?? ["en"]
}
convert(
obj: any,
context: string
): { result: LayerConfig; errors: string[]; warnings: string[] } {
const errors = []
const warnings: string[] = []
convert(obj: any, context: ConversionContext): LayerConfig {
const translations = Translation.ExtractAllTranslationsFrom(obj)
for (const neededLanguage of this._languages) {
translations
@ -48,23 +43,20 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
t.tr.translations["*"] === undefined
)
.forEach((missing) => {
errors.push(
context +
"A theme should be translation-complete for " +
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en")
)
context
.enter(missing.context.split("."))
.err(
`The theme ${obj.id} should be translation-complete for ` +
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en")
)
})
}
return {
result: obj,
errors,
warnings,
}
return obj
}
}
@ -84,58 +76,47 @@ export class DoesImageExist extends DesugaringStep<string> {
this.doesPathExist = checkExistsSync
}
convert(
image: string,
context: string
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
convert(image: string, context: ConversionContext): string {
if (this._ignore?.has(image)) {
return { result: image }
return image
}
const errors = []
const warnings = []
const information = []
if (image.indexOf("{") >= 0) {
information.push("Ignoring image with { in the path: " + image)
return { result: image }
context.info("Ignoring image with { in the path: " + image)
return image
}
if (image === "assets/SocialImage.png") {
return { result: image }
return image
}
if (image.match(/[a-z]*/)) {
if (Svg.All[image + ".svg"] !== undefined) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
return { result: image }
return image
}
}
if (image.startsWith("<") && image.endsWith(">")) {
// This is probably HTML, you're on your own here
return { result: image }
return image
}
if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) {
errors.push(
context.err(
`Image with path ${image} not found or not attributed; it is used in ${context}`
)
} 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.`
)
} 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`
)
}
}
return {
result: image,
errors,
warnings,
information,
}
return image
}
}
@ -165,28 +146,20 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
}
convert(
json: LayoutConfigJson,
context: string
): { result: LayoutConfigJson; errors: string[]; warnings: string[]; information: string[] } {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const theme = new LayoutConfig(json, this._isBuiltin)
{
// Legacy format checks
if (this._isBuiltin) {
if (json["units"] !== undefined) {
errors.push(
context.err(
"The theme " +
json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
)
}
if (json["roamingRenderings"] !== undefined) {
errors.push(
context.err(
"Theme " +
json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
@ -196,10 +169,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
if (this._isBuiltin && this._extractImages !== undefined) {
// 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)
for (const remoteImage of remoteImages) {
errors.push(
context.err(
"Found a remote image: " +
remoteImage +
" in theme " +
@ -208,20 +181,14 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
)
}
for (const image of images) {
this._validateImage.convertJoin(
image.path,
context === undefined ? "" : ` in the theme ${context} at ${image.context}`,
errors,
warnings,
information
)
this._validateImage.convert(image.path, context.enters(image.context))
}
}
try {
if (this._isBuiltin) {
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(
@ -229,7 +196,7 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
this._path.length - 5
)
if (theme.id !== filename) {
errors.push(
context.err(
"Theme ids should be the same as the name.json, but we got id: " +
theme.id +
" and filename " +
@ -239,54 +206,41 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
")"
)
}
this._validateImage.convertJoin(
theme.icon,
context + ".icon",
errors,
warnings,
information
)
this._validateImage.convert(theme.icon, context.enter("icon"))
}
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) {
errors.push(
context.err(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`
)
}
if (json["mustHaveLanguage"] !== undefined) {
const checked = new ValidateLanguageCompleteness(
...json["mustHaveLanguage"]
).convert(theme, theme.id)
errors.push(...checked.errors)
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
theme,
context
)
}
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
const targetLanguage = theme.title.SupportedLanguages()[0]
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`
)
}
// Official, public themes must have a full english translation
const checked = new ValidateLanguageCompleteness("en").convert(theme, theme.id)
errors.push(...checked.errors)
new ValidateLanguageCompleteness("en").convert(theme, context)
}
} catch (e) {
errors.push(e)
context.err(e)
}
if (theme.id !== "personal") {
new DetectDuplicatePresets().convertJoin(theme, context, errors, warnings, information)
new DetectDuplicatePresets().convert(theme, context)
}
return {
result: json,
errors,
warnings,
information,
}
return json
}
}
@ -314,16 +268,12 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
)
}
convert(
json: LayoutConfigJson,
_: string
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
const overrideAll = json.overrideAll
if (overrideAll === undefined) {
return { result: json }
return json
}
const errors = []
const withOverride = json.layers.filter((l) => l["override"] !== undefined)
for (const layer of withOverride) {
@ -342,12 +292,12 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
" has a shadowed property: " +
key +
" 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")
}
convert(
json: LayoutConfigJson,
context: string
): {
result: LayoutConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const warnings = []
const errors = []
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
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 === "") {
warnings.push("Social image for theme " + json.id + " is the emtpy string")
}
return {
result: json,
warnings,
errors,
context.warn("Social image for theme " + json.id + " is the emtpy string")
}
return json
}
}
@ -400,17 +336,9 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
)
}
convert(
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (!(json.mappings?.length > 0)) {
return { result: json }
return json
}
const tagRendering = new TagRenderingConfig(json)
@ -438,10 +366,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
}
}
return {
result: json,
errors,
}
return json
}
}
@ -504,14 +429,9 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* 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
*/
convert(
json: TagRenderingConfigJson,
context: string
): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
const errors = []
const warnings = []
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
return { result: json }
return json
}
const defaultProperties = {}
for (const calculatedTagName of this._calculatedTagNames) {
@ -547,12 +467,12 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
json.mappings[j]["hideInAnswer"] === true &&
json.mappings[i]["hideInAnswer"] !== true
) {
warnings.push(
`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.`
context.warn(
`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) {
// 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(
false,
false,
@ -573,11 +493,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
}
}
return {
errors,
warnings,
result: json,
}
return json
}
}
@ -613,56 +529,40 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* r.errors.length > 0 // => true
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/
convert(
json: TagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
return { result: json }
return json
}
const ignoreToken = "ignore-image-in-then"
for (let i = 0; i < json.mappings.length; i++) {
const mapping = json.mappings[i]
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
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 (!ignore) {
errors.push(
`${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(
ctx.err(
`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`
)
} else {
information.push(
`${ctx}: Ignored image ${images.join(
ctx.info(
`Ignored image ${images.join(
", "
)} in 'then'-clause of a mapping as this check has been disabled`
)
for (const image of images) {
this._doesImageExist.convertJoin(image, ctx, errors, warnings, information)
this._doesImageExist.convert(image, ctx)
}
}
} else if (ignore) {
warnings.push(`${ctx}: unused '${ignoreToken}' - please remove this`)
ctx.warn(`Unused '${ignoreToken}' - please remove this`)
}
}
return {
errors,
warnings,
information,
result: json,
}
return json
}
}
@ -701,20 +601,12 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
convert(
json: string | Record<string, string>,
context: string
): {
result: string | Record<string, string>
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors = []
context: ConversionContext
): string | Record<string, string> {
if (typeof json === "string") {
if (this.isTabnabbingProne(json)) {
errors.push(
"At " +
context +
": the string " +
context.err(
"The string " +
json +
" 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 {
for (const k in json) {
if (this.isTabnabbingProne(json[k])) {
errors.push(
`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`
context.err(
`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 {
errors,
result: json,
}
return json
}
}
@ -745,50 +634,31 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
convert(
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
context: string
): {
result: TagRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
const warnings = []
const errors = []
context: ConversionContext
): TagRenderingConfigJson {
if (json["special"] !== undefined) {
errors.push(
"At " +
context +
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
context.err(
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
)
}
if (json["group"]) {
errors.push(
"At " +
context +
': groups are deprecated, use `"label": ["' +
json["group"] +
'"]` instead'
)
context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead')
}
const freeformType = json["freeform"]?.["type"]
if (freeformType) {
if (Validators.availableTypes.indexOf(freeformType) < 0) {
throw (
"At " +
context +
".freeform.type is an unknown type: " +
freeformType +
"; try one of " +
Validators.availableTypes.join(", ")
)
context
.enters("freeform", "type")
.err(
"Unknown type: " +
freeformType +
"; try one of " +
Validators.availableTypes.join(", ")
)
}
}
return {
result: json,
errors,
warnings,
}
return json
}
}
@ -828,24 +698,21 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
this._doesImageExist = doesImageExist
}
convert(
json: LayerConfigJson,
context: string
): { result: LayerConfigJson; errors: string[]; warnings?: string[]; information?: string[] } {
const errors = []
const warnings = []
const information = []
context = "While validating a layer: " + context
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
context = context.inOperation(this.name)
if (typeof json === "string") {
errors.push(context + ": This layer hasn't been expanded: " + json)
return {
result: null,
errors,
}
context.err("This layer hasn't been expanded: " + json)
return null
}
const layerConfig = new LayerConfig(json, "validation", true)
for (const [attribute, code, isStrict] of layerConfig.calculatedTags ?? []) {
let layerConfig: LayerConfig
try {
layerConfig = new LayerConfig(json, "validation", true)
} catch (e) {
context.err(e)
return undefined
}
for (const [_, code, __] of layerConfig.calculatedTags ?? []) {
try {
new Function("feat", "return " + code + ";")
} catch (e) {
@ -855,9 +722,8 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.source === "special") {
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
errors.push(
context +
": layer " +
context.err(
"Layer " +
json.id +
" 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.title === undefined && json.source !== "special:library") {
errors.push(
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."
context.err(
"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) {
information.push(
context +
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
context.info(
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
)
}
}
if (json["builtin"] !== undefined) {
errors.push(context + ": This layer hasn't been expanded: " + json)
return {
result: null,
errors,
}
context.err("This layer hasn't been expanded: " + json)
return null
}
if (json.minzoom > Constants.minZoomLevelToAddNewPoint) {
;(json.presets?.length > 0 ? errors : warnings).push(
`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 c = context.enter("minzoom")
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"])))
)
if (duplicates.length > 0) {
console.log(json.tagRenderings)
errors.push(
"At " +
context +
": some tagrenderings have a duplicate id: " +
duplicates.join(", ")
)
context
.enter("tagRenderings")
.err("Some tagrenderings have a duplicate id: " + duplicates.join(", "))
}
}
if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) {
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
if (json["overpassTags"] !== undefined) {
errors.push(
context.err(
"Layer " +
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)'
@ -938,18 +799,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
]
for (const forbiddenKey of forbiddenTopLevel) {
if (json[forbiddenKey] !== undefined)
errors.push(
context +
": layer " +
json.id +
" still has a forbidden key " +
forbiddenKey
context.err(
"Layer " + json.id + " still has a forbidden key " + forbiddenKey
)
}
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
errors.push(
context +
": layer " +
context.err(
"Layer " +
json.id +
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
@ -959,14 +815,14 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
json.isShown !== 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) {
// Check location of layer file
const expected: string = `assets/layers/${json.id}/${json.id}.json`
if (this._path != undefined && this._path.indexOf(expected) < 0) {
errors.push(
context.err(
"Layer is in an incorrect place. The path is " +
this._path +
", but expected " +
@ -984,11 +840,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
emptyIndexes.push(i)
}
}
errors.push(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${context}.tagRenderings.[${emptyIndexes.join(
","
)}])`
)
context
.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(
@ -997,29 +855,26 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
.filter((id) => id !== "questions")
)
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
errors.push(
`Some tagRenderings have a duplicate id: ${duplicateIds} (at ${context}.tagRenderings)`
)
context
.enter("tagRenderings")
.err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
}
if (json.description === undefined) {
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 {
warnings.push(context + ": A builtin layer should have a description")
context.warn("A builtin layer should have a description")
}
}
}
if (json.filter) {
const r = new On("filter", new Each(new ValidateFilter())).convert(json, context)
warnings.push(...(r.warnings ?? []))
errors.push(...(r.errors ?? []))
information.push(...(r.information ?? []))
new On("filter", new Each(new ValidateFilter())).convert(json, context)
}
if (json.tagRenderings !== undefined) {
const r = new On(
new On(
"tagRenderings",
new Each(
new ValidateTagRenderings(json, this._doesImageExist, {
@ -1027,9 +882,6 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
})
)
).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
)
if (hasCondition?.length > 0) {
errors.push(
"At " +
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" +
context.err(
"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, " ")
)
}
@ -1048,7 +898,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.presets !== undefined) {
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
const baseTags = TagUtils.Tag(json.source["osmTags"])
@ -1063,28 +913,22 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
const doMatch = baseTags.matchesProperties(properties)
if (!doMatch) {
errors.push(
context +
".presets[" +
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: " +
JSON.stringify(properties) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
context
.enters("presets", i)
.err(
"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) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
}
}
}
} catch (e) {
errors.push(e)
context.err(e)
}
return {
result: json,
errors,
warnings,
information,
}
return json
}
}
@ -1093,33 +937,27 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
super("Detect common errors in the filters", [], "ValidateFilter")
}
convert(
filter: FilterConfigJson,
context: string
): {
result: FilterConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(filter: FilterConfigJson, context: ConversionContext): FilterConfigJson {
if (typeof filter === "string") {
// Calling another filter, we skip
return { result: filter }
return filter
}
const errors = []
for (const option of filter.options) {
for (let i = 0; i < option.fields?.length ?? 0; i++) {
const field = option.fields[i]
const type = field.type ?? "string"
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(
Validators.availableTypes
).join(",")}`
errors.push(err)
context
.enters("fields", i)
.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(
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
__: string
): {
result: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }
errors?: string[]
warnings?: string[]
information?: string[]
} {
const errors: string[] = []
const warnings: string[] = []
const information: string[] = []
context: ConversionContext
): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } {
const { layers, themes } = json
const perOsmTag = new Map<
string,
@ -1191,15 +1020,10 @@ export class DetectDuplicateFilters extends DesugaringStep<{
}
msg += `\n - ${id}${layer.id}.${filter.id}`
}
warnings.push(msg)
context.warn(msg)
})
return {
result: json,
errors,
warnings,
information,
}
return json
}
/**
@ -1258,18 +1082,10 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
"DetectDuplicatePresets"
)
}
convert(
json: LayoutConfig,
context: string
): {
result: LayoutConfig
errors?: string[]
warnings?: string[]
information?: string[]
} {
convert(json: LayoutConfig, context: ConversionContext): LayoutConfig {
const presets: PresetConfig[] = [].concat(...json.layers.map((l) => l.presets))
const errors = []
const enNames = presets.map((p) => p.title.textFor("en"))
if (new Set(enNames).size != enNames.length) {
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)
)
const layerIds = layersWithDup.map((l) => l.id)
errors.push(
`At ${context}: this themes has multiple presets which are named:${dups}, namely layers ${layerIds.join(
context.err(
`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`
)
@ -1298,8 +1114,8 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
presetB.preciseInput.snapToLayers
)
) {
errors.push(
`At ${context}: this themes has multiple presets with the same tags: ${presetATags.asHumanString(
context.err(
`This themes has multiple presets with the same tags: ${presetATags.asHumanString(
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?: {
/**
* 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
*/
key: string
@ -206,11 +206,14 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* question: What is the input type?
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* 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]}))
*/
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
* type: translation
*/
placeholder?: string | any
@ -236,8 +239,9 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
inline?: boolean
/**
* default value to enter if no previous tagging is present.
* Normally undefined (aka do not enter anything)
* question: What value should be entered in the text field if no value is set?
* This can help people to quickly enter the most common option
* ifunset: do not prefill the textfield
*/
default?: string
}

View file

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

View file

@ -14,6 +14,7 @@ import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Marker from "../../UI/Map/Marker.svelte"
import DynamicMarker from "../../UI/Map/DynamicMarker.svelte"
export class IconConfig extends WithContextLoader {
public readonly icon: TagRenderingConfig
@ -45,8 +46,7 @@ export default class PointRenderingConfig extends WithContextLoader {
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
>
// public readonly icon?: TagRenderingConfig
private readonly marker: IconConfig[]
public readonly marker: IconConfig[]
public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[]
public readonly iconSize: TagRenderingConfig
public readonly anchor: TagRenderingConfig
@ -192,7 +192,7 @@ export default class PointRenderingConfig extends WithContextLoader {
}
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(
tags: Store<Record<string, string>>,
@ -244,7 +244,9 @@ export default class PointRenderingConfig extends WithContextLoader {
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
if (options?.includeBadges ?? true) {
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">
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";
@ -27,60 +25,56 @@
*
* 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";
export let icon: string | undefined;
export let color: string | undefined;
</script>
{#if iconItem}
{#if icon}
<div class="absolute top-0 left-0 w-full h-full">
{#if iconItem === "pin"}
{#if icon === "pin"}
<Pin {color} />
{:else if iconItem === "square"}
{:else if icon === "square"}
<Square {color} />
{:else if iconItem === "circle"}
{:else if icon === "circle"}
<Circle {color} />
{:else if iconItem === "checkmark"}
{:else if icon === "checkmark"}
<Checkmark {color} />
{:else if iconItem === "clock"}
{:else if icon === "clock"}
<Clock {color} />
{:else if iconItem === "close"}
{:else if icon === "close"}
<Close {color} />
{:else if iconItem === "crosshair"}
{:else if icon === "crosshair"}
<Crosshair {color} />
{:else if iconItem === "help"}
{:else if icon === "help"}
<Help {color} />
{:else if iconItem === "home"}
{:else if icon === "home"}
<Home {color} />
{:else if iconItem === "invalid"}
{:else if icon === "invalid"}
<Invalid {color} />
{:else if iconItem === "location"}
{:else if icon === "location"}
<Location {color} />
{:else if iconItem === "location_empty"}
{:else if icon === "location_empty"}
<Location_empty {color} />
{:else if iconItem === "location_locked"}
{:else if icon === "location_locked"}
<Location_locked {color} />
{:else if iconItem === "note"}
{:else if icon === "note"}
<Note {color} />
{:else if iconItem === "resolved"}
{:else if icon === "resolved"}
<Resolved {color} />
{:else if iconItem === "ring"}
{:else if icon === "ring"}
<Ring {color} />
{:else if iconItem === "scissors"}
{:else if icon === "scissors"}
<Scissors {color} />
{:else if iconItem === "teardrop"}
{:else if icon === "teardrop"}
<Teardrop {color} />
{:else if iconItem === "teardrop_with_hole_green"}
{:else if icon === "teardrop_with_hole_green"}
<Teardrop_with_hole_green {color} />
{:else if iconItem === "triangle"}
{:else if icon === "triangle"}
<Triangle {color} />
{:else}
<img class="w-full h-full" src={iconItem} />
<img class="w-full h-full" src={icon} />
{/if}
</div>
{/if}

View file

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

View file

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

View file

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

View file

@ -65,8 +65,13 @@ console.log("For ", schema.path, "got subparts", subparts)
}
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();
store.ping()
}
</script>

View file

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

View file

@ -214,5 +214,4 @@
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
{/each}
{/if}
{chosenOption}
</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 { Utils } from "../Utils";
import { UIEventSource } from "../Logic/UIEventSource";
import Constants from "../Models/Constants";
import ValidatedInput from "./InputElement/ValidatedInput.svelte";
import EditLayerState from "./Studio/EditLayerState";
import EditLayer from "./Studio/EditLayer.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";
let overview = UIEventSource.FromPromise<{ allFiles: string[] }>(Utils.downloadJson(studioUrl + "/overview"));
let layers = overview.map(overview => {
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);
});
const studio = new StudioServer(studioUrl);
let layers = UIEventSource.FromPromise(studio.fetchLayerOverview());
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 layerIdFeedback = new UIEventSource<string>(undefined);
newLayerId.addCallbackD(layerId => {
if (layerId === "") {
return;
}
if (layers.data.indexOf(layerId) >= 0) {
if (layers.data.has(layerId)) {
layerIdFeedback.setData("This id is already used");
}
}, [layers]);
@ -38,7 +35,28 @@
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>
<LoginToggle state={{osmConnection}}>
<div slot="not-logged-in" >
<NextButton clss="primary">
Please log in to use MapComplete Studio
</NextButton>
</div>
{#if state === undefined}
<h1>MapComplete Studio</h1>
<div class="w-full flex flex-col">
@ -58,13 +76,16 @@
</div>
{:else if state === "edit_layer"}
<div class="flex flex-wrap">
{#each $layers as layerId}
{#each Array.from($layers) as layerId}
<NextButton clss="small" on:click={async () => {
console.log("Editing layer",layerId)
state = "loading"
initialLayerConfig = await Utils.downloadJson(studioUrl+"/layers/"+layerId+"/"+layerId+".json")
initialLayerConfig = await studio.fetchLayer(layerId)
state = "editing_layer"
}}>
<div class="w-4 h-4 mr-1">
<Marker icons={fetchIconDescription(layerId)} />
</div>
{layerId}
</NextButton>
{/each}
@ -76,12 +97,22 @@
{$layerIdFeedback}
</div>
{: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
</NextButton>
{/if}
{:else if state === "loading"}
<Loading />
<div class="w-8 h-8">
<Loading />
</div>
{:else if state === "editing_layer"}
<EditLayer {initialLayerConfig} />
{/if}
</LoginToggle>

View file

@ -11745,6 +11745,10 @@
"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."
},
{
"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",
"then": "hackerspace - Hackerspace"
@ -12102,15 +12106,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -12129,7 +12133,7 @@
"type": "boolean"
},
"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"
}
},
@ -12816,10 +12820,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -12830,6 +12835,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -12902,6 +12908,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -12962,9 +12975,12 @@
"default"
],
"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",
"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": [
@ -13837,10 +13853,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -13852,6 +13869,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -13924,6 +13942,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -13989,9 +14014,12 @@
"default"
],
"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",
"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": [
@ -14888,10 +14916,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -14903,6 +14932,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -14975,6 +15005,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -15040,9 +15077,12 @@
"default"
],
"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",
"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": [
@ -15951,10 +15991,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -15967,6 +16008,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -16039,6 +16081,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -16109,9 +16158,12 @@
"default"
],
"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",
"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": [

View file

@ -663,15 +663,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -690,7 +690,7 @@
"type": "boolean"
},
"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"
}
},
@ -13206,6 +13206,10 @@
"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."
},
{
"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",
"then": "hackerspace - Hackerspace"
@ -13565,15 +13569,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -13592,7 +13596,7 @@
"type": "boolean"
},
"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"
}
},
@ -14300,10 +14304,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -14315,6 +14320,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -14387,6 +14393,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -14452,9 +14465,12 @@
"default"
],
"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",
"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": [
@ -15363,10 +15379,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -15379,6 +15396,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -15451,6 +15469,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -15521,9 +15546,12 @@
"default"
],
"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",
"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": [
@ -16457,10 +16485,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -16473,6 +16502,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -16545,6 +16575,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -16615,9 +16652,12 @@
"default"
],
"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",
"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": [
@ -17562,10 +17602,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -17579,6 +17620,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -17651,6 +17693,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -17726,9 +17775,12 @@
"default"
],
"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",
"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": [
@ -31420,6 +31472,10 @@
"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."
},
{
"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",
"then": "hackerspace - Hackerspace"
@ -31781,15 +31837,15 @@
"type": "object",
"properties": {
"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": {
"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"
},
"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": {
"description": "Extra parameters to initialize the input helper arguments.\nFor semantics, see the 'SpecialInputElements.md'",
@ -31808,7 +31864,7 @@
"type": "boolean"
},
"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"
}
},
@ -32537,10 +32593,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -32553,6 +32610,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -32625,6 +32683,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -32695,9 +32760,12 @@
"default"
],
"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",
"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": [
@ -33642,10 +33710,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -33659,6 +33728,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -33731,6 +33801,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -33806,9 +33883,12 @@
"default"
],
"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",
"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": [
@ -34779,10 +34859,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -34796,6 +34877,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -34868,6 +34950,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -34943,9 +35032,12 @@
"default"
],
"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",
"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": [
@ -35926,10 +36018,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -35944,6 +36037,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -36016,6 +36110,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -36096,9 +36197,12 @@
"default"
],
"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",
"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": [

View file

@ -473,10 +473,11 @@
],
"required": true,
"hints": {
"question": "What is the name of the attribute that should be written to?",
"ifunset": "do not offer a freeform textfield as answer option"
},
"type": "string",
"description": "question What is the name of the attribute that should be written to?"
"description": ""
},
{
"path": [
@ -486,6 +487,7 @@
"required": false,
"hints": {
"question": "What is the input type?",
"ifunset": "use an unconstrained <b>string</b> as input (default)",
"suggestions": [
{
"if": "value=string",
@ -558,6 +560,10 @@
{
"if": "value=fediverse",
"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"
],
"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"
},
{
@ -613,9 +622,12 @@
"default"
],
"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",
"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": [