Add various improvements and fixes to studio, should fix #2055

This commit is contained in:
Pieter Vander Vennet 2024-08-02 19:06:14 +02:00
parent b19d9ef077
commit d1ec9a43fc
19 changed files with 532 additions and 419 deletions

View file

@ -993,10 +993,6 @@ video {
margin-right: 4rem; margin-right: 4rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 { .mt-4 {
margin-top: 1rem; margin-top: 1rem;
} }
@ -1029,6 +1025,10 @@ video {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mb-4 {
margin-bottom: 1rem;
}
.ml-1 { .ml-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
@ -4686,6 +4686,16 @@ textarea {
color: black; color: black;
} }
h2.group {
/* For flowbite accordions */
margin: 0;
}
.group button {
/* For flowbite accordions */
border-radius: 0;
}
/************************* OTHER CATEGORIES ********************************/ /************************* OTHER CATEGORIES ********************************/
/** /**

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */ /** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging { export class ThemeMetaTagging {
public static readonly themeName = "usersettings" public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) { public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () => Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
feat.properties._description Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
?.at(1) Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
) Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
Utils.AddLazyProperty( feat.properties['__current_backgroun'] = 'initial_value'
feat.properties, }
"_d", }
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -73,15 +73,20 @@ export abstract class DesugaringStep<T> extends Conversion<T, T> {}
export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> { export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
private readonly _step0: Conversion<TIn, TInter> private readonly _step0: Conversion<TIn, TInter>
private readonly _step1: Conversion<TInter, TOut> private readonly _step1: Conversion<TInter, TOut>
private readonly _failfast: boolean
constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>) { constructor(step0: Conversion<TIn, TInter>, step1: Conversion<TInter, TOut>, failfast = false) {
super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`) super("Merges two steps with different types", [], `Pipe(${step0.name}, ${step1.name})`)
this._step0 = step0 this._step0 = step0
this._step1 = step1 this._step1 = step1
this._failfast = failfast
} }
convert(json: TIn, context: ConversionContext): TOut { convert(json: TIn, context: ConversionContext): TOut {
const r0 = this._step0.convert(json, context.inOperation(this._step0.name)) const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
if(context.hasErrors() && this._failfast){
return undefined
}
return this._step1.convert(r0, context.inOperation(this._step1.name)) return this._step1.convert(r0, context.inOperation(this._step1.name))
} }
} }

View file

@ -21,6 +21,7 @@ import DependencyCalculator from "../DependencyCalculator"
import { AddContextToTranslations } from "./AddContextToTranslations" import { AddContextToTranslations } from "./AddContextToTranslations"
import ValidationUtils from "./ValidationUtils" import ValidationUtils from "./ValidationUtils"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { PrevalidateTheme } from "./Validation"
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> { class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
private readonly _state: DesugaringContext private readonly _state: DesugaringContext
@ -664,7 +665,6 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
) { ) {
super( super(
"Fully prepares and expands a theme", "Fully prepares and expands a theme",
new AddContextToTranslationsInLayout(), new AddContextToTranslationsInLayout(),
new PreparePersonalTheme(state), new PreparePersonalTheme(state),
new WarnForUnsubstitutedLayersInTheme(), new WarnForUnsubstitutedLayersInTheme(),

View file

@ -36,7 +36,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
super( super(
"Checks that the given object is fully translated in the specified languages", "Checks that the given object is fully translated in the specified languages",
[], [],
"ValidateLanguageCompleteness" "ValidateLanguageCompleteness",
) )
this._languages = languages ?? ["en"] this._languages = languages ?? ["en"]
} }
@ -50,18 +50,18 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
.filter( .filter(
(t) => (t) =>
t.tr.translations[neededLanguage] === undefined && t.tr.translations[neededLanguage] === undefined &&
t.tr.translations["*"] === undefined t.tr.translations["*"] === undefined,
) )
.forEach((missing) => { .forEach((missing) => {
context context
.enter(missing.context.split(".")) .enter(missing.context.split("."))
.err( .err(
`The theme ${obj.id} should be translation-complete for ` + `The theme ${obj.id} should be translation-complete for ` +
neededLanguage + neededLanguage +
", but it lacks a translation for " + ", but it lacks a translation for " +
missing.context + missing.context +
".\n\tThe known translation is " + ".\n\tThe known translation is " +
missing.tr.textFor("en") missing.tr.textFor("en"),
) )
}) })
} }
@ -78,7 +78,7 @@ export class DoesImageExist extends DesugaringStep<string> {
constructor( constructor(
knownImagePaths: Set<string>, knownImagePaths: Set<string>,
checkExistsSync: (path: string) => boolean = undefined, checkExistsSync: (path: string) => boolean = undefined,
ignore?: Set<string> ignore?: Set<string>,
) { ) {
super("Checks if an image exists", [], "DoesImageExist") super("Checks if an image exists", [], "DoesImageExist")
this._ignore = ignore this._ignore = ignore
@ -114,15 +114,15 @@ export class DoesImageExist extends DesugaringStep<string> {
if (!this._knownImagePaths.has(image)) { if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) { if (this.doesPathExist === undefined) {
context.err( context.err(
`Image with path ${image} not found or not attributed; it is used in ${context}` `Image with path ${image} not found or not attributed; it is used in ${context}`,
) )
} else if (!this.doesPathExist(image)) { } else if (!this.doesPathExist(image)) {
context.err( context.err(
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.` `Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`,
) )
} else { } else {
context.err( context.err(
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info` `Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`,
) )
} }
} }
@ -146,7 +146,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist,
path: string, path: string,
isBuiltin: boolean, isBuiltin: boolean,
sharedTagRenderings?: Set<string> sharedTagRenderings?: Set<string>,
) { ) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme") super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
this._validateImage = doesImageExist this._validateImage = doesImageExist
@ -165,15 +165,15 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
if (json["units"] !== undefined) { if (json["units"] !== undefined) {
context.err( context.err(
"The theme " + "The theme " +
json.id + json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ",
) )
} }
if (json["roamingRenderings"] !== undefined) { if (json["roamingRenderings"] !== undefined) {
context.err( context.err(
"Theme " + "Theme " +
json.id + json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead" " contains an old 'roamingRenderings'. Use an 'overrideAll' instead",
) )
} }
} }
@ -191,10 +191,10 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
for (const remoteImage of remoteImages) { for (const remoteImage of remoteImages) {
context.err( context.err(
"Found a remote image: " + "Found a remote image: " +
remoteImage.path + remoteImage.path +
" in theme " + " in theme " +
json.id + json.id +
", please download it." ", please download it.",
) )
} }
for (const image of images) { for (const image of images) {
@ -210,17 +210,17 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const filename = this._path.substring( const filename = this._path.substring(
this._path.lastIndexOf("/") + 1, this._path.lastIndexOf("/") + 1,
this._path.length - 5 this._path.length - 5,
) )
if (theme.id !== filename) { if (theme.id !== filename) {
context.err( context.err(
"Theme ids should be the same as the name.json, but we got id: " + "Theme ids should be the same as the name.json, but we got id: " +
theme.id + theme.id +
" and filename " + " and filename " +
filename + filename +
" (" + " (" +
this._path + this._path +
")" ")",
) )
} }
this._validateImage.convert(theme.icon, context.enter("icon")) this._validateImage.convert(theme.icon, context.enter("icon"))
@ -228,13 +228,13 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"])) const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) { if (dups.length > 0) {
context.err( context.err(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}` `The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`,
) )
} }
if (json["mustHaveLanguage"] !== undefined) { if (json["mustHaveLanguage"] !== undefined) {
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert( new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
theme, theme,
context context,
) )
} }
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) { if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
@ -242,7 +242,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const targetLanguage = theme.title.SupportedLanguages()[0] const targetLanguage = theme.title.SupportedLanguages()[0]
if (targetLanguage !== "en") { if (targetLanguage !== "en") {
context.err( context.err(
`TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key` `TargetLanguage is not 'en' for public theme ${theme.id}, it is ${targetLanguage}. Move 'en' up in the title of the theme and set it as the first key`,
) )
} }
@ -286,7 +286,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
.err( .err(
`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby `This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby
.slice(0, 5) .slice(0, 5)
.join(", ")}` .join(", ")}`,
) )
} }
} }
@ -309,7 +309,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist,
path: string, path: string,
isBuiltin: boolean, isBuiltin: boolean,
sharedTagRenderings?: Set<string> sharedTagRenderings?: Set<string>,
) { ) {
super( super(
"Validates a theme and the contained layers", "Validates a theme and the contained layers",
@ -319,10 +319,10 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
new Each( new Each(
new Bypass( new Bypass(
(layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0, (layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0,
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true),
) ),
) ),
) ),
) )
} }
} }
@ -332,7 +332,7 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
super( super(
"Checks that an 'overrideAll' does not override a single override", "Checks that an 'overrideAll' does not override a single override",
[], [],
"OverrideShadowingCheck" "OverrideShadowingCheck",
) )
} }
@ -378,6 +378,9 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) { if (json.id !== "personal" && (json.layers === undefined || json.layers.length === 0)) {
context.err("The theme " + json.id + " has no 'layers' defined") context.err("The theme " + json.id + " has no 'layers' defined")
} }
if (!Array.isArray(json.layers)) {
context.enter("layers").err("The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?")
}
if (json.socialImage === "") { if (json.socialImage === "") {
context.warn("Social image for theme " + json.id + " is the emtpy string") context.warn("Social image for theme " + json.id + " is the emtpy string")
} }
@ -406,7 +409,7 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
context context
.enter("overideAll") .enter("overideAll")
.err( .err(
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them." "'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.",
) )
} }
return json return json
@ -418,7 +421,7 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
super( super(
"Various consistency checks on the raw JSON", "Various consistency checks on the raw JSON",
new MiscThemeChecks(), new MiscThemeChecks(),
new OverrideShadowingCheck() new OverrideShadowingCheck(),
) )
} }
} }
@ -428,7 +431,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
super( super(
"The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values", "The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values",
[], [],
"DetectConflictingAddExtraTags" "DetectConflictingAddExtraTags",
) )
} }
@ -455,7 +458,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
.enters("mappings", i) .enters("mappings", i)
.err( .err(
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " + "AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
duplicateKeys.join(", ") duplicateKeys.join(", "),
) )
} }
} }
@ -473,13 +476,13 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
super( super(
"A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys", "A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys",
[], [],
"DetectNonErasedKeysInMappings" "DetectNonErasedKeysInMappings",
) )
} }
convert( convert(
json: QuestionableTagRenderingConfigJson, json: QuestionableTagRenderingConfigJson,
context: ConversionContext context: ConversionContext,
): QuestionableTagRenderingConfigJson { ): QuestionableTagRenderingConfigJson {
if (json.multiAnswer) { if (json.multiAnswer) {
// No need to check this here, this has its own validation // No need to check this here, this has its own validation
@ -533,8 +536,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("freeform") .enters("freeform")
.warn( .warn(
"The freeform block does not modify the key `" + "The freeform block does not modify the key `" +
neededKey + neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it" "` which is set in a mapping. Use `addExtraTags` to overwrite it",
) )
} }
} }
@ -552,8 +555,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("mappings", i) .enters("mappings", i)
.warn( .warn(
"This mapping does not modify the key `" + "This mapping does not modify the key `" +
neededKey + neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it" "` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it",
) )
} }
} }
@ -570,7 +573,7 @@ export class DetectMappingsShadowedByCondition extends DesugaringStep<TagRenderi
super( super(
"Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings", "Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings",
[], [],
"DetectMappingsShadowedByCondition" "DetectMappingsShadowedByCondition",
) )
this._forceError = forceError this._forceError = forceError
} }
@ -642,7 +645,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"] * DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
*/ */
private static extractCalculatedTagNames( private static extractCalculatedTagNames(
layerConfig?: LayerConfigJson | { calculatedTags: string[] } layerConfig?: LayerConfigJson | { calculatedTags: string[] },
) { ) {
return ( return (
layerConfig?.calculatedTags?.map((ct) => { layerConfig?.calculatedTags?.map((ct) => {
@ -728,16 +731,16 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
json.mappings[i]["hideInAnswer"] !== true json.mappings[i]["hideInAnswer"] !== true
) { ) {
context.warn( 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.` `Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`,
) )
} else if (doesMatch) { } else if (doesMatch) {
// The current mapping is shadowed! // The current mapping is shadowed!
context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown: context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
The mapping ${parsedConditions[i].asHumanString( The mapping ${parsedConditions[i].asHumanString(
false, false,
false, false,
{} {},
)} is fully matched by a previous mapping (namely ${j}), which matches: )} is fully matched by a previous mapping (namely ${j}), which matches:
${parsedConditions[j].asHumanString(false, false, {})}. ${parsedConditions[j].asHumanString(false, false, {})}.
To fix this problem, you can try to: To fix this problem, you can try to:
@ -764,7 +767,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
super( super(
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead", "Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
[], [],
"DetectMappingsWithImages" "DetectMappingsWithImages",
) )
this._doesImageExist = doesImageExist this._doesImageExist = doesImageExist
} }
@ -804,14 +807,14 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
if (!ignore) { if (!ignore) {
ctx.err( 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( `A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
", " ", ",
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged` )}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`,
) )
} else { } else {
ctx.info( ctx.info(
`Ignored image ${images.join( `Ignored image ${images.join(
", " ", ",
)} in 'then'-clause of a mapping as this check has been disabled` )} in 'then'-clause of a mapping as this check has been disabled`,
) )
for (const image of images) { for (const image of images) {
@ -832,7 +835,7 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
super( super(
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set", "Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
[], [],
"ValidatePossibleLinks" "ValidatePossibleLinks",
) )
} }
@ -862,21 +865,21 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
convert( convert(
json: string | Record<string, string>, json: string | Record<string, string>,
context: ConversionContext context: ConversionContext,
): string | Record<string, string> { ): string | Record<string, string> {
if (typeof json === "string") { if (typeof json === "string") {
if (this.isTabnabbingProne(json)) { if (this.isTabnabbingProne(json)) {
context.err( context.err(
"The string " + "The string " +
json + json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping" " has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping",
) )
} }
} else { } else {
for (const k in json) { for (const k in json) {
if (this.isTabnabbingProne(json[k])) { if (this.isTabnabbingProne(json[k])) {
context.err( 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` `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`,
) )
} }
} }
@ -894,7 +897,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
super( super(
"Checks that a translation is valid and internally consistent", "Checks that a translation is valid and internally consistent",
["*"], ["*"],
"CheckTranslation" "CheckTranslation",
) )
this._allowUndefined = allowUndefined this._allowUndefined = allowUndefined
} }
@ -935,6 +938,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> { class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
private readonly _layerConfig: LayerConfigJson private readonly _layerConfig: LayerConfigJson
constructor(layerConfig?: LayerConfigJson) { constructor(layerConfig?: LayerConfigJson) {
super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks") super("Miscellaneous checks on the tagrendering", ["special"], "MiscTagRenderingChecks")
this._layerConfig = layerConfig this._layerConfig = layerConfig
@ -942,17 +946,17 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
convert( convert(
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson, json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
context: ConversionContext context: ConversionContext,
): TagRenderingConfigJson { ): TagRenderingConfigJson {
if (json["special"] !== undefined) { if (json["special"] !== undefined) {
context.err( context.err(
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`' "Detected `special` on the top level. Did you mean `{\"render\":{ \"special\": ... }}`",
) )
} }
if (Object.keys(json).length === 1 && typeof json["render"] === "string") { if (Object.keys(json).length === 1 && typeof json["render"] === "string") {
context.warn( context.warn(
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}` `use the content directly instead of {render: ${JSON.stringify(json["render"])}}`,
) )
} }
@ -964,7 +968,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
const mapping: MappingConfigJson = json.mappings[i] const mapping: MappingConfigJson = json.mappings[i]
CheckTranslation.noUndefined.convert( CheckTranslation.noUndefined.convert(
mapping.then, mapping.then,
context.enters("mappings", i, "then") context.enters("mappings", i, "then"),
) )
if (!mapping.if) { if (!mapping.if) {
console.log( console.log(
@ -973,7 +977,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
"if", "if",
mapping.if, mapping.if,
context.path.join("."), context.path.join("."),
mapping.then mapping.then,
) )
context.enters("mappings", i, "if").err("No `if` is defined") context.enters("mappings", i, "if").err("No `if` is defined")
} }
@ -983,7 +987,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enters("mappings", i, "addExtraTags", j) .enters("mappings", i, "addExtraTags", j)
.err( .err(
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item" "Detected a 'null' or 'undefined' value. Either specify a tag or delete this item",
) )
} }
} }
@ -994,18 +998,18 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enters("mappings", i, "then") .enters("mappings", i, "then")
.warn( .warn(
"A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box" "A mapping should not start with 'yes' or 'no'. If the attribute is known, it will only show 'yes' or 'no' <i>without</i> the question, resulting in a weird phrasing in the information box",
) )
} }
} }
} }
if (json["group"]) { if (json["group"]) {
context.err('Groups are deprecated, use `"label": ["' + json["group"] + '"]` instead') context.err("Groups are deprecated, use `\"label\": [\"" + json["group"] + "\"]` instead")
} }
if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) { if (json["question"] && json.freeform?.key === undefined && json.mappings === undefined) {
context.err( context.err(
"A question is defined, but no mappings nor freeform (key) are. Add at least one of them" "A question is defined, but no mappings nor freeform (key) are. Add at least one of them",
) )
} }
if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) { if (json["question"] && !json.freeform && (json.mappings?.length ?? 0) == 1) {
@ -1015,7 +1019,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enter("questionHint") .enter("questionHint")
.err( .err(
"A questionHint is defined, but no question is given. As such, the questionHint will never be shown" "A questionHint is defined, but no question is given. As such, the questionHint will never be shown",
) )
} }
@ -1023,7 +1027,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enters("icon", "size") .enters("icon", "size")
.err( .err(
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`" "size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`",
) )
} }
@ -1033,10 +1037,10 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
.enter("render") .enter("render")
.err( .err(
"This tagRendering allows to set a value to key " + "This tagRendering allows to set a value to key " +
json.freeform.key + json.freeform.key +
", but does not define a `render`. Please, add a value here which contains `{" + ", but does not define a `render`. Please, add a value here which contains `{" +
json.freeform.key + json.freeform.key +
"}`" "}`",
) )
} else { } else {
const render = new Translation(<any>json.render) const render = new Translation(<any>json.render)
@ -1067,7 +1071,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
const keyFirstArg = ["canonical", "fediverse_link", "translated"] const keyFirstArg = ["canonical", "fediverse_link", "translated"]
if ( if (
keyFirstArg.some( keyFirstArg.some(
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0 (funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0,
) )
) { ) {
continue continue
@ -1091,16 +1095,16 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enter("render") .enter("render")
.err( .err(
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?` `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`,
) )
continue continue
} }
if(txt.indexOf(json.freeform.key) >= 0 && txt.indexOf("{"+json.freeform.key+"}") < 0){ if (txt.indexOf(json.freeform.key) >= 0 && txt.indexOf("{" + json.freeform.key + "}") < 0) {
context context
.enter("render") .enter("render")
.err( .err(
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}` `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. However, it does contain ${json.freeform.key} without braces. Did you forget the braces?\n\tThe current text is ${txt}`,
) )
continue continue
} }
@ -1109,7 +1113,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context context
.enter("render") .enter("render")
.err( .err(
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}` `The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!\n\tThe current text is ${txt}`,
) )
} }
} }
@ -1124,22 +1128,22 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
.enters("freeform", "type") .enters("freeform", "type")
.err( .err(
"No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " + "No entry found in the 'Name Suggestion Index'. None of the 'osmSource'-tags match an entry in the NSI.\n\tOsmSource-tags are " +
tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; ") tags.map((t) => new Tag(t.key, t.value).asHumanString()).join(" ; "),
) )
} }
} else if (json.freeform.type === "nsi") { } else if (json.freeform.type === "nsi") {
context context
.enters("freeform", "type") .enters("freeform", "type")
.warn( .warn(
"No need to explicitly set type to 'NSI', autodetected based on freeform type" "No need to explicitly set type to 'NSI', autodetected based on freeform type",
) )
} }
} }
if (json.render && json["question"] && json.freeform === undefined) { if (json.render && json["question"] && json.freeform === undefined) {
context.err( context.err(
`Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation( `Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation(
json["question"] json["question"],
).textFor("en")}` ).textFor("en")}`,
) )
} }
@ -1150,9 +1154,9 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
.enters("freeform", "type") .enters("freeform", "type")
.err( .err(
"Unknown type: " + "Unknown type: " +
freeformType + freeformType +
"; try one of " + "; try one of " +
Validators.availableTypes.join(", ") Validators.availableTypes.join(", "),
) )
} }
} }
@ -1190,7 +1194,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
new On("question", new ValidatePossibleLinks()), new On("question", new ValidatePossibleLinks()),
new On("questionHint", new ValidatePossibleLinks()), new On("questionHint", new ValidatePossibleLinks()),
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))), new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
new MiscTagRenderingChecks(layerConfig) new MiscTagRenderingChecks(layerConfig),
) )
} }
} }
@ -1209,7 +1213,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
path: string, path: string,
isBuiltin: boolean, isBuiltin: boolean,
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist,
studioValidations: boolean studioValidations: boolean,
) { ) {
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer") super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
this._path = path this._path = path
@ -1235,7 +1239,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context context
.enter("source") .enter("source")
.err( .err(
"No source section is defined; please define one as data is not loaded otherwise" "No source section is defined; please define one as data is not loaded otherwise",
) )
} else { } else {
if (json.source === "special" || json.source === "special:library") { if (json.source === "special" || json.source === "special:library") {
@ -1243,7 +1247,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context context
.enters("source", "osmTags") .enters("source", "osmTags")
.err( .err(
"No osmTags defined in the source section - these should always be present, even for geojson layer" "No osmTags defined in the source section - these should always be present, even for geojson layer",
) )
} else { } else {
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags") const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
@ -1252,7 +1256,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enters("source", "osmTags") .enters("source", "osmTags")
.err( .err(
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" + "The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
osmTags.asHumanString(false, false, {}) osmTags.asHumanString(false, false, {}),
) )
} }
} }
@ -1278,10 +1282,10 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enter("syncSelection") .enter("syncSelection")
.err( .err(
"Invalid sync-selection: must be one of " + "Invalid sync-selection: must be one of " +
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") + LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
" but got '" + " but got '" +
json.syncSelection + json.syncSelection +
"'" "'",
) )
} }
if (json["pointRenderings"]?.length > 0) { if (json["pointRenderings"]?.length > 0) {
@ -1300,7 +1304,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
} }
json.pointRendering?.forEach((pr, i) => json.pointRendering?.forEach((pr, i) =>
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)) this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)),
) )
if (json["mapRendering"]) { if (json["mapRendering"]) {
@ -1317,8 +1321,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (!Constants.priviliged_layers.find((x) => x == json.id)) { if (!Constants.priviliged_layers.find((x) => x == json.id)) {
context.err( context.err(
"Layer " + "Layer " +
json.id + json.id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer" " uses 'special' as source.osmTags. However, this layer is not a priviliged layer",
) )
} }
} }
@ -1333,19 +1337,19 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context context
.enter("title") .enter("title")
.err( .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." "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error.",
) )
} }
if (json.title === null) { if (json.title === null) {
context.info( context.info(
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set.",
) )
} }
{ {
// Check for multiple, identical builtin questions - usability for studio users // Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils.Duplicates( const duplicates = Utils.Duplicates(
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string") <string[]>json.tagRenderings.filter((tr) => typeof tr === "string"),
) )
for (let i = 0; i < json.tagRenderings.length; i++) { for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i] const tagRendering = json.tagRenderings[i]
@ -1375,7 +1379,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{ {
// duplicate ids in tagrenderings check // duplicate ids in tagrenderings check
const duplicates = Utils.NoNull( const duplicates = Utils.NoNull(
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))),
) )
if (duplicates.length > 0) { if (duplicates.length > 0) {
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
@ -1383,11 +1387,11 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enter("tagRenderings") .enter("tagRenderings")
.err( .err(
"Some tagrenderings have a duplicate id: " + "Some tagrenderings have a duplicate id: " +
duplicates.join(", ") + duplicates.join(", ") +
"\n" + "\n" +
JSON.stringify( JSON.stringify(
json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0) json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0),
) ),
) )
} }
} }
@ -1420,8 +1424,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (json["overpassTags"] !== undefined) { if (json["overpassTags"] !== undefined) {
context.err( context.err(
"Layer " + "Layer " +
json.id + json.id +
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)' "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)",
) )
} }
const forbiddenTopLevel = [ const forbiddenTopLevel = [
@ -1441,7 +1445,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
} }
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) { if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
context.err( context.err(
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'" "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'",
) )
} }
@ -1458,9 +1462,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (this._path != undefined && this._path.indexOf(expected) < 0) { if (this._path != undefined && this._path.indexOf(expected) < 0) {
context.err( context.err(
"Layer is in an incorrect place. The path is " + "Layer is in an incorrect place. The path is " +
this._path + this._path +
", but expected " + ", but expected " +
expected expected,
) )
} }
} }
@ -1478,13 +1482,13 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enter(["tagRenderings", ...emptyIndexes]) .enter(["tagRenderings", ...emptyIndexes])
.err( .err(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join( `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
"," ",",
)}])` )}])`,
) )
} }
const duplicateIds = Utils.Duplicates( const duplicateIds = Utils.Duplicates(
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions") (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"),
) )
if (duplicateIds.length > 0 && !Utils.runningFromConsole) { if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
context context
@ -1508,7 +1512,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.tagRenderings !== undefined) { if (json.tagRenderings !== undefined) {
new On( new On(
"tagRenderings", "tagRenderings",
new Each(new ValidateTagRenderings(json, this._doesImageExist)) new Each(new ValidateTagRenderings(json, this._doesImageExist)),
).convert(json, context) ).convert(json, context)
} }
@ -1535,7 +1539,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context context
.enters("pointRendering", i, "marker", indexM, "icon", "condition") .enters("pointRendering", i, "marker", indexM, "icon", "condition")
.err( .err(
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead." "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead.",
) )
} }
} }
@ -1573,9 +1577,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enters("presets", i, "tags") .enters("presets", i, "tags")
.err( .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: " + "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: " +
tags.asHumanString(false, false, {}) + tags.asHumanString(false, false, {}) +
"\n The required tags are: " + "\n The required tags are: " +
baseTags.asHumanString(false, false, {}) baseTags.asHumanString(false, false, {}),
) )
} }
} }
@ -1592,7 +1596,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin: boolean, isBuiltin: boolean,
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist,
studioValidations: boolean = false, studioValidations: boolean = false,
skipDefaultLayers: boolean = false skipDefaultLayers: boolean = false,
) { ) {
super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
this.validator = new ValidateLayer( this.validator = new ValidateLayer(
@ -1600,7 +1604,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin, isBuiltin,
doesImageExist, doesImageExist,
studioValidations, studioValidations,
skipDefaultLayers skipDefaultLayers,
) )
} }
@ -1628,7 +1632,7 @@ class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
context context
.enter("markers") .enter("markers")
.err( .err(
`Detected a field 'markerS' in pointRendering. It is written as a singular case` `Detected a field 'markerS' in pointRendering. It is written as a singular case`,
) )
} }
if (json.marker && !Array.isArray(json.marker)) { if (json.marker && !Array.isArray(json.marker)) {
@ -1638,7 +1642,7 @@ class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
context context
.enter("location") .enter("location")
.err( .err(
"A pointRendering should have at least one 'location' to defined where it should be rendered. " "A pointRendering should have at least one 'location' to defined where it should be rendered. ",
) )
} }
return json return json
@ -1657,26 +1661,26 @@ export class ValidateLayer extends Conversion<
isBuiltin: boolean, isBuiltin: boolean,
doesImageExist: DoesImageExist, doesImageExist: DoesImageExist,
studioValidations: boolean = false, studioValidations: boolean = false,
skipDefaultLayers: boolean = false skipDefaultLayers: boolean = false,
) { ) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer") super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
this._prevalidation = new PrevalidateLayer( this._prevalidation = new PrevalidateLayer(
path, path,
isBuiltin, isBuiltin,
doesImageExist, doesImageExist,
studioValidations studioValidations,
) )
this._skipDefaultLayers = skipDefaultLayers this._skipDefaultLayers = skipDefaultLayers
} }
convert( convert(
json: LayerConfigJson, json: LayerConfigJson,
context: ConversionContext context: ConversionContext,
): { parsed: LayerConfig; raw: LayerConfigJson } { ): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name) context = context.inOperation(this.name)
if (typeof json === "string") { if (typeof json === "string") {
context.err( context.err(
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed` `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`,
) )
return undefined return undefined
} }
@ -1707,7 +1711,7 @@ export class ValidateLayer extends Conversion<
context context
.enters("calculatedTags", i) .enters("calculatedTags", i)
.err( .err(
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}` `Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`,
) )
} }
} }
@ -1755,7 +1759,7 @@ export class ValidateLayer extends Conversion<
context context
.enters("allowMove", "enableAccuracy") .enters("allowMove", "enableAccuracy")
.err( .err(
"`enableAccuracy` is written with two C in the first occurrence and only one in the last" "`enableAccuracy` is written with two C in the first occurrence and only one in the last",
) )
} }
@ -1786,8 +1790,8 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
.enters("fields", i) .enters("fields", i)
.err( .err(
`Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from( `Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
Validators.availableTypes Validators.availableTypes,
).join(",")}` ).join(",")}`,
) )
} }
} }
@ -1804,13 +1808,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{
super( super(
"Tries to detect layers where a shared filter can be used (or where similar filters occur)", "Tries to detect layers where a shared filter can be used (or where similar filters occur)",
[], [],
"DetectDuplicateFilters" "DetectDuplicateFilters",
) )
} }
convert( convert(
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
context: ConversionContext context: ConversionContext,
): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } { ): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } {
const { layers, themes } = json const { layers, themes } = json
const perOsmTag = new Map< const perOsmTag = new Map<
@ -1874,7 +1878,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
filter: FilterConfigJson filter: FilterConfigJson
}[] }[]
>, >,
layout?: LayoutConfigJson | undefined layout?: LayoutConfigJson | undefined,
): void { ): void {
if (layer.filter === undefined || layer.filter === null) { if (layer.filter === undefined || layer.filter === null) {
return return
@ -1914,7 +1918,7 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
super( super(
"Detects mappings which have identical (english) names or identical mappings.", "Detects mappings which have identical (english) names or identical mappings.",
["presets"], ["presets"],
"DetectDuplicatePresets" "DetectDuplicatePresets",
) )
} }
@ -1925,13 +1929,13 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
if (new Set(enNames).size != enNames.length) { if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames) const dups = Utils.Duplicates(enNames)
const layersWithDup = json.layers.filter((l) => const layersWithDup = json.layers.filter((l) =>
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0) l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0),
) )
const layerIds = layersWithDup.map((l) => l.id) const layerIds = layersWithDup.map((l) => l.id)
context.err( context.err(
`This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join( `This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join(
", " ", ",
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets` )} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`,
) )
} }
@ -1946,17 +1950,17 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
Utils.SameObject(presetATags, presetBTags) && Utils.SameObject(presetATags, presetBTags) &&
Utils.sameList( Utils.sameList(
presetA.preciseInput.snapToLayers, presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers presetB.preciseInput.snapToLayers,
) )
) { ) {
context.err( context.err(
`This theme has multiple presets with the same tags: ${presetATags.asHumanString( `This theme has multiple presets with the same tags: ${presetATags.asHumanString(
false, false,
false, false,
{} {},
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[ )}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
j j
].title.textFor("en")}'` ].title.textFor("en")}'`,
) )
} }
} }
@ -1981,13 +1985,13 @@ export class ValidateThemeEnsemble extends Conversion<
super( super(
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes", "Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes",
[], [],
"ValidateThemeEnsemble" "ValidateThemeEnsemble",
) )
} }
convert( convert(
json: LayoutConfig[], json: LayoutConfig[],
context: ConversionContext context: ConversionContext,
): Map< ): Map<
string, string,
{ {
@ -2038,11 +2042,11 @@ export class ValidateThemeEnsemble extends Conversion<
context.err( context.err(
[ [
"The layer with id '" + "The layer with id '" +
id + id +
"' is found in multiple themes with different tag definitions:", "' is found in multiple themes with different tag definitions:",
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}), "\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}), "\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
].join("\n") ].join("\n"),
) )
} }
} }

View file

@ -80,6 +80,7 @@ export interface LayoutConfigJson {
* Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64) * Either a URL or a base64 encoded value (which should include 'data:image/svg+xml;base64)
* *
* Type: icon * Type: icon
* suggestions: return Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))
* group: basic * group: basic
* *
*/ */
@ -156,6 +157,7 @@ export interface LayoutConfigJson {
* type: layer[] * type: layer[]
* types: hidden | layer | hidden * types: hidden | layer | hidden
* group: layers * group: layers
* title: value["builtin"] ?? value["id"] ?? value
* suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: "<b>"+key+"</b> (builtin) - "+layers.get(key).description})) * suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: "<b>"+key+"</b> (builtin) - "+layers.get(key).description}))
* *
* A theme must contain at least one layer. * A theme must contain at least one layer.

View file

@ -6,7 +6,7 @@
<Accordion> <Accordion>
<AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black"> <AccordionItem open={expanded} paddingDefault="p-0" inactiveClass="text-black">
<span slot="header" class="p-2 text-base"> <span slot="header" class="p-2 text-base w-full">
<slot name="header" /> <slot name="header" />
</span> </span>
<div class="low-interaction rounded-b p-2"> <div class="low-interaction rounded-b p-2">

View file

@ -373,7 +373,6 @@
{feedback} {feedback}
{unit} {unit}
{state} {state}
{extraTags}
feature={selectedElement} feature={selectedElement}
value={freeformInput} value={freeformInput}
unvalidatedText={freeformInputUnvalidated} unvalidatedText={freeformInputUnvalidated}
@ -418,7 +417,6 @@
{feedback} {feedback}
{unit} {unit}
{state} {state}
{extraTags}
feature={selectedElement} feature={selectedElement}
value={freeformInput} value={freeformInput}
unvalidatedText={freeformInputUnvalidated} unvalidatedText={freeformInputUnvalidated}
@ -464,7 +462,6 @@
{feedback} {feedback}
{unit} {unit}
{state} {state}
{extraTags}
feature={selectedElement} feature={selectedElement}
value={freeformInput} value={freeformInput}
unvalidatedText={freeformInputUnvalidated} unvalidatedText={freeformInputUnvalidated}

View file

@ -0,0 +1,207 @@
<script lang="ts">
import Translations from "../i18n/Translations"
import type { ConfigMeta } from "./configMeta"
import Icon from "../Map/Icon.svelte"
import Tr from "../Base/Tr.svelte"
import { Translation } from "../i18n/Translation"
import { UIEventSource } from "../../Logic/UIEventSource"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import QuestionPreview from "./QuestionPreview.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import { EditJsonState } from "./EditLayerState"
import type {
QuestionableTagRenderingConfigJson,
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { AccordionItem } from "flowbite-svelte"
export let state: EditJsonState<any>
export let isTagRenderingBlock: boolean
export let schema: ConfigMeta
export let currentValue: UIEventSource<(string | QuestionableTagRenderingConfigJson)[]>
export let value: any
export let singular: string
export let i: number
export let path: (string | number)[] = []
export let expanded = new UIEventSource(false)
const subparts: ConfigMeta[] = state
.getSchemaStartingWith(schema.path)
.filter((part) => part.path.length - 1 === schema.path.length)
function schemaForMultitype() {
const sch = { ...schema }
sch.hints.typehint = undefined
return sch
}
function fusePath(i: number, subpartPath: string[]): (string | number)[] {
const newPath = [...path, i]
const toAdd = [...subpartPath]
for (const part of path) {
if (toAdd[0] === part) {
toAdd.splice(0, 1)
} else {
break
}
}
newPath.push(...toAdd)
return newPath
}
function del(i: number) {
expanded.setData(false)
currentValue.data.splice(i, 1)
currentValue.ping()
}
function swap(i: number, j: number) {
expanded.setData(false)
const x = currentValue.data[i]
currentValue.data[i] = currentValue.data[j]
currentValue.data[j] = x
currentValue.ping()
}
function moveTo(source: number, target: number) {
expanded.setData(false)
const x = currentValue.data[source]
currentValue.data.splice(source, 1)
currentValue.data.splice(target, 0, x)
currentValue.ping()
}
function genTitle(value: any, singular: string, i: number): Translation {
try {
if (schema.hints.title) {
const v = Function("value", "return " + schema.hints.title)(value)
return Translations.T(v)
}
} catch (e) {
console.log(
"Warning: could not translate a title for " +
`${singular} ${i} with function ` +
schema.hints.title +
" and value " +
JSON.stringify(value),
)
}
return Translations.T(`${singular} ${i}`)
}
let genIconF: (x: any) => { icon: string; color: string } = <any>(
Function("value", "return " + schema.hints.icon)
)
function genIcon(value: any): string {
return genIconF(value)?.icon
}
function genColor(value: any): string {
if (!schema.hints.icon) {
return undefined
}
return genIconF(value)?.color
}
</script>
<AccordionItem open={$expanded} paddingDefault="p-0" inactiveClass="text-black m-0" >
<div slot="header" class="p-1 text-base w-full m-0 text-black">
{#if !isTagRenderingBlock}
<div class="flex items-center justify-between w-full">
<div class="m-0 flex">
{#if schema.hints.icon}
<Icon clss="w-6 h-6" icon={genIcon(value)} color={genColor(value)} />
{/if}
{#if schema.hints.title}
<Tr t={genTitle(value, singular, i)} />
<div class="subtle ml-2">
{singular}
{i}
</div>
{:else}
{singular}
{i}
{/if}
</div>
<button
class="h-fit w-fit rounded-full border border-black p-1"
on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
</button>
</div>
{:else if typeof value === "string"}
Builtin: <b>{value}</b>
{:else if value["builtin"]}
reused tagrendering <span class="font-bold">{JSON.stringify(value["builtin"])}</span>
{:else}
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
{/if}
</div>
<div class="normal-background p-2">
{#if isTagRenderingBlock}
<QuestionPreview {state} {path} {schema}>
<button
on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
Delete this question
</button>
{#if i > 0}
<button
on:click={() => {
moveTo(i, 0)
}}
>
Move to front
</button>
<button
on:click={() => {
swap(i, i - 1)
}}
>
Move up
</button>
{/if}
{#if i + 1 < $currentValue.length}
<button
on:click={() => {
swap(i, i + 1)
}}
>
Move down
</button>
<button
on:click={() => {
moveTo(i, $currentValue.length - 1)
}}
>
Move to back
</button>
{/if}
</QuestionPreview>
{:else if schema.hints.types}
<SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} />
{:else}
{#each subparts as subpart}
<SchemaBasedInput
{state}
path={fusePath(i, [subpart.path.at(-1)])}
schema={subpart}
/>
{/each}
{/if}
</div>
</AccordionItem>

View file

@ -8,7 +8,7 @@ import {
Pipe, Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion" } from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation" import { PrevalidateTheme, ValidateLayer, ValidateTheme } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
@ -33,6 +33,8 @@ export abstract class EditJsonState<T> {
public readonly schema: ConfigMeta[] public readonly schema: ConfigMeta[]
public readonly category: "layers" | "themes" public readonly category: "layers" | "themes"
public readonly server: StudioServer public readonly server: StudioServer
public readonly osmConnection: OsmConnection
public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = <any>( public readonly showIntro: UIEventSource<"no" | "intro" | "tagrenderings"> = <any>(
LocalStorageSource.Get("studio-show-intro", "intro") LocalStorageSource.Get("studio-show-intro", "intro")
) )
@ -51,7 +53,7 @@ export abstract class EditJsonState<T> {
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out * The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
*/ */
public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource( public readonly highlightedItem: UIEventSource<HighlightedTagRendering> = new UIEventSource(
undefined undefined,
) )
private sendingUpdates = false private sendingUpdates = false
private readonly _stores = new Map<string, UIEventSource<any>>() private readonly _stores = new Map<string, UIEventSource<any>>()
@ -60,10 +62,12 @@ export abstract class EditJsonState<T> {
schema: ConfigMeta[], schema: ConfigMeta[],
server: StudioServer, server: StudioServer,
category: "layers" | "themes", category: "layers" | "themes",
osmConnection: OsmConnection,
options?: { options?: {
expertMode?: UIEventSource<boolean> expertMode?: UIEventSource<boolean>
} },
) { ) {
this.osmConnection = osmConnection
this.schema = schema this.schema = schema
this.server = server this.server = server
this.category = category this.category = category
@ -88,6 +92,10 @@ export abstract class EditJsonState<T> {
await this.server.update(id, config, this.category) await this.server.update(id, config, this.category)
}) })
this.messages = this.createMessagesStore() this.messages = this.createMessagesStore()
this.register(["credits"], this.osmConnection.userDetails.mapD(u => u.name), false)
this.register(["credits:uid"], this.osmConnection.userDetails.mapD(u => u.uid), false)
} }
public startSavingUpdates(enabled = true) { public startSavingUpdates(enabled = true) {
@ -132,7 +140,7 @@ export abstract class EditJsonState<T> {
public register( public register(
path: ReadonlyArray<string | number>, path: ReadonlyArray<string | number>,
value: Store<any>, value: Store<any>,
noInitialSync: boolean = true noInitialSync: boolean = true,
): () => void { ): () => void {
const unsync = value.addCallback((v) => { const unsync = value.addCallback((v) => {
this.setValueAt(path, v) this.setValueAt(path, v)
@ -146,7 +154,7 @@ export abstract class EditJsonState<T> {
public getSchemaStartingWith(path: string[]) { public getSchemaStartingWith(path: string[]) {
return this.schema.filter( return this.schema.filter(
(sch) => (sch) =>
!path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)) !path.some((part, i) => !(sch.path.length > path.length && sch.path[i] === part)),
) )
} }
@ -167,7 +175,7 @@ export abstract class EditJsonState<T> {
const schemas = this.schema.filter( const schemas = this.schema.filter(
(sch) => (sch) =>
sch !== undefined && sch !== undefined &&
!path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)) !path.some((part, i) => !(sch.path.length == path.length && sch.path[i] === part)),
) )
if (schemas.length == 0) { if (schemas.length == 0) {
console.warn("No schemas found for path", path.join(".")) console.warn("No schemas found for path", path.join("."))
@ -257,12 +265,12 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
constructor( constructor(
state: DesugaringContext, state: DesugaringContext,
step: Conversion<LayerConfigJson, T>, step: Conversion<LayerConfigJson, T>,
getTagRenderings: (t: T) => TagRenderingConfigJson[] getTagRenderings: (t: T) => TagRenderingConfigJson[],
) { ) {
super( super(
"When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this", "When validating a layer, the tagRenderings are first expanded. Some builtin tagRendering-calls (e.g. `contact`) will introduce _multiple_ tagRenderings, causing the count to be off. This class rewrites the error messages to fix this",
[], [],
"ContextRewritingStep" "ContextRewritingStep",
) )
this._state = state this._state = state
this._step = step this._step = step
@ -272,7 +280,7 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
convert(json: LayerConfigJson, context: ConversionContext): T { convert(json: LayerConfigJson, context: ConversionContext): T {
const converted = this._step.convert(json, context) const converted = this._step.convert(json, context)
const originalIds = json.tagRenderings?.map( const originalIds = json.tagRenderings?.map(
(tr) => (<QuestionableTagRenderingConfigJson>tr)["id"] (tr) => (<QuestionableTagRenderingConfigJson>tr)["id"],
) )
if (!originalIds) { if (!originalIds) {
return converted return converted
@ -307,7 +315,6 @@ class ContextRewritingStep<T> extends Conversion<LayerConfigJson, T> {
export default class EditLayerState extends EditJsonState<LayerConfigJson> { export default class EditLayerState extends EditJsonState<LayerConfigJson> {
// Needed for the special visualisations // Needed for the special visualisations
public readonly osmConnection: OsmConnection
public readonly imageUploadManager = { public readonly imageUploadManager = {
getCountsFor() { getCountsFor() {
return 0 return 0
@ -335,10 +342,9 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
schema: ConfigMeta[], schema: ConfigMeta[],
server: StudioServer, server: StudioServer,
osmConnection: OsmConnection, osmConnection: OsmConnection,
options: { expertMode: UIEventSource<boolean> } options: { expertMode: UIEventSource<boolean> },
) { ) {
super(schema, server, "layers", options) super(schema, server, "layers", osmConnection, options)
this.osmConnection = osmConnection
this.layout = { this.layout = {
getMatchingLayer: () => { getMatchingLayer: () => {
try { try {
@ -393,7 +399,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
return new ContextRewritingStep( return new ContextRewritingStep(
state, state,
new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)), new Pipe(new PrepareLayer(state), new ValidateLayer("dynamic", false, undefined, true)),
(t) => <TagRenderingConfigJson[]>t.raw.tagRenderings (t) => <TagRenderingConfigJson[]>t.raw.tagRenderings,
) )
} }
@ -427,7 +433,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
} }
protected async validate( protected async validate(
configuration: Partial<LayerConfigJson> configuration: Partial<LayerConfigJson>,
): Promise<ConversionMessage[]> { ): Promise<ConversionMessage[]> {
const layers = AllSharedLayers.getSharedLayersConfigs() const layers = AllSharedLayers.getSharedLayersConfigs()
@ -456,16 +462,19 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
constructor( constructor(
schema: ConfigMeta[], schema: ConfigMeta[],
server: StudioServer, server: StudioServer,
options: { expertMode: UIEventSource<boolean> } osmConnection: OsmConnection,
options: { expertMode: UIEventSource<boolean> },
) { ) {
super(schema, server, "themes", options) super(schema, server, "themes", osmConnection, options)
this.setupFixers() this.setupFixers()
} }
protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> { protected buildValidation(state: DesugaringContext): Conversion<LayoutConfigJson, any> {
return new Pipe( return new Pipe(new PrevalidateTheme(),
new PrepareTheme(state), new Pipe(
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())) new PrepareTheme(state),
new ValidateTheme(undefined, "", false, new Set(state.tagRenderings.keys())),
), true,
) )
} }

View file

@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { ConfigMeta } from "./configMeta" import type { ConfigMeta } from "./configMeta"
import EditLayerState from "./EditLayerState" import EditLayerState, { EditJsonState } from "./EditLayerState"
import * as questions from "../../assets/generated/layers/questions.json" import * as questions from "../../assets/generated/layers/questions.json"
import { ImmutableStore, Store } from "../../Logic/UIEventSource" import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js" import type {
QuestionableTagRenderingConfigJson,
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
import FromHtml from "../Base/FromHtml.svelte" import FromHtml from "../Base/FromHtml.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte" import ShowConversionMessage from "./ShowConversionMessage.svelte"
@ -31,15 +33,25 @@
if (typeof x === "string") { if (typeof x === "string") {
return perId[x] return perId[x]
} else { } else {
return [x] return <any>[x]
} }
}) })
let configs: Store<TagRenderingConfig[]> = configJson.map((configs) => { let configs: Store<TagRenderingConfig[]> = configJson.map((configs) => {
if (!configs) { if (!configs) {
return [{ error: "No configuartions found" }] return [{ error: "No configuartions found" }]
} }
console.log("Regenerating configs")
return configs.map((config) => { return configs.map((config) => {
if (config["builtin"]) {
let override = ""
if (config["override"]) {
override = ". Some items are changed with an override. Editing this is not yet supported with Studio."
}
return new TagRenderingConfig({
render: {
"en": "This question reuses <b>" + JSON.stringify(config["builtin"]) + "</b>" + override,
},
})
}
try { try {
return new TagRenderingConfig(config) return new TagRenderingConfig(config)
} catch (e) { } catch (e) {
@ -47,15 +59,6 @@
} }
}) })
}) })
let id: Store<string> = value.mapD((c) => {
if (c?.id) {
return c.id
}
if (typeof c === "string") {
return c
}
return undefined
})
let tags = state.testTags let tags = state.testTags
@ -66,11 +69,18 @@
<div class="flex"> <div class="flex">
<div class="m-4 flex w-full flex-col"> <div class="m-4 flex w-full flex-col">
<NextButton clss="primary" on:click={() => state.highlightedItem.setData({ path, schema })}> {#if $configJson.some(config => config["builtin"] !== undefined)}
{#if schema.hints.question} <div class="interactive p-2 rounded-2xl">
{schema.hints.question} This question uses an advanced 'builtin'+'override' construction in the source code.
{/if} Editing this with Studio is not supported.
</NextButton> </div>
{:else}
<NextButton clss="primary" on:click={() => state.highlightedItem.setData({ path, schema })}>
{#if schema.hints.question}
{schema.hints.question}
{/if}
</NextButton>
{/if}
{#if description} {#if description}
<Markdown src={description} /> <Markdown src={description} />
{/if} {/if}
@ -86,6 +96,7 @@
{#each $configs as config} {#each $configs as config}
{#if config.error !== undefined} {#if config.error !== undefined}
<div class="alert">Could not create a preview of this tagRendering: {config.error}</div> <div class="alert">Could not create a preview of this tagRendering: {config.error}</div>
{JSON.stringify($value)}
{:else if config.condition && !config.condition.matchesProperties($tags)} {:else if config.condition && !config.condition.matchesProperties($tags)}
This tagRendering is currently not shown. It will appear if the feature matches the This tagRendering is currently not shown. It will appear if the feature matches the
condition condition

View file

@ -8,6 +8,15 @@
export let state: EditLayerState | EditThemeState export let state: EditLayerState | EditThemeState
let rawConfig = state.configuration.sync(f => JSON.stringify(f, null, " "), [], json => {
try {
return JSON.parse(json)
} catch (e) {
console.error("Could not parse", json)
return undefined
}
})
let container: HTMLDivElement let container: HTMLDivElement
let monaco: typeof Monaco let monaco: typeof Monaco
let editor: Monaco.editor.IStandaloneCodeEditor let editor: Monaco.editor.IStandaloneCodeEditor
@ -34,13 +43,19 @@
return () => window.removeEventListener("keydown", handler) return () => window.removeEventListener("keydown", handler)
}) })
let useFallback = false
onMount(async () => { onMount(async () => {
const monacoEditor = await import("monaco-editor") const monacoEditor = await import("monaco-editor")
loader.config({ loader.config({
monaco: monacoEditor.default, monaco: monacoEditor.default,
}) })
monaco = await loader.init() try {
monaco = await loader.init()
} catch (e) {
console.error("Could not load Monaco Editor, falling back", e)
useFallback = true
}
// Determine schema based on the state // Determine schema based on the state
let schemaUri: string let schemaUri: string
@ -50,7 +65,7 @@
schemaUri = "https://mapcomplete.org/schemas/layoutconfig.json" schemaUri = "https://mapcomplete.org/schemas/layoutconfig.json"
} }
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monaco?.languages?.json?.jsonDefaults?.setDiagnosticsOptions({
validate: true, validate: true,
schemas: [ schemas: [
{ {
@ -64,23 +79,28 @@
], ],
}) })
let modelUri = monaco.Uri.parse("inmemory://inmemory/file.json") let modelUri = monaco?.Uri?.parse("inmemory://inmemory/file.json")
// Create a new model // Create a new model
model = monaco.editor.createModel( try {
JSON.stringify(state.configuration.data, null, " "), model = monaco?.editor?.createModel(
"json", JSON.stringify(state.configuration.data, null, " "),
modelUri "json",
) modelUri,
)
} catch (e) {
console.error("Could not create model in MOnaco Editor", e)
useFallback = true
}
editor = monaco.editor.create(container, { editor = monaco?.editor?.create(container, {
model, model,
automaticLayout: true, automaticLayout: true,
}) })
// When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid // When the editor is changed, update the configuration, but only if the user hasn't typed for 500ms and the JSON is valid
let timeout: number let timeout: number
editor.onDidChangeModelContent(() => { editor?.onDidChangeModelContent(() => {
clearTimeout(timeout) clearTimeout(timeout)
timeout = setTimeout(() => { timeout = setTimeout(() => {
save() save()
@ -98,4 +118,8 @@
}) })
</script> </script>
<div bind:this={container} class="h-full w-full" /> {#if useFallback}
<textarea class="w-full" rows="25" bind:value={$rawConfig} />
{:else}
<div bind:this={container} class="h-full w-full" />
{/if}

View file

@ -4,9 +4,10 @@
* They will typically be a subset of some properties * They will typically be a subset of some properties
*/ */
import type { ConfigMeta } from "./configMeta" import type { ConfigMeta } from "./configMeta"
import EditLayerState from "./EditLayerState" import EditLayerState, { EditJsonState } from "./EditLayerState"
import SchemaBasedInput from "./SchemaBasedInput.svelte" import SchemaBasedInput from "./SchemaBasedInput.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { Utils } from "../../Utils"
export let state: EditLayerState export let state: EditLayerState
export let configs: ConfigMeta[] export let configs: ConfigMeta[]
@ -30,8 +31,8 @@
<div slot="header">{title}</div> <div slot="header">{title}</div>
<div class="flex w-full flex-col gap-y-1 pl-2"> <div class="flex w-full flex-col gap-y-1 pl-2">
<slot name="description" /> <slot name="description" />
{#each configsFiltered as config} {#each configsFiltered as config (config.path)}
<SchemaBasedInput {state} path={config.path} schema={config} /> <SchemaBasedInput {state} path={config.path} } />
{/each} {/each}
</div> </div>
</AccordionSingle> </AccordionSingle>

View file

@ -1,24 +1,21 @@
<script lang="ts"> <script lang="ts">
import EditLayerState from "./EditLayerState" import { EditJsonState } from "./EditLayerState"
import type { ConfigMeta } from "./configMeta" import type { ConfigMeta } from "./configMeta"
import { UIEventSource } from "../../Logic/UIEventSource"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import SchemaBasedField from "./SchemaBasedField.svelte" import SchemaBasedField from "./SchemaBasedField.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini" import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import QuestionPreview from "./QuestionPreview.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte" import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte" import Markdown from "../Base/Markdown.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import { Utils } from "../../Utils"
import Icon from "../Map/Icon.svelte" import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import Tr from "../Base/Tr.svelte" import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
import Translations from "../i18n/Translations" import { Accordion } from "flowbite-svelte"
export let state: EditLayerState export let state: EditJsonState<any>
export let path: (string | number)[] = []
export let schema: ConfigMeta export let schema: ConfigMeta
schema = Utils.Clone(schema)
let title = schema.path.at(-1) let title = schema.path.at(-1)
console.log(">>>", schema)
let singular = title let singular = title
if (title?.endsWith("s")) { if (title?.endsWith("s")) {
singular = title.slice(0, title.length - 1) singular = title.slice(0, title.length - 1)
@ -27,7 +24,6 @@
if (singular?.match(/^[aeoui]/)) { if (singular?.match(/^[aeoui]/)) {
article = "an" article = "an"
} }
export let path: (string | number)[] = []
const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings" const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings"
@ -41,12 +37,12 @@
.filter((part) => part.path.length - 1 === schema.path.length) .filter((part) => part.path.length - 1 === schema.path.length)
let messages = state.messagesFor(path) let messages = state.messagesFor(path)
const currentValue: UIEventSource<any[]> = state.getStoreFor(path) const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(path)
if (currentValue.data === undefined) { if (currentValue.data === undefined) {
currentValue.setData([]) currentValue.setData([])
} }
function createItem(valueToSet?: any) { function createItem(valueToSet?: string | QuestionableTagRenderingConfigJson) {
if (currentValue.data === undefined) { if (currentValue.data === undefined) {
currentValue.setData([]) currentValue.setData([])
} }
@ -72,64 +68,13 @@
return newPath return newPath
} }
function schemaForMultitype() {
const sch = { ...schema }
sch.hints.typehint = undefined
return sch
}
function del(i: number) { function del(i: number) {
currentValue.data.splice(i, 1) currentValue.data.splice(i, 1)
currentValue.ping() currentValue.ping()
} }
function swap(i: number, j: number) {
const x = currentValue.data[i]
currentValue.data[i] = currentValue.data[j]
currentValue.data[j] = x
currentValue.ping()
}
function moveTo(source: number, target: number) {
const x = currentValue.data[source]
currentValue.data.splice(source, 1)
currentValue.data.splice(target, 0, x)
currentValue.ping()
}
function genTitle(value: any, singular: string, i: number): Translation {
try {
if (schema.hints.title) {
const v = Function("value", "return " + schema.hints.title)(value)
return Translations.T(v)
}
} catch (e) {
console.log(
"Warning: could not translate a title for " +
`${singular} ${i} with function ` +
schema.hints.title +
" and value " +
JSON.stringify(value)
)
}
return Translations.T(`${singular} ${i}`)
}
let genIconF: (x: any) => { icon: string; color: string } = <any>(
Function("value", "return " + schema.hints.icon)
)
console.log("Icon lambda is", schema.hints.icon, path, genIconF("test"))
function genIcon(value: any): string {
return genIconF(value)?.icon
}
function genColor(value: any): string {
if (!schema.hints.icon) {
return undefined
}
return genIconF(value)?.color
}
</script> </script>
<div class="pl-2"> <div class="pl-2">
@ -163,99 +108,11 @@
</div> </div>
{/each} {/each}
{:else} {:else}
{#each $currentValue as value, i} <Accordion>
<AccordionSingle expanded={false}> {#each $currentValue as value, i (value)}
<span slot="header"> <CollapsedTagRenderingPreview {state} {isTagRenderingBlock} {schema} {currentValue} {value} {i} {singular} path={fusePath(i, [])}/>
{#if !isTagRenderingBlock}
<div class="flex items-center justify-between">
<h3 class="m-0 flex">
{#if schema.hints.icon}
<Icon clss="w-6 h-6" icon={genIcon(value)} color={genColor(value)} />
{/if}
{singular}
{i}
{#if schema.hints.title}
<div class="subtle ml-2">
<Tr t={genTitle(value, singular, i)} />
</div>
{/if}
</h3>
<button
class="h-fit w-fit rounded-full border border-black p-1"
on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
</button>
</div>
{:else if typeof value === "string"}
Builtin: <b>{value}</b>
{:else}
<Tr cls="font-bold" t={Translations.T(value?.question ?? value?.render)} />
{/if}
</span>
<div class="normal-background p-2">
{#if isTagRenderingBlock}
<QuestionPreview {state} path={fusePath(i, [])} {schema}>
<button
on:click={() => {
del(i)
}}
>
<TrashIcon class="h-4 w-4" />
Delete this question
</button>
{#if i > 0}
<button
on:click={() => {
moveTo(i, 0)
}}
>
Move to front
</button>
<button
on:click={() => {
swap(i, i - 1)
}}
>
Move up
</button>
{/if}
{#if i + 1 < $currentValue.length}
<button
on:click={() => {
swap(i, i + 1)
}}
>
Move down
</button>
<button
on:click={() => {
moveTo(i, $currentValue.length - 1)
}}
>
Move to back
</button>
{/if}
</QuestionPreview>
{:else if schema.hints.types}
<SchemaBasedMultiType {state} path={fusePath(i, [])} schema={schemaForMultitype()} />
{:else}
{#each subparts as subpart}
<SchemaBasedInput
{state}
path={fusePath(i, [subpart.path.at(-1)])}
schema={subpart}
/>
{/each}
{/if}
</div>
</AccordionSingle>
{/each} {/each}
</Accordion>
{/if} {/if}
<div class="flex"> <div class="flex">
<button on:click={() => createItem()}>Add {article} {singular}</button> <button on:click={() => createItem()}>Add {article} {singular}</button>

View file

@ -4,13 +4,13 @@
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte" import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import EditLayerState from "./EditLayerState" import { EditJsonState } from "./EditLayerState"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import type { JsonSchemaType } from "./jsonSchema" import type { JsonSchemaType } from "./jsonSchema"
import { ConfigMetaUtils } from "./configMeta" import { ConfigMetaUtils } from "./configMeta"
import ShowConversionMessage from "./ShowConversionMessage.svelte" import ShowConversionMessage from "./ShowConversionMessage.svelte"
export let state: EditLayerState export let state: EditJsonState<any>
export let path: (string | number)[] = [] export let path: (string | number)[] = []
export let schema: ConfigMeta export let schema: ConfigMeta
export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset export let startInEditModeIfUnset: boolean = schema.hints && !schema.hints.ifunset

View file

@ -1,14 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { ConfigMeta } from "./configMeta" import type { ConfigMeta } from "./configMeta"
import SchemaBasedField from "./SchemaBasedField.svelte" import SchemaBasedField from "./SchemaBasedField.svelte"
import EditLayerState from "./EditLayerState" import { EditJsonState } from "./EditLayerState"
import SchemaBasedArray from "./SchemaBasedArray.svelte" import SchemaBasedArray from "./SchemaBasedArray.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte" import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte" import ArrayMultiAnswer from "./ArrayMultiAnswer.svelte"
export let schema: ConfigMeta export let state: EditJsonState<any>
export let state: EditLayerState
export let path: (string | number)[] = [] export let path: (string | number)[] = []
console.log("Fetched schema:", path, state.getSchema(<any> path))
let schema: ConfigMeta = state.getSchema(<any> path)[0]
let expertMode = state.expertMode let expertMode = state.expertMode
</script> </script>

View file

@ -204,9 +204,10 @@
</script> </script>
<div class="m-1 flex flex-col gap-y-2 border-2 border-dashed border-gray-300 p-2"> <div class="m-1 flex flex-col gap-y-2 border-2 border-dashed border-gray-300 p-2">
{#if schema.hints.title !== undefined} {#if schema.hints.title !== undefined && typeof path.at(-1) !== "number"}
<h3>{schema.hints.title}</h3> <h3>{schema.hints.title}</h3>
<div>{schema.description}</div> <div>{schema.description}</div>
{path.join(".")}
{/if} {/if}
{#if hasOverride} {#if hasOverride}
This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited This object refers to {existingValue.builtin} and overrides some properties. This cannot be edited
@ -221,7 +222,6 @@
{#if $expertMode || subschema.hints?.group !== "expert"} {#if $expertMode || subschema.hints?.group !== "expert"}
<SchemaBasedInput <SchemaBasedInput
{state} {state}
schema={subschema}
path={[...subpath, subschema?.path?.at(-1) ?? "???"]} path={[...subpath, subschema?.path?.at(-1) ?? "???"]}
/> />
{:else if window.location.hostname === "127.0.0.1"} {:else if window.location.hostname === "127.0.0.1"}

View file

@ -42,9 +42,11 @@
undefined, undefined,
"Used to complete the login" "Used to complete the login"
) )
const fakeUser = UIEventSource.asBoolean( QueryParameters.GetQueryParameter("fake-user", "Test switch for fake login"))
let osmConnection = new OsmConnection({ let osmConnection = new OsmConnection({
oauth_token, oauth_token,
checkOnlineRegularly: true, checkOnlineRegularly: true,
fakeUser: fakeUser.data
}) })
const expertMode = UIEventSource.asBoolean( const expertMode = UIEventSource.asBoolean(
osmConnection.GetPreference("studio-expert-mode", "false", { osmConnection.GetPreference("studio-expert-mode", "false", {
@ -129,7 +131,7 @@
let showIntro = editLayerState.showIntro let showIntro = editLayerState.showIntro
const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw const layoutSchema: ConfigMeta[] = <any>layoutSchemaRaw
let editThemeState = new EditThemeState(layoutSchema, studio, { expertMode }) let editThemeState = new EditThemeState(layoutSchema, studio, osmConnection, { expertMode })
const version = meta.version const version = meta.version

View file

@ -335,6 +335,17 @@ textarea {
color: black; color: black;
} }
h2.group {
/* For flowbite accordions */
margin: 0;
}
.group button {
/* For flowbite accordions */
border-radius: 0;
}
/************************* OTHER CATEGORIES ********************************/ /************************* OTHER CATEGORIES ********************************/
/** /**