2022-04-03 02:37:31 +02:00
import { Conversion , DesugaringContext , Fuse , OnEveryConcat , SetDefault } from "./Conversion" ;
2022-01-21 01:57:16 +01:00
import { LayerConfigJson } from "../Json/LayerConfigJson" ;
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" ;
import { Utils } from "../../../Utils" ;
2022-02-28 18:52:28 +01:00
import RewritableConfigJson from "../Json/RewritableConfigJson" ;
2022-04-03 02:37:31 +02:00
import Translations from "../../../UI/i18n/Translations" ;
2022-01-21 01:57:16 +01:00
class ExpandTagRendering extends Conversion < string | TagRenderingConfigJson | { builtin : string | string [ ] , override : any } , TagRenderingConfigJson [ ] > {
2022-02-04 01:05:35 +01:00
private readonly _state : DesugaringContext ;
2022-02-28 18:52:28 +01:00
2022-02-04 01:05:35 +01:00
constructor ( state : DesugaringContext ) {
2022-02-28 18:52:28 +01:00
super ( "Converts a tagRenderingSpec into the full tagRendering" , [ ] , "ExpandTagRendering" ) ;
2022-02-04 01:05:35 +01:00
this . _state = state ;
2022-01-21 01:57:16 +01:00
}
2022-02-04 01:05:35 +01:00
convert ( json : string | TagRenderingConfigJson | { builtin : string | string [ ] ; override : any } , context : string ) : { result : TagRenderingConfigJson [ ] ; errors : string [ ] ; warnings : string [ ] } {
2022-01-21 01:57:16 +01:00
const errors = [ ]
const warnings = [ ]
return {
2022-02-04 01:05:35 +01:00
result : this.convertUntilStable ( json , warnings , errors , context ) ,
2022-01-21 01:57:16 +01:00
errors , warnings
} ;
}
2022-02-04 01:05:35 +01:00
private lookup ( name : string ) : TagRenderingConfigJson [ ] {
const state = this . _state ;
2022-01-21 01:57:16 +01:00
if ( state . tagRenderings . has ( name ) ) {
return [ state . tagRenderings . get ( name ) ]
}
if ( name . indexOf ( "." ) >= 0 ) {
const spl = name . split ( "." ) ;
const layer = state . sharedLayers . get ( spl [ 0 ] )
if ( spl . length === 2 && layer !== undefined ) {
const id = spl [ 1 ] ;
const layerTrs = < TagRenderingConfigJson [ ] > layer . tagRenderings . filter ( tr = > tr [ "id" ] !== undefined )
let matchingTrs : TagRenderingConfigJson [ ]
if ( id === "*" ) {
matchingTrs = layerTrs
} else if ( id . startsWith ( "*" ) ) {
const id_ = id . substring ( 1 )
matchingTrs = layerTrs . filter ( tr = > tr . group === id_ )
} else {
matchingTrs = layerTrs . filter ( tr = > tr . id === id )
}
for ( let i = 0 ; i < matchingTrs . length ; i ++ ) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
const found = Utils . Clone ( matchingTrs [ i ] ) ;
if ( found . condition === undefined ) {
found . condition = layer . source . osmTags
} else {
found . condition = { and : [ found . condition , layer . source . osmTags ] }
}
matchingTrs [ i ] = found
}
if ( matchingTrs . length !== 0 ) {
return matchingTrs
}
}
}
return undefined ;
}
2022-02-04 01:05:35 +01:00
private convertOnce ( tr : string | any , warnings : string [ ] , errors : string [ ] , ctx : string ) : TagRenderingConfigJson [ ] {
const state = this . _state
2022-01-21 01:57:16 +01:00
if ( tr === "questions" ) {
return [ {
id : "questions"
} ]
}
if ( typeof tr === "string" ) {
2022-02-04 01:05:35 +01:00
const lookup = this . lookup ( tr ) ;
2022-02-18 23:10:27 +01:00
if ( lookup === undefined ) {
warnings . push ( ctx + "A literal rendering was detected: " + tr )
return [ {
render : tr ,
id : tr.replace ( /![a-zA-Z0-9]/g , "" )
} ]
2022-01-21 01:57:16 +01:00
}
2022-02-18 23:10:27 +01:00
return lookup
2022-01-21 01:57:16 +01:00
}
if ( tr [ "builtin" ] !== undefined ) {
let names = tr [ "builtin" ]
if ( typeof names === "string" ) {
names = [ names ]
}
for ( const key of Object . keys ( tr ) ) {
if ( key === "builtin" || key === "override" || key === "id" || key . startsWith ( "#" ) ) {
continue
}
errors . push ( "At " + ctx + ": an object calling a builtin can only have keys `builtin` or `override`, but a key with name `" + key + "` was found. This won't be picked up! The full object is: " + JSON . stringify ( tr ) )
}
const trs : TagRenderingConfigJson [ ] = [ ]
for ( const name of names ) {
2022-02-04 01:05:35 +01:00
const lookup = this . lookup ( name )
2022-01-21 01:57:16 +01:00
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 ( ", " ) + "?" )
continue
}
for ( let foundTr of lookup ) {
foundTr = Utils . Clone < any > ( foundTr )
Utils . Merge ( tr [ "override" ] ? ? { } , foundTr )
trs . push ( foundTr )
}
}
return trs ;
}
return [ tr ]
}
2022-02-04 01:05:35 +01:00
private convertUntilStable ( spec : string | any , warnings : string [ ] , errors : string [ ] , ctx : string ) : TagRenderingConfigJson [ ] {
const trs = this . convertOnce ( spec , warnings , errors , ctx ) ;
2022-01-21 01:57:16 +01:00
const result = [ ]
for ( const tr of trs ) {
2022-02-18 23:10:27 +01:00
if ( typeof tr === "string" || tr [ "builtin" ] !== undefined ) {
2022-02-04 01:05:35 +01:00
const stable = this . convertUntilStable ( tr , warnings , errors , ctx + "(RECURSIVE RESOLVE)" )
2022-01-21 01:57:16 +01:00
result . push ( . . . stable )
} else {
result . push ( tr )
}
}
return result ;
}
}
class ExpandGroupRewrite extends Conversion < {
rewrite : {
sourceString : string ,
into : string [ ]
} [ ] ,
renderings : ( string | { builtin : string , override : any } | TagRenderingConfigJson ) [ ]
} | TagRenderingConfigJson , TagRenderingConfigJson [ ] > {
2022-02-04 01:05:35 +01:00
private _expandSubTagRenderings ;
2022-01-21 01:57:16 +01:00
2022-02-04 01:05:35 +01:00
constructor ( state : DesugaringContext ) {
2022-01-21 01:57:16 +01:00
super (
2022-02-28 18:52:28 +01:00
"Converts a rewrite config for tagRenderings into the expanded form" , [ ] ,
2022-02-14 02:26:03 +01:00
"ExpandGroupRewrite"
2022-01-21 01:57:16 +01:00
) ;
2022-02-04 01:05:35 +01:00
this . _expandSubTagRenderings = new ExpandTagRendering ( state )
2022-01-21 01:57:16 +01:00
}
2022-02-28 18:52:28 +01:00
convert ( json :
{
rewrite :
{ sourceString : string ; into : string [ ] } [ ] ; renderings : ( string | { builtin : string ; override : any } | TagRenderingConfigJson ) [ ]
} | TagRenderingConfigJson , context : string ) : { result : TagRenderingConfigJson [ ] ; errors : string [ ] ; warnings? : string [ ] } {
2022-01-21 01:57:16 +01:00
if ( json [ "rewrite" ] === undefined ) {
return { result : [ < TagRenderingConfigJson > json ] , errors : [ ] , warnings : [ ] }
}
let config = < {
rewrite :
2022-01-29 02:45:59 +01:00
{ sourceString : string [ ] ; into : ( string | any ) [ ] [ ] } ;
2022-01-21 01:57:16 +01:00
renderings : ( string | { builtin : string ; override : any } | TagRenderingConfigJson ) [ ]
} > json ;
2022-02-28 18:52:28 +01:00
2022-01-21 01:57:16 +01:00
2022-01-29 02:45:59 +01:00
{
const errors = [ ]
2022-02-14 02:39:33 +01:00
2022-02-28 18:52:28 +01:00
if ( ! Array . isArray ( config . rewrite . sourceString ) ) {
2022-02-14 02:39:33 +01:00
let extra = "" ;
2022-02-28 18:52:28 +01:00
if ( typeof config . rewrite . sourceString === "string" ) {
extra = ` <br/>Try <span class='literal-code'>"sourceString": [ " ${ config . rewrite . sourceString } " ] </span> instead (note the [ and ]) `
2022-02-14 02:39:33 +01:00
}
2022-02-28 18:52:28 +01:00
const msg = context + "<br/>Invalid format: a rewrite block is defined, but the 'sourceString' should be an array of strings, but it is a " + typeof config . rewrite . sourceString + extra
2022-02-14 02:39:33 +01:00
errors . push ( msg )
}
2022-01-29 02:45:59 +01:00
const expectedLength = config . rewrite . sourceString . length
2022-02-28 18:52:28 +01:00
for ( let i = 0 ; i < config . rewrite . into . length ; i ++ ) {
2022-01-29 02:45:59 +01:00
const targets = config . rewrite . into [ i ] ;
2022-02-28 18:52:28 +01:00
if ( ! Array . isArray ( targets ) ) {
errors . push ( ` ${ context } .rewrite.into[ ${ i } ] should be an array of values, but it is a ` + typeof targets )
} else if ( targets . length !== expectedLength ) {
2022-02-14 02:39:33 +01:00
errors . push ( ` ${ context } .rewrite.into[ ${ i } ]:<br/>The rewrite specified ${ config . rewrite . sourceString } as sourcestring, which consists of ${ expectedLength } values. The target ${ JSON . stringify ( targets ) } has ${ targets . length } items ` )
2022-02-28 18:52:28 +01:00
if ( typeof targets [ 0 ] !== "string" ) {
errors . push ( context + ".rewrite.into[" + i + "]: expected a string as first rewrite value values, but got " + targets [ 0 ] )
2022-01-29 02:45:59 +01:00
2022-02-28 18:52:28 +01:00
}
2022-02-14 02:39:33 +01:00
}
2022-01-29 02:45:59 +01:00
}
2022-01-21 01:57:16 +01:00
2022-01-29 02:45:59 +01:00
if ( errors . length > 0 ) {
return {
errors ,
warnings : [ ] ,
result : undefined
}
}
}
2022-02-28 18:52:28 +01:00
const subRenderingsRes = < { result : TagRenderingConfigJson [ ] [ ] , errors , warnings } > this . _expandSubTagRenderings . convertAll ( config . renderings , context ) ;
2022-01-29 02:45:59 +01:00
const subRenderings : TagRenderingConfigJson [ ] = [ ] . concat ( . . . subRenderingsRes . result ) ;
2022-01-21 01:57:16 +01:00
const errors = subRenderingsRes . errors ;
const warnings = subRenderingsRes . warnings ;
const rewrittenPerGroup = new Map < string , TagRenderingConfigJson [ ] > ( )
// The actual rewriting
2022-01-29 02:45:59 +01:00
const sourceStrings = config . rewrite . sourceString ;
for ( const targets of config . rewrite . into ) {
const groupName = targets [ 0 ] ;
2022-02-28 18:52:28 +01:00
if ( typeof groupName !== "string" ) {
2022-01-29 02:45:59 +01:00
throw "The first string of 'targets' should always be a string"
}
const trs : TagRenderingConfigJson [ ] = [ ]
for ( const tr of subRenderings ) {
let rewritten = tr ;
for ( let i = 0 ; i < sourceStrings . length ; i ++ ) {
const source = sourceStrings [ i ]
const target = targets [ i ] // This is a string OR a translation
2022-02-28 18:52:28 +01:00
rewritten = ExpandRewrite . RewriteParts ( source , target , rewritten )
2022-01-21 01:57:16 +01:00
}
2022-01-29 02:45:59 +01:00
rewritten . group = rewritten . group ? ? groupName
trs . push ( rewritten )
}
2022-01-21 01:57:16 +01:00
2022-01-29 02:45:59 +01:00
if ( rewrittenPerGroup . has ( groupName ) ) {
rewrittenPerGroup . get ( groupName ) . push ( . . . trs )
} else {
rewrittenPerGroup . set ( groupName , trs )
2022-01-21 01:57:16 +01:00
}
}
// Add questions box for this category
rewrittenPerGroup . forEach ( ( group , groupName ) = > {
group . push ( < TagRenderingConfigJson > {
id : "questions" ,
group : groupName
} )
} )
rewrittenPerGroup . forEach ( ( group , _ ) = > {
group . forEach ( tr = > {
if ( tr . id === undefined || tr . id === "" ) {
2022-02-28 18:52:28 +01:00
errors . push ( "A tagrendering has an empty ID after expanding the tag; the tagrendering is: " + JSON . stringify ( tr ) )
2022-01-21 01:57:16 +01:00
}
} )
} )
return {
result : [ ] . concat ( . . . Array . from ( rewrittenPerGroup . values ( ) ) ) ,
errors , warnings
} ;
}
2022-02-28 18:52:28 +01:00
}
2022-04-03 02:37:31 +02:00
export class ExpandRewrite < T > extends Conversion < T | RewritableConfigJson < T > , T [ ] > {
2022-02-28 18:52:28 +01:00
constructor ( ) {
super ( "Applies a rewrite" , [ ] , "ExpandRewrite" ) ;
}
2022-04-03 02:37:31 +02:00
/ * *
* 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"}
*
* /
2022-02-28 18:52:28 +01:00
public static RewriteParts < T > ( keyToRewrite : string , target : string | any , tr : T ) : T {
2022-04-03 02:37:31 +02:00
const targetIsTranslation = Translations . isProbablyATranslation ( target )
function replaceRecursive ( obj : string | any , target ) {
2022-01-29 02:45:59 +01:00
2022-04-03 02:37:31 +02:00
if ( obj === keyToRewrite ) {
2022-03-08 01:05:54 +01:00
return target
}
2022-04-03 02:37:31 +02:00
if ( typeof obj === "string" ) {
2022-01-29 02:45:59 +01:00
// This is a simple string - we do a simple replace
2022-04-03 02:37:31 +02:00
return obj . replace ( keyToRewrite , target )
2022-01-21 01:57:16 +01:00
}
2022-04-03 02:37:31 +02:00
if ( Array . isArray ( obj ) ) {
2022-01-29 02:45:59 +01:00
// This is a list of items
2022-04-03 02:37:31 +02:00
return obj . map ( o = > replaceRecursive ( o , target ) )
2022-01-21 01:57:16 +01:00
}
2022-01-29 02:45:59 +01:00
2022-04-03 02:37:31 +02:00
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 )
2022-03-08 01:05:54 +01:00
}
2022-04-03 02:37:31 +02:00
return obj
2022-01-21 01:57:16 +01:00
}
2022-04-03 02:37:31 +02:00
return obj
2022-01-21 01:57:16 +01:00
}
2022-04-03 02:37:31 +02:00
return replaceRecursive ( tr , target )
2022-01-21 01:57:16 +01:00
}
2022-04-03 02:37:31 +02:00
/ * *
* // 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 , "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 , "test" ) // => expected
* /
2022-02-28 18:52:28 +01:00
convert ( json : T | RewritableConfigJson < T > , context : string ) : { result : T [ ] ; errors? : string [ ] ; warnings? : string [ ] ; information? : string [ ] } {
2022-04-03 02:37:31 +02:00
if ( json === null || json === undefined ) {
2022-02-28 20:21:37 +01:00
return { result : [ ] }
}
2022-04-03 02:37:31 +02:00
2022-02-28 18:52:28 +01:00
if ( json [ "rewrite" ] === undefined ) {
2022-04-03 02:37:31 +02:00
2022-02-28 18:52:28 +01:00
// not a rewrite
return { result : [ ( < T > json ) ] }
}
const rewrite = < RewritableConfigJson < T > > json ;
2022-04-03 02:37:31 +02:00
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 ) {
throw ` ${ context } Error in rewrite: 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 ) {
throw ` ${ context } .into. ${ i } Error in rewrite: there are ${ rewrite . rewrite . sourceString . length } keys to rewrite, but entry ${ i } has only ${ into . length } values `
2022-03-08 01:05:54 +01:00
}
}
}
2022-04-03 02:37:31 +02:00
for ( let i = 0 ; i < keysToRewrite . into . length ; i ++ ) {
2022-02-28 18:52:28 +01:00
let t = Utils . Clone ( rewrite . renderings )
2022-04-03 02:37:31 +02:00
for ( let j = 0 ; j < keysToRewrite . sourceString . length ; j ++ ) {
const key = keysToRewrite . sourceString [ j ] ;
const target = keysToRewrite . into [ i ] [ j ]
2022-02-28 18:52:28 +01:00
t = ExpandRewrite . RewriteParts ( key , target , t )
}
ts . push ( t )
}
return { result : ts } ;
}
}
2022-01-21 01:57:16 +01:00
export class PrepareLayer extends Fuse < LayerConfigJson > {
2022-03-24 03:11:52 +01:00
2022-02-04 01:05:35 +01:00
constructor ( state : DesugaringContext ) {
2022-01-21 01:57:16 +01:00
super (
"Fully prepares and expands a layer for the LayerConfig." ,
2022-02-04 01:05:35 +01:00
new OnEveryConcat ( "tagRenderings" , new ExpandGroupRewrite ( state ) ) ,
new OnEveryConcat ( "tagRenderings" , new ExpandTagRendering ( state ) ) ,
2022-02-28 18:52:28 +01:00
new OnEveryConcat ( "mapRendering" , new ExpandRewrite ( ) ) ,
2022-01-21 01:57:16 +01:00
new SetDefault ( "titleIcons" , [ "defaults" ] ) ,
2022-02-04 01:05:35 +01:00
new OnEveryConcat ( "titleIcons" , new ExpandTagRendering ( state ) )
2022-01-21 01:57:16 +01:00
) ;
}
}