From 01680f236caf1a9b1bd6b0d4c802ad4a07aa6ef8 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Mon, 21 Apr 2025 02:51:41 +0200 Subject: [PATCH] Themes: allow to easily import tagrenderings and add a prefix key to all tags --- .../AddPrefixToTagRenderingConfig.ts | 134 ++++++++++++++++++ .../Conversion/ExpandTagRendering.ts | 38 +++++ .../ThemeConfig/Conversion/PrepareTheme.ts | 17 +-- .../ThemeConfig/Json/LayerConfigJson.ts | 9 +- .../QuestionableTagRenderingConfigJson.ts | 2 +- .../ImageVisualisations.ts | 4 +- src/UI/SpecialVisualization.ts | 12 +- src/UI/SpecialVisualizations.ts | 13 +- src/Utils.ts | 3 +- 9 files changed, 201 insertions(+), 31 deletions(-) create mode 100644 src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts diff --git a/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts b/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts new file mode 100644 index 000000000..6aa347207 --- /dev/null +++ b/src/Models/ThemeConfig/Conversion/AddPrefixToTagRenderingConfig.ts @@ -0,0 +1,134 @@ +import { DesugaringStep } from "./Conversion" +import { ConversionContext } from "./ConversionContext" +import SpecialVisualizations from "../../../UI/SpecialVisualizations" +import { Translatable } from "../Json/Translatable" +import { TagConfigJson } from "../Json/TagConfigJson" +import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" + +export default class AddPrefixToTagRenderingConfig extends DesugaringStep { + + + private readonly _prefix: string + + constructor(prefix: string) { + super("Adds `prefix` to _all_ keys. Used to add information about a subamenity withing a bigger amenity (e.g. toilets in a restaurant, a sauna in a water park, ...)", ["*"], "AddPrefixToTagRenderingConfig") + this._prefix = prefix + } + + /** + * + * const edit = new AddPrefixToTagRenderingConfig("PREFIX") + * edit.updateString("Some string") // => "Some string" + * edit.updateString("Some string {key0}") // => "Some string {PREFIX:key0}" + * + * // Should prefix a key in a special visualisation + * new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table(opening_hours)}") // => "{opening_hours_table(PREFIX:opening_hours,,)}" + * + * // Should prefix the default key in a special visualisation + * new AddPrefixToTagRenderingConfig("PREFIX").updateString("{opening_hours_table()}") // => "{opening_hours_table(PREFIX:opening_hours,,)}" + */ + private updateString(str: string): string { + const parsed = SpecialVisualizations.constructSpecification(str) + const fixedSpec: string[] = [] + for (const spec of parsed) { + if (typeof spec === "string") { + const part = spec.replace(/{([a-zA-Z0-9:_-]+)}/g, `{${this._prefix}:$1}`) + fixedSpec.push(part) + } else { + const newArgs: string[] = [] + for (let i = 0; i < spec.func.args.length; i++) { + const argDoc = spec.func.args[i] + const argV = spec.args[i] + if (argDoc.type === "key") { + newArgs.push(this._prefix + ":" + (argV ?? argDoc.defaultValue ?? "")) + } else { + newArgs.push(argV ?? "") + } + } + fixedSpec.push("{" + spec.func.funcName + "(" + newArgs.join(",") + ")}") + } + } + return fixedSpec.join("") + + } + + private updateTranslatable(val: Translatable | undefined): Translatable | undefined { + if (!val) { + return val + } + if (typeof val === "string") { + return this.updateString(val) + } + const newTranslations: Record = {} + for (const lng in val) { + newTranslations[lng] = this.updateString(val[lng]) + } + return newTranslations + } + + private updateTag(tags: string): string; + private updateTag(tags: TagConfigJson): TagConfigJson; + private updateTag(tags: TagConfigJson): TagConfigJson { + if (!tags) { + return tags + } + if (tags["and"]) { + return { and: this.updateTags(tags["and"]) } + } + if (tags["or"]) { + return { or: this.updateTags(tags["or"]) } + } + return this._prefix + ":" + tags + } + + private updateTags(tags: ReadonlyArray): string[] { + return tags?.map(tag => this.updateTag(tag)) + } + + private updateMapping(mapping: Readonly): MappingConfigJson { + return { + ...mapping, + addExtraTags: this.updateTags(mapping.addExtraTags), + if: this.updateTag(mapping.if), + then: this.updateTranslatable(mapping.then), + alsoShowIf: this.updateTag(mapping.alsoShowIf), + ifnot: this.updateTag(mapping.ifnot), + priorityIf: this.updateTag(mapping.priorityIf), + hideInAnswer: mapping.hideInAnswer === true || mapping.hideInAnswer === false ? mapping.hideInAnswer : this.updateTag(mapping.hideInAnswer) + } + } + + public convert(json: Readonly, context: ConversionContext): QuestionableTagRenderingConfigJson { + let freeform = json.freeform + if (freeform) { + const ff = json.freeform + freeform = { + ...ff, + key: this._prefix + ":" + ff.key, + addExtraTags: this.updateTags(ff.addExtraTags) + } + } + + return { + ...json, + id: this._prefix + "_" + json.id, + + question: this.updateTranslatable(json.question), + questionHint: this.updateTranslatable(json.questionHint), + + render: this.updateTranslatable(json.render), + freeform, + editButtonAriaLabel: json.editButtonAriaLabel, + onSoftDelete: this.updateTags(json.onSoftDelete), + invalidValues: this.updateTag(json.invalidValues), + mappings: json.mappings?.map(mapping => this.updateMapping(mapping)), + + condition: this.updateTag(json.condition), + metacondition: json.metacondition, // no update here + filter: json.filter === true, // We break references to filters, as those references won't have the updated tags + _appliedPrefix: this._prefix + } + } + + +} diff --git a/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts index df1697c87..8429a0479 100644 --- a/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts +++ b/src/Models/ThemeConfig/Conversion/ExpandTagRendering.ts @@ -6,6 +6,8 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende import { TagUtils } from "../../../Logic/Tags/TagUtils" import { Utils } from "../../../Utils" import { AddContextToTranslations } from "./AddContextToTranslations" +import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig" +import { Translatable } from "../Json/Translatable" export class ExpandTagRendering extends Conversion< | string @@ -208,6 +210,21 @@ export class ExpandTagRendering extends Conversion< let matchingTrs: (TagRenderingConfigJson & { id: string })[] if (id === "*") { matchingTrs = layerTrs + } else if (id === "title") { + const title = layer.title + if (title["render"] || title["mappings"]) { + const titleTr = layer.title + return [{ + ...titleTr, + id: layer.id + "_title" + }] + } else { + const transl = layer.title + return [{ + render: transl, + id: layer.id + "_title" + }] + } } else if (id.startsWith("*")) { const id_ = id.substring(1) matchingTrs = layerTrs.filter((tr) => tr["labels"]?.indexOf(id_) >= 0) @@ -249,6 +266,25 @@ export class ExpandTagRendering extends Conversion< return undefined } + /** + * Returns a variation of 'tr' where every key has been prefixed by the given 'prefix'-key. + * If the given key is undefined, returns the original tagRendering. + * + * Note: metacondition will _not_ be prefixed + * @param key + * @param tr + * @private + */ + private static applyKeyPrefix(key: string | undefined, tr: Readonly, ctx: ConversionContext): QuestionableTagRenderingConfigJson { + if (key === undefined || key === null) { + return tr + } + if (key.endsWith(":")) { + ctx.err("A 'prefix'-key should not end with a colon. The offending prefix value is: " + key) + } + return new AddPrefixToTagRenderingConfig(key).convert(tr, ctx.enter("prefix")) + } + private convertOnce( tr: string | { builtin: string | string[] } | TagRenderingConfigJson, ctx: ConversionContext @@ -310,6 +346,7 @@ export class ExpandTagRendering extends Conversion< if ( key === "builtin" || key === "override" || + key === "prefix" || key === "id" || key.startsWith("#") ) { @@ -379,6 +416,7 @@ export class ExpandTagRendering extends Conversion< } for (let foundTr of lookup) { foundTr = Utils.Clone(foundTr) + foundTr = ExpandTagRendering.applyKeyPrefix(tr["prefix"], foundTr, ctx) ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr) if (names.length == 1) { foundTr["id"] = tr["id"] ?? foundTr["id"] diff --git a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts index 261f6fbf8..8dfd15376 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -1,14 +1,4 @@ -import { - Concat, - Conversion, - DesugaringContext, - DesugaringStep, - Each, - Fuse, - On, - Pass, - SetDefault, -} from "./Conversion" +import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion" import { ThemeConfigJson } from "../Json/ThemeConfigJson" import { PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { LayerConfigJson } from "../Json/LayerConfigJson" @@ -163,9 +153,8 @@ class SubstituteLayer extends Conversion !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" + `You are attempting to import layer '${found.id}' in this theme. This layer import specifies that certain tagrenderings have to be removed based on forbidden ids and/or labels. One or more of these forbidden ids did not match any tagRenderings and caused no deletions: ${unused.join(", ")} + 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 diff --git a/src/Models/ThemeConfig/Json/LayerConfigJson.ts b/src/Models/ThemeConfig/Json/LayerConfigJson.ts index 533911ae8..48abaae63 100644 --- a/src/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/src/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -422,8 +422,15 @@ export interface LayerConfigJson { | string | { id?: string + /** + * Special value: ".title" will return the layer's title for an element + */ builtin: string | string[] - override: Partial + override: Partial, + /** + * Add this prefix to all keys. This is applied _before_ the override, thus keys added in 'override' will not be prefixed + */ + prefix?: string } | QuestionableTagRenderingConfigJson | (RewritableConfigJson< diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index 37da3a4c3..f7d9e957a 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -289,7 +289,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * Extra arguments to configure the input element * group: hidden */ - helperArgs: any + helperArgs?: any } /** diff --git a/src/UI/SpecialVisualisations/ImageVisualisations.ts b/src/UI/SpecialVisualisations/ImageVisualisations.ts index f3a56239a..1f35bfbce 100644 --- a/src/UI/SpecialVisualisations/ImageVisualisations.ts +++ b/src/UI/SpecialVisualisations/ImageVisualisations.ts @@ -14,7 +14,7 @@ import NearbyImagesCollapsed from "../Image/NearbyImagesCollapsed.svelte" class NearbyImageVis implements SpecialVisualizationSvelte { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests - args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ + args: [ { name: "mode", defaultValue: "closed", @@ -65,6 +65,7 @@ export class ImageVisualisations { args: [ { name: "image_key", + type: "key", defaultValue: AllImageProviders.defaultKeys.join(";"), doc: "The keys given to the images, e.g. if image is given, the first picture URL will be added as image, the second as image:0, the third as image:1, etc... Multiple values are allowed if ';'-separated ", }, @@ -95,6 +96,7 @@ export class ImageVisualisations { needsUrls: [Imgur.apiUrl, ...Imgur.supportingUrls], args: [ { + type: "key", name: "image_key", doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)", defaultValue: "panoramax", diff --git a/src/UI/SpecialVisualization.ts b/src/UI/SpecialVisualization.ts index 2e89f4e00..df980e4b2 100644 --- a/src/UI/SpecialVisualization.ts +++ b/src/UI/SpecialVisualization.ts @@ -1,11 +1,7 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" import BaseUIElement from "./BaseUIElement" import ThemeConfig from "../Models/ThemeConfig/ThemeConfig" -import { - FeatureSource, - IndexedFeatureSource, - WritableFeatureSource, -} from "../Logic/FeatureSource/FeatureSource" +import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { Changes } from "../Logic/Osm/Changes" import { ExportableMap, MapProperties } from "../Models/MapProperties" @@ -100,7 +96,8 @@ export interface SpecialVisualization { name: string defaultValue?: string doc: string - required?: false | boolean + required?: false | boolean, + type?: "key" }[] readonly getLayerDependencies?: (argument: string[]) => string[] @@ -133,7 +130,8 @@ export interface SpecialVisualizationSvelte { name: string defaultValue?: string doc: string - required?: false | boolean + required?: false | boolean, + type?: "key" | string }[] readonly getLayerDependencies?: (argument: string[]) => string[] diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index 0c1c89f30..43afabe9c 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -257,6 +257,7 @@ export default class SpecialVisualizations { { name: "key", defaultValue: "opening_hours", + type: "key", doc: "The tagkey from which the table is constructed.", }, { @@ -284,6 +285,7 @@ export default class SpecialVisualizations { args: [ { name: "key", + type: "key", defaultValue: "opening_hours", doc: "The tagkey from which the opening hours are read.", }, @@ -324,6 +326,7 @@ export default class SpecialVisualizations { args: [ { name: "key", + type: "key", doc: "The key of the tag to give the canonical text for", required: true, }, @@ -412,6 +415,7 @@ export default class SpecialVisualizations { args: [ { name: "key", + type: "key", doc: "The attribute to interpret as json", defaultValue: "value", }, @@ -463,6 +467,7 @@ export default class SpecialVisualizations { args: [ { name: "key", + type: "key", defaultValue: "value", doc: "The key to look for the tags", }, @@ -470,9 +475,7 @@ export default class SpecialVisualizations { constr( state: SpecialVisualizationState, tagSource: UIEventSource>, - argument: string[], - feature: Feature, - layer: LayerConfig + argument: string[] ): BaseUIElement { const key = argument[0] ?? "value" return new VariableUiElement( @@ -508,8 +511,7 @@ export default class SpecialVisualizations { state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], - feature: Feature, - layer: LayerConfig + feature: Feature ): BaseUIElement { return new SvelteUIElement(DirectionIndicator, { state, feature }) }, @@ -520,6 +522,7 @@ export default class SpecialVisualizations { args: [ { name: "key", + type: "key", doc: "The attribute containing the degrees", defaultValue: "_direction:centerpoint", }, diff --git a/src/Utils.ts b/src/Utils.ts index b705d51cd..5df75124f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -466,8 +466,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (v !== undefined && v !== null) { if (v["toISOString"] != undefined) { // This is a date, probably the timestamp of the object - // @ts-ignore - const date: Date = el + const date: Date = v v = date.toISOString() }