Refactoring: automatically generate code files from layer/theme files to avoid using 'Eval'
This commit is contained in:
parent
865b0bc44f
commit
39944a01fb
17 changed files with 269 additions and 31 deletions
1
404.html
1
404.html
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://gc.zgo.at/; img-src *; connect-src 'self' https://www.openstreetmap.org/ https://api.openstreetmap.org/;">
|
||||||
<link href="./css/mobile.css" rel="stylesheet"/>
|
<link href="./css/mobile.css" rel="stylesheet"/>
|
||||||
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
||||||
<link href="./css/index-tailwind-output.css" rel="stylesheet"/>
|
<link href="./css/index-tailwind-output.css" rel="stylesheet"/>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://gc.zgo.at/; img-src *; connect-src 'self' https://www.openstreetmap.org/ https://api.openstreetmap.org/;">
|
||||||
<link href="./css/mobile.css" rel="stylesheet"/>
|
<link href="./css/mobile.css" rel="stylesheet"/>
|
||||||
<link href="./css/openinghourstable.css" rel="stylesheet"/>
|
<link href="./css/openinghourstable.css" rel="stylesheet"/>
|
||||||
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
||||||
|
@ -16,8 +17,6 @@
|
||||||
<title>MapComplete</title>
|
<title>MapComplete</title>
|
||||||
<link href="./index.webmanifest" rel="manifest">
|
<link href="./index.webmanifest" rel="manifest">
|
||||||
|
|
||||||
<!-- Mastodon link verification: https://docs.joinmastodon.org/user/profile/#Link%20verification -->
|
|
||||||
<a rel="me" href="https://en.osm.town/@MapComplete" style="display: none">Mastodon</a>
|
|
||||||
<link href="./assets/svg/add.svg" rel="icon" sizes="any" type="image/svg+xml">
|
<link href="./assets/svg/add.svg" rel="icon" sizes="any" type="image/svg+xml">
|
||||||
<meta content="./assets/SocialImage.png" property="og:image">
|
<meta content="./assets/SocialImage.png" property="og:image">
|
||||||
<meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title">
|
<meta content="MapComplete - editable, thematic maps with OpenStreetMap" property="og:title">
|
||||||
|
@ -48,10 +47,12 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<!-- Mastodon link verification: https://docs.joinmastodon.org/user/profile/#Link%20verification -->
|
||||||
|
<a rel="me" href="https://en.osm.town/@MapComplete" class="hidden">Mastodon</a>
|
||||||
|
|
||||||
<div id="main"></div>
|
<div id="main"></div>
|
||||||
<script type="module" src="./src/all_themes_index.ts"></script>
|
<script type="module" src="./src/all_themes_index.ts"></script>
|
||||||
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js" crossorigin="anonymous"
|
<script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="https://gc.zgo.at/count.js" crossorigin="anonymous"
|
||||||
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
|
integrity="sha384-gtO6vSydQeOAGGK19NHrlVLNtaDSJjN4aGMWschK+dwAZOdPQWbjXgL+FM5XsgFJ"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -761,6 +761,10 @@ video {
|
||||||
isolation: auto;
|
isolation: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.-z-10 {
|
||||||
|
z-index: -10;
|
||||||
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
@ -1096,10 +1100,6 @@ video {
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-10 {
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-48 {
|
.h-48 {
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
}
|
}
|
||||||
|
@ -1108,6 +1108,10 @@ video {
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-10 {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-80 {
|
.h-80 {
|
||||||
height: 20rem;
|
height: 20rem;
|
||||||
}
|
}
|
||||||
|
@ -1632,16 +1636,16 @@ video {
|
||||||
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
|
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-white {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-black {
|
.bg-black {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.bg-gray-200 {
|
.bg-gray-200 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
|
||||||
|
|
|
@ -12,8 +12,8 @@ mkdir dist/assets 2> /dev/null
|
||||||
export NODE_OPTIONS="--max-old-space-size=8192"
|
export NODE_OPTIONS="--max-old-space-size=8192"
|
||||||
|
|
||||||
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
|
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
|
||||||
npm run generate:editor-layer-index &&
|
# npm run generate:editor-layer-index &&
|
||||||
npm run generate &&
|
# npm run generate &&
|
||||||
npm run generate:layouts
|
npm run generate:layouts
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { Utils } from "../src/Utils"
|
||||||
import Script from "./Script"
|
import Script from "./Script"
|
||||||
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
|
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
|
||||||
import { parse as parse_html } from "node-html-parser"
|
import { parse as parse_html } from "node-html-parser"
|
||||||
|
import { ExtraFunctions } from "../src/Logic/ExtraFunctions"
|
||||||
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
|
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
|
||||||
// It spits out an overview of those to be used to load them
|
// It spits out an overview of those to be used to load them
|
||||||
|
|
||||||
|
@ -395,10 +396,129 @@ class LayerOverviewUtils extends Script {
|
||||||
skippedLayers.length +
|
skippedLayers.length +
|
||||||
" layers"
|
" layers"
|
||||||
)
|
)
|
||||||
|
// We always need the calculated tags of 'usersettings', so we export them separately
|
||||||
|
this.extractJavascriptCodeForLayer(
|
||||||
|
state.sharedLayers.get("usersettings"),
|
||||||
|
"./src/Logic/State/UserSettingsMetaTagging.ts"
|
||||||
|
)
|
||||||
|
|
||||||
return sharedLayers
|
return sharedLayers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given: a fully expanded themeConfigJson
|
||||||
|
*
|
||||||
|
* Will extract a dictionary of the special code and write it into a javascript file which can be imported.
|
||||||
|
* This removes the need for _eval_, allowing for a correct CSP
|
||||||
|
* @param themeFile
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private extractJavascriptCode(themeFile: LayoutConfigJson) {
|
||||||
|
const allCode = [
|
||||||
|
"import {Feature} from 'geojson'",
|
||||||
|
'import { ExtraFuncType } from "../../../Logic/ExtraFunctions";',
|
||||||
|
'import { Utils } from "../../../Utils"',
|
||||||
|
"export class ThemeMetaTagging {",
|
||||||
|
" public static readonly themeName = " + JSON.stringify(themeFile.id),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for (const layer of themeFile.layers) {
|
||||||
|
const l = <LayerConfigJson>layer
|
||||||
|
const code = l.calculatedTags ?? []
|
||||||
|
|
||||||
|
allCode.push(
|
||||||
|
" public metaTaggging_for_" +
|
||||||
|
l.id +
|
||||||
|
"(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {"
|
||||||
|
)
|
||||||
|
allCode.push(" const {" + ExtraFunctions.types.join(", ") + "} = helperFunctions")
|
||||||
|
for (const line of code) {
|
||||||
|
const firstEq = line.indexOf("=")
|
||||||
|
let attributeName = line.substring(0, firstEq).trim()
|
||||||
|
const expression = line.substring(firstEq + 1)
|
||||||
|
const isStrict = attributeName.endsWith(":")
|
||||||
|
if (!isStrict) {
|
||||||
|
allCode.push(
|
||||||
|
" Utils.AddLazyProperty(feat.properties, '" +
|
||||||
|
attributeName +
|
||||||
|
"', () => " +
|
||||||
|
expression +
|
||||||
|
" ) "
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
|
||||||
|
allCode.push(" feat.properties['" + attributeName + "'] = " + expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allCode.push(" }")
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDir = "./src/assets/generated/metatagging/"
|
||||||
|
if (!existsSync(targetDir)) {
|
||||||
|
mkdirSync(targetDir)
|
||||||
|
}
|
||||||
|
allCode.push("}")
|
||||||
|
|
||||||
|
writeFileSync(targetDir + themeFile.id + ".ts", allCode.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractJavascriptCodeForLayer(l: LayerConfigJson, targetPath?: string) {
|
||||||
|
let importPath = "../../../"
|
||||||
|
if (targetPath) {
|
||||||
|
const l = targetPath.split("/")
|
||||||
|
if (l.length == 1) {
|
||||||
|
importPath = "./"
|
||||||
|
} else {
|
||||||
|
importPath = ""
|
||||||
|
for (let i = 0; i < l.length - 3; i++) {
|
||||||
|
const _ = l[i]
|
||||||
|
importPath += "../"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const allCode = [
|
||||||
|
`import { Utils } from "${importPath}Utils"`,
|
||||||
|
`/** This code is autogenerated - do not edit. Edit ./assets/layers/${l.id}/${l.id}.json instead */`,
|
||||||
|
"export class ThemeMetaTagging {",
|
||||||
|
" public static readonly themeName = " + JSON.stringify(l.id),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
const code = l.calculatedTags ?? []
|
||||||
|
|
||||||
|
allCode.push(
|
||||||
|
" public metaTaggging_for_" + l.id + "(feat: {properties: Record<string, string>}) {"
|
||||||
|
)
|
||||||
|
for (const line of code) {
|
||||||
|
const firstEq = line.indexOf("=")
|
||||||
|
let attributeName = line.substring(0, firstEq).trim()
|
||||||
|
const expression = line.substring(firstEq + 1)
|
||||||
|
const isStrict = attributeName.endsWith(":")
|
||||||
|
if (!isStrict) {
|
||||||
|
allCode.push(
|
||||||
|
" Utils.AddLazyProperty(feat.properties, '" +
|
||||||
|
attributeName +
|
||||||
|
"', () => " +
|
||||||
|
expression +
|
||||||
|
" ) "
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
attributeName = attributeName.substring(0, attributeName.length - 2).trim()
|
||||||
|
allCode.push(" feat.properties['" + attributeName + "'] = " + expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allCode.push(" }")
|
||||||
|
allCode.push("}")
|
||||||
|
|
||||||
|
const targetDir = "./src/assets/generated/metatagging/"
|
||||||
|
if (!targetPath) {
|
||||||
|
if (!existsSync(targetDir)) {
|
||||||
|
mkdirSync(targetDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(targetPath ?? targetDir + "layer_" + l.id + ".ts", allCode.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
private buildThemeIndex(
|
private buildThemeIndex(
|
||||||
licensePaths: Set<string>,
|
licensePaths: Set<string>,
|
||||||
sharedLayers: Map<string, LayerConfigJson>,
|
sharedLayers: Map<string, LayerConfigJson>,
|
||||||
|
@ -436,6 +556,7 @@ class LayerOverviewUtils extends Script {
|
||||||
})
|
})
|
||||||
|
|
||||||
const skippedThemes: string[] = []
|
const skippedThemes: string[] = []
|
||||||
|
|
||||||
for (let i = 0; i < themeFiles.length; i++) {
|
for (let i = 0; i < themeFiles.length; i++) {
|
||||||
const themeInfo = themeFiles[i]
|
const themeInfo = themeFiles[i]
|
||||||
const themePath = themeInfo.path
|
const themePath = themeInfo.path
|
||||||
|
@ -443,6 +564,7 @@ class LayerOverviewUtils extends Script {
|
||||||
|
|
||||||
const targetPath =
|
const targetPath =
|
||||||
LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
|
LayerOverviewUtils.themePath + "/" + themePath.substring(themePath.lastIndexOf("/"))
|
||||||
|
|
||||||
const usedLayers = Array.from(
|
const usedLayers = Array.from(
|
||||||
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
|
LayerOverviewUtils.extractLayerIdsFrom(themeFile, false)
|
||||||
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
|
).map((id) => LayerOverviewUtils.layerPath + id + ".json")
|
||||||
|
@ -504,6 +626,8 @@ class LayerOverviewUtils extends Script {
|
||||||
|
|
||||||
this.writeTheme(themeFile)
|
this.writeTheme(themeFile)
|
||||||
fixed.set(themeFile.id, themeFile)
|
fixed.set(themeFile.id, themeFile)
|
||||||
|
|
||||||
|
this.extractJavascriptCode(themeFile)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("ERROR: could not prepare theme " + themePath + " due to " + e)
|
console.error("ERROR: could not prepare theme " + themePath + " due to " + e)
|
||||||
throw e
|
throw e
|
||||||
|
|
|
@ -200,6 +200,26 @@ function asLangSpan(t: Translation, tag = "span"): string {
|
||||||
return values.join("\n")
|
return values.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cspCached: string = undefined
|
||||||
|
function generateCsp(): string {
|
||||||
|
if (cspCached !== undefined) {
|
||||||
|
return cspCached
|
||||||
|
}
|
||||||
|
|
||||||
|
const csp = {
|
||||||
|
"default-src": "'self'",
|
||||||
|
"script-src": "'self'",
|
||||||
|
"img-src": "*",
|
||||||
|
"connect-src": "*",
|
||||||
|
}
|
||||||
|
const content = Object.keys(csp)
|
||||||
|
.map((k) => k + ": " + csp[k])
|
||||||
|
.join("; ")
|
||||||
|
|
||||||
|
cspCached = `<meta http-equiv="Content-Security-Policy" content="${content}">`
|
||||||
|
return cspCached
|
||||||
|
}
|
||||||
|
|
||||||
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
|
async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alreadyWritten) {
|
||||||
Locale.language.setData(layout.language[0])
|
Locale.language.setData(layout.language[0])
|
||||||
const targetLanguage = layout.language[0]
|
const targetLanguage = layout.language[0]
|
||||||
|
@ -279,6 +299,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
|
||||||
Translations.t.general.poweredByOsm.textFor(targetLanguage)
|
Translations.t.general.poweredByOsm.textFor(targetLanguage)
|
||||||
)
|
)
|
||||||
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
|
.replace(/<!-- THEME-SPECIFIC -->.*<!-- THEME-SPECIFIC-END-->/s, themeSpecific)
|
||||||
|
.replace(/<!-- CSP -->/, generateCsp())
|
||||||
.replace(
|
.replace(
|
||||||
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
|
/<!-- DESCRIPTION START -->.*<!-- DESCRIPTION END -->/s,
|
||||||
asLangSpan(layout.shortDescription)
|
asLangSpan(layout.shortDescription)
|
||||||
|
@ -298,7 +319,12 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
|
||||||
|
|
||||||
async function createIndexFor(theme: LayoutConfig) {
|
async function createIndexFor(theme: LayoutConfig) {
|
||||||
const filename = "index_" + theme.id + ".ts"
|
const filename = "index_" + theme.id + ".ts"
|
||||||
writeFileSync(filename, `import layout from "./src/assets/generated/themes/${theme.id}.json"\n`)
|
|
||||||
|
const imports = [
|
||||||
|
`import layout from "./src/assets/generated/themes/${theme.id}.json"`,
|
||||||
|
`import { ThemeMetaTagging } from "./src/assets/generated/metatagging/${theme.id}"`,
|
||||||
|
]
|
||||||
|
writeFileSync(filename, imports.join("\n") + "\n")
|
||||||
|
|
||||||
appendFileSync(filename, codeTemplate)
|
appendFileSync(filename, codeTemplate)
|
||||||
}
|
}
|
||||||
|
|
|
@ -454,12 +454,16 @@ export class ExtraFunctions {
|
||||||
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
|
"To enable this feature, add a field `calculatedTags` in the layer object, e.g.:",
|
||||||
"````",
|
"````",
|
||||||
'"calculatedTags": [',
|
'"calculatedTags": [',
|
||||||
' "_someKey=javascript-expression",',
|
' "_someKey=javascript-expression (lazy execution)",',
|
||||||
|
' "_some_other_key:=javascript expression (strict execution)',
|
||||||
' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",',
|
' "name=feat.properties.name ?? feat.properties.ref ?? feat.properties.operator",',
|
||||||
" \"_distanceCloserThen3Km=distanceTo(feat)( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
" \"_distanceCloserThen3Km=distanceTo(feat)( some_lon, some_lat) < 3 ? 'yes' : 'no'\" ",
|
||||||
" ]",
|
" ]",
|
||||||
"````",
|
"````",
|
||||||
"",
|
"",
|
||||||
|
"By using `:=` as separator, the attribute will be calculated as soone as the data is loaded (strict evaluation)",
|
||||||
|
"The default behaviour, using `=` as separator, is lazy loading",
|
||||||
|
"",
|
||||||
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
|
"The above code will be executed for every feature in the layer. The feature is accessible as `feat` and is an amended geojson object:",
|
||||||
|
|
||||||
new List([
|
new List([
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { IndexedFeatureSource } from "./FeatureSource/FeatureSource"
|
||||||
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
|
||||||
import { Utils } from "../Utils"
|
import { Utils } from "../Utils"
|
||||||
import { Store, UIEventSource } from "./UIEventSource"
|
import { Store, UIEventSource } from "./UIEventSource"
|
||||||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
|
||||||
|
@ -19,6 +18,7 @@ import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||||
export default class MetaTagging {
|
export default class MetaTagging {
|
||||||
private static errorPrintCount = 0
|
private static errorPrintCount = 0
|
||||||
private static readonly stopErrorOutputAt = 10
|
private static readonly stopErrorOutputAt = 10
|
||||||
|
private static metataggingObject: any = undefined
|
||||||
private static retaggingFuncCache = new Map<
|
private static retaggingFuncCache = new Map<
|
||||||
string,
|
string,
|
||||||
((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]
|
((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]
|
||||||
|
@ -77,6 +77,23 @@ export default class MetaTagging {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
/**
|
||||||
|
* The 'metaTagging'-object is an object which contains some functions.
|
||||||
|
* Those functions are named `metaTaggging_for_<layer_name>` and are constructed based on the 'calculatedField' for this layer.
|
||||||
|
*
|
||||||
|
* If they are set, those functions will be used instead of parsing them at runtime.
|
||||||
|
*
|
||||||
|
* This means that we can avoid using eval, resulting in faster and safer code (at the cost of more complexity) - at least for official themes.
|
||||||
|
*
|
||||||
|
* Note: this function might appear unused while developing, it is used in the generated `index_<themename>.ts` files.
|
||||||
|
*
|
||||||
|
* @param metatagging
|
||||||
|
*/
|
||||||
|
public static setThemeMetatagging(metatagging: any) {
|
||||||
|
MetaTagging.metataggingObject = metatagging
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method (re)calculates all metatags and calculated tags on every given feature.
|
* This method (re)calculates all metatags and calculated tags on every given feature.
|
||||||
* The given features should be part of the given layer
|
* The given features should be part of the given layer
|
||||||
|
@ -298,6 +315,38 @@ export default class MetaTagging {
|
||||||
layer: LayerConfig,
|
layer: LayerConfig,
|
||||||
helpers: Record<ExtraFuncType, (feature: Feature) => Function>
|
helpers: Record<ExtraFuncType, (feature: Feature) => Function>
|
||||||
): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean {
|
): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean {
|
||||||
|
if (MetaTagging.metataggingObject) {
|
||||||
|
const funcName = "metaTaggging_for_" + layer.id
|
||||||
|
if (typeof MetaTagging.metataggingObject[funcName] !== "function") {
|
||||||
|
console.log(MetaTagging.metataggingObject)
|
||||||
|
throw (
|
||||||
|
"Error: metatagging-object for this theme does not have an entry at " +
|
||||||
|
funcName +
|
||||||
|
" (or it is not a function)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// public metaTaggging_for_walls_and_buildings(feat: Feature, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>) {
|
||||||
|
//
|
||||||
|
const func: (feat: Feature, helperFunctions: Record<string, any>) => void =
|
||||||
|
MetaTagging.metataggingObject[funcName]
|
||||||
|
return (feature: Feature) => {
|
||||||
|
const tags = feature.properties
|
||||||
|
if (tags === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
func(feature, helpers)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not calculate calculated tags in exported class: ", e)
|
||||||
|
}
|
||||||
|
return true // Something changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"Static MetataggingObject for theme is not set; using `new Function` (aka `eval`) to get calculated tags. This might trip up the CSP"
|
||||||
|
)
|
||||||
|
|
||||||
const calculatedTags: [string, string, boolean][] = layer.calculatedTags
|
const calculatedTags: [string, string, boolean][] = layer.calculatedTags
|
||||||
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
if (calculatedTags === undefined || calculatedTags.length === 0) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
@ -16,6 +16,7 @@ import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
||||||
import FeatureSwitchState from "./FeatureSwitchState"
|
import FeatureSwitchState from "./FeatureSwitchState"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters"
|
||||||
|
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||||
|
@ -326,12 +327,15 @@ export default class UserRelatedState {
|
||||||
},
|
},
|
||||||
[translationMode]
|
[translationMode]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const usersettingMetaTagging = new ThemeMetaTagging()
|
||||||
osmConnection.userDetails.addCallback((userDetails) => {
|
osmConnection.userDetails.addCallback((userDetails) => {
|
||||||
for (const k in userDetails) {
|
for (const k in userDetails) {
|
||||||
amendedPrefs.data["_" + k] = "" + userDetails[k]
|
amendedPrefs.data["_" + k] = "" + userDetails[k]
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [name, code, _] of usersettingsConfig.calculatedTags) {
|
usersettingMetaTagging.metaTaggging_for_usersettings({ properties: amendedPrefs.data })
|
||||||
|
/*for (const [name, code, _] of usersettingsConfig.calculatedTags) {
|
||||||
try {
|
try {
|
||||||
let result = new Function("feat", "return " + code + ";")({
|
let result = new Function("feat", "return " + code + ";")({
|
||||||
properties: amendedPrefs.data,
|
properties: amendedPrefs.data,
|
||||||
|
@ -349,7 +353,7 @@ export default class UserRelatedState {
|
||||||
e
|
e
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
|
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
|
||||||
const isTranslator = translators.contributors.find(
|
const isTranslator = translators.contributors.find(
|
||||||
|
|
13
src/Logic/State/UserSettingsMetaTagging.ts
Normal file
13
src/Logic/State/UserSettingsMetaTagging.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||||
|
export class ThemeMetaTagging {
|
||||||
|
public static readonly themeName = "usersettings"
|
||||||
|
|
||||||
|
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||||
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||||
|
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||||
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||||
|
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||||
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||||
|
}
|
||||||
|
}
|
|
@ -838,6 +838,15 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layerConfig = new LayerConfig(json, "validation", true)
|
||||||
|
for (const [attribute, code, isStrict] of layerConfig.calculatedTags ?? []) {
|
||||||
|
try {
|
||||||
|
new Function("feat", "return " + code + ";")
|
||||||
|
} catch (e) {
|
||||||
|
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (json.source === "special") {
|
if (json.source === "special") {
|
||||||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||||
errors.push(
|
errors.push(
|
||||||
|
|
|
@ -204,12 +204,6 @@ export default class LayerConfig extends WithContextLoader {
|
||||||
}
|
}
|
||||||
const code = kv.substring(index + 1)
|
const code = kv.substring(index + 1)
|
||||||
|
|
||||||
try {
|
|
||||||
new Function("feat", "return " + code + ";")
|
|
||||||
} catch (e) {
|
|
||||||
throw `Invalid function definition: the custom javascript is invalid:${e} (at ${context}). The offending javascript code is:\n ${code}`
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calculatedTags.push([key, code, isStrict])
|
this.calculatedTags.push([key, code, isStrict])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1171,7 +1171,7 @@ export default class SpecialVisualizations {
|
||||||
new Link(
|
new Link(
|
||||||
Utils.SubstituteKeys(text, tags),
|
Utils.SubstituteKeys(text, tags),
|
||||||
Utils.SubstituteKeys(href, tags),
|
Utils.SubstituteKeys(href, tags),
|
||||||
download === undefined,
|
download === undefined && !href.startsWith("#"),
|
||||||
Utils.SubstituteKeys(download, tags)
|
Utils.SubstituteKeys(download, tags)
|
||||||
).SetClass(classnames)
|
).SetClass(classnames)
|
||||||
)
|
)
|
||||||
|
|
|
@ -152,7 +152,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
|
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
|
||||||
// set all elements owning target to target=_blank + add noopener noreferrer
|
// set all elements owning target to target=_blank + add noopener noreferrer
|
||||||
if ("target" in node) {
|
const target = node.getAttribute("target")
|
||||||
|
if (target) {
|
||||||
node.setAttribute("target", "_blank")
|
node.setAttribute("target", "_blank")
|
||||||
node.setAttribute("rel", "noopener noreferrer")
|
node.setAttribute("rel", "noopener noreferrer")
|
||||||
}
|
}
|
||||||
|
@ -307,10 +308,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a property to the given object, but the value will _only_ be calculated when it is actually requested
|
* Adds a property to the given object, but the value will _only_ be calculated when it is actually requested.
|
||||||
|
* This calculation will run once
|
||||||
* @param object
|
* @param object
|
||||||
* @param name
|
* @param name
|
||||||
* @param init
|
* @param init
|
||||||
|
* @param whenDone: called when the value is updated. Note that this will be called at most once
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
public static AddLazyProperty(
|
public static AddLazyProperty(
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,7 +2,10 @@ import ThemeViewState from "./src/Models/ThemeViewState"
|
||||||
import SvelteUIElement from "./src/UI/Base/SvelteUIElement"
|
import SvelteUIElement from "./src/UI/Base/SvelteUIElement"
|
||||||
import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
|
import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
|
||||||
import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig";
|
import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig";
|
||||||
|
import MetaTagging from "./src/Logic/MetaTagging";
|
||||||
|
|
||||||
|
|
||||||
|
MetaTagging.setThemeMetatagging(new ThemeMetaTagging())
|
||||||
const state = new ThemeViewState(new LayoutConfig(<any> layout))
|
const state = new ThemeViewState(new LayoutConfig(<any> layout))
|
||||||
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
||||||
main.AttachTo("maindiv")
|
main.AttachTo("maindiv")
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0, user-scalable=no" name="viewport">
|
||||||
|
<!-- CSP // disabled -->
|
||||||
<link href="./css/mobile.css" rel="stylesheet"/>
|
<link href="./css/mobile.css" rel="stylesheet"/>
|
||||||
<link href="./css/openinghourstable.css" rel="stylesheet"/>
|
<link href="./css/openinghourstable.css" rel="stylesheet"/>
|
||||||
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
<link href="./css/tagrendering.css" rel="stylesheet"/>
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="belowmap" class="absolute top-0 left-0" style="z-index: -1;">Below</div>
|
<div id="belowmap" class="absolute top-0 left-0 -z-10">Below</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue