2022-04-06 03:06:50 +02:00
import { DesugaringStep , Each , Fuse , On } from "./Conversion" ;
2022-02-04 00:44:09 +01:00
import { LayerConfigJson } from "../Json/LayerConfigJson" ;
import LayerConfig from "../LayerConfig" ;
import { Utils } from "../../../Utils" ;
import Constants from "../../Constants" ;
import { Translation } from "../../../UI/i18n/Translation" ;
import { LayoutConfigJson } from "../Json/LayoutConfigJson" ;
import LayoutConfig from "../LayoutConfig" ;
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" ;
import { TagUtils } from "../../../Logic/Tags/TagUtils" ;
2022-02-09 22:37:21 +01:00
import { ExtractImages } from "./FixImages" ;
2022-02-10 23:16:14 +01:00
import ScriptUtils from "../../../scripts/ScriptUtils" ;
2022-02-14 15:40:38 +01:00
import { And } from "../../../Logic/Tags/And" ;
2022-02-17 23:54:14 +01:00
import Translations from "../../../UI/i18n/Translations" ;
2022-02-18 23:10:27 +01:00
import Svg from "../../../Svg" ;
2022-02-28 17:17:38 +01:00
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" ;
2022-02-04 00:44:09 +01:00
class ValidateLanguageCompleteness extends DesugaringStep < any > {
2022-06-20 01:42:30 +02:00
2022-02-04 00:44:09 +01:00
private readonly _languages : string [ ] ;
constructor ( . . . languages : string [ ] ) {
2022-02-14 02:26:03 +01:00
super ( "Checks that the given object is fully translated in the specified languages" , [ ] , "ValidateLanguageCompleteness" ) ;
2022-04-06 03:06:50 +02:00
this . _languages = languages ? ? [ "en" ] ;
2022-02-04 00:44:09 +01:00
}
2022-02-04 01:05:35 +01:00
convert ( obj : any , context : string ) : { result : LayerConfig ; errors : string [ ] } {
2022-02-04 00:44:09 +01:00
const errors = [ ]
const translations = Translation . ExtractAllTranslationsFrom (
obj
)
2022-04-06 03:06:50 +02:00
for ( const neededLanguage of this . _languages ) {
2022-02-04 00:44:09 +01:00
translations
. filter ( t = > t . tr . translations [ neededLanguage ] === undefined && t . tr . translations [ "*" ] === undefined )
. forEach ( missing = > {
2022-02-16 22:18:58 +01:00
errors . push ( context + "A theme should be translation-complete for " + neededLanguage + ", but it lacks a translation for " + missing . context + ".\n\tThe known translation is " + missing . tr . textFor ( 'en' ) )
2022-02-04 00:44:09 +01:00
} )
}
return {
result : obj ,
2022-02-04 01:05:35 +01:00
errors
2022-02-04 00:44:09 +01:00
} ;
}
}
2022-07-06 12:57:23 +02:00
export class DoesImageExist extends DesugaringStep < string > {
2022-07-06 11:14:19 +02:00
private readonly _knownImagePaths : Set < string > ;
2022-07-06 12:57:23 +02:00
private readonly doesPathExist : ( path : string ) = > boolean = undefined ;
constructor ( knownImagePaths : Set < string > , checkExistsSync : ( path : string ) = > boolean = undefined ) {
2022-07-06 11:14:19 +02:00
super ( "Checks if an image exists" , [ ] , "DoesImageExist" ) ;
this . _knownImagePaths = knownImagePaths ;
2022-07-06 12:57:23 +02:00
this . doesPathExist = checkExistsSync ;
2022-07-06 11:14:19 +02:00
}
convert ( image : string , context : string ) : { result : string ; errors? : string [ ] ; warnings? : string [ ] ; information? : string [ ] } {
const errors = [ ]
const warnings = [ ]
const information = [ ]
if ( image . indexOf ( "{" ) >= 0 ) {
information . push ( "Ignoring image with { in the path: " + image )
return { result : image }
}
if ( image === "assets/SocialImage.png" ) {
return { result : image }
}
if ( image . match ( /[a-z]*/ ) ) {
if ( Svg . All [ image + ".svg" ] !== undefined ) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
return { result : image } ;
}
}
2022-07-08 03:14:55 +02:00
if ( ! this . _knownImagePaths . has ( image ) ) {
2022-07-06 12:57:23 +02:00
if ( this . doesPathExist === undefined ) {
2022-07-06 11:14:19 +02:00
errors . push ( ` Image with path ${ image } not found or not attributed; it is used in ${ context } ` )
2022-07-06 12:57:23 +02:00
} else if ( ! this . doesPathExist ( image ) ) {
2022-07-06 11:14:19 +02:00
errors . push ( ` Image with path ${ image } does not exist; it is used in ${ context } . \ n Check for typo's and missing directories in the path. ` )
2022-07-06 12:57:23 +02:00
} else {
2022-07-06 11:14:19 +02:00
errors . push ( ` Image with path ${ image } is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info ` )
}
}
return {
result : image ,
errors , warnings , information
}
}
2022-07-06 12:57:23 +02:00
2022-07-06 11:14:19 +02:00
}
2022-02-04 00:44:09 +01:00
class ValidateTheme extends DesugaringStep < LayoutConfigJson > {
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* @private
* /
private readonly _path? : string ;
private readonly _isBuiltin : boolean ;
2022-02-18 23:10:27 +01:00
private _sharedTagRenderings : Map < string , any > ;
2022-07-06 12:57:23 +02:00
private readonly _validateImage : DesugaringStep < string > ;
constructor ( doesImageExist : DoesImageExist , path : string , isBuiltin : boolean , sharedTagRenderings : Map < string , any > ) {
2022-02-17 23:54:14 +01:00
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateTheme" ) ;
2022-07-06 12:57:23 +02:00
this . _validateImage = doesImageExist ;
2022-02-04 00:44:09 +01:00
this . _path = path ;
this . _isBuiltin = isBuiltin ;
2022-02-18 23:10:27 +01:00
this . _sharedTagRenderings = sharedTagRenderings ;
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
convert ( json : LayoutConfigJson , context : string ) : { result : LayoutConfigJson ; errors : string [ ] , warnings : string [ ] , information : string [ ] } {
2022-02-04 00:44:09 +01:00
const errors = [ ]
2022-02-09 22:37:21 +01:00
const warnings = [ ]
2022-02-10 23:16:14 +01:00
const information = [ ]
2022-02-17 23:54:14 +01:00
2022-04-18 02:39:30 +02:00
const theme = new LayoutConfig ( json , true )
2022-02-17 23:54:14 +01:00
2022-02-04 00:44:09 +01:00
{
// Legacy format checks
if ( this . _isBuiltin ) {
if ( json [ "units" ] !== undefined ) {
errors . push ( "The theme " + json . id + " has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) " )
}
if ( json [ "roamingRenderings" ] !== undefined ) {
errors . push ( "Theme " + json . id + " contains an old 'roamingRenderings'. Use an 'overrideAll' instead" )
}
}
}
2022-02-09 22:37:21 +01:00
{
2022-02-10 23:16:14 +01:00
// Check images: are they local, are the licenses there, is the theme icon square, ...
2022-02-18 23:10:27 +01:00
const images = new ExtractImages ( this . _isBuiltin , this . _sharedTagRenderings ) . convertStrict ( json , "validation" )
2022-02-09 22:37:21 +01:00
const remoteImages = images . filter ( img = > img . indexOf ( "http" ) == 0 )
for ( const remoteImage of remoteImages ) {
errors . push ( "Found a remote image: " + remoteImage + " in theme " + json . id + ", please download it." )
}
for ( const image of images ) {
2022-07-06 11:14:19 +02:00
this . _validateImage . convertJoin ( image , context === undefined ? "" : ` in a layer defined in the theme ${ context } ` , errors , warnings , information )
2022-02-09 22:37:21 +01:00
}
2022-02-04 00:44:09 +01:00
2022-02-10 23:16:14 +01:00
if ( json . icon . endsWith ( ".svg" ) ) {
try {
ScriptUtils . ReadSvgSync ( json . icon , svg = > {
const width : string = svg . $ . width ;
const height : string = svg . $ . height ;
if ( width !== height ) {
2022-02-17 23:54:14 +01:00
const e = ` the icon for theme ${ json . id } is not square. Please square the icon at ${ json . icon } ` +
2022-02-10 23:16:14 +01:00
` Width = ${ width } height = ${ height } ` ;
( json . hideFromOverview ? warnings : errors ) . push ( e )
}
2022-06-20 01:42:30 +02:00
2022-05-14 20:11:59 +02:00
const w = parseInt ( width ) ;
const h = parseInt ( height )
2022-06-20 01:42:30 +02:00
if ( w < 370 || h < 370 ) {
const e : string = [
2022-05-14 20:11:59 +02:00
` the icon for theme ${ json . id } is too small. Please rescale the icon at ${ json . icon } ` ,
` Even though an SVG is 'infinitely scaleable', the icon should be dimensioned bigger. One of the build steps of the theme does convert the image to a PNG (to serve as PWA-icon) and having a small dimension will cause blurry images. ` ,
` Width = ${ width } height = ${ height } ; we recommend a size of at least 500px * 500px and to use a square aspect ratio. ` ,
2022-06-20 01:42:30 +02:00
] . join ( "\n" ) ;
2022-05-14 20:11:59 +02:00
( json . hideFromOverview ? warnings : errors ) . push ( e )
}
2022-06-20 01:42:30 +02:00
2022-02-10 23:16:14 +01:00
} )
} catch ( e ) {
console . error ( "Could not read " + json . icon + " due to " + e )
}
}
2022-02-09 22:37:21 +01:00
}
2022-02-17 23:54:14 +01:00
2022-02-04 00:44:09 +01:00
try {
if ( theme . id !== theme . id . toLowerCase ( ) ) {
errors . push ( "Theme ids should be in lowercase, but it is " + theme . id )
}
const filename = this . _path . substring ( this . _path . lastIndexOf ( "/" ) + 1 , this . _path . length - 5 )
if ( theme . id !== filename ) {
errors . push ( "Theme ids should be the same as the name.json, but we got id: " + theme . id + " and filename " + filename + " (" + this . _path + ")" )
}
2022-07-06 12:57:23 +02:00
this . _validateImage . convertJoin ( theme . icon , context + ".icon" , errors , warnings , information ) ;
2022-02-04 00:44:09 +01:00
const dups = Utils . Dupiclates ( json . layers . map ( layer = > layer [ "id" ] ) )
if ( dups . length > 0 ) {
errors . push ( ` The theme ${ json . id } defines multiple layers with id ${ dups . join ( ", " ) } ` )
}
if ( json [ "mustHaveLanguage" ] !== undefined ) {
const checked = new ValidateLanguageCompleteness ( . . . json [ "mustHaveLanguage" ] )
2022-02-04 01:05:35 +01:00
. convert ( theme , theme . id )
2022-02-04 00:44:09 +01:00
errors . push ( . . . checked . errors )
}
2022-02-17 23:54:14 +01:00
if ( ! json . hideFromOverview && theme . id !== "personal" ) {
2022-06-23 12:31:47 +02:00
// The first key in the the title-field must be english, otherwise the title in the loading page will be the different language
const targetLanguage = theme . title . SupportedLanguages ( ) [ 0 ]
2022-07-06 12:57:23 +02:00
if ( targetLanguage !== "en" ) {
2022-06-23 12:31:47 +02:00
warnings . push ( ` TargetLanguage is not 'en' for public theme ${ theme . id } , it is ${ targetLanguage } . Move 'en' up in the title of the theme and set it as the first key ` )
}
2022-07-06 12:57:23 +02:00
2022-02-16 22:18:58 +01:00
// Official, public themes must have a full english translation
2022-02-16 02:23:50 +01:00
const checked = new ValidateLanguageCompleteness ( "en" )
. convert ( theme , theme . id )
2022-02-16 22:18:58 +01:00
errors . push ( . . . checked . errors )
2022-07-06 12:57:23 +02:00
2022-02-16 02:23:50 +01:00
}
2022-02-04 00:44:09 +01:00
} catch ( e ) {
errors . push ( e )
}
return {
result : json ,
2022-02-09 22:37:21 +01:00
errors ,
2022-02-10 23:16:14 +01:00
warnings ,
information
2022-02-04 00:44:09 +01:00
} ;
}
}
export class ValidateThemeAndLayers extends Fuse < LayoutConfigJson > {
2022-07-06 12:57:23 +02:00
constructor ( doesImageExist : DoesImageExist , path : string , isBuiltin : boolean , sharedTagRenderings : Map < string , any > ) {
2022-02-04 00:44:09 +01:00
super ( "Validates a theme and the contained layers" ,
2022-07-06 12:57:23 +02:00
new ValidateTheme ( doesImageExist , path , isBuiltin , sharedTagRenderings ) ,
new On ( "layers" , new Each ( new ValidateLayer ( undefined , false , doesImageExist ) ) )
2022-02-04 00:44:09 +01:00
) ;
}
}
2022-02-10 23:16:14 +01:00
class OverrideShadowingCheck extends DesugaringStep < LayoutConfigJson > {
2022-02-04 00:44:09 +01:00
constructor ( ) {
2022-02-17 23:54:14 +01:00
super ( "Checks that an 'overrideAll' does not override a single override" , [ ] , "OverrideShadowingCheck" ) ;
2022-02-04 00:44:09 +01:00
}
2022-02-04 01:05:35 +01:00
convert ( json : LayoutConfigJson , context : string ) : { result : LayoutConfigJson ; errors? : string [ ] ; warnings? : string [ ] } {
2022-02-10 23:16:14 +01:00
2022-02-04 00:44:09 +01:00
const overrideAll = json . overrideAll ;
2022-02-10 23:16:14 +01:00
if ( overrideAll === undefined ) {
2022-02-04 00:44:09 +01:00
return { result : json }
}
2022-02-10 23:16:14 +01:00
2022-02-04 00:44:09 +01:00
const errors = [ ]
2022-02-10 23:16:14 +01:00
const withOverride = json . layers . filter ( l = > l [ "override" ] !== undefined )
2022-02-04 00:44:09 +01:00
for ( const layer of withOverride ) {
for ( const key in overrideAll ) {
2022-07-19 09:46:06 +02:00
if ( key . endsWith ( "+" ) || key . startsWith ( "+" ) ) {
// This key will _add_ to the list, not overwrite it - so no warning is needed
continue
}
2022-02-10 23:16:14 +01:00
if ( layer [ "override" ] [ key ] !== undefined || layer [ "override" ] [ "=" + key ] !== undefined ) {
const w = "The override of layer " + JSON . stringify ( layer [ "builtin" ] ) + " has a shadowed property: " + key + " is overriden by overrideAll of the theme" ;
errors . push ( w )
}
2022-02-04 00:44:09 +01:00
}
}
2022-02-10 23:16:14 +01:00
return { result : json , errors }
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2022-02-04 00:44:09 +01:00
}
2022-06-20 01:42:30 +02:00
class MiscThemeChecks extends DesugaringStep < LayoutConfigJson > {
2022-02-19 17:39:16 +01:00
constructor ( ) {
2022-06-20 01:42:30 +02:00
super ( "Miscelleanous checks on the theme" , [ ] , "MiscThemesChecks" ) ;
2022-02-19 17:39:16 +01:00
}
2022-06-20 01:42:30 +02:00
2022-02-19 17:39:16 +01:00
convert ( json : LayoutConfigJson , context : string ) : { result : LayoutConfigJson ; errors? : string [ ] ; warnings? : string [ ] ; information? : string [ ] } {
const warnings = [ ]
2022-04-22 03:17:40 +02:00
const errors = [ ]
2022-06-20 01:42:30 +02:00
if ( json . id !== "personal" && ( json . layers === undefined || json . layers . length === 0 ) ) {
errors . push ( "The theme " + json . id + " has no 'layers' defined (" + context + ")" )
2022-04-22 03:17:40 +02:00
}
2022-06-20 01:42:30 +02:00
if ( json . socialImage === "" ) {
warnings . push ( "Social image for theme " + json . id + " is the emtpy string" )
2022-02-19 17:39:16 +01:00
}
return {
2022-06-20 01:42:30 +02:00
result : json ,
2022-04-22 03:17:40 +02:00
warnings ,
errors
2022-02-19 17:39:16 +01:00
} ;
}
}
2022-02-10 23:16:14 +01:00
export class PrevalidateTheme extends Fuse < LayoutConfigJson > {
2022-02-04 00:44:09 +01:00
constructor ( ) {
super ( "Various consistency checks on the raw JSON" ,
2022-04-22 03:17:40 +02:00
new MiscThemeChecks ( ) ,
new OverrideShadowingCheck ( )
2022-02-10 23:16:14 +01:00
) ;
2022-02-04 00:44:09 +01:00
}
}
2022-02-28 17:17:38 +01:00
export class DetectShadowedMappings extends DesugaringStep < QuestionableTagRenderingConfigJson > {
2022-03-17 23:04:00 +01:00
private readonly _calculatedTagNames : string [ ] ;
2022-06-20 01:42:30 +02:00
2022-03-17 23:04:00 +01:00
constructor ( layerConfig? : LayerConfigJson ) {
2022-02-17 23:54:14 +01:00
super ( "Checks that the mappings don't shadow each other" , [ ] , "DetectShadowedMappings" ) ;
2022-03-17 23:04:00 +01:00
this . _calculatedTagNames = DetectShadowedMappings . extractCalculatedTagNames ( layerConfig ) ;
}
/ * *
2022-06-20 01:42:30 +02:00
*
2022-03-17 23:04:00 +01:00
* DetectShadowedMappings . extractCalculatedTagNames ( { calculatedTags : [ "_abc:=js()" ] } ) // => ["_abc"]
* DetectShadowedMappings . extractCalculatedTagNames ( { calculatedTags : [ "_abc=js()" ] } ) // => ["_abc"]
* /
2022-06-20 01:42:30 +02:00
private static extractCalculatedTagNames ( layerConfig? : LayerConfigJson | { calculatedTags : string [ ] } ) {
2022-03-17 23:04:00 +01:00
return layerConfig ? . calculatedTags ? . map ( ct = > {
2022-06-20 01:42:30 +02:00
if ( ct . indexOf ( ':=' ) >= 0 ) {
2022-03-17 23:04:00 +01:00
return ct . split ( ':=' ) [ 0 ]
}
return ct . split ( "=" ) [ 0 ]
} ) ? ? [ ]
2022-06-20 01:42:30 +02:00
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2022-03-23 19:48:06 +01:00
/ * *
2022-06-20 01:42:30 +02:00
*
2022-03-23 19:48:06 +01:00
* // should detect a simple shadowed mapping
* const tr = { mappings : [
* {
* if : { or : [ "key=value" , "x=y" ] } ,
* then : "Case A"
* } ,
* {
* if : "key=value" ,
* then : "Shadowed"
* }
* ]
* }
* const r = new DetectShadowedMappings ( ) . convert ( tr , "test" ) ;
* r . errors . length // => 1
* r . errors [ 0 ] . indexOf ( "The mapping key=value is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
*
* const tr = { mappings : [
* {
* if : { or : [ "key=value" , "x=y" ] } ,
* then : "Case A"
* } ,
* {
* if : { and : [ "key=value" , "x=y" ] } ,
* then : "Shadowed"
* }
* ]
* }
* const r = new DetectShadowedMappings ( ) . convert ( tr , "test" ) ;
* r . errors . length // => 1
* r . errors [ 0 ] . indexOf ( "The mapping key=value&x=y is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
* /
2022-02-28 17:17:38 +01:00
convert ( json : QuestionableTagRenderingConfigJson , context : string ) : { result : QuestionableTagRenderingConfigJson ; errors? : string [ ] ; warnings? : string [ ] } {
2022-02-04 00:44:09 +01:00
const errors = [ ]
2022-02-17 23:54:14 +01:00
const warnings = [ ]
2022-02-10 23:16:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
2022-02-04 00:44:09 +01:00
return { result : json }
}
2022-03-17 23:04:00 +01:00
const defaultProperties = { }
for ( const calculatedTagName of this . _calculatedTagNames ) {
2022-06-20 01:42:30 +02:00
defaultProperties [ calculatedTagName ] = "some_calculated_tag_value_for_" + calculatedTagName
2022-03-17 23:04:00 +01:00
}
2022-06-20 01:42:30 +02:00
const parsedConditions = json . mappings . map ( ( m , i ) = > {
2022-06-09 16:50:53 +02:00
const ctx = ` ${ context } .mappings[ ${ i } ] `
const ifTags = TagUtils . Tag ( m . if , ctx ) ;
2022-06-20 01:42:30 +02:00
if ( m . hideInAnswer !== undefined && m . hideInAnswer !== false && m . hideInAnswer !== true ) {
let conditionTags = TagUtils . Tag ( m . hideInAnswer )
2022-02-20 01:39:12 +01:00
// Merge the condition too!
return new And ( [ conditionTags , ifTags ] )
}
return ifTags
} )
2022-02-10 23:16:14 +01:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
2022-06-20 01:42:30 +02:00
if ( ! parsedConditions [ i ] . isUsableAsAnswer ( ) ) {
2022-02-20 01:39:12 +01:00
// There is no straightforward way to convert this mapping.if into a properties-object, so we simply skip this one
// Yes, it might be shadowed, but running this check is to difficult right now
2022-02-04 00:44:09 +01:00
continue
}
2022-03-17 23:04:00 +01:00
const keyValues = parsedConditions [ i ] . asChange ( defaultProperties ) ;
2022-02-20 02:02:09 +01:00
const properties = { }
2022-02-04 00:44:09 +01:00
keyValues . forEach ( ( { k , v } ) = > {
properties [ k ] = v
} )
2022-02-10 23:16:14 +01:00
for ( let j = 0 ; j < i ; j ++ ) {
2022-02-04 00:44:09 +01:00
const doesMatch = parsedConditions [ j ] . matchesProperties ( properties )
2022-07-06 12:57:23 +02:00
if ( doesMatch && json . mappings [ j ] . hideInAnswer === true && json . mappings [ i ] . hideInAnswer !== true ) {
2022-06-28 01:04:45 +02:00
warnings . push ( ` At ${ context } : Mapping ${ i } is shadowed by mapping ${ j } . However, mapping ${ j } has 'hideInAnswer' set, which will result in a different rendering in question-mode. ` )
} else if ( doesMatch ) {
2022-02-04 00:44:09 +01:00
// The current mapping is shadowed!
2022-02-18 00:12:32 +01:00
errors . push ( ` At ${ context } : Mapping ${ i } is shadowed by mapping ${ j } and will thus never be shown:
2022-02-20 01:39:12 +01:00
The mapping $ { parsedConditions [ i ] . asHumanString ( false , false , { } ) } is fully matched by a previous mapping ( namely $ { j } ) , which matches :
2022-02-04 00:44:09 +01:00
$ { parsedConditions [ j ] . asHumanString ( false , false , { } ) } .
2022-03-17 22:03:41 +01:00
To fix this problem , you can try to :
- Move the shadowed mapping up
2022-06-28 01:04:45 +02:00
- Do you want to use a different text in 'question mode' ? Add 'hideInAnswer=true' to the first mapping
2022-03-17 22:03:41 +01:00
- Use "addExtraTags" : [ "key=value" , . . . ] in order to avoid a different rendering
( e . g . [ { "if" : "fee=no" , "then" : "Free to use" , "hideInAnswer" : true } ,
{ "if" : { "and" : [ "fee=no" , "charge=" ] } , "then" : "Free to use" } ]
can be replaced by
[ { "if" : "fee=no" , "then" : "Free to use" , "addExtraTags" : [ "charge=" ] } ]
2022-02-04 00:44:09 +01:00
` )
}
}
}
2022-02-10 23:16:14 +01:00
2022-02-04 00:44:09 +01:00
return {
errors ,
2022-02-17 23:54:14 +01:00
warnings ,
2022-02-04 00:44:09 +01:00
result : json
} ;
}
}
2022-02-17 23:54:14 +01:00
export class DetectMappingsWithImages extends DesugaringStep < TagRenderingConfigJson > {
2022-07-06 12:57:23 +02:00
private readonly _doesImageExist : DoesImageExist ;
constructor ( doesImageExist : DoesImageExist ) {
2022-02-17 23:54:14 +01:00
super ( "Checks that 'then'clauses in mappings don't have images, but use 'icon' instead" , [ ] , "DetectMappingsWithImages" ) ;
2022-07-06 12:57:23 +02:00
this . _doesImageExist = doesImageExist ;
2022-02-17 23:54:14 +01:00
}
2022-03-23 19:48:06 +01:00
/ * *
2022-07-06 14:00:39 +02:00
* const r = new DetectMappingsWithImages ( new DoesImageExist ( new Set < string > ( ) ) ) . convert ( {
2022-03-23 19:48:06 +01:00
* "mappings" : [
* {
* "if" : "bicycle_parking=stands" ,
* "then" : {
* "en" : "Staple racks <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "nl" : "Nietjes <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "fr" : "Arceaux <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "gl" : "De roda (Stands) <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "de" : "Fahrradbügel <img style='width: 25%'' src='./assets/layers/bike_parking/staple.svg'>" ,
* "hu" : "Korlát <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "it" : "Archetti <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>" ,
* "zh_Hant" : "單車架 <img style='width: 25%' src='./assets/layers/bike_parking/staple.svg'>"
* }
* } ]
* } , "test" ) ;
* r . errors . length > 0 // => true
* r . errors . some ( msg = > msg . indexOf ( "./assets/layers/bike_parking/staple.svg" ) >= 0 ) // => true
* /
2022-02-19 17:57:34 +01:00
convert ( json : TagRenderingConfigJson , context : string ) : { result : TagRenderingConfigJson ; errors? : string [ ] ; warnings? : string [ ] , information? : string [ ] } {
2022-07-06 12:57:23 +02:00
const errors : string [ ] = [ ]
const warnings : string [ ] = [ ]
const information : string [ ] = [ ]
2022-02-17 23:54:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
return { result : json }
}
2022-02-19 17:57:34 +01:00
const ignoreToken = "ignore-image-in-then"
2022-02-17 23:54:14 +01:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
const mapping = json . mappings [ i ]
2022-06-20 01:42:30 +02:00
const ignore = mapping [ "#" ] ? . indexOf ( ignoreToken ) >= 0
2022-06-09 16:50:53 +02:00
const images = Utils . Dedup ( Translations . T ( mapping . then ) ? . ExtractImages ( ) ? ? [ ] )
2022-02-19 17:57:34 +01:00
const ctx = ` ${ context } .mappings[ ${ i } ] `
2022-02-17 23:54:14 +01:00
if ( images . length > 0 ) {
2022-06-20 01:42:30 +02:00
if ( ! ignore ) {
2022-02-20 00:51:11 +01:00
errors . push ( ` ${ ctx } : A mapping has an image in the 'then'-clause. Remove the image there and use \` "icon": <your-image> \` instead. The images found are ${ images . join ( ", " ) } . (This check can be turned of by adding "#": " ${ ignoreToken } " in the mapping, but this is discouraged ` )
2022-06-20 01:42:30 +02:00
} else {
2022-02-20 00:51:11 +01:00
information . push ( ` ${ ctx } : Ignored image ${ images . join ( ", " ) } in 'then'-clause of a mapping as this check has been disabled ` )
2022-06-20 01:42:30 +02:00
for ( const image of images ) {
2022-07-06 12:57:23 +02:00
this . _doesImageExist . convertJoin ( image , ctx , errors , warnings , information ) ;
2022-06-20 01:42:30 +02:00
}
2022-07-06 12:57:23 +02:00
2022-02-19 17:57:34 +01:00
}
2022-06-20 01:42:30 +02:00
} else if ( ignore ) {
2022-02-19 17:57:34 +01:00
warnings . push ( ` ${ ctx } : unused ' ${ ignoreToken } ' - please remove this ` )
2022-02-17 23:54:14 +01:00
}
}
2022-06-20 01:42:30 +02:00
return {
2022-02-20 01:39:12 +01:00
errors ,
warnings ,
information ,
2022-02-17 23:54:14 +01:00
result : json
} ;
}
}
export class ValidateTagRenderings extends Fuse < TagRenderingConfigJson > {
2022-07-06 12:57:23 +02:00
constructor ( layerConfig? : LayerConfigJson , doesImageExist? : DoesImageExist ) {
2022-02-17 23:54:14 +01:00
super ( "Various validation on tagRenderingConfigs" ,
2022-06-20 01:42:30 +02:00
new DetectShadowedMappings ( layerConfig ) ,
2022-07-06 12:57:23 +02:00
new DetectMappingsWithImages ( doesImageExist )
2022-02-17 23:54:14 +01:00
) ;
}
}
2022-02-04 00:44:09 +01:00
export class ValidateLayer extends DesugaringStep < LayerConfigJson > {
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* @private
* /
private readonly _path? : string ;
private readonly _isBuiltin : boolean ;
2022-07-06 12:57:23 +02:00
private readonly _doesImageExist : DoesImageExist ;
2022-02-04 00:44:09 +01:00
2022-07-06 12:57:23 +02:00
constructor ( path : string , isBuiltin : boolean , doesImageExist : DoesImageExist ) {
2022-02-17 23:54:14 +01:00
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateLayer" ) ;
2022-02-04 00:44:09 +01:00
this . _path = path ;
this . _isBuiltin = isBuiltin ;
2022-07-06 12:57:23 +02:00
this . _doesImageExist = doesImageExist
2022-02-04 00:44:09 +01:00
}
2022-02-17 23:54:14 +01:00
convert ( json : LayerConfigJson , context : string ) : { result : LayerConfigJson ; errors : string [ ] ; warnings? : string [ ] , information? : string [ ] } {
2022-02-04 00:44:09 +01:00
const errors = [ ]
const warnings = [ ]
2022-02-17 23:54:14 +01:00
const information = [ ]
2022-07-11 09:14:26 +02:00
context = "While validating a layer: " + context
2022-02-04 00:44:09 +01:00
if ( typeof json === "string" ) {
errors . push ( context + ": This layer hasn't been expanded: " + json )
return {
result : null ,
errors
}
}
2022-07-19 13:30:26 +02:00
2022-07-20 22:57:39 +02:00
if ( json . tagRenderings !== undefined && json . tagRenderings . length > 0 ) {
if ( json . title === undefined ) {
errors . push ( context + ": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error." )
}
if ( json . title === null ) {
information . push ( context + ": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set." )
}
2022-07-19 13:30:26 +02:00
}
2022-02-04 00:44:09 +01:00
if ( json [ "builtin" ] !== undefined ) {
errors . push ( context + ": This layer hasn't been expanded: " + json )
return {
result : null ,
errors
}
}
2022-08-24 01:29:11 +02:00
if ( json . minzoom > Constants . userJourney . minZoomLevelToAddNewPoints ) {
( json . presets ? . length > 0 ? errors : warnings ) . push ( ` At ${ context } : minzoom is ${ json . minzoom } , this should be at most ${ Constants . userJourney . minZoomLevelToAddNewPoints } as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates ` )
}
2022-02-04 00:44:09 +01:00
2022-04-08 22:12:43 +02:00
{
// duplicate ids in tagrenderings check
2022-06-20 01:42:30 +02:00
const duplicates = Utils . Dedup ( Utils . Dupiclates ( Utils . NoNull ( ( json . tagRenderings ? ? [ ] ) . map ( tr = > tr [ "id" ] ) ) ) )
. filter ( dupl = > dupl !== "questions" )
if ( duplicates . length > 0 ) {
errors . push ( "At " + context + ": some tagrenderings have a duplicate id: " + duplicates . join ( ", " ) )
2022-04-08 22:12:43 +02:00
}
}
2022-06-20 01:42:30 +02:00
2022-02-04 00:44:09 +01:00
try {
{
// Some checks for legacy elements
if ( json [ "overpassTags" ] !== undefined ) {
errors . push ( "Layer " + json . id + "still uses the old 'overpassTags'-format. Please use \"source\": {\"osmTags\": <tags>}' instead of \"overpassTags\": <tags> (note: this isn't your fault, the custom theme generator still spits out the old format)" )
}
const forbiddenTopLevel = [ "icon" , "wayHandling" , "roamingRenderings" , "roamingRendering" , "label" , "width" , "color" , "colour" , "iconOverlays" ]
for ( const forbiddenKey of forbiddenTopLevel ) {
if ( json [ forbiddenKey ] !== undefined )
errors . push ( context + ": layer " + json . id + " still has a forbidden key " + forbiddenKey )
}
if ( json [ "hideUnderlayingFeaturesMinPercentage" ] !== undefined ) {
errors . push ( context + ": layer " + json . id + " contains an old 'hideUnderlayingFeaturesMinPercentage'" )
}
2022-07-18 02:00:32 +02:00
if ( json . isShown !== undefined && ( json . isShown [ "render" ] !== undefined || json . isShown [ "mappings" ] !== undefined ) ) {
warnings . push ( context + " has a tagRendering as `isShown`" )
}
2022-02-04 00:44:09 +01:00
}
{
2022-04-08 22:12:43 +02:00
// Check location of layer file
2022-02-04 00:44:09 +01:00
const expected : string = ` assets/layers/ ${ json . id } / ${ json . id } .json `
if ( this . _path != undefined && this . _path . indexOf ( expected ) < 0 ) {
errors . push ( "Layer is in an incorrect place. The path is " + this . _path + ", but expected " + expected )
}
}
if ( this . _isBuiltin ) {
// Check for correct IDs
if ( json . tagRenderings ? . some ( tr = > tr [ "id" ] === "" ) ) {
const emptyIndexes : number [ ] = [ ]
for ( let i = 0 ; i < json . tagRenderings . length ; i ++ ) {
const tagRendering = json . tagRenderings [ i ] ;
if ( tagRendering [ "id" ] === "" ) {
emptyIndexes . push ( i )
}
}
errors . push ( ` Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${ context } .tagRenderings.[ ${ emptyIndexes . join ( "," ) } ]) ` )
}
const duplicateIds = Utils . Dupiclates ( ( json . tagRenderings ? ? [ ] ) ? . map ( f = > f [ "id" ] ) . filter ( id = > id !== "questions" ) )
if ( duplicateIds . length > 0 && ! Utils . runningFromConsole ) {
errors . push ( ` Some tagRenderings have a duplicate id: ${ duplicateIds } (at ${ context } .tagRenderings) ` )
}
if ( json . description === undefined ) {
if ( Constants . priviliged_layers . indexOf ( json . id ) >= 0 ) {
errors . push (
context + ": A priviliged layer must have a description"
)
} else {
warnings . push (
context + ": A builtin layer should have a description"
)
}
}
}
2022-02-10 23:16:14 +01:00
if ( json . tagRenderings !== undefined ) {
2022-07-06 12:57:23 +02:00
const r = new On ( "tagRenderings" , new Each ( new ValidateTagRenderings ( json , this . _doesImageExist ) ) ) . convert ( json , context )
2022-06-20 01:42:30 +02:00
warnings . push ( . . . ( r . warnings ? ? [ ] ) )
errors . push ( . . . ( r . errors ? ? [ ] ) )
information . push ( . . . ( r . information ? ? [ ] ) )
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2022-08-18 14:39:40 +02:00
{
const hasCondition = json . mapRendering ? . filter ( mr = > mr [ "icon" ] !== undefined && mr [ "icon" ] [ "condition" ] !== undefined )
if ( hasCondition ? . length > 0 ) {
errors . push ( "At " + context + ":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" + JSON . stringify ( hasCondition , null , " " ) )
}
}
2022-02-17 23:54:14 +01:00
if ( json . presets !== undefined ) {
2022-02-14 15:40:38 +01:00
// Check that a preset will be picked up by the layer itself
2022-02-17 23:54:14 +01:00
const baseTags = TagUtils . Tag ( json . source . osmTags )
for ( let i = 0 ; i < json . presets . length ; i ++ ) {
2022-02-14 15:40:38 +01:00
const preset = json . presets [ i ] ;
2022-02-17 23:54:14 +01:00
const tags : { k : string , v : string } [ ] = new And ( preset . tags . map ( t = > TagUtils . Tag ( t ) ) ) . asChange ( { id : "node/-1" } )
2022-02-14 15:40:38 +01:00
const properties = { }
for ( const tag of tags ) {
properties [ tag . k ] = tag . v
}
const doMatch = baseTags . matchesProperties ( properties )
2022-02-17 23:54:14 +01:00
if ( ! doMatch ) {
errors . push ( context + ".presets[" + i + "]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " + JSON . stringify ( properties ) + "\n The required tags are: " + baseTags . asHumanString ( false , false , { } ) )
2022-02-14 15:40:38 +01:00
}
}
}
2022-02-04 00:44:09 +01:00
} catch ( e ) {
errors . push ( e )
}
2022-02-10 23:16:14 +01:00
2022-02-04 00:44:09 +01:00
return {
result : json ,
errors ,
2022-02-17 23:54:14 +01:00
warnings ,
information
2022-02-04 00:44:09 +01:00
} ;
}
}