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 extends Conversion, 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(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(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 = >{ * 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 = >{ * 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 = >{ * 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 = >{ * 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, context: ConversionContext): T[] { if (json === null || json === undefined) { return [] } if (json["rewrite"] === undefined) { // not a rewrite return [json] } const rewrite = >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[] = Utils.Clone(renderings) for (const tx of ts) { let 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 } }