2020-11-06 01:58:26 +01:00
import { Translation } from "../../UI/i18n/Translation" ;
2021-08-07 23:11:34 +02:00
import { TagsFilter } from "../../Logic/Tags/TagsFilter" ;
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" ;
import Translations from "../../UI/i18n/Translations" ;
2021-03-29 00:41:53 +02:00
import { TagUtils } from "../../Logic/Tags/TagUtils" ;
import { And } from "../../Logic/Tags/And" ;
2021-08-07 23:11:34 +02:00
import ValidatedTextField from "../../UI/Input/ValidatedTextField" ;
import { Utils } from "../../Utils" ;
2021-10-26 22:53:27 +02:00
import { Tag } from "../../Logic/Tags/Tag" ;
2020-10-27 01:01:34 +01:00
/ * * *
* The parsed version of TagRenderingConfigJSON
* Identical data , but with some methods and validation
* /
export default class TagRenderingConfig {
2021-09-26 18:15:21 +02:00
readonly id : string ;
2021-10-22 18:53:07 +02:00
readonly group : string ;
2021-01-08 03:57:18 +01:00
readonly render? : Translation ;
readonly question? : Translation ;
readonly condition? : TagsFilter ;
2021-03-31 15:50:29 +02:00
readonly configuration_warnings : string [ ] = [ ]
2020-10-27 01:01:34 +01:00
2021-01-08 03:57:18 +01:00
readonly freeform ? : {
2021-03-31 15:50:29 +02:00
readonly key : string ,
readonly type : string ,
readonly addExtraTags : TagsFilter [ ] ;
2021-07-11 15:44:17 +02:00
readonly inline : boolean ,
2021-07-20 01:33:58 +02:00
readonly default ? : string ,
readonly helperArgs ? : ( string | number | boolean ) [ ]
2020-10-27 01:01:34 +01:00
} ;
2021-01-06 01:11:07 +01:00
readonly multiAnswer : boolean ;
2020-10-27 01:01:34 +01:00
2021-01-08 03:57:18 +01:00
readonly mappings ? : {
2021-02-20 16:48:42 +01:00
readonly if : TagsFilter ,
readonly ifnot? : TagsFilter ,
readonly then : Translation
readonly hideInAnswer : boolean | TagsFilter
2021-10-26 22:53:27 +02:00
readonly addExtraTags : Tag [ ]
2020-10-27 01:01:34 +01:00
} [ ]
2021-10-19 03:00:57 +02:00
constructor ( json : string | TagRenderingConfigJson , context? : string ) {
2020-10-27 01:01:34 +01:00
2020-12-08 23:44:34 +01:00
if ( json === "questions" ) {
// Very special value
this . render = null ;
this . question = null ;
this . condition = null ;
2021-11-07 18:37:42 +01:00
this . id = "questions"
this . group = ""
return ;
2020-12-08 23:44:34 +01:00
}
2021-11-07 16:34:51 +01:00
if ( typeof json === "number" ) {
2021-11-08 15:08:24 +01:00
this . render = Translations . T ( "" + json , context + ".render" )
2021-10-22 01:07:32 +02:00
return ;
}
2021-11-07 16:34:51 +01:00
2020-12-08 23:44:34 +01:00
if ( json === undefined ) {
throw "Initing a TagRenderingConfig with undefined in " + context ;
2020-10-27 01:01:34 +01:00
}
if ( typeof json === "string" ) {
2021-03-31 15:50:29 +02:00
this . render = Translations . T ( json , context + ".render" ) ;
2020-10-27 01:01:34 +01:00
this . multiAnswer = false ;
return ;
}
2021-01-08 03:57:18 +01:00
2021-11-07 16:34:51 +01:00
2021-09-26 17:54:49 +02:00
this . id = json . id ? ? "" ;
2021-11-11 17:14:03 +01:00
if ( this . id . match ( /^[a-zA-Z0-9 ()?\/=:;,_-]*$/ ) === null ) {
throw "Invalid ID in " + context + ": an id can only contain [a-zA-Z0-0_-] as characters. The offending id is: " + this . id
}
2021-10-22 18:53:07 +02:00
this . group = json . group ? ? "" ;
2021-03-31 15:50:29 +02:00
this . render = Translations . T ( json . render , context + ".render" ) ;
this . question = Translations . T ( json . question , context + ".question" ) ;
2021-10-19 03:00:57 +02:00
this . condition = TagUtils . Tag ( json . condition ? ? { "and" : [ ] } , ` ${ context } .condition ` ) ;
2020-10-27 01:01:34 +01:00
if ( json . freeform ) {
2021-06-22 00:29:07 +02:00
2021-11-07 16:34:51 +01:00
if ( json . freeform . addExtraTags !== undefined && json . freeform . addExtraTags . map === undefined ) {
2021-09-09 20:26:12 +02:00
throw ` Freeform.addExtraTags should be a list of strings - not a single string (at ${ context } ) `
}
2020-10-27 01:01:34 +01:00
this . freeform = {
key : json.freeform.key ,
type : json . freeform . type ? ? "string" ,
addExtraTags : json.freeform.addExtraTags?.map ( ( tg , i ) = >
2021-08-07 23:11:34 +02:00
TagUtils . Tag ( tg , ` ${ context } .extratag[ ${ i } ] ` ) ) ? ? [ ] ,
2021-07-11 15:44:17 +02:00
inline : json.freeform.inline ? ? false ,
2021-07-20 01:33:58 +02:00
default : json . freeform . default ,
helperArgs : json.freeform.helperArgs
2021-06-27 19:21:31 +02:00
2020-10-27 01:01:34 +01:00
}
2021-04-09 02:57:06 +02:00
if ( json . freeform [ "extraTags" ] !== undefined ) {
2021-03-31 15:50:29 +02:00
throw ` Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${ context } ) `
}
if ( this . freeform . key === undefined || this . freeform . key === "" ) {
2021-03-10 20:18:05 +01:00
throw ` Freeform.key is undefined or the empty string - this is not allowed; either fill out something or remove the freeform block alltogether. Error in ${ context } `
}
2021-08-07 23:11:34 +02:00
if ( json . freeform [ "args" ] !== undefined ) {
2021-07-29 00:29:29 +02:00
throw ` Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${ context } ) `
}
2021-11-12 01:44:13 +01:00
if ( json . freeform . key === "questions" ) {
if ( this . id !== "questions" ) {
throw ` If you use a freeform key 'questions', the ID must be 'questions' too to trigger the special behaviour. The current id is ' ${ this . id } ' (at ${ context } ) `
}
}
2021-06-27 19:21:31 +02:00
2020-10-27 01:01:34 +01:00
if ( ValidatedTextField . AllTypes [ this . freeform . type ] === undefined ) {
2021-04-18 14:24:30 +02:00
const knownKeys = ValidatedTextField . tpList . map ( tp = > tp . name ) . join ( ", " ) ;
throw ` Freeform.key ${ this . freeform . key } is an invalid type. Known keys are ${ knownKeys } `
2020-10-27 01:01:34 +01:00
}
2021-03-31 15:50:29 +02:00
if ( this . freeform . addExtraTags ) {
2021-03-10 20:18:05 +01:00
const usedKeys = new And ( this . freeform . addExtraTags ) . usedKeys ( ) ;
2021-03-31 15:50:29 +02:00
if ( usedKeys . indexOf ( this . freeform . key ) >= 0 ) {
2021-03-10 20:18:05 +01:00
throw ` The freeform key ${ this . freeform . key } will be overwritten by one of the extra tags, as they use the same key too. This is in ${ context } ` ;
}
}
2020-10-27 01:01:34 +01:00
}
this . multiAnswer = json . multiAnswer ? ? false
if ( json . mappings ) {
2021-03-31 15:50:29 +02:00
2021-06-22 00:29:07 +02:00
if ( ! Array . isArray ( json . mappings ) ) {
throw "Tagrendering has a 'mappings'-object, but expected a list (" + context + ")"
2021-04-10 03:50:44 +02:00
}
2021-03-31 15:50:29 +02:00
2020-10-27 01:01:34 +01:00
this . mappings = json . mappings . map ( ( mapping , i ) = > {
2021-10-26 22:53:27 +02:00
const ctx = ` ${ context } .mapping[ ${ i } ] `
2020-10-27 01:01:34 +01:00
if ( mapping . then === undefined ) {
2021-10-26 22:53:27 +02:00
throw ` ${ ctx } : Invalid mapping: if without body `
2021-02-20 16:48:42 +01:00
}
if ( mapping . ifnot !== undefined && ! this . multiAnswer ) {
2021-10-26 22:53:27 +02:00
throw ` ${ ctx } : Invalid mapping: ifnot defined, but the tagrendering is not a multianswer `
2020-10-27 01:01:34 +01:00
}
2021-06-22 00:29:07 +02:00
if ( mapping . if === undefined ) {
2021-10-26 22:53:27 +02:00
throw ` ${ ctx } : Invalid mapping: "if" is not defined, but the tagrendering is not a multianswer `
2021-05-17 17:10:54 +02:00
}
2021-06-22 00:29:07 +02:00
if ( typeof mapping . if !== "string" && mapping . if [ "length" ] !== undefined ) {
2021-10-26 22:53:27 +02:00
throw ` ${ ctx } : Invalid mapping: "if" is defined as an array. Use {"and": <your conditions>} or {"or": <your conditions>} instead `
}
2021-11-07 16:34:51 +01:00
if ( mapping . addExtraTags !== undefined && this . multiAnswer ) {
2021-10-26 22:53:27 +02:00
throw ` ${ ctx } : Invalid mapping: got a multi-Answer with addExtraTags; this is not allowed `
2021-05-17 17:10:54 +02:00
}
2021-06-22 00:29:07 +02:00
2021-01-06 01:11:07 +01:00
let hideInAnswer : boolean | TagsFilter = false ;
2020-12-08 23:44:34 +01:00
if ( typeof mapping . hideInAnswer === "boolean" ) {
2020-12-07 03:02:50 +01:00
hideInAnswer = mapping . hideInAnswer ;
2020-12-08 23:44:34 +01:00
} else if ( mapping . hideInAnswer !== undefined ) {
2021-08-07 23:11:34 +02:00
hideInAnswer = TagUtils . Tag ( mapping . hideInAnswer , ` ${ context } .mapping[ ${ i } ].hideInAnswer ` ) ;
2020-12-07 03:02:50 +01:00
}
2021-02-20 16:48:42 +01:00
const mp = {
2021-10-26 22:53:27 +02:00
if : TagUtils . Tag ( mapping . if , ` ${ ctx } .if ` ) ,
ifnot : ( mapping . ifnot !== undefined ? TagUtils . Tag ( mapping . ifnot , ` ${ ctx } .ifnot ` ) : undefined ) ,
2021-10-28 00:13:18 +02:00
then : Translations.T ( mapping . then , ` ${ ctx } .then ` ) ,
2021-10-26 22:53:27 +02:00
hideInAnswer : hideInAnswer ,
2021-11-07 16:34:51 +01:00
addExtraTags : ( mapping . addExtraTags ? ? [ ] ) . map ( ( str , j ) = > TagUtils . SimpleTag ( str , ` ${ ctx } .addExtraTags[ ${ j } ] ` ) )
2020-10-27 01:01:34 +01:00
} ;
2021-02-20 16:48:42 +01:00
if ( this . question ) {
2021-03-14 01:40:35 +01:00
if ( hideInAnswer !== true && mp . if !== undefined && ! mp . if . isUsableAsAnswer ( ) ) {
2021-02-20 16:48:42 +01:00
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 ;
2020-10-27 01:01:34 +01:00
} ) ;
}
if ( this . question && this . freeform ? . key === undefined && this . mappings === undefined ) {
2021-02-20 16:48:42 +01:00
throw ` ${ context } : A question is defined, but no mappings nor freeform (key) are. The question is ${ this . question . txt } at ${ context } `
2020-10-27 01:01:34 +01:00
}
2021-02-20 16:48:42 +01:00
2021-11-11 17:14:03 +01:00
if ( this . id === "questions" && this . render !== undefined ) {
for ( const ln in this . render . translations ) {
const txt :string = this . render . translations [ ln ]
if ( txt . indexOf ( "{questions}" ) >= 0 ) {
continue
}
throw ` ${ context } : The rendering for language ${ ln } does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown! `
}
2021-11-12 01:44:13 +01:00
if ( this . freeform ? . key !== undefined && this . freeform ? . key !== "questions" ) {
throw ` ${ context } : If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together `
}
2021-11-11 17:14:03 +01:00
}
2021-11-10 18:42:31 +01:00
if ( this . freeform ) {
if ( this . render === undefined ) {
throw ` ${ context } : Detected a freeform key without rendering... Key: ${ this . freeform . key } in ${ context } `
}
for ( const ln in this . render . translations ) {
const txt :string = this . render . translations [ ln ]
if ( txt === "" ) {
throw context + " Rendering for language " + ln + " is empty"
}
if ( txt . indexOf ( "{" + this . freeform . key + "}" ) >= 0 ) {
continue
}
2021-11-12 01:44:13 +01:00
if ( txt . indexOf ( "{" + this . freeform . key + ":" ) >= 0 ) {
continue
}
2021-11-10 18:42:31 +01:00
if ( txt . indexOf ( "{canonical(" + this . freeform . key + ")" ) >= 0 ) {
continue
}
if ( this . freeform . type === "opening_hours" && txt . indexOf ( "{opening_hours_table(" ) >= 0 ) {
continue
}
if ( this . freeform . type === "wikidata" && txt . indexOf ( "{wikipedia(" + this . freeform . key ) >= 0 ) {
continue
}
if ( this . freeform . key === "wikidata" && txt . indexOf ( "{wikipedia()" ) >= 0 ) {
continue
}
throw ` ${ context } : The rendering for language ${ ln } does not contain the freeform key { ${ this . freeform . key } }. This is a bug, as this rendering should show exactly this freeform key! \ nThe rendering is ${ txt } `
}
}
2021-03-31 15:50:29 +02:00
2021-11-10 18:42:31 +01:00
2021-03-31 15:50:29 +02:00
if ( this . render && this . question && this . freeform === undefined ) {
2021-03-26 00:14:17 +01:00
throw ` ${ context } : Detected a tagrendering which takes input without freeform key in ${ context } ; the question is ${ this . question . txt } `
2021-03-10 12:55:39 +01:00
}
2021-03-31 15:50:29 +02:00
if ( ! json . multiAnswer && this . mappings !== undefined && this . question !== undefined ) {
2021-03-14 01:40:35 +01:00
let keys = [ ]
2021-03-31 15:50:29 +02:00
for ( let i = 0 ; i < this . mappings . length ; i ++ ) {
2021-03-14 01:40:35 +01:00
const mapping = this . mappings [ i ] ;
2021-03-31 15:50:29 +02:00
if ( mapping . if === undefined ) {
2021-03-14 01:40:35 +01:00
throw ` ${ context } .mappings[ ${ i } ].if is undefined `
}
keys . push ( . . . mapping . if . usedKeys ( ) )
}
keys = Utils . Dedup ( keys )
2021-03-31 15:50:29 +02:00
for ( let i = 0 ; i < this . mappings . length ; i ++ ) {
2021-03-14 01:40:35 +01:00
const mapping = this . mappings [ i ] ;
2021-03-31 15:50:29 +02:00
if ( mapping . hideInAnswer ) {
2021-03-14 01:40:35 +01:00
continue
}
2021-03-31 15:50:29 +02:00
2021-03-14 01:40:35 +01:00
const usedKeys = mapping . if . usedKeys ( ) ;
for ( const expectedKey of keys ) {
2021-03-31 15:50:29 +02:00
if ( usedKeys . indexOf ( expectedKey ) < 0 ) {
2021-03-14 01:40:35 +01:00
const msg = ` ${ context } .mappings[ ${ i } ]: This mapping only defines values for ${ usedKeys . join ( ', ' ) } , but it should also give a value for ${ expectedKey } `
2021-03-14 20:14:51 +01:00
this . configuration_warnings . push ( msg )
2021-03-14 01:40:35 +01:00
}
}
}
}
2020-10-27 01:01:34 +01:00
2021-02-20 16:48:42 +01:00
if ( this . question !== undefined && json . multiAnswer ) {
2020-10-27 01:01:34 +01:00
if ( ( this . mappings ? . length ? ? 0 ) === 0 ) {
2021-02-20 16:48:42 +01:00
throw ` ${ context } MultiAnswer is set, but no mappings are defined `
}
let allKeys = [ ] ;
let allHaveIfNot = true ;
for ( const mapping of this . mappings ) {
if ( mapping . hideInAnswer ) {
continue ;
}
if ( mapping . ifnot === undefined ) {
allHaveIfNot = false ;
}
allKeys = allKeys . concat ( mapping . if . usedKeys ( ) ) ;
}
allKeys = Utils . Dedup ( allKeys ) ;
if ( allKeys . length > 1 && ! allHaveIfNot ) {
throw ` ${ context } : A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping `
2020-10-27 01:01:34 +01:00
}
}
}
2021-03-13 17:25:44 +01:00
/ * *
* Returns true if it is known or not shown , false if the question should be asked
* @constructor
* /
public IsKnown ( tags : any ) : boolean {
if ( this . condition &&
! this . condition . matchesProperties ( tags ) ) {
2021-11-10 18:42:31 +01:00
// Filtered away by the condition, so it is kindof known
2021-03-13 17:25:44 +01:00
return true ;
}
2021-03-31 15:50:29 +02:00
if ( this . multiAnswer ) {
2021-04-18 14:24:30 +02:00
for ( const m of this . mappings ? ? [ ] ) {
2021-03-31 15:50:29 +02:00
if ( TagUtils . MatchesMultiAnswer ( m . if , tags ) ) {
2021-03-13 17:25:44 +01:00
return true ;
}
}
const free = this . freeform ? . key
2021-03-31 15:50:29 +02:00
if ( free !== undefined ) {
2021-03-13 17:25:44 +01:00
return tags [ free ] !== undefined
}
return false
}
if ( this . GetRenderValue ( tags ) !== undefined ) {
// This value is known and can be rendered
return true ;
}
return false ;
}
2021-11-07 16:34:51 +01:00
2021-06-14 02:39:23 +02:00
/ * *
* Gets all the render values . Will return multiple render values if 'multianswer' is enabled .
* The result will equal [ GetRenderValue ] if not 'multiAnswer'
* @param tags
* @constructor
* /
2021-06-22 00:29:07 +02:00
public GetRenderValues ( tags : any ) : Translation [ ] {
if ( ! this . multiAnswer ) {
2021-06-14 02:39:23 +02:00
return [ this . GetRenderValue ( tags ) ]
}
// A flag to check that the freeform key isn't matched multiple times
// If it is undefined, it is "used" already, or at least we don't have to check for it anymore
2021-06-22 00:29:07 +02:00
let freeformKeyUsed = this . freeform ? . key === undefined ;
2021-06-14 02:39:23 +02:00
// We run over all the mappings first, to check if the mapping matches
const applicableMappings : Translation [ ] = Utils . NoNull ( ( this . mappings ? ? [ ] ) ? . map ( mapping = > {
if ( mapping . if === undefined ) {
return mapping . then ;
}
if ( TagUtils . MatchesMultiAnswer ( mapping . if , tags ) ) {
2021-06-22 00:29:07 +02:00
if ( ! freeformKeyUsed ) {
if ( mapping . if . usedKeys ( ) . indexOf ( this . freeform . key ) >= 0 ) {
2021-06-14 02:39:23 +02:00
// This mapping matches the freeform key - we mark the freeform key to be ignored!
freeformKeyUsed = true ;
}
}
return mapping . then ;
}
return undefined ;
} ) )
2021-06-22 00:29:07 +02:00
2021-06-14 02:39:23 +02:00
if ( ! freeformKeyUsed
&& tags [ this . freeform . key ] !== undefined ) {
applicableMappings . push ( this . render )
}
return applicableMappings
}
2021-06-22 00:29:07 +02:00
2020-10-27 01:01:34 +01:00
/ * *
* Gets the correct rendering value ( or undefined if not known )
2021-06-22 00:29:07 +02:00
* Not compatible with multiAnswer - use GetRenderValueS instead in that case
2020-10-27 01:01:34 +01:00
* @constructor
* /
2021-10-21 21:41:45 +02:00
public GetRenderValue ( tags : any , defltValue : any = undefined ) : Translation {
2020-10-27 01:01:34 +01:00
if ( this . mappings !== undefined && ! this . multiAnswer ) {
for ( const mapping of this . mappings ) {
if ( mapping . if === undefined ) {
return mapping . then ;
}
if ( mapping . if . matchesProperties ( tags ) ) {
return mapping . then ;
}
}
}
2021-11-12 01:44:13 +01:00
if ( this . id === "questions" ) {
return this . render
}
2021-01-06 01:11:07 +01:00
if ( this . freeform ? . key === undefined ) {
2020-10-27 01:01:34 +01:00
return this . render ;
}
if ( tags [ this . freeform . key ] !== undefined ) {
return this . render ;
}
2021-10-21 21:41:45 +02:00
return defltValue ;
2020-10-27 01:01:34 +01:00
}
2021-04-09 02:57:06 +02:00
public ExtractImages ( isIcon : boolean ) : Set < string > {
2021-06-22 00:29:07 +02:00
2021-04-09 02:57:06 +02:00
const usedIcons = new Set < string > ( )
this . render ? . ExtractImages ( isIcon ) ? . forEach ( usedIcons . add , usedIcons )
for ( const mapping of this . mappings ? ? [ ] ) {
mapping . then . ExtractImages ( isIcon ) . forEach ( usedIcons . add , usedIcons )
}
2021-06-22 00:29:07 +02:00
2021-04-09 02:57:06 +02:00
return usedIcons ;
}
2020-10-27 01:01:34 +01:00
2021-08-07 23:11:34 +02:00
2020-10-27 01:01:34 +01:00
}