Refactoring: use more accurate context in conversion, fix tests

This commit is contained in:
Pieter Vander Vennet 2023-10-12 16:55:26 +02:00
parent 86d0de3806
commit f77d99f8ed
43 changed files with 999 additions and 367 deletions

View file

@ -27,14 +27,14 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
* builtin: ["abc"],
* override: {
* title:{
* _context: "prefix:context.layers.0.override.title"
* _context: "prefix:layers.0.override.title"
* en: "Some title"
* }
* }
@ -57,14 +57,14 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
* tagRenderings:[
* {id: "some-tr",
* question:{
* _context: "prefix:context.layers.0.tagRenderings.some-tr.question"
* _context: "prefix:layers.0.tagRenderings.some-tr.question"
* en:"Question?"
* }
* }
@ -85,7 +85,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* const expected = {
* layers: [
* {
@ -113,7 +113,7 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
* }
* ]
* }
* const rewritten = new AddContextToTranslations<any>("prefix:").convert(theme, "context").result
* const rewritten = new AddContextToTranslations<any>("prefix:").convertStrict(theme, ConversionContext.test())
* rewritten // => theme
*
*/
@ -139,7 +139,10 @@ export class AddContextToTranslations<T> extends DesugaringStep<T> {
}
}
return { ...leaf, _context: this._prefix + context + "." + path.join(".") }
return {
...leaf,
_context: this._prefix + context.path.concat(path).join("."),
}
} else {
return leaf
}

View file

