forked from MapComplete/MapComplete
329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
import { Conversion } from "./Conversion"
|
|
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
|
import Translations from "../../../UI/i18n/Translations"
|
|
import { ConversionContext } from "./ConversionContext"
|
|
import { Utils } from "../../../Utils"
|
|
|
|
export class ExpandRewrite<T> extends Conversion<T | RewritableConfigJson<T>, T[]> {
|
|
constructor() {
|
|
super("Applies a rewrite", [], "ExpandRewrite")
|
|
}
|
|
|
|
/**
|
|
* Used for left|right group creation and replacement.
|
|
* Every 'keyToRewrite' will be replaced with 'target' recursively. This substitution will happen in place in the object 'tr'
|
|
*
|
|
* The 'target' object will be cloned, the changes will be applied in this clone
|
|
*
|
|
* // should substitute strings
|
|
* const spec = {
|
|
* "someKey": "somevalue {xyz}"
|
|
* }
|
|
* ExpandRewrite.RewriteParts("{xyz}", "rewritten", spec) // => {"someKey": "somevalue rewritten"}
|
|
*
|
|
* // should substitute all occurances in strings
|
|
* const spec = {
|
|
* "someKey": "The left|right side has {key:left|right}"
|
|
* }
|
|
* ExpandRewrite.RewriteParts("left|right", "left", spec) // => {"someKey": "The left side has {key:left}"}
|
|
*
|
|
*/
|
|
public static RewriteParts<T>(keyToRewrite: string, target: string | any, tr: T): T {
|
|
const targetIsTranslation = Translations.isProbablyATranslation(target)
|
|
|
|
function replaceRecursive(obj: string | any, target) {
|
|
if (obj === keyToRewrite) {
|
|
return target
|
|
}
|
|
|
|
if (typeof obj === "string") {
|
|
// This is a simple string - we do a simple replace
|
|
while (obj.indexOf(keyToRewrite) >= 0) {
|
|
obj = obj.replace(keyToRewrite, target)
|
|
}
|
|
return obj
|
|
}
|
|
if (Array.isArray(obj)) {
|
|
// This is a list of items
|
|
return obj.map((o) => replaceRecursive(o, target))
|
|
}
|
|
|
|
if (typeof obj === "object") {
|
|
obj = { ...obj }
|
|
|
|
const isTr = targetIsTranslation && Translations.isProbablyATranslation(obj)
|
|
|
|
for (const key in obj) {
|
|
let subtarget = target
|
|
if (isTr) {
|
|
// The target is a translation AND the current object is a translation
|
|
// This means we should recursively replace with the translated value
|
|
if (target[key]) {
|
|
// A translation is available!
|
|
subtarget = target[key]
|
|
} else if (target["en"]) {
|
|
subtarget = target["en"]
|
|
} else {
|
|
// Take the first
|
|
subtarget = target[Object.keys(target)[0]]
|
|
}
|
|
}
|
|
obj[key] = replaceRecursive(obj[key], subtarget)
|
|
}
|
|
return obj
|
|
}
|
|
return obj
|
|
}
|
|
|
|
return replaceRecursive(tr, target)
|
|
}
|
|
|
|
/**
|
|
* Used for check that a key is present in a string somewhere in the object
|
|
*
|
|
* // should substitute strings
|
|
* const spec = {
|
|
* "someKey": "somevalue {xyz}"
|
|
* }
|
|
* ExpandRewrite.contains("{xyz}", spec) // => true
|
|
* ExpandRewrite.contains("{abc}", spec) // => false
|
|
*
|
|
*/
|
|
public static contains<T>(keyToRewrite: string, tr: T): boolean {
|
|
function findRecursive(obj: string | any): boolean {
|
|
if (obj === keyToRewrite) {
|
|
return true
|
|
}
|
|
|
|
if (typeof obj === "string") {
|
|
// This is a simple string - we do a simple replace
|
|
return obj.indexOf(keyToRewrite) >= 0
|
|
}
|
|
if (Array.isArray(obj)) {
|
|
// This is a list of items
|
|
return obj.some((o) => findRecursive(o))
|
|
}
|
|
|
|
if (typeof obj === "object") {
|
|
obj = { ...obj }
|
|
|
|
for (const key in obj) {
|
|
if (findRecursive(obj[key])) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
return findRecursive(tr)
|
|
}
|
|
|
|
/**
|
|
* // should convert simple strings
|
|
* const spec = <RewritableConfigJson<string>>{
|
|
* rewrite: {
|
|
* sourceString: ["xyz","abc"],
|
|
* into: [
|
|
* ["X", "A"],
|
|
* ["Y", "B"],
|
|
* ["Z", "C"]],
|
|
* },
|
|
* renderings: "The value of xyz is abc"
|
|
* }
|
|
* 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>>{
|
|
* rewrite: {
|
|
* sourceString: ["xyz","abc"],
|
|
* into: [
|
|
* ["X", {en: "value", nl: "waarde"}],
|
|
* ["Y", {en: "some other value", nl: "een andere waarde"}],
|
|
* },
|
|
* renderings: {en: "The value of xyz is abc", nl: "De waarde van xyz is abc"}
|
|
* }
|
|
* const expected = [
|
|
* {
|
|
* en: "The value of X is value",
|
|
* nl: "De waarde van X is waarde"
|
|
* },
|
|
* {
|
|
* en: "The value of Y is some other value",
|
|
* nl: "De waarde van Y is een andere waarde"
|
|
* }
|
|
* ]
|
|
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => expected
|
|
*
|
|
*
|
|
* // should expand sublists
|
|
* const spec = <RewritableConfigJson<any>>{
|
|
* rewrite: {
|
|
* sourceString: ["{{key}}","{{values}}"],
|
|
* into: [
|
|
* ["a", [1,2,3] ],
|
|
* ["b", [42, 43] ],
|
|
* },
|
|
* subexpand: {"options": ["{{values}}"]},
|
|
* renderings: {question: "What are values for {{key}}?", options: [{if: "x={{values}}", then: "{{values}} is value" }] }
|
|
* }
|
|
* const expected = [
|
|
* {question: "What are values for a?",
|
|
* options: [{if: "x=1", then: "1 is value" },
|
|
* {if: "x=2", then: "2 is value" },
|
|
* {if: "x=3", then: "3 is value" }
|
|
* ] }
|
|
* {question: "What are values for b?", options: [
|
|
* {if: "x=42", then: "42 is value" },
|
|
* {if: "x=43", then: "43 is value" }
|
|
* ] }
|
|
* ]
|
|
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => expected
|
|
*
|
|
*
|
|
* // should expand sublists if there is one
|
|
* const spec = <RewritableConfigJson<any>>{
|
|
* rewrite: {
|
|
* sourceString: ["{{key}}","{{values}}"],
|
|
* into: [
|
|
* ["a", [] ],
|
|
* ["b", [42, 43] ],
|
|
* ["c", null ],
|
|
* },
|
|
* subexpand: {"options": ["{{values}}"]},
|
|
* renderings: [
|
|
* {question: "What is {{key}}?", options: [{if: "x={{values}}", then: "{{values}} is value" }],
|
|
* {question: "How is {{key}}?", options: [{a: 5}, {b: 6}] },
|
|
* {question: "Why is {{key}}?" }
|
|
* ]
|
|
* }
|
|
* const expected = [
|
|
* {question: "What is a?",
|
|
* options: []},
|
|
* {question: "How is a?", options: [{a: 5}, {b: 6}] },
|
|
* {question: "Why is a?" },
|
|
* {question: "What is b?", options: [
|
|
* {if: "x=42", then: "42 is value" },
|
|
* {if: "x=43", then: "43 is value" }
|
|
* ] },
|
|
* {question: "How is b?", options: [{a: 5}, {b: 6}] },
|
|
* {question: "Why is b?" },
|
|
* {question: "What is c?"},
|
|
* {question: "How is c?", options: [{a: 5}, {b: 6}] },
|
|
* {question: "Why is c?" },
|
|
* ]
|
|
* new ExpandRewrite().convertStrict(spec, ConversionContext.test()) // => expected
|
|
*/
|
|
convert(json: T | RewritableConfigJson<T>, context: ConversionContext): T[] {
|
|
if (json === null || json === undefined) {
|
|
return []
|
|
}
|
|
|
|
if (json["rewrite"] === undefined) {
|
|
// not a rewrite
|
|
return [<T>json]
|
|
}
|
|
|
|
const rewrite = <RewritableConfigJson<T>>json
|
|
const keysToRewrite = rewrite.rewrite
|
|
const results: T[] = []
|
|
|
|
{
|
|
// sanity check: rewrite: ["xyz", "longer_xyz"] is not allowed as "longer_xyz" will never be triggered
|
|
for (let i = 0; i < keysToRewrite.sourceString.length; i++) {
|
|
const guard = keysToRewrite.sourceString[i]
|
|
for (let j = i + 1; j < keysToRewrite.sourceString.length; j++) {
|
|
const toRewrite = keysToRewrite.sourceString[j]
|
|
if (toRewrite.indexOf(guard) >= 0) {
|
|
context.err(
|
|
`sourcestring[${i}] is a substring of sourcestring[${j}]: ${guard} will be substituted away before ${toRewrite} is reached.`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// sanity check: {rewrite: ["a", "b"] should have the right amount of 'intos' in every case
|
|
for (let i = 0; i < rewrite.rewrite.into.length; i++) {
|
|
const into = keysToRewrite.into[i]
|
|
if (into.length !== rewrite.rewrite.sourceString.length) {
|
|
context
|
|
.enters("into", i)
|
|
.err(
|
|
`Error in rewrite: there are ${rewrite.rewrite.sourceString.length} keys to rewrite, but entry ${i} has only ${into.length} values`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
let renderings = Array.isArray(rewrite.renderings)
|
|
? rewrite.renderings
|
|
: [rewrite.renderings]
|
|
for (let i = 0; i < keysToRewrite.into.length; i++) {
|
|
let ts: T[] = <T[]>Utils.Clone(renderings)
|
|
for (const tx of ts) {
|
|
let t = <T>tx
|
|
const sourceKeysToIgnore: string[] = []
|
|
for (const listKey in rewrite.subexpand) {
|
|
const original = t[listKey]
|
|
if (!original) {
|
|
continue
|
|
}
|
|
const sourceKeys = rewrite.subexpand[listKey].filter((sk) =>
|
|
ExpandRewrite.contains(sk, original)
|
|
)
|
|
if (sourceKeys.length === 0) {
|
|
// no delete t[listKey] needed, fixed values we need to retain
|
|
continue
|
|
}
|
|
|
|
if (sourceKeys.length > 1) {
|
|
throw (
|
|
"Too much matching sourcekeys for sublist `" +
|
|
listKey +
|
|
"`: it matches all of " +
|
|
sourceKeys.join(", ")
|
|
)
|
|
}
|
|
|
|
const sourceKey = sourceKeys[0]
|
|
sourceKeysToIgnore.push(sourceKey)
|
|
const rw = rewrite.rewrite
|
|
const values = rw.into[i][rw.sourceString.indexOf(sourceKey)]
|
|
|
|
if (!values) {
|
|
delete t[listKey]
|
|
continue
|
|
}
|
|
if (!Array.isArray(values)) {
|
|
throw (
|
|
"Sublist expansion of `" +
|
|
listKey +
|
|
"` failed: not an array to expand with:" +
|
|
JSON.stringify(values)
|
|
)
|
|
}
|
|
t[listKey] = [].concat(
|
|
...values.map((v) => ExpandRewrite.RewriteParts(sourceKey, v, original))
|
|
)
|
|
}
|
|
|
|
for (let j = 0; j < keysToRewrite.sourceString.length; j++) {
|
|
// The string that should be replaced everywhere in `t`
|
|
const key = keysToRewrite.sourceString[j]
|
|
if (sourceKeysToIgnore.indexOf(key) >= 0) {
|
|
continue
|
|
}
|
|
// The object that `key` should be replaced with
|
|
const target = keysToRewrite.into[i][j]
|
|
t = ExpandRewrite.RewriteParts(key, target, t)
|
|
}
|
|
results.push(t)
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
}
|