Themes: improve ability of 'rewrite' config

This commit is contained in:
Pieter Vander Vennet 2023-12-12 03:43:55 +01:00
parent 5da895276c
commit f015604000
5 changed files with 335 additions and 161 deletions

View file

@ -0,0 +1,322 @@
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 && target[key] !== undefined) {
// The target is a translation AND the current object is a translation
// This means we should recursively replace with the translated value
subtarget = target[key]
}
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
}
}

View file

@ -1,5 +1,4 @@
import {
Bypass,
Concat,
Conversion,
DesugaringContext,
@ -32,6 +31,7 @@ import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRende
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
import { ExpandRewrite } from "./ExpandRewrite"
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static readonly predefinedFilters = ExpandFilter.load_filters()
@ -677,164 +677,6 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
}
}
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'
*
* // 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 && target[key] !== undefined) {
// The target is a translation AND the current object is a translation
// This means we should recursively replace with the translated value
subtarget = target[key]
}
obj[key] = replaceRecursive(obj[key], subtarget)
}
return obj
}
return obj
}
return replaceRecursive(tr, target)
}
/**
* // 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
*/
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 ts: 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`
)
}
}
}
for (let i = 0; i < keysToRewrite.into.length; i++) {
let t = Utils.Clone(rewrite.renderings)
for (let j = 0; j < keysToRewrite.sourceString.length; j++) {
const key = keysToRewrite.sourceString[j]
const target = keysToRewrite.into[i][j]
t = ExpandRewrite.RewriteParts(key, target, t)
}
ts.push(t)
}
return ts
}
}
/**
* Converts a 'special' translation into a regular translation which uses parameters
*/

View file

@ -3,6 +3,7 @@ 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(
@ -43,6 +44,14 @@ export default class ValidationUtils {
if (renderingConfig[cacheName]) {
return renderingConfig[cacheName]
}
if (!Array.isArray(renderingConfig.mappings ?? [])) {
throw (
"Mappings of renderingconfig " +
(renderingConfig["id"] ?? renderingConfig.render ?? "") +
" are supposed to be an array but it is: " +
JSON.stringify(renderingConfig.mappings)
)
}
const translations: any[] = Utils.NoNull([
renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then),

View file

@ -47,5 +47,6 @@ export default interface RewritableConfigJson<T> {
sourceString: string[]
into: (string | any)[][]
}
renderings: T
subexpand?: Record<string, string[]>
renderings: T | T[]
}

View file

@ -1,7 +1,6 @@
import { LayerConfigJson } from "../../../../src/Models/ThemeConfig/Json/LayerConfigJson"
import LineRenderingConfigJson from "../../../../src/Models/ThemeConfig/Json/LineRenderingConfigJson"
import {
ExpandRewrite,
PrepareLayer,
RewriteSpecial,
} from "../../../../src/Models/ThemeConfig/Conversion/PrepareLayer"
@ -10,6 +9,7 @@ import RewritableConfigJson from "../../../../src/Models/ThemeConfig/Json/Rewrit
import { describe, expect, it } from "vitest"
import { ConversionContext } from "../../../../src/Models/ThemeConfig/Conversion/ConversionContext"
import { ExpandRewrite } from "../../../../src/Models/ThemeConfig/Conversion/ExpandRewrite"
describe("ExpandRewrite", () => {
it("should not allow overlapping keys", () => {