Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-02-15 17:48:26 +01:00
commit f0823f4c4d
524 changed files with 18747 additions and 8546 deletions

View file

@ -2,7 +2,6 @@ import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
import { T } from "vitest/dist/types-aac763a5"
export interface DesugaringContext {
tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
@ -11,10 +10,11 @@ export interface DesugaringContext {
}
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
export interface ConversionMessage {
context: ConversionContext
message: string
level: ConversionMsgLevel
readonly context: ConversionContext
readonly message: string
readonly level: ConversionMsgLevel
}
export abstract class Conversion<TIn, TOut> {
@ -85,6 +85,7 @@ export class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
export class Bypass<T> extends DesugaringStep<T> {
private readonly _applyIf: (t: T) => boolean
private readonly _step: DesugaringStep<T>
constructor(applyIf: (t: T) => boolean, step: DesugaringStep<T>) {
super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass")
this._applyIf = applyIf
@ -102,7 +103,6 @@ export class Bypass<T> extends DesugaringStep<T> {
export class Each<X, Y> extends Conversion<X[], Y[]> {
private readonly _step: Conversion<X, Y>
private readonly _msg: string
private readonly _filter: (x: X) => boolean
constructor(step: Conversion<X, Y>, options?: { msg?: string }) {
super(
@ -173,7 +173,7 @@ export class Pass<T> extends Conversion<T, T> {
super(message ?? "Does nothing, often to swap out steps in testing", [], "Pass")
}
convert(json: T, context: ConversionContext): T {
convert(json: T, _: ConversionContext): T {
return json
}
}
@ -224,6 +224,7 @@ export class FirstOf<T, X> extends Conversion<T, X> {
export class Cached<TIn, TOut> extends Conversion<TIn, TOut> {
private _step: Conversion<TIn, TOut>
private readonly key: string
constructor(step: Conversion<TIn, TOut>) {
super("Secretly caches the output for the given input", [], "cached")
this._step = step
@ -242,9 +243,11 @@ export class Cached<TIn, TOut> extends Conversion<TIn, TOut> {
return converted
}
}
export class Fuse<T> extends DesugaringStep<T> {
private readonly steps: DesugaringStep<T>[]
protected debug = false
private readonly steps: DesugaringStep<T>[]
constructor(doc: string, ...steps: DesugaringStep<T>[]) {
super(
(doc ?? "") +
@ -301,7 +304,7 @@ export class SetDefault<T> extends DesugaringStep<T> {
this._overrideEmptyString = overrideEmptyString
}
convert(json: T, context: ConversionContext): T {
convert(json: T, _: ConversionContext): T {
if (json === undefined) {
return undefined
}

View file

@ -1,6 +1,7 @@
import { ConversionMessage, ConversionMsgLevel } from "./Conversion"
export class ConversionContext {
private static reported = false
/**
* The path within the data structure where we are currently operating
*/
@ -10,7 +11,6 @@ export class ConversionContext {
*/
readonly operation: ReadonlyArray<string>
readonly messages: ConversionMessage[]
private _hasErrors: boolean = false
private constructor(
@ -32,7 +32,6 @@ export class ConversionContext {
}
}
}
private static reported = false
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([], [...path], [...operation])
@ -76,6 +75,31 @@ export class ConversionContext {
return "\x1b[31m" + s + "\x1b[0m"
}
/**
* Does an inline edit of the messages for which a new path is defined
* This is a slight hack
* @param rewritePath
*/
public rewriteMessages(
rewritePath: (
p: ReadonlyArray<number | string>
) => undefined | ReadonlyArray<number | string>
): void {
for (let i = 0; i < this.messages.length; i++) {
const m = this.messages[i]
const newPath = rewritePath(m.context.path)
if (!newPath) {
continue
}
const rewrittenContext = new ConversionContext(
this.messages,
newPath,
m.context.operation
)
this.messages[i] = <ConversionMessage>{ ...m, context: rewrittenContext }
}
}
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
if (typeof key === "number" && key < 0) {

View file

@ -24,7 +24,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
this._includeClosedNotesDays = includeClosedNotesDays
}
convert(layerJson: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(layerJson: LayerConfigJson, _: ConversionContext): LayerConfigJson {
const t = Translations.t.importLayer
/**

View file

@ -589,7 +589,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
this._desugaring = desugaring
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
if (this._desugaring.tagRenderings === null) {
return json
}
@ -762,7 +762,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* }
* }}
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded(special, context) // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE)}{_entrances_count_without_width_count} entrances don't have width information yet"}
* RewriteSpecial.convertIfNeeded(special, context) // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE,)}{_entrances_count_without_width_count} entrances don't have width information yet"}
* context.getAll("error") // => []
*
* // another actual test
@ -773,7 +773,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* "tagrendering": "<b>{id}</b> ({distance}m) {tagApply(a,b,c)}"
* }}
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded(special, context) // => {"*": "{multi(_nearby_bicycle_parkings:props,<b>&LBRACEid&RBRACE</b> &LPARENS&LBRACEdistance&RBRACEm&RPARENS &LBRACEtagApply&LPARENSa&COMMAb&COMMAc&RPARENS&RBRACE)}"}
* RewriteSpecial.convertIfNeeded(special, context) // => {"*": "{multi(_nearby_bicycle_parkings:props,<b>&LBRACEid&RBRACE</b> &LPARENS&LBRACEdistance&RBRACEm&RPARENS &LBRACEtagApply&LPARENSa&COMMAb&COMMAc&RPARENS&RBRACE,)}"}
* context.getAll("error") // => []
*/
private static convertIfNeeded(
@ -1039,7 +1039,7 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
if (!needsSpecial) {
return json
}
context.info("Layer " + json.id + " needs the fullNodeDatabase")
context.debug("Layer " + json.id + " needs the fullNodeDatabase")
return { ...json, fullNodeDatabase: true }
}
}
@ -1088,7 +1088,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
)
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
if (json.source === "special" || json.source === "special:library") {
return json
}
@ -1113,7 +1113,7 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
)
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
if (!json.tagRenderings) {
return json
}

View file

@ -1,4 +1,14 @@
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 { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { PrepareLayer } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -19,7 +29,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
super(
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form. Note that 'tagRenderings+' will be inserted before 'leftover-questions'",
[],
"SubstituteLayer",
"SubstituteLayer"
)
this._state = state
}
@ -61,6 +71,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
for (const name of names) {
const found = Utils.Clone(state.sharedLayers.get(name))
found["_basedOn"] = name
if (found === undefined) {
reportNotFound(name)
continue
@ -70,15 +81,16 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
(found["tagRenderings"] ?? []).length > 0
) {
context.err(
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`,
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
)
}
try {
const trPlus = json["override"]["tagRenderings+"]
if(trPlus){
let index = found.tagRenderings.findIndex(tr => tr["id"] === "leftover-questions")
if(index < 0){
if (trPlus) {
let index = found.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions"
)
if (index < 0) {
index = found.tagRenderings.length
}
found.tagRenderings.splice(index, 0, ...trPlus)
@ -90,14 +102,18 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
} catch (e) {
context.err(
`Could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
json["override"],
)}`,
json["override"]
)}`
)
}
if (json["hideTagRenderingsWithLabels"]) {
if (typeof json["hideTagRenderingsWithLabels"] === "string") {
throw "At " + context + ".hideTagRenderingsWithLabels should be a list containing strings, you specified a string"
throw (
"At " +
context +
".hideTagRenderingsWithLabels should be a list containing strings, you specified a string"
)
}
const hideLabels: Set<string> = new Set(json["hideTagRenderingsWithLabels"])
// These labels caused at least one deletion
@ -111,9 +127,9 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(labels[forbiddenLabel])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel],
tr["id"] +
" as it has a forbidden label: " +
labels[forbiddenLabel]
)
continue
}
@ -122,7 +138,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
if (hideLabels.has(tr["id"])) {
usedLabels.add(tr["id"])
context.info(
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label",
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
)
continue
}
@ -131,10 +147,10 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
usedLabels.add(tr["group"])
context.info(
"Dropping tagRendering " +
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label",
tr["id"] +
" as its group `" +
tr["group"] +
"` is a forbidden label"
)
continue
}
@ -145,8 +161,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
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",
unused.join(", ") +
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
)
}
found.tagRenderings = filtered
@ -163,7 +179,7 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
super(
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
["layers"],
"AddDefaultLayers",
"AddDefaultLayers"
)
this._state = state
}
@ -186,10 +202,10 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
if (alreadyLoaded.has(v.id)) {
context.warn(
"Layout " +
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer",
context +
" already has a layer with name " +
v.id +
"; skipping inclusion of this builtin layer"
)
continue
}
@ -205,14 +221,14 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
super(
"For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)",
["layers"],
"AddImportLayers",
"AddImportLayers"
)
}
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
if (!(json.enableNoteImports ?? true)) {
context.info(
"Not creating a note import layers for theme " + json.id + " as they are disabled",
"Not creating a note import layers for theme " + json.id + " as they are disabled"
)
return json
}
@ -247,7 +263,7 @@ class AddImportLayers extends DesugaringStep<LayoutConfigJson> {
try {
const importLayerResult = creator.convert(
layer,
context.inOperation(this.name).enter(i1),
context.inOperation(this.name).enter(i1)
)
if (importLayerResult !== undefined) {
json.layers.push(importLayerResult)
@ -266,7 +282,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<LayoutConfigJson>
super(
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
["_context"],
"AddContextToTranlationsInLayout",
"AddContextToTranlationsInLayout"
)
}
@ -281,11 +297,11 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
super(
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
["overrideAll", "layers"],
"ApplyOverrideAll",
"ApplyOverrideAll"
)
}
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
convert(json: LayoutConfigJson, _: ConversionContext): LayoutConfigJson {
const overrideAll = json.overrideAll
if (overrideAll === undefined) {
return json
@ -309,8 +325,9 @@ class ApplyOverrideAll extends DesugaringStep<LayoutConfigJson> {
if (!layer.tagRenderings) {
layer.tagRenderings = tagRenderingsPlus
} else {
let index = layer.tagRenderings.findIndex(tr => tr["id"] === "leftover-questions")
let index = layer.tagRenderings.findIndex(
(tr) => tr["id"] === "leftover-questions"
)
if (index < 0) {
index = layer.tagRenderings.length - 1
}
@ -337,7 +354,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
Some layers (e.g. \`all_buildings_and_walls\' or \'streets_with_a_name\') are invisible, so by default, \'force_load\' is set too.
`,
["layers"],
"AddDependencyLayersToTheme",
"AddDependencyLayersToTheme"
)
this._state = state
}
@ -345,7 +362,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
private static CalculateDependencies(
alreadyLoaded: LayerConfigJson[],
allKnownLayers: Map<string, LayerConfigJson>,
themeId: string,
themeId: string
): { config: LayerConfigJson; reason: string }[] {
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l.id))
@ -368,7 +385,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
for (const layerConfig of alreadyLoaded) {
try {
const layerDeps = DependencyCalculator.getLayerDependencies(
new LayerConfig(layerConfig, themeId + "(dependencies)"),
new LayerConfig(layerConfig, themeId + "(dependencies)")
)
dependencies.push(...layerDeps)
} catch (e) {
@ -405,10 +422,10 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
if (dep === undefined) {
const message = [
"Loading a dependency failed: layer " +
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
unmetDependency.neededLayer +
" is not found, neither as layer of " +
themeId +
" nor as builtin layer.",
reason,
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
]
@ -424,7 +441,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
})
loadedLayerIds.add(dep.id)
unmetDependencies = unmetDependencies.filter(
(d) => d.neededLayer !== unmetDependency.neededLayer,
(d) => d.neededLayer !== unmetDependency.neededLayer
)
}
} while (unmetDependencies.length > 0)
@ -445,14 +462,12 @@ class AddDependencyLayersToTheme extends DesugaringStep<LayoutConfigJson> {
const dependencies = AddDependencyLayersToTheme.CalculateDependencies(
layers,
allKnownLayers,
theme.id,
theme.id
)
for (const dependency of dependencies) {
}
if (dependencies.length > 0) {
for (const dependency of dependencies) {
context.info(
"Added " + dependency.config.id + " to the theme. " + dependency.reason,
"Added " + dependency.config.id + " to the theme. " + dependency.reason
)
}
}
@ -494,7 +509,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
super(
"Generates a warning if a theme uses an unsubstituted layer",
["layers"],
"WarnForUnsubstitutedLayersInTheme",
"WarnForUnsubstitutedLayersInTheme"
)
}
@ -506,7 +521,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
context
.enter("layers")
.err(
"No layers are defined. You must define at least one layer to have a valid theme",
"No layers are defined. You must define at least one layer to have a valid theme"
)
return json
}
@ -530,16 +545,64 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<LayoutConfigJson>
context.warn(
"The theme " +
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged.",
json.id +
" has an inline layer: " +
layer["id"] +
". This is discouraged."
)
}
return json
}
}
class PostvalidateTheme extends DesugaringStep<LayoutConfigJson> {
private readonly _state: DesugaringContext
constructor(state: DesugaringContext) {
super("Various validation steps when everything is done", [], "PostvalidateTheme")
this._state = state
}
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
for (const l of json.layers) {
const layer = <LayerConfigJson>l
const basedOn = <string>layer["_basedOn"]
const basedOnDef = this._state.sharedLayers.get(basedOn)
if (!basedOn) {
continue
}
if (layer["name"] === null) {
continue
}
const sameBasedOn = <LayerConfigJson[]>(
json.layers.filter(
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id
)
)
const minZoomAll = Math.min(...sameBasedOn.map((sbo) => sbo.minzoom))
const sameNameDetected = sameBasedOn.some(
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"])
)
if (!sameNameDetected) {
// The name is unique, so it'll won't be confusing
continue
}
if (minZoomAll < layer.minzoom) {
context.err(
"There are multiple layers based on " +
basedOn +
". The layer with id " +
layer.id +
" has a minzoom of " +
layer.minzoom +
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer."
)
}
}
return json
}
}
export class PrepareTheme extends Fuse<LayoutConfigJson> {
private state: DesugaringContext
@ -547,7 +610,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
state: DesugaringContext,
options?: {
skipDefaultLayers: false | boolean
},
}
) {
super(
"Fully prepares and expands a theme",
@ -559,7 +622,8 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
new SetDefault("socialImage", "assets/SocialImage.png", true),
// We expand all tagrenderings first...
new On("layers", new Each(new PrepareLayer(state))),
// Then we apply the override all. Note that it'll cheat with tagRenderings+
// Then we apply the override all. We must first expand everything in case that we override something in an expanded tag
// Note that it'll cheat with tagRenderings+
new ApplyOverrideAll(),
// And then we prepare all the layers _again_ in case that an override all contained unexpanded tagrenderings!
new On("layers", new Each(new PrepareLayer(state))),
@ -568,6 +632,7 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
: new AddDefaultLayers(state),
new AddDependencyLayersToTheme(state),
new AddImportLayers(),
new PostvalidateTheme(state)
)
this.state = state
}
@ -582,13 +647,13 @@ export class PrepareTheme extends Fuse<LayoutConfigJson> {
const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
l.tagRenderings?.some((tr) =>
ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
(special) => special.needsNodeDatabase,
),
),
(special) => special.needsNodeDatabase
)
)
)
if (needsNodeDatabase) {
context.info(
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes",
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
)
result.enableNodeDatabase = true
}