@ -9,17 +9,33 @@ export interface DesugaringContext {
}
export class ConversionContext {
/**
* The path within the data structure where we are currently operating
*/
readonly path: ReadonlyArray<string | number>
/**
* Some information about the current operation
*/
readonly operation: ReadonlyArray<string>
readonly messages: ConversionMessage[] = []
readonly messages: ConversionMessage[]
private constructor(path: ReadonlyArray<string | number>, operation?: ReadonlyArray<string>) {
private constructor(
messages: ConversionMessage[],
path: ReadonlyArray<string | number>,
operation?: ReadonlyArray<string>
) {
this.path = path
this.operation = operation ?? []
// Messages is shared by reference amonst all 'context'-objects for performance
this.messages = messages
}
public static construct(path: (string | number)[], operation: string[]) {
return new ConversionContext([...path], [...operation])
return new ConversionContext([], [...path], [...operation])
}
public static test(msg?: string) {
return new ConversionContext([], msg ? [msg] : [], ["test"])
}
static print(msg: ConversionMessage) {
@ -38,12 +54,7 @@ export class ConversionContext {
msg.context.operation.join(".")
)
} else {
console.log(
" ",
msg.context.path.join("."),
msg.message,
msg.context.operation.join(".")
)
console.log(" ", msg.context.path.join("."), msg.message)
}
}
@ -57,9 +68,9 @@ export class ConversionContext {
public enter(key: string | number | (string | number)[]) {
if (!Array.isArray(key)) {
return new ConversionContext([...this.path, key], this.operation)
return new ConversionContext(this.messages, [...this.path, key], this.operation)
}
return new ConversionContext([...this.path, ...key], this.operation)
return new ConversionContext(this.messages, [...this.path, ...key], this.operation)
}
public enters(...key: (string | number)[]) {
@ -67,7 +78,7 @@ export class ConversionContext {
}
public inOperation(key: string) {
return new ConversionContext(this.path, [...this.operation, key])
return new ConversionContext(this.messages, this.path, [...this.operation, key])
}
warn(message: string) {
@ -82,15 +93,19 @@ export class ConversionContext {
this.messages.push({ context: this, level: "information", message })
}
getAll(mode: ConversionMsgLevel): ConversionMessage[] {
return this.messages.filter((m) => m.level === mode)
}
public hasErrors() {
return this.messages?.find((m) => m.level === "error") !== undefined
}
}
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
export interface ConversionMessage {
context: ConversionContext
message: string
level: "debug" | "information" | "warning" | "error"
level: ConversionMsgLevel
}
export abstract class Conversion<TIn, TOut> {
@ -106,7 +121,7 @@ export abstract class Conversion<TIn, TOut> {
public convertStrict(json: TIn, context?: ConversionContext): TOut {
context ??= ConversionContext.construct([], [])
context = context.enter(this.name)
context = context.inOperation(this.name)
const fixed = this.convert(json, context)
for (const msg of context.messages) {
ConversionContext.print(msg)
@ -126,7 +141,7 @@ export abstract class Conversion<TIn, TOut> {
export abstract class DesugaringStep<T> extends Conversion<T, T> {}
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 _step1: Conversion<TInter, TOut>
@ -145,7 +160,7 @@ class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
}
}
class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
export class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
private readonly _f: (t: TIn) => TOut
constructor(f: (t: TIn) => TOut) {
@ -205,14 +220,14 @@ export class On<P, T> extends DesugaringStep<T> {
}
convert(json: T, context: ConversionContext): T {
json = { ...json }
const step = this.step(json)
const key = this.key
const value: P = json[key]
if (value === undefined || value === null) {
return undefined
return json
}
json = { ...json }
const step = this.step(json)
json[key] = step.convert(value, context.enter(key).inOperation("on[" + key + "]"))
return json
}
@ -280,7 +295,7 @@ export class Fuse<T> extends DesugaringStep<T> {
"This fused pipeline of the following steps: " +
steps.map((s) => s.name).join(", "),
Utils.Dedup([].concat(...steps.map((step) => step.modifiedAttributes))),
"Fuse of " + steps.map((s) => s.name).join(", ")
"Fuse(" + steps.map((s) => s.name).join(", ") + ")"
)
this.steps = Utils.NoNull(steps)
}
@ -290,7 +305,7 @@ export class Fuse<T> extends DesugaringStep<T> {
const step = this.steps[i]
try {
const r = step.convert(json, context.inOperation(step.name))
if (r === undefined) {
if (r === undefined || r === null) {
break
}
if (context.hasErrors()) {

View file

@ -33,21 +33,28 @@ export class ExtractImages extends Conversion<
}
public static mightBeTagRendering(metapath: { type?: string | string[] }): boolean {
if (!Array.isArray(metapath.type)) {
if (!metapath.type) {
return false
}
return (
metapath.type?.some(
(t) =>
t !== null &&
(t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson")
) ?? false
let type: any[]
if (!Array.isArray(metapath.type)) {
type = [metapath.type]
} else {
type = metapath.type
}
return type.some(
(t) =>
t !== null &&
(t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/MinimalTagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson" ||
(t["properties"]?.render !== undefined &&
t["properties"]?.mappings !== undefined))
)
}
/**
* const images = new ExtractImages(true, new Map<string, any>()).convert(<any>{
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{
* "layers": [
* {
* tagRenderings: [
@ -75,14 +82,14 @@ export class ExtractImages extends Conversion<
* ]
* }
* ]
* }, "test").result.map(i => i.path);
* }, ConversionContext.test()).map(i => i.path);
* images.length // => 2
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") >= 0 // => true
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0 // => true
*
* // should not pickup rotation, should drop color
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
* }, "test").result
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{"layers": [{"pointRendering": [{"location": ["point", "centroid"],marker: [{"icon": "pin:black"}],rotation: 180,iconSize: "40,40,center"}]}]
* }, ConversionContext.test())
* images.length // => 1
* images[0].path // => "pin"
*
@ -233,9 +240,9 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* "id": "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/verkeerdeborden.json"
* "layers": [
* {
* "mapRendering": [
* "pointRendering": [
* {
* "icon": "./TS_bolt.svg",
* marker: [{"icon": "./TS_bolt.svg"}],
* iconBadges: [{
* if: "id=yes",
* then: {
@ -256,9 +263,9 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
* }
* ],
* }
* const fixed = new FixImages(new Set<string>()).convert(<any> theme, "test").result
* fixed.layers[0]["mapRendering"][0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["mapRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
* const fixed = new FixImages(new Set<string>()).convert(<any> theme, ConversionContext.test())
* fixed.layers[0]["pointRendering"][0].marker[0].icon // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/TS_bolt.svg"
* fixed.layers[0]["pointRendering"][0].iconBadges[0].then.mappings[0].then // => "https://raw.githubusercontent.com/seppesantens/MapComplete-Themes/main/VerkeerdeBordenDatabank/Something.svg"
*/
convert(json: LayoutConfigJson, context: ConversionContext): LayoutConfigJson {
let url: URL

View file

@ -11,7 +11,10 @@ import {
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -27,6 +30,7 @@ import ValidationUtils from "./ValidationUtils"
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -157,6 +161,25 @@ class ExpandTagRendering extends Conversion<
}
}
public convert(
spec: string | any,
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
result.push(...stable)
} else {
result.push(tr)
}
}
return result
}
private lookup(name: string): TagRenderingConfigJson[] | undefined {
const direct = this.directLookup(name)
@ -386,25 +409,6 @@ class ExpandTagRendering extends Conversion<
return [tr]
}
public convert(
spec: string | any,
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
result.push(...stable)
} else {
result.push(tr)
}
}
return result
}
}
class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
@ -711,7 +715,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* },
* renderings: "The value of xyz is abc"
* }
* new ExpandRewrite().convertStrict(spec, "test") // => ["The value of X is A", "The value of Y is B", "The value of Z is C"]
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => ["The value of X is A", "The value of Y is B", "The value of Z is C"]
*
* // should rewrite with translations
* const spec = <RewritableConfigJson<any>>{
@ -733,7 +737,7 @@ export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[
* nl: "De waarde van Y is een andere waarde"
* }
* ]
* new ExpandRewrite().convertStrict(spec, "test") // => expected
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => expected
*/
convert(json: T | RewritableConfigJson<T>, context: ConversionContext): T[] {
if (json === null || json === undefined) {
@ -808,39 +812,38 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* Does the heavy lifting and conversion
*
* // should not do anything if no 'special'-key is present
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, [], "test") // => {"en": "xyz", "nl": "abc"}
* RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, ConversionContext.test()) // => {"en": "xyz", "nl": "abc"}
*
* // should handle a simple special case
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, [], "test") // => {'*': "{image_carousel()}"}
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, ConversionContext.test()) // => {'*': "{image_carousel()}"}
*
* // should handle special case with a parameter
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, [], "test") // => {'*': "{image_carousel(some_image_key)}"}
* RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, ConversionContext.test()) // => {'*': "{image_carousel(some_image_key)}"}
*
* // should handle special case with a translated parameter
* const spec = {"special": {"type":"image_upload", "label": {"en": "Add a picture to this object", "nl": "Voeg een afbeelding toe"}}}
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
* r // => {"en": "{image_upload(,Add a picture to this object)}", "nl": "{image_upload(,Voeg een afbeelding toe)}" }
*
* // should handle special case with a prefix and postfix
* const spec = {"special": {"type":"image_upload" }, before: {"en": "PREFIX "}, after: {"en": " POSTFIX", nl: " Achtervoegsel"} }
* const r = RewriteSpecial.convertIfNeeded(spec, [], "test")
* const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
* r // => {"en": "PREFIX {image_upload(,)} POSTFIX", "nl": "PREFIX {image_upload(,)} Achtervoegsel" }
*
* // should warn for unexpected keys
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, errors, "test") // => {'*': "{image_carousel()}"}
* errors // => ["At test: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"]
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, context) // => {'*': "{image_carousel()}"}
* context.getAll("error")[0].message // => "The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put 'en' into the special block?"
*
* // should give an error on unknown visualisations
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, errors, "test") // => undefined
* errors.length // => 1
* errors[0].indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, context) // => undefined
* context.getAll("error")[0].message.indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
*
* // should give an error is 'type' is missing
* const errors = []
* RewriteSpecial.convertIfNeeded({"special": {}}, errors, "test") // => undefined
* errors // => ["A 'special'-block should define 'type' to indicate which visualisation should be used"]
* const context = ConversionContext.test()
* RewriteSpecial.convertIfNeeded({"special": {}}, context) // => undefined
* context.getAll("error")[0].message // => "A 'special'-block should define 'type' to indicate which visualisation should be used"
*
*
* // an actual test
@ -858,9 +861,9 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* "en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
* }
* }}
* const errors = []
* RewriteSpecial.convertIfNeeded(special, errors, "test") // => {"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"}
* errors // => []
* 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"}
* context.getAll("error") // => []
*/
private static convertIfNeeded(
input:
@ -870,8 +873,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
})
| any,
errors: string[],
context: string
context: ConversionContext
): any {
const special = input["special"]
if (special === undefined) {
@ -880,7 +882,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
const type = special["type"]
if (type === undefined) {
errors.push(
context.err(
"A 'special'-block should define 'type' to indicate which visualisation should be used"
)
return undefined
@ -893,37 +895,35 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
SpecialVisualizations.specialVisualizations,
(sp) => sp.funcName
)
errors.push(
context.err(
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
)
return undefined
}
errors.push(
...Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `At ${context}: The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
)
Array.from(Object.keys(input))
.filter((k) => k !== "special" && k !== "before" && k !== "after")
.map((k) => {
return `The only keys allowed next to a 'special'-block are 'before' and 'after'. Perhaps you meant to put '${k}' into the special block?`
})
.forEach((e) => context.err(e))
const argNamesList = vis.args.map((a) => a.name)
const argNames = new Set<string>(argNamesList)
// Check for obsolete and misspelled arguments
errors.push(
...Object.keys(special)
.filter((k) => !argNames.has(k))
.filter((k) => k !== "type" && k !== "before" && k !== "after")
.map((wrongArg) => {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x
)
return `At ${context}: Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
)
Object.keys(special)
.filter((k) => !argNames.has(k))
.filter((k) => k !== "type" && k !== "before" && k !== "after")
.map((wrongArg) => {
const byDistance = Utils.sortedByLevenshteinDistance(
wrongArg,
argNamesList,
(x) => x
)
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
byDistance[0]
}?\n\tAll known arguments are ${argNamesList.join(", ")}`
})
.forEach((e) => context.err(e))
// Check that all obligated arguments are present. They are obligated if they don't have a preset value
for (const arg of vis.args) {
@ -932,10 +932,8 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
const param = special[arg.name]
if (param === undefined) {
errors.push(
`At ${context}: Obligated parameter '${
arg.name
}' in special rendering of type ${
context.err(
`Obligated parameter '${arg.name}' in special rendering of type ${
vis.funcName
} not found.\n The full special rendering specification is: '${JSON.stringify(
input
@ -1014,7 +1012,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* }
* ]
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then: {'*': "{image_carousel(other_image_key)}"}} ]}
* result // => expected
*
@ -1022,7 +1020,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'en': "Some introduction{image_carousel(image)}"}}
* result // => expected
*
@ -1030,12 +1028,11 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
* const tr = {
* render: {special: {type: "image_carousel", image_key: "image"}, after: {en: "Some footer"} },
* }
* const result = new RewriteSpecial().convert(tr,"test").result
* const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
* const expected = {render: {'en': "{image_carousel(image)}Some footer"}}
* result // => expected
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
const errors = []
json = Utils.Clone(json)
const paths: ConfigMeta[] = tagrenderingconfigmeta
for (const path of paths) {
@ -1043,7 +1040,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
continue
}
Utils.WalkPath(path.path, json, (leaf, travelled) =>
RewriteSpecial.convertIfNeeded(leaf, errors, context + ":" + travelled.join("."))
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
)
}
@ -1067,15 +1064,13 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
const iconBadges: {
if: TagConfigJson
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
}[] = []
const errs: string[] = []
const warns: string[] = []
for (let i = 0; i < badgesJson.length; i++) {
const iconBadge: {
if: TagConfigJson
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
} = badgesJson[i]
const expanded = this._expand.convert(
<QuestionableTagRenderingConfigJson>iconBadge.then,
@ -1089,7 +1084,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
iconBadges.push(
...expanded.map((resolved) => ({
if: iconBadge.if,
then: resolved,
then: <MinimalTagRenderingConfigJson>resolved,
}))
)
}
@ -1103,8 +1098,13 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
super(
"Prepares point renderings by expanding 'icon' and 'iconBadges'",
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
"marker",
new Each(
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
)
)
),
new ExpandIconBadges(state, layer)
)
@ -1189,15 +1189,17 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
convert(json: IconConfigJson, context: ConversionContext): IconConfigJson {
const expander = new ExpandTagRendering(this._state, this._layer)
const result: IconConfigJson = { icon: undefined, color: undefined }
const errors: string[] = []
const warnings: string[] = []
if (json.icon && json.icon["builtin"]) {
result.icon = expander.convert(<any>json.icon, context.enter("icon"))[0]
result.icon = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.icon, context.enter("icon"))[0]
)
} else {
result.icon = json.icon
}
if (json.color && json.color["builtin"]) {
result.color = expander.convert(<any>json.color, context.enter("color"))[0]
result.color = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.color, context.enter("color"))[0]
)
} else {
result.color = json.color
}
@ -1217,6 +1219,10 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new AddMiniMap(state),
new AddEditingElements(state),
new SetFullNodeDatabase(),
new On<
(LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
LayerConfigJson
>("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering",
(layer) =>

View file

@ -172,7 +172,13 @@ class AddDefaultLayers extends DesugaringStep<LayoutConfigJson> {
for (const layerName of Constants.added_by_default) {
const v = state.sharedLayers.get(layerName)
if (v === undefined) {
context.err("Default layer " + layerName + " not found")
context.err(
"Default layer " +
layerName +
" not found. " +
state.sharedLayers.size +
" layers are available"
)
continue
}
if (alreadyLoaded.has(v.id)) {

View file

@ -1,4 +1,13 @@
import { ConversionContext, DesugaringStep, Each, Fuse, On } from "./Conversion"
import {
Conversion,
ConversionContext,
DesugaringStep,
Each,
Fuse,
On,
Pipe,
Pure,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import LayerConfig from "../LayerConfig"
import { Utils } from "../../../Utils"
@ -254,7 +263,15 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
super(
"Validates a theme and the contained layers",
new ValidateTheme(doesImageExist, path, isBuiltin, sharedTagRenderings),
new On("layers", new Each(new ValidateLayer(undefined, isBuiltin, doesImageExist)))
new On(
"layers",
new Each(
new Pipe(
new ValidateLayer(undefined, isBuiltin, doesImageExist),
new Pure((x) => x.raw)
)
)
)
)
}
}
@ -410,9 +427,10 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* }
* ]
* }
* const r = new DetectShadowedMappings().convert(tr, "test");
* r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value is fully matched by a previous mapping (namely 0)") >= 0 // => true
* const context = ConversionContext.test()
* const r = new DetectShadowedMappings().convert(tr, context);
* context.getAll("error").length // => 1
* context.getAll("error")[0].message.indexOf("The mapping key=value is fully matched by a previous mapping (namely 0)") >= 0 // => true
*
* const tr = {mappings: [
* {
@ -425,9 +443,10 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
* }
* ]
* }
* const r = new DetectShadowedMappings().convert(tr, "test");
* r.errors.length // => 1
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
* const context = ConversionContext.test()
* const r = new DetectShadowedMappings().convert(tr, context);
* context.getAll("error").length // => 1
* context.getAll("error")[0].message.indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
@ -510,6 +529,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
}
/**
* const context = ConversionContext.test()
* const r = new DetectMappingsWithImages(new DoesImageExist(new Set<string>())).convert({
* "mappings": [
* {
@ -525,9 +545,9 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
* "zh_Hant": "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
* }
* }]
* }, "test");
* r.errors.length > 0 // => true
* r.errors.some(msg => msg.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
* }, context);
* context.hasErrors() // => true
* context.getAll("error").some(msg => msg.message.indexOf("./assets/layers/bike_parking/staple.svg") >= 0) // => true
*/
convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
if (json.mappings === undefined || json.mappings.length === 0) {
@ -682,7 +702,10 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
}
}
export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
export class ValidateLayer extends Conversion<
LayerConfigJson,
{ parsed: LayerConfig; raw: LayerConfigJson }
> {
/**
* The paths where this layer is originally saved. Triggers some extra checks
* @private
@ -698,7 +721,10 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
this._doesImageExist = doesImageExist
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
convert(
json: LayerConfigJson,
context: ConversionContext
): { parsed: LayerConfig; raw: LayerConfigJson } {
context = context.inOperation(this.name)
if (typeof json === "string") {
context.err("This layer hasn't been expanded: " + json)
@ -887,15 +913,27 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
{
const hasCondition = json.pointRendering?.filter(
(mr) => mr["icon"] !== undefined && mr["icon"]["condition"] !== undefined
)
if (hasCondition?.length > 0) {
context.err(
"One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
JSON.stringify(hasCondition, null, " ")
)
}
json.pointRendering?.forEach((pointRendering, index) => {
pointRendering?.marker?.forEach((icon, indexM) => {
if (!icon.icon) {
return
}
if (icon.icon["condition"]) {
context
.enters(
"pointRendering",
index,
"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."
)
}
})
})
}
if (json.presets !== undefined) {
@ -927,10 +965,10 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
}
}
} catch (e) {
context.err(e)
context.err("Could not validate layer due to: " + e + e.stack)
}
return json
return { raw: json, parsed: layerConfig }
}
}

View file

@ -1,4 +1,4 @@
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./TagRenderingConfigJson"
import { TagConfigJson } from "./TagConfigJson"
export interface IconConfigJson {
@ -7,13 +7,13 @@ export interface IconConfigJson {
* type: icon
* suggestions: return ["pin","square","circle","checkmark","clock","close","crosshair","help","home","invalid","location","location_empty","location_locked","note","resolved","ring","scissors","teardrop","teardrop_with_hole_green","triangle"].map(i => ({if: "value="+i, then: i, icon: i}))
*/
icon: string | TagRenderingConfigJson | { builtin: string; override: any }
icon: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
/**
* question: What colour should the icon be?
* This will only work for the default icons such as `pin`,`circle`,...
* type: color
*/
color?: string | TagRenderingConfigJson | { builtin: string; override: any }
color?: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
}
/**
@ -57,7 +57,7 @@ export default interface PointRenderingConfigJson {
* Badge to show
* Type: icon
*/
then: string | TagRenderingConfigJson
then: string | MinimalTagRenderingConfigJson
}[]
/**

View file

@ -1,6 +1,7 @@
import { TagConfigJson } from "./TagConfigJson"
import { TagRenderingConfigJson } from "./TagRenderingConfigJson"
import type { Translatable } from "./Translatable"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
export interface MappingConfigJson {
/**
@ -244,6 +245,12 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
* ifunset: do not prefill the textfield
*/
default?: string
/**
* question: What values of the freeform key should be interpreted as 'unknown'?
* For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked
* ifunset: The question will be considered answered if any value is set for the key
*/
invalidValues?: string[]
}
/**