Move 'mappings' to a separate interface

This commit is contained in:
pietervdvn 2022-07-03 13:18:05 +02:00
parent bb10f60636
commit 0146d1e4f7
4 changed files with 270 additions and 225 deletions

View file

@ -200,6 +200,24 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
}
}
export class FirstOf<T, X> extends Conversion<T, X>{
private readonly _conversion: Conversion<T, X[]>;
constructor(conversion: Conversion<T, X[]>) {
super("Picks the first result of the conversion step", [], "FirstOf("+conversion.name+")");
this._conversion = conversion;
}
convert(json: T, context: string): { result: X; errors?: string[]; warnings?: string[]; information?: string[] } {
const reslt = this._conversion.convert(json, context);
return {
...reslt,
result: reslt.result[0]
};
}
}
export class Fuse<T> extends DesugaringStep<T> {
private readonly steps: DesugaringStep<T>[];

View file

@ -1,4 +1,4 @@
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, SetDefault} from "./Conversion";
import {Concat, Conversion, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault} from "./Conversion";
import {LayerConfigJson} from "../Json/LayerConfigJson";
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson";
import {Utils} from "../../../Utils";
@ -8,7 +8,7 @@ import Translations from "../../../UI/i18n/Translations";
import {Translation} from "../../../UI/i18n/Translation";
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
import {AddContextToTranslations} from "./AddContextToTranslations";
import spec = Mocha.reporters.spec;
class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | { builtin: string | string[], override: any }, TagRenderingConfigJson[]> {
private readonly _state: DesugaringContext;
@ -85,17 +85,20 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
if (typeof tr === "string") {
const lookup = this.lookup(tr);
if (lookup === undefined) {
const isTagRendering = ctx.indexOf("On(mapRendering") < 0
if(isTagRendering){
warnings.push(ctx + "A literal rendering was detected: " + tr)
}
return [{
render: tr,
id: tr.replace(/![a-zA-Z0-9]/g, "")
id: tr.replace(/[^a-zA-Z0-9]/g, "")
}]
}
return lookup
}
if (tr["builtin"] !== undefined) {
let names = tr["builtin"]
let names: string | string[] = tr["builtin"]
if (typeof names === "string") {
names = [names]
}
@ -111,7 +114,13 @@ class ExpandTagRendering extends Conversion<string | TagRenderingConfigJson | {
for (const name of names) {
const lookup = this.lookup(name)
if (lookup === undefined) {
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " + Array.from(state.tagRenderings.keys()).join(", ") + "?")
let candidates = Array.from(state.tagRenderings.keys())
if(name.indexOf(".") > 0){
const [layer, search] = name.split(".")
candidates = Utils.NoNull( state.sharedLayers.get(layer).tagRenderings.map(tr => tr["id"])).map(id => layer+"."+id)
}
candidates = Utils.sortedByLevenshteinDistance(name, candidates, i => i);
errors.push(ctx + ": The tagRendering with identifier " + name + " was not found.\n\tDid you mean one of " +candidates.join(", ") + "?")
continue
}
for (let foundTr of lookup) {
@ -485,6 +494,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("tagRenderings", new Concat(new ExpandTagRendering(state))),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("mapRendering",new Each( new On("icon", new FirstOf(new ExpandTagRendering(state))))),
new SetDefault("titleIcons", ["defaults"]),
new On("titleIcons", new Concat(new ExpandTagRendering(state)))
);

View file

@ -1,74 +1,8 @@
import {AndOrTagConfigJson} from "./TagConfigJson";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
/**
* A QuestionableTagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.
* If the desired tags are missing and a question is defined, a question will be shown instead.
*/
export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson {
/**
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question.
* If undefined, the question is never asked and this tagrendering is read-only
*/
question?: string | any,
/**
* Allow freeform text input from the user
*/
freeform?: {
/**
* @inheritDoc
*/
key: string
/**
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
*/
type?: string,
/**
* A (translated) text that is shown (as gray text) within the textfield
*/
placeholder?: string | any
/**
* Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md'
*/
helperArgs?: (string | number | boolean | any)[];
/**
* If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked'
**/
addExtraTags?: string[];
/**
* When set, influences the way a question is asked.
* Instead of showing a full-widht text field, the text field will be shown within the rendering of the question.
*
* This combines badly with special input elements, as it'll distort the layout.
*/
inline?: boolean
/**
* default value to enter if no previous tagging is present.
* Normally undefined (aka do not enter anything)
*/
default?: string
},
/**
* If true, use checkboxes instead of radio buttons when asking the question
*/
multiAnswer?: boolean,
/**
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/
mappings?: {
export interface MappingConfigJson {
/**
* @inheritDoc
@ -172,5 +106,79 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
*/
addExtraTags?: string[]
}[]
/**
* Searchterms (per language) to easily find an option if there are many options
*/
searchTerms?: Record<string, string[]>
}
/**
* A QuestionableTagRenderingConfigJson is a single piece of code which converts one ore more tags into a HTML-snippet.
* If the desired tags are missing and a question is defined, a question will be shown instead.
*/
export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJson {
/**
* If it turns out that this tagRendering doesn't match _any_ value, then we show this question.
* If undefined, the question is never asked and this tagrendering is read-only
*/
question?: string | any,
/**
* Allow freeform text input from the user
*/
freeform?: {
/**
* @inheritDoc
*/
key: string
/**
* The type of the text-field, e.g. 'string', 'nat', 'float', 'date',...
* See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values
*/
type?: string,
/**
* A (translated) text that is shown (as gray text) within the textfield
*/
placeholder?: string | any
/**
* Extra parameters to initialize the input helper arguments.
* For semantics, see the 'SpecialInputElements.md'
*/
helperArgs?: (string | number | boolean | any)[];
/**
* If a value is added with the textfield, these extra tag is addded.
* Useful to add a 'fixme=freeform textfield used - to be checked'
**/
addExtraTags?: string[];
/**
* When set, influences the way a question is asked.
* Instead of showing a full-widht text field, the text field will be shown within the rendering of the question.
*
* This combines badly with special input elements, as it'll distort the layout.
*/
inline?: boolean
/**
* default value to enter if no previous tagging is present.
* Normally undefined (aka do not enter anything)
*/
default?: string
},
/**
* If true, use checkboxes instead of radio buttons when asking the question
*/
multiAnswer?: boolean,
/**
* Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes
*/
mappings?: MappingConfigJson[]
}

View file

@ -11,10 +11,21 @@ import Combine from "../../UI/Base/Combine";
import Title from "../../UI/Base/Title";
import Link from "../../UI/Base/Link";
import List from "../../UI/Base/List";
import {QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
import {MappingConfigJson, QuestionableTagRenderingConfigJson} from "./Json/QuestionableTagRenderingConfigJson";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import {Paragraph} from "../../UI/Base/Paragraph";
export interface Mapping {
readonly if: TagsFilter,
readonly ifnot?: TagsFilter,
readonly then: TypedTranslation<object>,
readonly icon: string,
readonly iconClass: string
readonly hideInAnswer: boolean | TagsFilter
readonly addExtraTags: Tag[],
readonly searchTerms?: Record<string, string[]>
}
/***
* The parsed version of TagRenderingConfigJSON
* Identical data, but with some methods and validation
@ -41,15 +52,7 @@ export default class TagRenderingConfig {
public readonly multiAnswer: boolean;
public readonly mappings?: {
readonly if: TagsFilter,
readonly ifnot?: TagsFilter,
readonly then: TypedTranslation<object>,
readonly icon: string,
readonly iconClass: string
readonly hideInAnswer: boolean | TagsFilter
readonly addExtraTags: Tag[]
}[]
public readonly mappings?: Mapping[]
public readonly labels: string[]
constructor(json: string | QuestionableTagRenderingConfigJson, context?: string) {
@ -174,75 +177,7 @@ export default class TagRenderingConfig {
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
}
this.mappings = json.mappings.map((mapping, i) => {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
}
if (mapping.then === undefined) {
if(mapping["render"] !== undefined){
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
}
if (mapping.ifnot !== undefined && !this.multiAnswer) {
throw `${ctx}: Invalid mapping: 'ifnot' is defined, but the tagrendering is not a multianswer. Either remove ifnot or set 'multiAnswer:true' to enable checkboxes instead of radiobuttons`
}
if(mapping["render"] !== undefined){
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
}
if (mapping.addExtraTags !== undefined && this.multiAnswer) {
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
}
let hideInAnswer: boolean | TagsFilter = false;
if (typeof mapping.hideInAnswer === "boolean") {
hideInAnswer = mapping.hideInAnswer;
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
if(hideInAnswer === true && addExtraTags.length > 0){
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
}
let icon = undefined;
let iconClass = "small"
if(mapping.icon !== undefined){
if (typeof mapping.icon === "string" && mapping.icon !== "") {
icon = mapping.icon
}else{
icon = mapping.icon["path"]
iconClass = mapping.icon["class"] ?? iconClass
}
}
const mp = {
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
then: Translations.T(mapping.then, `${ctx}.then`),
hideInAnswer,
icon,
iconClass,
addExtraTags
};
if (this.question) {
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
}
if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) {
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
}
}
return mp;
});
this.mappings = json.mappings.map((m, i) => TagRenderingConfig.ExtractMapping(m, i, translationKey, context, this.multiAnswer, this.question !== undefined));
}
if (this.question && this.freeform?.key === undefined && this.mappings === undefined) {
@ -351,6 +286,79 @@ export default class TagRenderingConfig {
}
}
public static ExtractMapping(mapping: MappingConfigJson, i: number, translationKey: string,
context: string,
multiAnswer?: boolean, isQuestionable?: boolean) {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
throw `${ctx}: Invalid mapping: "if" is not defined in ${JSON.stringify(mapping)}`
}
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(mapping)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
}
if (mapping.ifnot !== undefined && !multiAnswer) {
throw `${ctx}: Invalid mapping: 'ifnot' is defined, but the tagrendering is not a multianswer. Either remove ifnot or set 'multiAnswer:true' to enable checkboxes instead of radiobuttons`
}
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(mapping)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
throw `${ctx}: Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead`
}
if (mapping.addExtraTags !== undefined && multiAnswer) {
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed`
}
let hideInAnswer: boolean | TagsFilter = false;
if (typeof mapping.hideInAnswer === "boolean") {
hideInAnswer = mapping.hideInAnswer;
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(mapping.hideInAnswer, `${context}.mapping[${i}].hideInAnswer`);
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) => TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`));
if (hideInAnswer === true && addExtraTags.length > 0) {
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
}
let icon = undefined;
let iconClass = "small"
if (mapping.icon !== undefined) {
if (typeof mapping.icon === "string" && mapping.icon !== "") {
icon = mapping.icon
} else {
icon = mapping.icon["path"]
iconClass = mapping.icon["class"] ?? iconClass
}
}
const mp = <Mapping>{
if: TagUtils.Tag(mapping.if, `${ctx}.if`),
ifnot: (mapping.ifnot !== undefined ? TagUtils.Tag(mapping.ifnot, `${ctx}.ifnot`) : undefined),
then: Translations.T(mapping.then, `${ctx}.then`),
hideInAnswer,
icon,
iconClass,
addExtraTags,
searchTerms: mapping.searchTerms
};
if (isQuestionable) {
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
throw `${context}.mapping[${i}].if: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
}
if (hideInAnswer !== true && !(mp.ifnot?.isUsableAsAnswer() ?? true)) {
throw `${context}.mapping[${i}].ifnot: This value cannot be used to answer a question, probably because it contains a regex or an OR. Either change it or set 'hideInAnswer'`
}
}
return mp;
}
/**
* Returns true if it is known or not shown, false if the question should be asked
* @constructor
@ -421,7 +429,8 @@ export default class TagRenderingConfig {
const freeformValues = tags[this.freeform.key].split(";")
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
for (const leftover of leftovers) {
applicableMappings.push({then:
applicableMappings.push({
then:
new TypedTranslation<object>(this.render.replace("{" + this.freeform.key + "}", leftover).translations)
})
}