View file

@ -13,7 +13,10 @@ import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "../Json/QuestionableTagRenderingConfigJson"
import Validators from "../../../UI/InputElement/Validators"
import TagRenderingConfig from "../TagRenderingConfig"
import { parse as parse_html } from "node-html-parser"
@ -31,7 +34,7 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
super(
"Checks that the given object is fully translated in the specified languages",
[],
"ValidateLanguageCompleteness",
"ValidateLanguageCompleteness"
)
this._languages = languages ?? ["en"]
}
@ -45,18 +48,18 @@ class ValidateLanguageCompleteness extends DesugaringStep<LayoutConfig> {
.filter(
(t) =>
t.tr.translations[neededLanguage] === undefined &&
t.tr.translations["*"] === undefined,
t.tr.translations["*"] === undefined
)
.forEach((missing) => {
context
.enter(missing.context.split("."))
.err(
`The theme ${obj.id} should be translation-complete for ` +
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en"),
neededLanguage +
", but it lacks a translation for " +
missing.context +
".\n\tThe known translation is " +
missing.tr.textFor("en")
)
})
}
@ -73,7 +76,7 @@ export class DoesImageExist extends DesugaringStep<string> {
constructor(
knownImagePaths: Set<string>,
checkExistsSync: (path: string) => boolean = undefined,
ignore?: Set<string>,
ignore?: Set<string>
) {
super("Checks if an image exists", [], "DoesImageExist")
this._ignore = ignore
@ -109,15 +112,15 @@ export class DoesImageExist extends DesugaringStep<string> {
if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined) {
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)) {
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 {
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`
)
}
}
@ -141,7 +144,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings?: Set<string>,
sharedTagRenderings?: Set<string>
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
this._validateImage = doesImageExist
@ -160,15 +163,15 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
if (json["units"] !== undefined) {
context.err(
"The theme " +
json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) ",
json.id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
)
}
if (json["roamingRenderings"] !== undefined) {
context.err(
"Theme " +
json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead",
json.id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
)
}
}
@ -186,10 +189,10 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
for (const remoteImage of remoteImages) {
context.err(
"Found a remote image: " +
remoteImage.path +
" in theme " +
json.id +
", please download it.",
remoteImage.path +
" in theme " +
json.id +
", please download it."
)
}
for (const image of images) {
@ -205,17 +208,17 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const filename = this._path.substring(
this._path.lastIndexOf("/") + 1,
this._path.length - 5,
this._path.length - 5
)
if (theme.id !== filename) {
context.err(
"Theme ids should be the same as the name.json, but we got id: " +
theme.id +
" and filename " +
filename +
" (" +
this._path +
")",
theme.id +
" and filename " +
filename +
" (" +
this._path +
")"
)
}
this._validateImage.convert(theme.icon, context.enter("icon"))
@ -223,13 +226,13 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) {
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) {
new ValidateLanguageCompleteness(...json["mustHaveLanguage"]).convert(
theme,
context,
context
)
}
if (!json.hideFromOverview && theme.id !== "personal" && this._isBuiltin) {
@ -237,7 +240,7 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
const targetLanguage = theme.title.SupportedLanguages()[0]
if (targetLanguage !== "en") {
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`
)
}
@ -283,7 +286,9 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
for (let i = 0; i < theme.layers.length; i++) {
const layer = theme.layers[i]
if (!layer.id.match("[a-z][a-z0-9_]*")) {
context.enters("layers", i, "id").err("Invalid ID:" + layer.id + "should match [a-z][a-z0-9_]*")
context
.enters("layers", i, "id")
.err("Invalid ID:" + layer.id + "should match [a-z][a-z0-9_]*")
}
}
@ -296,7 +301,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings?: Set<string>,
sharedTagRenderings?: Set<string>
) {
super(
"Validates a theme and the contained layers",
@ -306,10 +311,10 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
new Each(
new Bypass(
(layer) => Constants.added_by_default.indexOf(<any>layer.id) < 0,
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true),
),
),
),
new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true)
)
)
)
)
}
}
@ -319,7 +324,7 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
super(
"Checks that an 'overrideAll' does not override a single override",
[],
"OverrideShadowingCheck",
"OverrideShadowingCheck"
)
}
@ -380,7 +385,9 @@ class MiscThemeChecks extends DesugaringStep<LayoutConfigJson> {
if (l["override"]["id"] !== undefined) {
continue
}
context.enters("layers", i).err("A layer which changes the source-tags must also change the ID")
context
.enters("layers", i)
.err("A layer which changes the source-tags must also change the ID")
}
}
@ -393,7 +400,7 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
super(
"Various consistency checks on the raw JSON",
new MiscThemeChecks(),
new OverrideShadowingCheck(),
new OverrideShadowingCheck()
)
}
}
@ -403,7 +410,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
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",
[],
"DetectConflictingAddExtraTags",
"DetectConflictingAddExtraTags"
)
}
@ -430,7 +437,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
.enters("mappings", i)
.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 " +
duplicateKeys.join(", "),
duplicateKeys.join(", ")
)
}
}
@ -448,13 +455,13 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
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",
[],
"DetectNonErasedKeysInMappings",
"DetectNonErasedKeysInMappings"
)
}
convert(
json: QuestionableTagRenderingConfigJson,
context: ConversionContext,
context: ConversionContext
): QuestionableTagRenderingConfigJson {
if (json.multiAnswer) {
// No need to check this here, this has its own validation
@ -508,8 +515,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("freeform")
.warn(
"The freeform block does not modify the key `" +
neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it",
neededKey +
"` which is set in a mapping. Use `addExtraTags` to overwrite it"
)
}
}
@ -527,8 +534,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
.enters("mappings", i)
.warn(
"This mapping does not modify the key `" +
neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it",
neededKey +
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it"
)
}
}
@ -552,7 +559,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
*/
private static extractCalculatedTagNames(
layerConfig?: LayerConfigJson | { calculatedTags: string[] },
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
) {
return (
layerConfig?.calculatedTags?.map((ct) => {
@ -638,16 +645,16 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
json.mappings[i]["hideInAnswer"] !== true
) {
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) {
// The current mapping is shadowed!
context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
The mapping ${parsedConditions[i].asHumanString(
false,
false,
{},
)} is fully matched by a previous mapping (namely ${j}), which matches:
false,
false,
{}
)} is fully matched by a previous mapping (namely ${j}), which matches:
${parsedConditions[j].asHumanString(false, false, {})}.
To fix this problem, you can try to:
@ -674,7 +681,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
super(
"Checks that 'then'clauses in mappings don't have images, but use 'icon' instead",
[],
"DetectMappingsWithImages",
"DetectMappingsWithImages"
)
this._doesImageExist = doesImageExist
}
@ -714,14 +721,14 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
if (!ignore) {
ctx.err(
`A mapping has an image in the 'then'-clause. Remove the image there and use \`"icon": <your-image>\` instead. The images found are ${images.join(
", ",
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`,
", "
)}. (This check can be turned of by adding "#": "${ignoreToken}" in the mapping, but this is discouraged`
)
} else {
ctx.info(
`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) {
@ -742,7 +749,7 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
super(
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
[],
"ValidatePossibleLinks",
"ValidatePossibleLinks"
)
}
@ -772,21 +779,21 @@ class ValidatePossibleLinks extends DesugaringStep<string | Record<string, strin
convert(
json: string | Record<string, string>,
context: ConversionContext,
context: ConversionContext
): string | Record<string, string> {
if (typeof json === "string") {
if (this.isTabnabbingProne(json)) {
context.err(
"The string " +
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping",
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
)
}
} else {
for (const k in json) {
if (this.isTabnabbingProne(json[k])) {
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`
)
}
}
@ -804,7 +811,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
super(
"Checks that a translation is valid and internally consistent",
["*"],
"CheckTranslation",
"CheckTranslation"
)
this._allowUndefined = allowUndefined
}
@ -850,17 +857,17 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
convert(
json: TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
context: ConversionContext,
context: ConversionContext
): TagRenderingConfigJson {
if (json["special"] !== undefined) {
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") {
context.warn(
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`,
`use the content directly instead of {render: ${JSON.stringify(json["render"])}}`
)
}
@ -869,13 +876,32 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
CheckTranslation.allowUndefined.convert(json[key], context.enter(key))
}
for (let i = 0; i < json.mappings?.length ?? 0; i++) {
const mapping = json.mappings[i]
const mapping: MappingConfigJson = json.mappings[i]
CheckTranslation.noUndefined.convert(
mapping.then,
context.enters("mappings", i, "then"),
context.enters("mappings", i, "then")
)
if (!mapping.if) {
context.enters("mappings", i).err("No `if` is defined")
console.log(
"Checking mappings",
i,
"if",
mapping.if,
context.path.join("."),
mapping.then
)
context.enters("mappings", i, "if").err("No `if` is defined")
}
if (mapping.addExtraTags) {
for (let j = 0; j < mapping.addExtraTags.length; j++) {
if (!mapping.addExtraTags[j]) {
context
.enters("mappings", i, "addExtraTags", j)
.err(
"Detected a 'null' or 'undefined' value. Either specify a tag or delete this item"
)
}
}
}
const en = mapping?.then?.["en"]
if (en && this.detectYesOrNo(en)) {
@ -883,18 +909,18 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context
.enters("mappings", i, "then")
.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"]) {
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) {
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) {
@ -904,7 +930,15 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context
.enter("questionHint")
.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"
)
}
if (json.icon?.["size"]) {
context
.enters("icon", "size")
.err(
"size is not a valid attribute. Did you mean 'class'? Class can be one of `small`, `medium` or `large`"
)
}
@ -914,10 +948,10 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
.enter("render")
.err(
"This tagRendering allows to set a value to key " +
json.freeform.key +
", but does not define a `render`. Please, add a value here which contains `{" +
json.freeform.key +
"}`",
json.freeform.key +
", but does not define a `render`. Please, add a value here which contains `{" +
json.freeform.key +
"}`"
)
} else {
const render = new Translation(<any>json.render)
@ -948,7 +982,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
const keyFirstArg = ["canonical", "fediverse_link", "translated"]
if (
keyFirstArg.some(
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0,
(funcName) => txt.indexOf(`{${funcName}(${json.freeform.key}`) >= 0
)
) {
continue
@ -971,7 +1005,7 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
context
.enter("render")
.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!`,
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. This is a bug, as this rendering should show exactly this freeform key!`
)
}
}
@ -979,8 +1013,8 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
if (json.render && json["question"] && json.freeform === undefined) {
context.err(
`Detected a tagrendering which takes input without freeform key in ${context}; the question is ${new Translation(
json["question"],
).textFor("en")}`,
json["question"]
).textFor("en")}`
)
}
@ -991,13 +1025,16 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
.enters("freeform", "type")
.err(
"Unknown type: " +
freeformType +
"; try one of " +
Validators.availableTypes.join(", "),
freeformType +
"; try one of " +
Validators.availableTypes.join(", ")
)
}
}
if (context.hasErrors()) {
return undefined
}
return json
}
@ -1017,6 +1054,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
constructor(layerConfig?: LayerConfigJson, doesImageExist?: DoesImageExist) {
super(
"Various validation on tagRenderingConfigs",
new MiscTagRenderingChecks(),
new DetectShadowedMappings(layerConfig),
new DetectConflictingAddExtraTags(),
// TODO enable new DetectNonErasedKeysInMappings(),
@ -1025,7 +1063,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
new On("question", new ValidatePossibleLinks()),
new On("questionHint", new ValidatePossibleLinks()),
new On("mappings", new Each(new On("then", new ValidatePossibleLinks()))),
new MiscTagRenderingChecks(),
new MiscTagRenderingChecks()
)
}
}
@ -1040,7 +1078,12 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
private readonly _studioValidations: boolean
private readonly _validatePointRendering = new ValidatePointRendering()
constructor(path: string, isBuiltin, doesImageExist, studioValidations) {
constructor(
path: string,
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean
) {
super("Runs various checks against common mistakes for a layer", [], "PrevalidateLayer")
this._path = path
this._isBuiltin = isBuiltin
@ -1065,7 +1108,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context
.enter("source")
.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 {
if (json.source === "special" || json.source === "special:library") {
@ -1073,7 +1116,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context
.enters("source", "osmTags")
.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 {
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
@ -1082,7 +1125,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enters("source", "osmTags")
.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" +
osmTags.asHumanString(false, false, {}),
osmTags.asHumanString(false, false, {})
)
}
}
@ -1108,10 +1151,10 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enter("syncSelection")
.err(
"Invalid sync-selection: must be one of " +
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
" but got '" +
json.syncSelection +
"'",
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
" but got '" +
json.syncSelection +
"'"
)
}
if (json["pointRenderings"]?.length > 0) {
@ -1129,7 +1172,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context.enter("pointRendering").err("There are no pointRenderings at all...")
}
json.pointRendering?.forEach((pr, i) => this._validatePointRendering.convert(pr, context.enters("pointeRendering", i)))
json.pointRendering?.forEach((pr, i) =>
this._validatePointRendering.convert(pr, context.enters("pointeRendering", i))
)
if (json["mapRendering"]) {
context.enter("mapRendering").err("This layer has a legacy 'mapRendering'")
@ -1145,8 +1190,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
context.err(
"Layer " +
json.id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer",
json.id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
)
}
}
@ -1156,24 +1201,24 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
new On("tagRendering", new Each(new ValidateTagRenderings(json)))
new On("tagRenderings", new Each(new ValidateTagRenderings(json)))
if (json.title === undefined && json.source !== "special:library") {
context
.enter("title")
.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) {
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
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++) {
const tagRendering = json.tagRenderings[i]
@ -1203,7 +1248,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{
// duplicate ids in tagrenderings check
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) {
// 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
@ -1241,8 +1286,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (json["overpassTags"] !== undefined) {
context.err(
"Layer " +
json.id +
"still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)",
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)'
)
}
const forbiddenTopLevel = [
@ -1262,7 +1307,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
}
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
context.err(
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'",
"Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
}
@ -1279,9 +1324,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (this._path != undefined && this._path.indexOf(expected) < 0) {
context.err(
"Layer is in an incorrect place. The path is " +
this._path +
", but expected " +
expected,
this._path +
", but expected " +
expected
)
}
}
@ -1299,13 +1344,13 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enter(["tagRenderings", ...emptyIndexes])
.err(
`Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
",",
)}])`,
","
)}])`
)
}
const duplicateIds = Utils.Duplicates(
(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) {
context
@ -1329,7 +1374,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
if (json.tagRenderings !== undefined) {
new On(
"tagRenderings",
new Each(new ValidateTagRenderings(json, this._doesImageExist)),
new Each(new ValidateTagRenderings(json, this._doesImageExist))
).convert(json, context)
}
@ -1356,7 +1401,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
context
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
.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."
)
}
}
@ -1394,9 +1439,9 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
.enters("presets", i, "tags")
.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: " +
tags.asHumanString(false, false, {}) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {}),
tags.asHumanString(false, false, {}) +
"\n The required tags are: " +
baseTags.asHumanString(false, false, {})
)
}
}
@ -1413,7 +1458,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
this.validator = new ValidateLayer(
@ -1421,7 +1466,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
isBuiltin,
doesImageExist,
studioValidations,
skipDefaultLayers,
skipDefaultLayers
)
}
@ -1446,21 +1491,23 @@ class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJson> {
}
if (json["markers"]) {
context.enter("markers").err(`Detected a field 'markerS' in pointRendering. It is written as a singular case`)
context
.enter("markers")
.err(
`Detected a field 'markerS' in pointRendering. It is written as a singular case`
)
}
if (json.marker && !Array.isArray(json.marker)) {
context.enter("marker").err(
"The marker in a pointRendering should be an array",
)
context.enter("marker").err("The marker in a pointRendering should be an array")
}
if (json.location.length == 0) {
context.enter("location").err(
"A pointRendering should have at least one 'location' to defined where it should be rendered. ",
)
context
.enter("location")
.err(
"A pointRendering should have at least one 'location' to defined where it should be rendered. "
)
}
return json
}
}
@ -1476,31 +1523,30 @@ export class ValidateLayer extends Conversion<
isBuiltin: boolean,
doesImageExist: DoesImageExist,
studioValidations: boolean = false,
skipDefaultLayers: boolean = false,
skipDefaultLayers: boolean = false
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
this._prevalidation = new PrevalidateLayer(
path,
isBuiltin,
doesImageExist,
studioValidations,
studioValidations
)
this._skipDefaultLayers = skipDefaultLayers
}
convert(
json: LayerConfigJson,
context: ConversionContext,
context: ConversionContext
): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name)
if (typeof json === "string") {
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
}
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
return { parsed: undefined, raw: json }
}
@ -1527,7 +1573,7 @@ export class ValidateLayer extends Conversion<
context
.enters("calculatedTags", i)
.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}`
)
}
}
@ -1578,8 +1624,8 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
.enters("fields", i)
.err(
`Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
Validators.availableTypes,
).join(",")}`,
Validators.availableTypes
).join(",")}`
)
}
}
@ -1596,13 +1642,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{
super(
"Tries to detect layers where a shared filter can be used (or where similar filters occur)",
[],
"DetectDuplicateFilters",
"DetectDuplicateFilters"
)
}
convert(
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
context: ConversionContext,
context: ConversionContext
): { layers: LayerConfigJson[]; themes: LayoutConfigJson[] } {
const { layers, themes } = json
const perOsmTag = new Map<
@ -1666,7 +1712,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
filter: FilterConfigJson
}[]
>,
layout?: LayoutConfigJson | undefined,
layout?: LayoutConfigJson | undefined
): void {
if (layer.filter === undefined || layer.filter === null) {
return
@ -1706,7 +1752,7 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
super(
"Detects mappings which have identical (english) names or identical mappings.",
["presets"],
"DetectDuplicatePresets",
"DetectDuplicatePresets"
)
}
@ -1717,13 +1763,13 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames)
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)
context.err(
`This themes has multiple presets which are named:${dups}, namely layers ${layerIds.join(
", ",
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`,
", "
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
)
}
@ -1738,17 +1784,17 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
Utils.SameObject(presetATags, presetBTags) &&
Utils.sameList(
presetA.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers,
presetB.preciseInput.snapToLayers
)
) {
context.err(
`This themes has multiple presets with the same tags: ${presetATags.asHumanString(
false,
false,
{},
{}
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
j
].title.textFor("en")}'`,
].title.textFor("en")}'`
)
}
}
@ -1758,21 +1804,35 @@ export class DetectDuplicatePresets extends DesugaringStep<LayoutConfig> {
}
}
export class ValidateThemeEnsemble extends Conversion<LayoutConfig[], Map<string, {
tags: TagsFilter,
foundInTheme: string[]
}>> {
export class ValidateThemeEnsemble extends Conversion<
LayoutConfig[],
Map<
string,
{
tags: TagsFilter
foundInTheme: string[]
}
>
> {
constructor() {
super("Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes", [], "ValidateThemeEnsemble")
super(
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes",
[],
"ValidateThemeEnsemble"
)
}
convert(json: LayoutConfig[], context: ConversionContext): Map<string, {
tags: TagsFilter,
foundInTheme: string[]
}> {
const idToSource = new Map<string, { tags: TagsFilter, foundInTheme: string[] }>()
convert(
json: LayoutConfig[],
context: ConversionContext
): Map<
string,
{
tags: TagsFilter
foundInTheme: string[]
}
> {
const idToSource = new Map<string, { tags: TagsFilter; foundInTheme: string[] }>()
for (const theme of json) {
for (const layer of theme.layers) {
@ -1804,16 +1864,18 @@ export class ValidateThemeEnsemble extends Conversion<LayoutConfig[], Map<string
oldTheme.push(theme.id)
continue
}
context.err(["The layer with id '" + id + "' is found in multiple themes with different tag definitions:",
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
].join("\n"))
context.err(
[
"The layer with id '" +
id +
"' is found in multiple themes with different tag definitions:",
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
].join("\n")
)
}
}
return idToSource
}
}

View file

@ -3,7 +3,6 @@ import { Utils } from "../../../Utils"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { render } from "sass"
export default class ValidationUtils {
public static getAllSpecialVisualisations(