Refactoring: use more accurate context in conversion, fix tests
This commit is contained in:
parent
86d0de3806
commit
f77d99f8ed
43 changed files with 999 additions and 367 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}[]
|
||||
|
||||
/**
|
||||
|
|
|
@ -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[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue