From f88fade35bc40b8cf521963d6ace156209726c66 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 6 Jun 2025 18:15:50 +0200 Subject: [PATCH] Themes: allow a questionbox to have both a whitelist and a blacklist --- .../ThemeConfig/Conversion/PrepareLayer.ts | 117 +++++++++++++----- .../UISpecialVisualisations.ts | 2 +- 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts index 344d62e7bd..795979d2f8 100644 --- a/src/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/src/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -170,7 +170,7 @@ class DetectInline extends DesugaringStep { export class AddQuestionBox extends DesugaringStep { constructor() { - super("AddQuestionBox", "Adds a 'questions'-object if no question element is added yet") + super("AddQuestionBox", "Adds a 'questions'-object if no question element is added yet. Will ignore all elements which were previously asked for (and questions labeled with 'hidden')") } /** @@ -202,43 +202,46 @@ export class AddQuestionBox extends DesugaringStep { (sp) => sp.args.length === 0 || sp.args[0].trim() === "" ) + if (noLabels.length > 1) { context.err( - "Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this. Did you perhaps import all questions from another layer?" + "Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this - questions will be shown twice. Did you perhaps import all questions from another layer?", ) } + + /** + * We want to construct a questionbox that shows all leftover questions. + * For this, we need to determine what those leftover questions _are_ in the first place. + * + * So, we gather the labels of the layer and compare that to the labels used by previous question boxes + */ + // ALl labels that are used in this layer const allLabels = new Set( - [].concat( - ...json.tagRenderings.map( + json.tagRenderings.flatMap( (tr) => (tr).labels ?? [] ) - ) ) - const seen: Set = new Set() + /** + * The essence of all questionboxes: what is whitelisted, what is blacklisted? + */ + const questionBoxes: { blacklist: string[], whitelist: string[] }[] = [] for (const questionSpecial of questionSpecials) { if (typeof questionSpecial === "string") { + // Probably a header or something continue } - const used = questionSpecial.args[0] + const whitelist = questionSpecial.args[0] ?.split(";") ?.map((a) => a.trim()) ?.filter((s) => s != "") - const blacklisted = questionSpecial.args[1] + const blacklist = questionSpecial.args[1] ?.split(";") ?.map((a) => a.trim()) ?.filter((s) => s != "") - if (blacklisted?.length > 0 && used?.length > 0) { - context.err( - "The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." + - "\n Whitelisted: " + - used.join(", ") + - "\n Blacklisted: " + - blacklisted.join(", ") - ) - } - for (const usedLabel of used) { + + for (const usedLabel of whitelist) { if (!allLabels.has(usedLabel)) { context.err( "This layers specifies a special question element for label `" + @@ -248,24 +251,76 @@ export class AddQuestionBox extends DesugaringStep { Array.from(allLabels).join(", ") ) } - seen.add(usedLabel) } + questionBoxes.push({ blacklist, whitelist }) } if (noLabels.length == 0) { - /* At this point, we know which question labels are not yet handled and which already are handled, and we - * know there is no previous catch-all questions - */ - seen.add("hidden") - const question: QuestionableTagRenderingConfigJson = { - id: "leftover-questions", - labels: ["ignore-docs", "added_by_default"], - render: { - "*": `{questions( ,${Array.from(seen).join(";")})}`, - }, - } - json.tagRenderings.push(question) + // We already have a questionbox handling _all_ questions + return json } + + const usedLabels: Set = new Set() + + for (const { blacklist, whitelist } of questionBoxes) { + if (whitelist.length > 0 && blacklist.length == 0) { + // All questions from "whitelist" are guaranteed to be used here + whitelist.forEach(label => usedLabels.add(label)) + } + } + + /** We should still check the weird questionboxes that have both a whitelist _and_ a blacklist. + * Can we say that the whitelisted items are fully consumed? + */ + let needsEvaluation = true + let toEvaluate = questionBoxes.filter(q => q.whitelist.length > 0 && q.blacklist.length > 0) + while (needsEvaluation && toEvaluate.length > 0) { + needsEvaluation = false + const toReEvaluate = [] + for (const { blacklist, whitelist } of toEvaluate) { + const blacklistRest = blacklist.filter(label => !usedLabels.has(label)) + if (blacklistRest.length == 0) { + // All items from the blacklist have been handled by a different questionbox + // We can safely say that all whitelisted items are consumed + if (whitelist.length == 0) { + // Even better: this questionbox will show all leftover questions + return json + } + whitelist.forEach(label => { + usedLabels.add(label) + }) + needsEvaluation = true + } else { + // Hmm, maybe in a next iteration? + toReEvaluate.push({ blacklist, whitelist }) + } + } + toEvaluate = toReEvaluate + } + + if (toEvaluate.length > 0) { + // If we end up here, we have a questionbox with a whitelist _and_ a blacklist. + // We cannot unambiguously create a leftover-questions box for this + + context.err( + "Could not calculate a non-ambiguous leftover questions block. A {questions()}-special rendering is found which has both a whitelist and a blacklist; where the blacklist was not fully consumed by other tagRenderings\n\t" + + JSON.stringify(toEvaluate), + ) + } + + + /* At this point, we know which question labels are not yet handled and which already are handled, and we + * know there is no previous catch-all questions + */ + usedLabels.add("hidden") + const question: QuestionableTagRenderingConfigJson = { + id: "leftover-questions", + labels: ["ignore-docs", "added_by_default"], + render: { + "*": `{questions( ,${Array.from(usedLabels).join(";")})}`, + }, + } + json.tagRenderings.push(question) return json } } diff --git a/src/UI/SpecialVisualisations/UISpecialVisualisations.ts b/src/UI/SpecialVisualisations/UISpecialVisualisations.ts index c488dc36c6..1d2d2f8acf 100644 --- a/src/UI/SpecialVisualisations/UISpecialVisualisations.ts +++ b/src/UI/SpecialVisualisations/UISpecialVisualisations.ts @@ -29,7 +29,7 @@ class QuestionViz implements SpecialVisualizationSvelte { }, { name: "blacklisted-labels", - doc: "One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'", + doc: "One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'. If both a whitelist and a blacklist are given, will show questions having at least one label from the whitelist but none of the blacklist.", }, { name: "show_all",