diff --git a/Logic/Tags/SubstitutingTag.ts b/Logic/Tags/SubstitutingTag.ts index ccc097956d..d121aaab5e 100644 --- a/Logic/Tags/SubstitutingTag.ts +++ b/Logic/Tags/SubstitutingTag.ts @@ -1,4 +1,6 @@ import {TagsFilter} from "./TagsFilter"; +import {Tag} from "./Tag"; +import {Utils} from "../../Utils"; /** * The substituting-tag uses the tags of a feature a variables and replaces them. @@ -27,6 +29,13 @@ export default class SubstitutingTag implements TagsFilter { return template.replace(/{.*}/g, ""); } + asTag(currentProperties: Record){ + if(this._invert){ + throw "Cannot convert an inverted substituting tag" + } + return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties)) + } + asHumanString(linkToWiki: boolean, shorten: boolean, properties) { return this._key + (this._invert ? '!' : '') + "=" + SubstitutingTag.substituteString(this._value, properties); } diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index c9c9836e75..1b975cff4b 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -11,6 +11,7 @@ import {isRegExp} from "util"; import * as key_counts from "../../assets/key_totals.json" type Tags = Record +export type UploadableTag = Tag | SubstitutingTag | And export class TagUtils { private static keyCounts: { keys: any, tags: any } = key_counts["default"] ?? key_counts @@ -58,7 +59,7 @@ export class TagUtils { return true; } - static SplitKeys(tagsFilters: TagsFilter[]): Record { + static SplitKeys(tagsFilters: UploadableTag[]): Record { return this.SplitKeysRegex(tagsFilters, false); } @@ -67,7 +68,7 @@ export class TagUtils { * * TagUtils.SplitKeysRegex([new Tag("isced:level", "bachelor; master")], true) // => {"isced:level": ["bachelor","master"]} */ - static SplitKeysRegex(tagsFilters: TagsFilter[], allowRegex: boolean): Record { + static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: boolean): Record { const keyValues: Record = {} tagsFilters = [...tagsFilters] // copy all, use as queue while (tagsFilters.length > 0) { @@ -78,7 +79,7 @@ export class TagUtils { } if (tagsFilter instanceof And) { - tagsFilters.push(...tagsFilter.and); + tagsFilters.push(...tagsFilter.and); continue; } @@ -112,10 +113,28 @@ export class TagUtils { } /** + * Flattens an 'uploadableTag' and replaces all 'SubstitutingTags' into normal tags + */ + static FlattenAnd(tagFilters: UploadableTag, currentProperties: Record): Tag[]{ + const tags : Tag[] = [] + tagFilters.visit((tf: UploadableTag) => { + if(tf instanceof Tag){ + tags.push(tf) + } + if(tf instanceof SubstitutingTag){ + tags.push(tf.asTag(currentProperties)) + } + }) + return tags + } + + + + /** * Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set. * E.g: * - * const tag = TagUtils.Tag({"and": [ + * const tag = TagUtils.ParseUploadableTag({"and": [ * { * and: [ "x=a", "y=0;1"], * }, @@ -131,13 +150,13 @@ export class TagUtils { * TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => new And([new Tag("x","y"), new Tag("a","b")]) * TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => new And([new Tag("x","")]) */ - static FlattenMultiAnswer(tagsFilters: TagsFilter[]): And { + static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And { if (tagsFilters === undefined) { return new And([]); } let keyValues = TagUtils.SplitKeys(tagsFilters); - const and: TagsFilter[] = [] + const and: UploadableTag[] = [] for (const key in keyValues) { const values = Utils.Dedup(keyValues[key]).filter(v => v !== "") values.sort() @@ -157,7 +176,7 @@ export class TagUtils { * // should match with a space too * TagUtils.MatchesMultiAnswer(new Tag("isced:level","master"), {"isced:level":"bachelor; master"}) // => true */ - static MatchesMultiAnswer(tag: TagsFilter, properties: Tags): boolean { + static MatchesMultiAnswer(tag: UploadableTag, properties: Tags): boolean { const splitted = TagUtils.SplitKeysRegex([tag], true); for (const splitKey in splitted) { const neededValues = splitted[splitKey]; @@ -250,13 +269,32 @@ export class TagUtils { */ public static Tag(json: TagConfigJson, context: string = ""): TagsFilter { try { - return this.TagUnsafe(json, context); + return this.ParseTagUnsafe(json, context); } catch (e) { console.error("Could not parse tag", json, "in context", context, "due to ", e) throw e; } } + public static ParseUploadableTag(json: TagConfigJson, context: string = ""): UploadableTag { + const t = this.Tag(json, context); + + t.visit((t : TagsFilter)=> { + if( t instanceof And){ + return + } + if(t instanceof Tag){ + return + } + if(t instanceof SubstitutingTag){ + return + } + throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(false, false, {})}` + }) + + return t + } + /** * Same as `.Tag`, except that this will return undefined if the json is undefined * @param json @@ -317,11 +355,11 @@ export class TagUtils { if (match == null) { return null; } - const [_, key, invert, modifier, value] = match; + const [ , key, invert, modifier, value] = match; return {key, value, invert: invert == "!", modifier: (modifier == "i~" ? "i" : "")}; } - private static TagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { + private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilter { if (json === undefined) { throw new Error(`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`) @@ -492,17 +530,6 @@ export class TagUtils { } return " (" + joined + ") " } - - public static ExtractSimpleTags(tf: TagsFilter) : Tag[] { - const result: Tag[] = [] - tf.visit(t => { - if(t instanceof Tag){ - result.push(t) - } - }) - return result; - } - /** * Returns 'true' is opposite tags are detected. * Note that this method will never work perfectly diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index 1685344b10..155581a2c8 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -1,7 +1,7 @@ import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import Translations from "../../UI/i18n/Translations"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; +import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"; import {And} from "../../Logic/Tags/And"; import ValidatedTextField from "../../UI/Input/ValidatedTextField"; import {Utils} from "../../Utils"; @@ -16,8 +16,8 @@ import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import {Paragraph} from "../../UI/Base/Paragraph"; export interface Mapping { - readonly if: TagsFilter, - readonly ifnot?: TagsFilter, + readonly if: UploadableTag, + readonly ifnot?: UploadableTag, readonly then: TypedTranslation, readonly icon: string, readonly iconClass: string | "small" | "medium" | "large" | "small-height" | "medium-height" | "large-height", @@ -46,7 +46,7 @@ export default class TagRenderingConfig { readonly key: string, readonly type: string, readonly placeholder: Translation, - readonly addExtraTags: TagsFilter[]; + readonly addExtraTags: UploadableTag[]; readonly inline: boolean, readonly default?: string, readonly helperArgs?: (string | number | boolean)[] @@ -138,7 +138,7 @@ export default class TagRenderingConfig { type, placeholder, addExtraTags: json.freeform.addExtraTags?.map((tg, i) => - TagUtils.Tag(tg, `${context}.extratag[${i}]`)) ?? [], + TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)) ?? [], inline: json.freeform.inline ?? false, default: json.freeform.default, helperArgs: json.freeform.helperArgs diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 3dd846f1d2..c3f22967e9 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -17,7 +17,7 @@ import {SubstitutedTranslation} from "../SubstitutedTranslation"; import {TagsFilter} from "../../Logic/Tags/TagsFilter"; import {Tag} from "../../Logic/Tags/Tag"; import {And} from "../../Logic/Tags/And"; -import {TagUtils} from "../../Logic/Tags/TagUtils"; +import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"; import BaseUIElement from "../BaseUIElement"; import {DropDown} from "../Input/DropDown"; import InputElementWrapper from "../Input/InputElementWrapper"; @@ -82,14 +82,16 @@ export default class TagRenderingQuestion extends Combine { const feedback = new UIEventSource(undefined) - const inputElement: ReadonlyInputElement = + const inputElement: ReadonlyInputElement = new VariableInputElement(applicableMappingsSrc.map(applicableMappings => { return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback) } )) const save = () => { - const selection = TagUtils.FlattenMultiAnswer([inputElement.GetValue().data]); + + + const selection = TagUtils.FlattenMultiAnswer(TagUtils.FlattenAnd( inputElement.GetValue().data, tags.data)); if (selection) { (state?.changes) .applyAction(new ChangeTagAction( @@ -147,11 +149,11 @@ export default class TagRenderingQuestion extends Combine { applicableUnit: Unit, tagsSource: UIEventSource, feedback: UIEventSource - ): ReadonlyInputElement { + ): ReadonlyInputElement { const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0 - let inputEls: InputElement[]; + let inputEls: InputElement[]; const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined) @@ -166,7 +168,7 @@ export default class TagRenderingQuestion extends Combine { // FreeForm input will be undefined if not present; will already contain a special input element if applicable const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback); - function allIfNotsExcept(excludeIndex: number): TagsFilter[] { + function allIfNotsExcept(excludeIndex: number): UploadableTag[] { if (configuration.mappings === undefined || configuration.mappings.length === 0) { return undefined } @@ -196,7 +198,7 @@ export default class TagRenderingQuestion extends Combine { inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(state, tagsSource, mapping, allIfNotsExcept(i))); inputEls = Utils.NoNull(inputEls); } else { - const dropdown: InputElement = new DropDown("", + const dropdown: InputElement = new DropDown("", applicableMappings.map((mapping, i) => { return { value: new And([mapping.if, ...allIfNotsExcept(i)]), @@ -327,7 +329,7 @@ export default class TagRenderingQuestion extends Combine { tagsSource: UIEventSource, options?: { search: UIEventSource - }): InputElement { + }): InputElement { const values = TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state) @@ -416,7 +418,6 @@ export default class TagRenderingQuestion extends Combine { return mapping.ifnot } })) - console.log("Got tags", tfs) return new And(tfs); }, (tf) => { @@ -438,10 +439,10 @@ export default class TagRenderingQuestion extends Combine { private static GenerateMultiAnswer( configuration: TagRenderingConfig, - elements: InputElement[], freeformField: InputElement, ifNotSelected: TagsFilter[]): InputElement { + elements: InputElement[], freeformField: InputElement, ifNotSelected: UploadableTag[]): InputElement { const checkBoxes = new CheckBoxes(elements); - const inputEl = new InputElementMap( + const inputEl = new InputElementMap( checkBoxes, (t0, t1) => { return t0?.shadows(t1) ?? false @@ -450,8 +451,8 @@ export default class TagRenderingQuestion extends Combine { if (indices.length === 0) { return undefined; } - const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data); - const oppositeTags: TagsFilter[] = []; + const tags: UploadableTag[] = indices.map(i => elements[i].GetValue().data); + const oppositeTags: UploadableTag[] = []; for (let i = 0; i < ifNotSelected.length; i++) { if (indices.indexOf(i) >= 0) { continue; @@ -465,8 +466,9 @@ export default class TagRenderingQuestion extends Combine { tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); return TagUtils.FlattenMultiAnswer(tags); }, - (tags: TagsFilter) => { + (tags: UploadableTag) => { // {key --> values[]} + const presentTags = TagUtils.SplitKeys([tags]); const indices: number[] = [] // We also collect the values that have to be added to the freeform field @@ -546,9 +548,9 @@ export default class TagRenderingQuestion extends Combine { private static GenerateMappingElement( state, tagsSource: UIEventSource, - mapping: Mapping, ifNot?: TagsFilter[]): InputElement { + mapping: Mapping, ifNot?: UploadableTag[]): InputElement { - let tagging: TagsFilter = mapping.if; + let tagging: UploadableTag = mapping.if; if (ifNot !== undefined) { tagging = new And([mapping.if, ...ifNot]) } @@ -572,7 +574,7 @@ export default class TagRenderingQuestion extends Combine { } private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource, feedback: UIEventSource) - : InputElement { + : InputElement { const freeform = configuration.freeform; if (freeform === undefined) { return undefined; @@ -639,7 +641,7 @@ export default class TagRenderingQuestion extends Combine { } }) - let inputTagsFilter: InputElement = new InputElementMap( + let inputTagsFilter: InputElement = new InputElementMap( input, (a, b) => a === b || (a?.shadows(b) ?? false), pickString, toString );