2023-10-12 16:55:26 +02:00
import {
Conversion ,
ConversionContext ,
DesugaringStep ,
Each ,
Fuse ,
On ,
Pipe ,
Pure ,
} from "./Conversion"
2023-09-21 15:29:34 +02: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"
import { ExtractImages } from "./FixImages"
import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations"
import Svg from "../../../Svg"
import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import Validators from "../../../UI/InputElement/Validators"
import TagRenderingConfig from "../TagRenderingConfig"
import { parse as parse_html } from "node-html-parser"
2023-09-24 00:25:10 +02:00
import PresetConfig from "../PresetConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
2022-02-04 00:44:09 +01:00
class ValidateLanguageCompleteness extends DesugaringStep < any > {
2023-09-21 15:29:34 +02:00
private readonly _languages : string [ ]
2022-02-04 00:44:09 +01:00
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"
2023-09-21 15:29:34 +02:00
)
this . _languages = languages ? ? [ "en" ]
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( obj : any , context : ConversionContext ) : LayerConfig {
2023-09-21 15:29:34 +02:00
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
2022-09-08 21:40:48 +02:00
)
2022-02-04 00:44:09 +01:00
. forEach ( ( missing ) = > {
2023-10-11 04:16:52 +02:00
context
. enter ( missing . context . split ( "." ) )
. err (
` The theme ${ obj . id } should be translation-complete for ` +
neededLanguage +
", but it lacks a translation for " +
missing . context +
".\n\tThe known translation is " +
missing . tr . textFor ( "en" )
)
2023-09-21 15:29:34 +02:00
} )
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
return obj
2022-02-04 00:44:09 +01:00
}
}
2022-07-06 12:57:23 +02:00
export class DoesImageExist extends DesugaringStep < string > {
2023-09-21 15:29:34 +02:00
private readonly _knownImagePaths : Set < string >
private readonly _ignore? : Set < string >
private readonly doesPathExist : ( path : string ) = > boolean = undefined
2022-09-08 21:40:48 +02:00
2022-07-06 12:57:23 +02:00
constructor (
knownImagePaths : Set < string > ,
2023-02-03 03:57:30 +01:00
checkExistsSync : ( path : string ) = > boolean = undefined ,
ignore? : Set < string >
2022-07-06 12:57:23 +02:00
) {
2023-09-21 15:29:34 +02:00
super ( "Checks if an image exists" , [ ] , "DoesImageExist" )
this . _ignore = ignore
this . _knownImagePaths = knownImagePaths
this . doesPathExist = checkExistsSync
2022-07-06 11:14:19 +02:00
}
2023-10-11 04:16:52 +02:00
convert ( image : string , context : ConversionContext ) : string {
2023-02-03 03:57:30 +01:00
if ( this . _ignore ? . has ( image ) ) {
2023-10-11 04:16:52 +02:00
return image
2023-02-03 03:57:30 +01:00
}
2022-07-06 11:14:19 +02:00
if ( image . indexOf ( "{" ) >= 0 ) {
2023-10-30 16:32:43 +01:00
context . debug ( "Ignoring image with { in the path: " + image )
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
if ( image === "assets/SocialImage.png" ) {
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
if ( image . match ( /[a-z]*/ ) ) {
if ( Svg . All [ image + ".svg" ] !== undefined ) {
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
}
2022-09-08 21:40:48 +02:00
2023-04-02 02:59:20 +02:00
if ( image . startsWith ( "<" ) && image . endsWith ( ">" ) ) {
// This is probably HTML, you're on your own here
2023-10-11 04:16:52 +02:00
return image
2023-04-02 02:59:20 +02:00
}
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 ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-07-06 11:14:19 +02:00
` Image with path ${ image } not found or not attributed; it is used in ${ context } `
2023-09-21 15:29:34 +02:00
)
2022-07-06 12:57:23 +02:00
} else if ( ! this . doesPathExist ( image ) ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-07-06 11:14:19 +02:00
` Image with path ${ image } does not exist; it is used in ${ context } . \ n Check for typo's and missing directories in the path. `
2023-09-21 15:29:34 +02:00
)
2022-07-06 12:57:23 +02:00
} else {
2023-10-11 04:16:52 +02:00
context . err (
2022-07-06 11:14:19 +02:00
` 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 `
2023-09-21 15:29:34 +02:00
)
2022-07-06 11:14:19 +02:00
}
}
2023-10-11 04:16:52 +02:00
return image
2022-07-06 11:14:19 +02:00
}
}
2023-10-30 13:45:44 +01:00
export class ValidateTheme extends DesugaringStep < LayoutConfigJson > {
2022-02-04 00:44:09 +01:00
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* @private
* /
2023-09-21 15:29:34 +02:00
private readonly _path? : string
private readonly _isBuiltin : boolean
2023-02-03 03:57:30 +01:00
//private readonly _sharedTagRenderings: Map<string, any>
2023-09-21 15:29:34 +02:00
private readonly _validateImage : DesugaringStep < string >
private readonly _extractImages : ExtractImages = undefined
2022-09-08 21:40:48 +02:00
2022-07-06 12:57:23 +02:00
constructor (
doesImageExist : DoesImageExist ,
path : string ,
isBuiltin : boolean ,
2023-02-03 03:57:30 +01:00
sharedTagRenderings? : Set < string >
2022-07-06 12:57:23 +02:00
) {
2023-09-21 15:29:34 +02:00
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateTheme" )
this . _validateImage = doesImageExist
this . _path = path
this . _isBuiltin = isBuiltin
2023-02-03 03:57:30 +01:00
if ( sharedTagRenderings ) {
2023-09-21 15:29:34 +02:00
this . _extractImages = new ExtractImages ( this . _isBuiltin , sharedTagRenderings )
2023-02-03 03:57:30 +01:00
}
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2023-09-21 15:29:34 +02:00
const theme = new LayoutConfig ( json , this . _isBuiltin )
2022-02-04 00:44:09 +01:00
{
// Legacy format checks
if ( this . _isBuiltin ) {
if ( json [ "units" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"The theme " +
2023-09-21 15:29:34 +02:00
json . id +
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
)
2022-02-04 00:44:09 +01:00
}
if ( json [ "roamingRenderings" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"Theme " +
2023-09-21 15:29:34 +02:00
json . id +
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
)
2022-02-04 00:44:09 +01:00
}
}
}
2023-10-30 13:45:44 +01:00
if ( ! json . title ) {
context . enter ( "title" ) . err ( ` The theme ${ json . id } does not have a title defined. ` )
}
2023-02-03 03:57:30 +01:00
if ( this . _isBuiltin && this . _extractImages !== undefined ) {
2022-02-10 23:16:14 +01:00
// Check images: are they local, are the licenses there, is the theme icon square, ...
2023-10-11 04:16:52 +02:00
const images = this . _extractImages . convert ( json , context . inOperation ( "ValidateTheme" ) )
2023-09-21 15:29:34 +02:00
const remoteImages = images . filter ( ( img ) = > img . path . indexOf ( "http" ) == 0 )
2022-02-09 22:37:21 +01:00
for ( const remoteImage of remoteImages ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-09 22:37:21 +01:00
"Found a remote image: " +
2023-10-30 14:32:31 +01:00
remoteImage . path +
2023-09-21 15:29:34 +02:00
" in theme " +
json . id +
", please download it."
)
2022-02-09 22:37:21 +01:00
}
for ( const image of images ) {
2023-10-11 04:16:52 +02:00
this . _validateImage . convert ( image . path , context . enters ( image . context ) )
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 {
2022-09-24 03:33:09 +02:00
if ( this . _isBuiltin ) {
if ( theme . id !== theme . id . toLowerCase ( ) ) {
2023-10-11 04:16:52 +02:00
context . err ( "Theme ids should be in lowercase, but it is " + theme . id )
2022-09-24 03:33:09 +02:00
}
2022-02-04 00:44:09 +01:00
2022-09-24 03:33:09 +02:00
const filename = this . _path . substring (
this . _path . lastIndexOf ( "/" ) + 1 ,
this . _path . length - 5
2023-09-21 15:29:34 +02:00
)
2022-09-24 03:33:09 +02:00
if ( theme . id !== filename ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-09-24 03:33:09 +02:00
"Theme ids should be the same as the name.json, but we got id: " +
2023-09-21 15:29:34 +02:00
theme . id +
" and filename " +
filename +
" (" +
this . _path +
")"
)
2022-09-24 03:33:09 +02:00
}
2023-10-11 04:16:52 +02:00
this . _validateImage . convert ( theme . icon , context . enter ( "icon" ) )
2022-02-04 00:44:09 +01:00
}
2023-09-24 00:25:10 +02:00
const dups = Utils . Duplicates ( json . layers . map ( ( layer ) = > layer [ "id" ] ) )
2022-02-04 00:44:09 +01:00
if ( dups . length > 0 ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
` The theme ${ json . id } defines multiple layers with id ${ dups . join ( ", " ) } `
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
if ( json [ "mustHaveLanguage" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
new ValidateLanguageCompleteness ( . . . json [ "mustHaveLanguage" ] ) . convert (
theme ,
context
)
2022-02-04 00:44:09 +01:00
}
2022-10-04 13:50:24 +02:00
if ( ! json . hideFromOverview && theme . id !== "personal" && this . _isBuiltin ) {
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
2023-09-21 15:29:34 +02:00
const targetLanguage = theme . title . SupportedLanguages ( ) [ 0 ]
2022-07-06 12:57:23 +02:00
if ( targetLanguage !== "en" ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-06-23 12:31:47 +02:00
` 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 `
2023-09-21 15:29:34 +02:00
)
2022-06-23 12:31:47 +02:00
}
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
2023-10-11 04:16:52 +02:00
new ValidateLanguageCompleteness ( "en" ) . convert ( theme , context )
2022-02-16 02:23:50 +01:00
}
2022-02-04 00:44:09 +01:00
} catch ( e ) {
2023-10-11 04:16:52 +02:00
context . err ( e )
2022-02-04 00:44:09 +01:00
}
2023-09-24 00:25:10 +02:00
if ( theme . id !== "personal" ) {
2023-10-11 04:16:52 +02:00
new DetectDuplicatePresets ( ) . convert ( theme , context )
2023-09-24 00:25:10 +02:00
}
2023-10-30 13:45:44 +01:00
if ( ! theme . title ) {
context . enter ( "title" ) . err ( "A theme must have a title" )
}
if ( ! theme . description ) {
context . enter ( "description" ) . err ( "A theme must have a description" )
}
if ( theme . overpassUrl && typeof theme . overpassUrl === "string" ) {
context
. enter ( "overpassUrl" )
. err ( "The overpassURL is a string, use a list of strings instead. Wrap it with [ ]" )
}
2023-10-11 04:16:52 +02:00
return json
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 ,
2023-10-31 11:49:14 +01:00
sharedTagRenderings? : Set < string >
2022-07-06 12:57:23 +02:00
) {
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 ) ,
2023-10-12 16:55:26 +02:00
new On (
"layers" ,
new Each (
new Pipe (
2023-10-30 16:32:43 +01:00
new ValidateLayer ( undefined , isBuiltin , doesImageExist , false , true ) ,
2023-10-12 16:55:26 +02:00
new Pure ( ( x ) = > x . raw )
2023-10-31 11:49:14 +01:00
)
2023-10-12 16:55:26 +02:00
)
)
2023-09-21 15:29:34 +02:00
)
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"
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2023-09-21 15:29:34 +02:00
const overrideAll = json . overrideAll
2022-02-10 23:16:14 +01:00
if ( overrideAll === undefined ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-09-21 15:29:34 +02: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
2023-09-21 15:29:34 +02:00
continue
2022-07-19 09:46:06 +02:00
}
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 +
2023-09-21 15:29:34 +02:00
" is overriden by overrideAll of the theme"
2023-10-11 04:16:52 +02:00
context . err ( w )
2022-02-10 23:16:14 +01:00
}
2022-02-04 00:44:09 +01:00
}
}
2022-02-10 23:16:14 +01:00
2023-10-11 04:16:52 +02:00
return json
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 ( ) {
2023-09-21 15:29:34 +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
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfigJson , context : ConversionContext ) : LayoutConfigJson {
2022-06-20 01:42:30 +02:00
if ( json . id !== "personal" && ( json . layers === undefined || json . layers . length === 0 ) ) {
2023-10-11 04:16:52 +02:00
context . err ( "The theme " + json . id + " has no 'layers' defined" )
2022-04-22 03:17:40 +02:00
}
2022-06-20 01:42:30 +02:00
if ( json . socialImage === "" ) {
2023-10-11 04:16:52 +02:00
context . warn ( "Social image for theme " + json . id + " is the emtpy string" )
2023-09-21 15:29:34 +02:00
}
2023-10-11 04:16:52 +02:00
return json
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 ( )
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
}
2023-07-28 14:11:00 +02:00
export class DetectConflictingAddExtraTags extends DesugaringStep < TagRenderingConfigJson > {
constructor ( ) {
2023-09-01 16:06:22 +02:00
super (
"The `if`-part in a mapping might set some keys. Those key are not allowed to be set in the `addExtraTags`, as this might result in conflicting values" ,
[ ] ,
"DetectConflictingAddExtraTags"
2023-09-21 15:29:34 +02:00
)
2023-07-28 14:11:00 +02:00
}
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2023-07-28 14:11:00 +02:00
if ( ! ( json . mappings ? . length > 0 ) ) {
2023-10-11 04:16:52 +02:00
return json
2023-07-28 14:11:00 +02:00
}
2023-09-21 15:29:34 +02:00
const tagRendering = new TagRenderingConfig ( json )
2023-07-28 14:11:00 +02:00
2023-09-21 15:29:34 +02:00
const errors = [ ]
2023-07-28 14:11:00 +02:00
for ( let i = 0 ; i < tagRendering . mappings . length ; i ++ ) {
2023-09-21 15:29:34 +02:00
const mapping = tagRendering . mappings [ i ]
2023-07-28 14:11:00 +02:00
if ( ! mapping . addExtraTags ) {
2023-09-21 15:29:34 +02:00
continue
2023-07-28 14:11:00 +02:00
}
2023-09-21 15:29:34 +02:00
const keysInMapping = new Set ( mapping . if . usedKeys ( ) )
2023-07-28 14:11:00 +02:00
2023-09-21 15:29:34 +02:00
const keysInAddExtraTags = mapping . addExtraTags . map ( ( t ) = > t . key )
2023-07-28 14:11:00 +02:00
2023-09-21 15:29:34 +02:00
const duplicateKeys = keysInAddExtraTags . filter ( ( k ) = > keysInMapping . has ( k ) )
2023-07-28 14:11:00 +02:00
if ( duplicateKeys . length > 0 ) {
errors . push (
2023-09-01 16:06:22 +02:00
"At " +
2023-09-21 15:29:34 +02:00
context +
".mappings[" +
i +
"]: AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
duplicateKeys . join ( ", " )
)
2023-07-28 14:11:00 +02:00
}
}
2023-10-11 04:16:52 +02:00
return json
2023-07-28 14:11:00 +02:00
}
}
2023-02-08 01:14:21 +01:00
export class DetectShadowedMappings extends DesugaringStep < TagRenderingConfigJson > {
2023-09-21 15:29:34 +02:00
private readonly _calculatedTagNames : string [ ]
2022-06-20 01:42:30 +02:00
2022-03-17 23:04:00 +01:00
constructor ( layerConfig? : LayerConfigJson ) {
2023-09-21 15:29:34 +02:00
super ( "Checks that the mappings don't shadow each other" , [ ] , "DetectShadowedMappings" )
this . _calculatedTagNames = DetectShadowedMappings . extractCalculatedTagNames ( layerConfig )
2022-03-17 23:04:00 +01:00
}
/ * *
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 ) {
2023-09-21 15:29:34 +02:00
return ct . split ( ":=" ) [ 0 ]
2022-03-17 23:04:00 +01:00
}
2023-09-21 15:29:34 +02:00
return ct . split ( "=" ) [ 0 ]
2022-03-17 23:04:00 +01:00
} ) ? ? [ ]
2023-09-21 15:29:34 +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"
* }
* ]
* }
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* const r = new DetectShadowedMappings ( ) . convert ( tr , context ) ;
* context . getAll ( "error" ) . length // => 1
* context . getAll ( "error" ) [ 0 ] . message . indexOf ( "The mapping key=value is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
2022-03-23 19:48:06 +01:00
*
* const tr = { mappings : [
* {
* if : { or : [ "key=value" , "x=y" ] } ,
* then : "Case A"
* } ,
* {
* if : { and : [ "key=value" , "x=y" ] } ,
* then : "Shadowed"
* }
* ]
* }
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
* const r = new DetectShadowedMappings ( ) . convert ( tr , context ) ;
* context . getAll ( "error" ) . length // => 1
* context . getAll ( "error" ) [ 0 ] . message . indexOf ( "The mapping key=value&x=y is fully matched by a previous mapping (namely 0)" ) >= 0 // => true
2022-03-23 19:48:06 +01:00
* /
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2022-02-10 23:16:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
2023-09-21 15:29:34 +02:00
const defaultProperties = { }
2022-03-17 23:04:00 +01:00
for ( const calculatedTagName of this . _calculatedTagNames ) {
2022-06-20 01:42:30 +02:00
defaultProperties [ calculatedTagName ] =
2023-09-21 15:29:34 +02:00
"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 ) = > {
2023-09-21 15:29:34 +02:00
const ctx = ` ${ context } .mappings[ ${ i } ] `
const ifTags = TagUtils . Tag ( m . if , ctx )
const hideInAnswer = m [ "hideInAnswer" ]
2023-02-08 01:14:21 +01:00
if ( hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true ) {
2023-09-21 15:29:34 +02:00
let conditionTags = TagUtils . Tag ( hideInAnswer )
2022-02-20 01:39:12 +01:00
// Merge the condition too!
2023-09-21 15:29:34 +02:00
return new And ( [ conditionTags , ifTags ] )
2022-02-20 01:39:12 +01:00
}
2023-09-21 15:29:34 +02:00
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
2023-09-21 15:29:34 +02:00
continue
2022-02-04 00:44:09 +01:00
}
2023-09-21 15:29:34 +02:00
const keyValues = parsedConditions [ i ] . asChange ( defaultProperties )
const properties = { }
2023-09-01 16:06:22 +02:00
keyValues . forEach ( ( { k , v } ) = > {
2023-09-21 15:29:34 +02:00
properties [ k ] = v
} )
2022-02-10 23:16:14 +01:00
for ( let j = 0 ; j < i ; j ++ ) {
2023-09-21 15:29:34 +02:00
const doesMatch = parsedConditions [ j ] . matchesProperties ( properties )
2022-07-06 12:57:23 +02:00
if (
doesMatch &&
2023-02-08 01:14:21 +01:00
json . mappings [ j ] [ "hideInAnswer" ] === true &&
json . mappings [ i ] [ "hideInAnswer" ] !== true
2022-07-06 12:57:23 +02:00
) {
2023-10-11 04:16:52 +02:00
context . warn (
` Mapping ${ i } is shadowed by mapping ${ j } . However, mapping ${ j } has 'hideInAnswer' set, which will result in a different rendering in question-mode. `
2023-09-21 15:29:34 +02:00
)
2022-06-28 01:04:45 +02:00
} else if ( doesMatch ) {
2022-02-04 00:44:09 +01:00
// The current mapping is shadowed!
2023-10-11 04:16:52 +02:00
context . err ( ` 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 (
2023-09-21 15:29:34 +02:00
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-09-11 01:49:07 +02:00
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=" ] } ]
2023-09-21 15:29:34 +02:00
` )
2022-02-04 00:44:09 +01:00
}
}
}
2022-02-10 23:16:14 +01:00
2023-10-11 04:16:52 +02:00
return json
2022-02-04 00:44:09 +01:00
}
}
2022-02-17 23:54:14 +01:00
export class DetectMappingsWithImages extends DesugaringStep < TagRenderingConfigJson > {
2023-09-21 15:29:34 +02:00
private readonly _doesImageExist : DoesImageExist
2022-07-06 12:57:23 +02:00
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"
2023-09-21 15:29:34 +02:00
)
this . _doesImageExist = doesImageExist
2022-02-17 23:54:14 +01:00
}
2022-03-23 19:48:06 +01:00
/ * *
2023-10-12 16:55:26 +02:00
* const context = ConversionContext . test ( )
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'>"
* }
* } ]
2023-10-12 16:55:26 +02:00
* } , context ) ;
* context . hasErrors ( ) // => true
* context . getAll ( "error" ) . some ( msg = > msg . message . indexOf ( "./assets/layers/bike_parking/staple.svg" ) >= 0 ) // => true
2022-03-23 19:48:06 +01:00
* /
2023-10-11 04:16:52 +02:00
convert ( json : TagRenderingConfigJson , context : ConversionContext ) : TagRenderingConfigJson {
2022-02-17 23:54:14 +01:00
if ( json . mappings === undefined || json . mappings . length === 0 ) {
2023-10-11 04:16:52 +02:00
return json
2022-02-17 23:54:14 +01:00
}
2023-09-21 15:29:34 +02:00
const ignoreToken = "ignore-image-in-then"
2022-02-17 23:54:14 +01:00
for ( let i = 0 ; i < json . mappings . length ; i ++ ) {
2023-09-21 15:29:34 +02:00
const mapping = json . mappings [ i ]
const ignore = mapping [ "#" ] ? . indexOf ( ignoreToken ) >= 0
const images = Utils . Dedup ( Translations . T ( mapping . then ) ? . ExtractImages ( ) ? ? [ ] )
2023-10-11 04:16:52 +02:00
const ctx = context . enters ( "mappings" , i )
2022-02-17 23:54:14 +01:00
if ( images . length > 0 ) {
2022-06-20 01:42:30 +02:00
if ( ! ignore ) {
2023-10-11 04:16:52 +02:00
ctx . err (
` 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 (
2022-02-20 00:51:11 +01:00
", "
) } . ( This check can be turned of by adding "#" : "${ignoreToken}" in the mapping , but this is discouraged `
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
} else {
2023-10-11 04:16:52 +02:00
ctx . info (
` Ignored image ${ images . join (
2022-02-20 00:51:11 +01:00
", "
) } in 'then' - clause of a mapping as this check has been disabled `
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
for ( const image of images ) {
2023-10-11 04:16:52 +02:00
this . _doesImageExist . convert ( image , ctx )
2022-06-20 01:42:30 +02:00
}
2022-02-19 17:57:34 +01:00
}
2022-06-20 01:42:30 +02:00
} else if ( ignore ) {
2023-10-11 04:16:52 +02:00
ctx . warn ( ` Unused ' ${ ignoreToken } ' - please remove this ` )
2022-02-17 23:54:14 +01:00
}
}
2023-10-11 04:16:52 +02:00
return json
2023-09-02 02:04:59 +02:00
}
}
class ValidatePossibleLinks extends DesugaringStep < string | Record < string , string > > {
constructor ( ) {
2023-09-21 15:29:34 +02:00
super (
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set" ,
[ ] ,
"ValidatePossibleLinks"
)
2023-09-02 02:04:59 +02:00
}
public isTabnabbingProne ( str : string ) : boolean {
2023-09-21 15:29:34 +02:00
const p = parse_html ( str )
const links = Array . from ( p . getElementsByTagName ( "a" ) )
2023-09-02 02:04:59 +02:00
if ( links . length == 0 ) {
2023-09-21 15:29:34 +02:00
return false
2022-02-17 23:54:14 +01:00
}
2023-09-02 02:04:59 +02:00
for ( const link of Array . from ( links ) ) {
if ( link . getAttribute ( "target" ) !== "_blank" ) {
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
const rel = new Set < string > ( link . getAttribute ( "rel" ) ? . split ( " " ) ? ? [ ] )
2023-09-02 02:04:59 +02:00
if ( rel . has ( "noopener" ) ) {
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
const source = link . getAttribute ( "href" )
2023-09-02 02:04:59 +02:00
if ( source . startsWith ( "http" ) ) {
// No variable part - we assume the link is safe
2023-09-21 15:29:34 +02:00
continue
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
return true
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
return false
2023-09-02 02:04:59 +02:00
}
2023-09-21 15:29:34 +02:00
convert (
json : string | Record < string , string > ,
2023-10-11 04:16:52 +02:00
context : ConversionContext
) : string | Record < string , string > {
2023-09-02 02:04:59 +02:00
if ( typeof json === "string" ) {
if ( this . isTabnabbingProne ( json ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"The string " +
2023-09-21 15:29:34 +02:00
json +
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
)
2023-09-02 02:04:59 +02:00
}
} else {
for ( const k in json ) {
if ( this . isTabnabbingProne ( json [ k ] ) ) {
2023-10-11 04:16:52 +02:00
context . err (
` The translation for ${ k } ' ${ json [ k ] } ' has a link targeting \` _blank \` , but it doesn't have \` rel='noopener' \` set. This gives rise to reverse tabnapping `
2023-09-21 15:29:34 +02:00
)
2023-09-02 02:04:59 +02:00
}
}
}
2023-10-11 04:16:52 +02:00
return json
2022-02-17 23:54:14 +01:00
}
}
2022-10-29 03:02:42 +02:00
class MiscTagRenderingChecks extends DesugaringStep < TagRenderingConfigJson > {
2023-09-21 15:29:34 +02:00
private _options : { noQuestionHintCheck : boolean }
2023-03-24 19:21:15 +01:00
2023-03-09 15:12:16 +01:00
constructor ( options : { noQuestionHintCheck : boolean } ) {
2023-09-21 15:29:34 +02:00
super ( "Miscellaneous checks on the tagrendering" , [ "special" ] , "MiscTagRenderingChecks" )
this . _options = options
2022-10-29 03:02:42 +02:00
}
2022-11-02 14:44:06 +01:00
convert (
2023-03-08 02:01:52 +01:00
json : TagRenderingConfigJson | QuestionableTagRenderingConfigJson ,
2023-10-11 04:16:52 +02:00
context : ConversionContext
) : TagRenderingConfigJson {
2022-11-02 14:44:06 +01:00
if ( json [ "special" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
'Detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
2023-09-21 15:29:34 +02:00
)
2022-10-29 03:02:42 +02:00
}
2023-03-31 03:28:11 +02:00
if ( json [ "group" ] ) {
2023-10-11 04:16:52 +02:00
context . err ( 'Groups are deprecated, use `"label": ["' + json [ "group" ] + '"]` instead' )
2023-03-29 18:54:00 +02:00
}
2023-09-02 02:04:59 +02:00
2023-10-17 16:06:58 +02:00
if ( json . freeform ) {
if ( json . render === undefined ) {
2023-10-24 22:01:10 +02:00
context
. enter ( "render" )
. err (
"This tagRendering allows to set a value to key " +
json . freeform . key +
", but does not define a `render`. Please, add a value here which contains `{" +
json . freeform . key +
"}`"
)
2023-10-17 16:06:58 +02:00
} else {
const render = new Translation ( < any > json . render )
for ( const ln in render . translations ) {
if ( ln . startsWith ( "_" ) ) {
continue
}
const txt : string = render . translations [ ln ]
if ( txt === "" ) {
2023-10-24 22:01:10 +02:00
context . enter ( "render" ) . err ( " Rendering for language " + ln + " is empty" )
2023-10-17 16:06:58 +02:00
}
if (
txt . indexOf ( "{" + json . freeform . key + "}" ) >= 0 ||
2023-10-24 22:01:10 +02:00
txt . indexOf ( "&LBRACE" + json . freeform . key + "&RBRACE" ) >= 0
2023-10-17 16:06:58 +02:00
) {
continue
}
if ( txt . indexOf ( "{" + json . freeform . key + ":" ) >= 0 ) {
continue
}
if (
json . freeform [ "type" ] === "opening_hours" &&
txt . indexOf ( "{opening_hours_table(" ) >= 0
) {
continue
}
const keyFirstArg = [ "canonical" , "fediverse_link" , "translated" ]
if (
keyFirstArg . some (
( funcName ) = > txt . indexOf ( ` { ${ funcName } ( ${ json . freeform . key } ` ) >= 0
)
) {
continue
}
if (
json . freeform [ "type" ] === "wikidata" &&
txt . indexOf ( "{wikipedia(" + json . freeform . key ) >= 0
) {
continue
}
if ( json . freeform . key === "wikidata" && txt . indexOf ( "{wikipedia()" ) >= 0 ) {
continue
}
if (
json . freeform [ "type" ] === "wikidata" &&
txt . indexOf ( ` {wikidata_label( ${ json . freeform . key } ) ` ) >= 0
) {
continue
}
2023-10-24 22:01:10 +02:00
context
. enter ( "render" )
. err (
` The rendering for language ${ ln } does not contain \` { ${ json . freeform . key } } \` . This is a bug, as this rendering should show exactly this freeform key! `
)
2023-10-17 16:06:58 +02:00
}
}
}
if ( json . render && json [ "question" ] && json . freeform === undefined ) {
context . err (
` Detected a tagrendering which takes input without freeform key in ${ context } ; the question is ${ new Translation (
json [ "question" ]
) . textFor ( "en" ) } `
)
}
2023-09-21 15:29:34 +02:00
const freeformType = json [ "freeform" ] ? . [ "type" ]
2023-03-24 19:21:15 +01:00
if ( freeformType ) {
2023-03-30 04:51:56 +02:00
if ( Validators . availableTypes . indexOf ( freeformType ) < 0 ) {
2023-10-11 04:16:52 +02:00
context
. enters ( "freeform" , "type" )
. err (
"Unknown type: " +
freeformType +
"; try one of " +
Validators . availableTypes . join ( ", " )
)
2023-03-24 19:21:15 +01:00
}
}
2023-10-11 04:16:52 +02:00
return json
2022-10-29 03:02:42 +02:00
}
}
2022-02-17 23:54:14 +01:00
export class ValidateTagRenderings extends Fuse < TagRenderingConfigJson > {
2023-03-09 15:12:16 +01:00
constructor (
layerConfig? : LayerConfigJson ,
doesImageExist? : DoesImageExist ,
options ? : { noQuestionHintCheck : boolean }
) {
2022-02-17 23:54:14 +01:00
super (
"Various validation on tagRenderingConfigs" ,
2022-06-20 01:42:30 +02:00
new DetectShadowedMappings ( layerConfig ) ,
2023-07-28 14:11:00 +02:00
new DetectConflictingAddExtraTags ( ) ,
2023-02-08 01:14:21 +01:00
new DetectMappingsWithImages ( doesImageExist ) ,
2023-09-21 15:29:34 +02:00
new On ( "render" , new ValidatePossibleLinks ( ) ) ,
new On ( "question" , new ValidatePossibleLinks ( ) ) ,
new On ( "questionHint" , new ValidatePossibleLinks ( ) ) ,
new On ( "mappings" , new Each ( new On ( "then" , new ValidatePossibleLinks ( ) ) ) ) ,
2023-03-09 15:12:16 +01:00
new MiscTagRenderingChecks ( options )
2023-09-21 15:29:34 +02:00
)
2022-02-17 23:54:14 +01:00
}
}
2023-10-12 16:55:26 +02:00
export class ValidateLayer extends Conversion <
LayerConfigJson ,
{ parsed : LayerConfig ; raw : LayerConfigJson }
> {
2022-02-04 00:44:09 +01:00
/ * *
* The paths where this layer is originally saved . Triggers some extra checks
* @private
* /
2023-09-21 15:29:34 +02:00
private readonly _path? : string
private readonly _isBuiltin : boolean
private readonly _doesImageExist : DoesImageExist
2023-10-24 22:01:10 +02:00
private readonly _studioValidations : boolean
2023-10-30 16:32:43 +01:00
private _skipDefaultLayers : boolean
2022-02-04 00:44:09 +01:00
2023-10-20 19:04:55 +02:00
constructor (
path : string ,
isBuiltin : boolean ,
doesImageExist : DoesImageExist ,
2023-10-30 16:32:43 +01:00
studioValidations : boolean = false ,
skipDefaultLayers : boolean = false
2023-10-20 19:04:55 +02:00
) {
2023-09-21 15:29:34 +02:00
super ( "Doesn't change anything, but emits warnings and errors" , [ ] , "ValidateLayer" )
this . _path = path
this . _isBuiltin = isBuiltin
this . _doesImageExist = doesImageExist
2023-10-20 19:04:55 +02:00
this . _studioValidations = studioValidations
2023-10-30 16:32:43 +01:00
this . _skipDefaultLayers = skipDefaultLayers
2022-02-04 00:44:09 +01:00
}
2023-10-12 16:55:26 +02:00
convert (
json : LayerConfigJson ,
context : ConversionContext
) : { parsed : LayerConfig ; raw : LayerConfigJson } {
2023-10-11 04:16:52 +02:00
context = context . inOperation ( this . name )
2022-02-04 00:44:09 +01:00
if ( typeof json === "string" ) {
2023-10-11 04:16:52 +02:00
context . err ( "This layer hasn't been expanded: " + json )
return null
2022-02-04 00:44:09 +01:00
}
2022-09-08 21:40:48 +02:00
2023-10-30 16:32:43 +01:00
if ( this . _skipDefaultLayers && Constants . added_by_default . indexOf ( < any > json . id ) >= 0 ) {
return { parsed : undefined , raw : json }
}
2023-10-13 18:46:56 +02:00
if ( typeof json === "string" ) {
context . err (
` Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed `
)
return undefined
}
if ( json . id === undefined ) {
2023-10-24 22:01:10 +02:00
context . enter ( "id" ) . err ( ` Not a valid layer: id is undefined ` )
2023-10-13 18:46:56 +02:00
}
if ( json . source === undefined ) {
context . enter ( "source" ) . err ( "No source section is defined" )
} else {
if ( json . source === "special" || json . source === "special:library" ) {
} else if ( json . source && json . source [ "osmTags" ] === undefined ) {
context
. enters ( "source" , "osmTags" )
. err (
"No osmTags defined in the source section - these should always be present, even for geojson layer"
)
} else {
const osmTags = TagUtils . Tag ( json . source [ "osmTags" ] , context + "source.osmTags" )
if ( osmTags . isNegative ( ) ) {
context
. enters ( "source" , "osmTags" )
. err (
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
osmTags . asHumanString ( false , false , { } )
)
}
}
if ( json . source [ "geoJsonSource" ] !== undefined ) {
context
. enters ( "source" , "geoJsonSource" )
. err ( "Use 'geoJson' instead of 'geoJsonSource'" )
}
if ( json . source [ "geojson" ] !== undefined ) {
context
. enters ( "source" , "geojson" )
. err ( "Use 'geoJson' instead of 'geojson' (the J is a capital letter)" )
}
}
if ( json . id ? . toLowerCase ( ) !== json . id ) {
context . enter ( "id" ) . err ( ` The id of a layer should be lowercase: ${ json . id } ` )
}
if ( json . id ? . match ( /[a-z0-9-_]/ ) == null ) {
context . enter ( "id" ) . err ( ` The id of a layer should match [a-z0-9-_]*: ${ json . id } ` )
}
if (
json . syncSelection !== undefined &&
LayerConfig . syncSelectionAllowed . indexOf ( json . syncSelection ) < 0
) {
context
. enter ( "syncSelection" )
. err (
"Invalid sync-selection: must be one of " +
LayerConfig . syncSelectionAllowed . map ( ( v ) = > ` ' ${ v } ' ` ) . join ( ", " ) +
" but got '" +
json . syncSelection +
"'"
)
}
2023-10-11 04:16:52 +02:00
let layerConfig : LayerConfig
try {
layerConfig = new LayerConfig ( json , "validation" , true )
} catch ( e ) {
2023-10-17 16:06:58 +02:00
console . error ( e )
context . err ( "Could not parse layer due to:" + e )
2023-10-11 04:16:52 +02:00
return undefined
}
2023-10-17 16:06:58 +02:00
for ( let i = 0 ; i < ( layerConfig . calculatedTags ? ? [ ] ) . length ; i ++ ) {
const [ _ , code , __ ] = layerConfig . calculatedTags [ i ]
2023-09-22 11:20:22 +02:00
try {
new Function ( "feat" , "return " + code + ";" )
} catch ( e ) {
2023-10-17 16:06:58 +02:00
context
. enters ( "calculatedTags" , i )
. err (
` Invalid function definition: the custom javascript is invalid: ${ e } . The offending javascript code is: \ n ${ code } `
)
2023-09-22 11:20:22 +02:00
}
}
2023-03-29 18:54:00 +02:00
if ( json . source === "special" ) {
if ( ! Constants . priviliged_layers . find ( ( x ) = > x == json . id ) ) {
2023-10-11 04:16:52 +02:00
context . err (
"Layer " +
2023-09-21 15:29:34 +02:00
json . id +
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
)
2023-03-29 18:54:00 +02:00
}
}
2022-07-20 22:57:39 +02:00
if ( json . tagRenderings !== undefined && json . tagRenderings . length > 0 ) {
2023-10-17 16:06:58 +02:00
new On ( "tagRendering" , new Each ( new ValidateTagRenderings ( json ) ) )
2023-07-15 18:04:30 +02:00
if ( json . title === undefined && json . source !== "special:library" ) {
2023-10-11 04:16:52 +02:00
context . err (
"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."
2023-09-21 15:29:34 +02:00
)
2022-07-20 22:57:39 +02:00
}
if ( json . title === null ) {
2023-10-11 04:16:52 +02:00
context . info (
"Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
2023-09-21 15:29:34 +02:00
)
2022-07-20 22:57:39 +02:00
}
2023-10-24 22:01:10 +02:00
{
// Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils . Duplicates (
< string [ ] > json . tagRenderings . filter ( ( tr ) = > typeof tr === "string" )
)
for ( let i = 0 ; i < json . tagRenderings . length ; i ++ ) {
const tagRendering = json . tagRenderings [ i ]
if ( typeof tagRendering === "string" && duplicates . indexOf ( tagRendering ) > 0 ) {
context
. enters ( "tagRenderings" , i )
. err ( ` This builtin question is used multiple times ( ${ tagRendering } ) ` )
}
}
}
2022-07-19 13:30:26 +02:00
}
2022-02-04 00:44:09 +01:00
if ( json [ "builtin" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err ( "This layer hasn't been expanded: " + json )
return null
2022-02-04 00:44:09 +01:00
}
2022-09-08 21:40:48 +02:00
2023-04-02 02:59:20 +02:00
if ( json . minzoom > Constants . minZoomLevelToAddNewPoint ) {
2023-10-11 04:16:52 +02:00
const c = context . enter ( "minzoom" )
2023-10-11 17:30:06 +02:00
const msg = ` Minzoom is ${ json . minzoom } , this should be at most ${ Constants . minZoomLevelToAddNewPoint } 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 `
if ( json . presets ? . length > 0 ) {
c . err ( msg )
} else {
c . warn ( msg )
}
2022-08-24 01:29:11 +02: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 (
2023-09-24 00:25:10 +02:00
Utils . Duplicates ( Utils . NoNull ( ( json . tagRenderings ? ? [ ] ) . map ( ( tr ) = > tr [ "id" ] ) ) )
2023-09-21 15:29:34 +02:00
)
2022-06-20 01:42:30 +02:00
if ( duplicates . length > 0 ) {
2023-10-11 04:16:52 +02:00
context
. enter ( "tagRenderings" )
. err ( "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-10-27 01:50:41 +02:00
if ( json . deletion !== undefined && json . deletion instanceof DeleteConfig ) {
if ( json . deletion . softDeletionTags === undefined ) {
2023-10-11 04:16:52 +02:00
context
. enter ( "deletion" )
. warn ( "No soft-deletion tags in deletion block for layer " + json . id )
2022-09-24 03:33:09 +02:00
}
}
2022-02-04 00:44:09 +01:00
try {
2022-09-24 03:33:09 +02:00
if ( this . _isBuiltin ) {
2022-02-04 00:44:09 +01:00
// Some checks for legacy elements
if ( json [ "overpassTags" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"Layer " +
2023-09-21 15:29:34 +02:00
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)'
)
2022-02-04 00:44:09 +01:00
}
const forbiddenTopLevel = [
"icon" ,
"wayHandling" ,
"roamingRenderings" ,
"roamingRendering" ,
"label" ,
"width" ,
"color" ,
"colour" ,
2023-09-21 15:29:34 +02:00
"iconOverlays" ,
]
2022-02-04 00:44:09 +01:00
for ( const forbiddenKey of forbiddenTopLevel ) {
if ( json [ forbiddenKey ] !== undefined )
2023-10-11 04:16:52 +02:00
context . err (
"Layer " + json . id + " still has a forbidden key " + forbiddenKey
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
}
if ( json [ "hideUnderlayingFeaturesMinPercentage" ] !== undefined ) {
2023-10-11 04:16:52 +02:00
context . err (
"Layer " +
2023-09-21 15:29:34 +02:00
json . id +
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
)
2022-02-04 00:44:09 +01:00
}
2022-09-08 21:40:48 +02:00
2022-07-18 02:00:32 +02:00
if (
json . isShown !== undefined &&
( json . isShown [ "render" ] !== undefined || json . isShown [ "mappings" ] !== undefined )
) {
2023-10-11 04:16:52 +02:00
context . warn ( "Has a tagRendering as `isShown`" )
2022-07-18 02:00:32 +02:00
}
2022-02-04 00:44:09 +01:00
}
2022-09-24 03:33:09 +02:00
if ( this . _isBuiltin ) {
2022-04-08 22:12:43 +02:00
// Check location of layer file
2023-09-21 15:29:34 +02:00
const expected : string = ` assets/layers/ ${ json . id } / ${ json . id } .json `
2022-02-04 00:44:09 +01:00
if ( this . _path != undefined && this . _path . indexOf ( expected ) < 0 ) {
2023-10-11 04:16:52 +02:00
context . err (
2022-02-04 00:44:09 +01:00
"Layer is in an incorrect place. The path is " +
2023-09-21 15:29:34 +02:00
this . _path +
", but expected " +
expected
)
2022-02-04 00:44:09 +01:00
}
}
if ( this . _isBuiltin ) {
// Check for correct IDs
if ( json . tagRenderings ? . some ( ( tr ) = > tr [ "id" ] === "" ) ) {
2023-09-21 15:29:34 +02:00
const emptyIndexes : number [ ] = [ ]
2022-02-04 00:44:09 +01:00
for ( let i = 0 ; i < json . tagRenderings . length ; i ++ ) {
2023-09-21 15:29:34 +02:00
const tagRendering = json . tagRenderings [ i ]
2022-02-04 00:44:09 +01:00
if ( tagRendering [ "id" ] === "" ) {
2023-09-21 15:29:34 +02:00
emptyIndexes . push ( i )
2022-02-04 00:44:09 +01:00
}
}
2023-10-11 04:16:52 +02:00
context
. enter ( [ "tagRenderings" , . . . emptyIndexes ] )
. err (
` Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${ emptyIndexes . join (
","
) } ] ) `
)
2022-02-04 00:44:09 +01:00
}
2023-09-24 00:25:10 +02:00
const duplicateIds = Utils . Duplicates (
2022-02-04 00:44:09 +01:00
( json . tagRenderings ? ? [ ] )
? . map ( ( f ) = > f [ "id" ] )
. filter ( ( id ) = > id !== "questions" )
2023-09-21 15:29:34 +02:00
)
2022-02-04 00:44:09 +01:00
if ( duplicateIds . length > 0 && ! Utils . runningFromConsole ) {
2023-10-11 04:16:52 +02:00
context
. enter ( "tagRenderings" )
. err ( ` Some tagRenderings have a duplicate id: ${ duplicateIds } ` )
2022-02-04 00:44:09 +01:00
}
if ( json . description === undefined ) {
2023-03-25 02:48:24 +01:00
if ( typeof json . source === null ) {
2023-10-11 04:16:52 +02:00
context . err ( "A priviliged layer must have a description" )
2022-02-04 00:44:09 +01:00
} else {
2023-10-11 04:16:52 +02:00
context . warn ( "A builtin layer should have a description" )
2022-02-04 00:44:09 +01:00
}
}
}
2022-09-11 01:49:07 +02:00
2023-07-28 14:11:00 +02:00
if ( json . filter ) {
2023-10-11 04:16:52 +02:00
new On ( "filter" , new Each ( new ValidateFilter ( ) ) ) . convert ( json , context )
2023-07-28 14:11:00 +02:00
}
2022-02-10 23:16:14 +01:00
if ( json . tagRenderings !== undefined ) {
2023-10-11 04:16:52 +02:00
new On (
2022-07-06 12:57:23 +02:00
"tagRenderings" ,
2023-03-09 15:12:16 +01:00
new Each (
new ValidateTagRenderings ( json , this . _doesImageExist , {
2023-09-21 15:29:34 +02:00
noQuestionHintCheck : json [ "#" ] ? . indexOf ( "no-question-hint-check" ) >= 0 ,
2023-03-09 15:12:16 +01:00
} )
)
2023-09-21 15:29:34 +02:00
) . convert ( json , context )
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-10-30 16:32:43 +01:00
if ( json . pointRendering !== null && json . pointRendering !== undefined ) {
2023-10-30 14:32:31 +01:00
if ( ! Array . isArray ( json . pointRendering ) ) {
throw (
"pointRendering in " +
json . id +
" is not iterable, it is: " +
typeof json . pointRendering
)
}
2023-10-30 16:32:43 +01:00
for ( let i = 0 ; i < json . pointRendering . length ; i ++ ) {
const pointRendering = json . pointRendering [ i ]
if ( pointRendering . marker === undefined ) {
continue
}
2023-10-30 14:32:31 +01:00
for ( const icon of pointRendering ? . marker ) {
const indexM = pointRendering ? . marker . indexOf ( icon )
2023-10-12 16:55:26 +02:00
if ( ! icon . icon ) {
2023-10-30 14:32:31 +01:00
continue
2023-10-12 16:55:26 +02:00
}
if ( icon . icon [ "condition" ] ) {
context
2023-10-30 16:32:43 +01:00
. enters ( "pointRendering" , i , "marker" , indexM , "icon" , "condition" )
2023-10-12 16:55:26 +02:00
. err (
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
)
}
2023-10-30 14:32:31 +01:00
}
}
2022-08-18 14:39:40 +02:00
}
2022-02-17 23:54:14 +01:00
if ( json . presets !== undefined ) {
2023-03-25 02:48:24 +01:00
if ( typeof json . source === "string" ) {
2023-10-11 04:16:52 +02:00
context . err ( "A special layer cannot have presets" )
2023-03-25 02:48:24 +01:00
}
2022-02-14 15:40:38 +01:00
// Check that a preset will be picked up by the layer itself
2023-09-21 15:29:34 +02:00
const baseTags = TagUtils . Tag ( json . source [ "osmTags" ] )
2022-02-17 23:54:14 +01:00
for ( let i = 0 ; i < json . presets . length ; i ++ ) {
2023-09-21 15:29:34 +02: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 ) )
2023-09-21 15:29:34 +02:00
) . asChange ( { id : "node/-1" } )
const properties = { }
2022-02-14 15:40:38 +01:00
for ( const tag of tags ) {
2023-09-21 15:29:34 +02:00
properties [ tag . k ] = tag . v
2022-02-14 15:40:38 +01:00
}
2023-09-21 15:29:34 +02:00
const doMatch = baseTags . matchesProperties ( properties )
2022-02-17 23:54:14 +01:00
if ( ! doMatch ) {
2023-10-11 04:16:52 +02:00
context
2023-10-22 01:30:05 +02:00
. enters ( "presets" , i , "tags" )
2023-10-11 04:16:52 +02:00
. err (
"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 ) {
2023-10-12 16:55:26 +02:00
context . err ( "Could not validate layer due to: " + e + e . stack )
2022-02-04 00:44:09 +01:00
}
2022-02-10 23:16:14 +01:00
2023-10-20 19:04:55 +02:00
if ( this . _studioValidations ) {
if ( ! json . description ) {
context . enter ( "description" ) . err ( "A description is required" )
}
if ( ! json . name ) {
context . enter ( "name" ) . err ( "A name is required" )
}
}
2023-10-12 16:55:26 +02:00
return { raw : json , parsed : layerConfig }
2022-02-04 00:44:09 +01:00
}
}
2022-09-24 03:33:09 +02:00
2023-03-24 19:21:15 +01:00
export class ValidateFilter extends DesugaringStep < FilterConfigJson > {
constructor ( ) {
2023-09-21 15:29:34 +02:00
super ( "Detect common errors in the filters" , [ ] , "ValidateFilter" )
2023-03-24 19:21:15 +01:00
}
2023-10-11 04:16:52 +02:00
convert ( filter : FilterConfigJson , context : ConversionContext ) : FilterConfigJson {
2023-07-28 14:11:00 +02:00
if ( typeof filter === "string" ) {
// Calling another filter, we skip
2023-10-11 04:16:52 +02:00
return filter
2023-07-28 14:11:00 +02:00
}
2023-03-24 19:21:15 +01:00
for ( const option of filter . options ) {
2023-07-28 14:11:00 +02:00
for ( let i = 0 ; i < option . fields ? . length ? ? 0 ; i ++ ) {
2023-09-21 15:29:34 +02:00
const field = option . fields [ i ]
const type = field . type ? ? "string"
2023-03-30 04:51:56 +02:00
if ( Validators . availableTypes . find ( ( t ) = > t === type ) === undefined ) {
2023-10-11 04:16:52 +02:00
context
. enters ( "fields" , i )
. err (
` Invalid filter: ${ type } is not a valid textfield type. \ n \ tTry one of ${ Array . from (
Validators . availableTypes
) . join ( "," ) } `
)
2023-03-24 19:21:15 +01:00
}
}
}
2023-10-11 04:16:52 +02:00
return filter
2023-03-24 19:21:15 +01:00
}
}
2022-10-27 01:50:41 +02:00
export class DetectDuplicateFilters extends DesugaringStep < {
layers : LayerConfigJson [ ]
themes : LayoutConfigJson [ ]
} > {
2022-09-24 03:33:09 +02:00
constructor ( ) {
2022-10-27 01:50:41 +02:00
super (
"Tries to detect layers where a shared filter can be used (or where similar filters occur)" ,
[ ] ,
"DetectDuplicateFilters"
2023-09-21 15:29:34 +02:00
)
2022-09-24 03:33:09 +02:00
}
2022-10-27 01:50:41 +02:00
convert (
json : { layers : LayerConfigJson [ ] ; themes : LayoutConfigJson [ ] } ,
2023-10-11 04:16:52 +02:00
context : ConversionContext
) : { layers : LayerConfigJson [ ] ; themes : LayoutConfigJson [ ] } {
2023-09-21 15:29:34 +02:00
const { layers , themes } = json
2022-10-27 01:50:41 +02:00
const perOsmTag = new Map <
string ,
{
layer : LayerConfigJson
layout : LayoutConfigJson | undefined
filter : FilterConfigJson
} [ ]
2023-09-21 15:29:34 +02:00
> ( )
2022-09-24 03:33:09 +02:00
for ( const layer of layers ) {
2023-09-21 15:29:34 +02:00
this . addLayerFilters ( layer , perOsmTag )
2022-09-24 03:33:09 +02:00
}
for ( const theme of themes ) {
2022-10-27 01:50:41 +02:00
if ( theme . id === "personal" ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
for ( const layer of theme . layers ) {
2022-10-27 01:50:41 +02:00
if ( typeof layer === "string" ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
2022-10-27 01:50:41 +02:00
if ( layer [ "builtin" ] !== undefined ) {
2023-09-21 15:29:34 +02:00
continue
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
this . addLayerFilters ( < LayerConfigJson > layer , perOsmTag , theme )
2022-09-24 03:33:09 +02:00
}
}
// At this point, we have gathered all filters per tag - time to find duplicates
perOsmTag . forEach ( ( value , key ) = > {
2022-10-27 01:50:41 +02:00
if ( value . length <= 1 ) {
2022-09-24 03:33:09 +02:00
// Seen this key just once, it is unique
2023-09-21 15:29:34 +02:00
return
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
let msg = "Possible duplicate filter: " + key
2023-09-01 16:06:22 +02:00
for ( const { filter , layer , layout } of value ) {
2023-09-21 15:29:34 +02:00
let id = ""
2022-10-27 01:50:41 +02:00
if ( layout !== undefined ) {
2023-09-21 15:29:34 +02:00
id = layout . id + ":"
2022-09-24 03:33:09 +02:00
}
2023-09-21 15:29:34 +02:00
msg += ` \ n - ${ id } ${ layer . id } . ${ filter . id } `
2022-09-24 03:33:09 +02:00
}
2023-10-11 04:16:52 +02:00
context . warn ( msg )
2023-09-21 15:29:34 +02:00
} )
2022-09-24 03:33:09 +02:00
2023-10-11 04:16:52 +02:00
return json
2022-09-24 03:33:09 +02:00
}
2023-02-03 03:57:30 +01:00
/ * *
* Add all filter options into 'perOsmTag'
* /
private addLayerFilters (
layer : LayerConfigJson ,
perOsmTag : Map <
string ,
{
layer : LayerConfigJson
layout : LayoutConfigJson | undefined
filter : FilterConfigJson
} [ ]
> ,
layout? : LayoutConfigJson | undefined
) : void {
if ( layer . filter === undefined || layer . filter === null ) {
2023-09-21 15:29:34 +02:00
return
2023-02-03 03:57:30 +01:00
}
if ( layer . filter [ "sameAs" ] !== undefined ) {
2023-09-21 15:29:34 +02:00
return
2023-02-03 03:57:30 +01:00
}
for ( const filter of < ( string | FilterConfigJson ) [ ] > layer . filter ) {
if ( typeof filter === "string" ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
if ( filter [ "#" ] ? . indexOf ( "ignore-possible-duplicate" ) >= 0 ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
for ( const option of filter . options ) {
if ( option . osmTags === undefined ) {
2023-09-21 15:29:34 +02:00
continue
2023-02-03 03:57:30 +01:00
}
2023-09-21 15:29:34 +02:00
const key = JSON . stringify ( option . osmTags )
2023-02-03 03:57:30 +01:00
if ( ! perOsmTag . has ( key ) ) {
2023-09-21 15:29:34 +02:00
perOsmTag . set ( key , [ ] )
2023-02-03 03:57:30 +01:00
}
perOsmTag . get ( key ) . push ( {
layer ,
filter ,
2023-09-21 15:29:34 +02:00
layout ,
} )
2023-02-03 03:57:30 +01:00
}
}
}
2022-09-24 03:33:09 +02:00
}
2023-09-24 00:25:10 +02:00
export class DetectDuplicatePresets extends DesugaringStep < LayoutConfig > {
constructor ( ) {
super (
"Detects mappings which have identical (english) names or identical mappings." ,
[ "presets" ] ,
"DetectDuplicatePresets"
)
}
2023-10-11 04:16:52 +02:00
convert ( json : LayoutConfig , context : ConversionContext ) : LayoutConfig {
2023-09-24 00:25:10 +02:00
const presets : PresetConfig [ ] = [ ] . concat ( . . . json . layers . map ( ( l ) = > l . presets ) )
const enNames = presets . map ( ( p ) = > p . title . textFor ( "en" ) )
if ( new Set ( enNames ) . size != enNames . length ) {
const dups = Utils . Duplicates ( enNames )
const layersWithDup = json . layers . filter ( ( l ) = >
l . presets . some ( ( p ) = > dups . indexOf ( p . title . textFor ( "en" ) ) >= 0 )
)
const layerIds = layersWithDup . map ( ( l ) = > l . id )
2023-10-11 04:16:52 +02:00
context . err (
` This themes has multiple presets which are named: ${ dups } , namely layers ${ layerIds . join (
2023-09-24 00:25:10 +02:00
", "
) } this is confusing for contributors and is probably the result of reusing the same layer multiple times . Use \ ` {"override": {"=presets": []}} \` to remove some presets `
)
}
const optimizedTags = < TagsFilter [ ] > presets . map ( ( p ) = > new And ( p . tags ) . optimize ( ) )
for ( let i = 0 ; i < presets . length ; i ++ ) {
const presetATags = optimizedTags [ i ]
const presetA = presets [ i ]
for ( let j = i + 1 ; j < presets . length ; j ++ ) {
const presetBTags = optimizedTags [ j ]
const presetB = presets [ j ]
if (
Utils . SameObject ( presetATags , presetBTags ) &&
Utils . sameList (
presetA . preciseInput . snapToLayers ,
presetB . preciseInput . snapToLayers
)
) {
2023-10-11 04:16:52 +02:00
context . err (
` This themes has multiple presets with the same tags: ${ presetATags . asHumanString (
2023-09-24 00:25:10 +02:00
false ,
false ,
{ }
) } , namely the preset '${presets[i].title.textFor("en")}' and ' $ { presets [
j
] . title . textFor ( "en" ) } ' `
)
}
}
}
2023-10-11 04:16:52 +02:00
return json
2023-09-24 00:25:10 +02:00
}
}