diff --git a/scripts/downloadCommons.ts b/scripts/downloadCommons.ts index dad3013e6..279092b36 100644 --- a/scripts/downloadCommons.ts +++ b/scripts/downloadCommons.ts @@ -2,7 +2,7 @@ * Script to download images from Wikimedia Commons, and save them together with license information. */ -import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" import { unescape } from "querystring" import SmallLicense from "../src/Models/smallLicense" @@ -123,7 +123,7 @@ const templateMapping = { "Template:CC0": "CC0 1.0", } -async function main(args: string[]) { +export async function main(args: string[]) { if (args.length < 2) { console.log("Usage: downloadCommons.ts .. ") console.log( @@ -345,4 +345,4 @@ function removeLinks(text: string): string { return text.replace(/(.*?)<\/a>/g, "$1") } -main(process.argv.slice(2)) +// main(process.argv.slice(2)) diff --git a/scripts/importCustomTheme.ts b/scripts/importCustomTheme.ts new file mode 100644 index 000000000..a94f29982 --- /dev/null +++ b/scripts/importCustomTheme.ts @@ -0,0 +1,73 @@ +import Script from "./Script" +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs" +import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" +import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" +import { DoesImageExist } from "../src/Models/ThemeConfig/Conversion/Validation" +import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" +import { ThemeConfigJson } from "../src/Models/ThemeConfig/Json/ThemeConfigJson" +import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" +import Constants from "../src/Models/Constants" +import { main as downloadCommons } from "./downloadCommons" +import { WikimediaImageProvider } from "../src/Logic/ImageProviders/WikimediaImageProvider" +import { Utils } from "../src/Utils" +import { GenerateLicenseInfo } from "./generateLicenseInfo" + +class ImportCustomTheme extends Script { + constructor() { + super("Given the path of a custom layer, will load the layer into mapcomplete as official") + } + + async main(args: string[]) { + const path = args[0] + + const layerconfig = JSON.parse(readFileSync(path, "utf-8")) + const id = layerconfig.id + const dirPath = "./assets/layers/" + id + if (!existsSync(dirPath)) { + mkdirSync(dirPath) + } + const imageFinder = new ExtractImages(true) + const theme: ThemeConfigJson = { + layers: [layerconfig], + id: "dummy-theme-during-import", + title: { + en: "Dummy", + }, + icon: "./assets/svg/plus.svg", + description: { + en: "Dummy", + }, + } + const usedImagesAll: { + path: string; + context: string, + location: (string | number)[] + }[] = imageFinder.convert(theme, ConversionContext.construct([], ["checking images"])) + const usedImages = usedImagesAll.filter(img => !img.path.startsWith("./assets")) + .filter(img => Constants.defaultPinIcons.indexOf(img.path) < 0) + console.log(usedImages) + const wm = WikimediaImageProvider.singleton + for (const usedImage of usedImages) { + if (usedImage.path.indexOf("wikimedia") >= 0) { + const filename = WikimediaImageProvider.extractFileName(usedImage.path) + console.log("Canonical URL is", filename) + await downloadCommons([dirPath, "https://commons.wikimedia.org/wiki/File:"+filename]) + console.log("Used image context:", usedImage.context) + const replaceAt = (usedImage.location) + .slice(2) // We drop ".layer.0" as we put this in a dummy theme + .map( + breadcrumb => isNaN(Number(breadcrumb)) ? breadcrumb : Number(breadcrumb), + ) + console.log("Replacement target location is", replaceAt) + Utils.WalkPath(replaceAt, layerconfig, (_, path) => { + console.log("Found",path, "to replace with", filename,"origvalue:", _) + return `./assets/layers/${id}/${filename}` + }) + } + } + writeFileSync("./assets/layers/"+id+"/"+id+".json", JSON.stringify(layerconfig, null, " ")) + } +} + +new ImportCustomTheme().run() +new GenerateLicenseInfo().run() diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index dc71c3dfa..173019a9b 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -43,6 +43,7 @@ export class WikimediaImageProvider extends ImageProvider { if (filename.startsWith("File:")) { filename = filename.substring(5) } + return filename.trim().replace(/\s+/g, "_") } @@ -51,10 +52,14 @@ export class WikimediaImageProvider extends ImageProvider { * WikimediaImageProvider.extractFileName("https://commons.wikimedia.org/wiki/File:Somefile.jpg") // => "Somefile.jpg" * WikimediaImageProvider.extractFileName("https://commons.wikimedia.org/wiki/File:S%C3%A8vres%20-%20square_madame_de_Pompadour_-_bo%C3%AEte_%C3%A0_livres.jpg?uselang=en") // => "Sèvres_-_square_madame_de_Pompadour_-_boîte_à_livres.jpg" */ - private static extractFileName(url: string) { + public static extractFileName(url: string) { if (!url.startsWith("http")) { return url } + const thumbMatch = url.match(this.thumbUrlRegex) + if (thumbMatch) { + return thumbMatch[1] + } const path = decodeURIComponent(new URL(url).pathname) return WikimediaImageProvider.makeCanonical(path.substring(path.lastIndexOf("/") + 1)) } @@ -76,16 +81,17 @@ export class WikimediaImageProvider extends ImageProvider { return WikimediaImageProvider.commonsPrefixes.some((prefix) => value.startsWith(prefix)) } + private static readonly thumbUrlRegex = /^https:\/\/upload.wikimedia.org\/.*\/([^/]+)\/.*-\1/ private static removeCommonsPrefix(value: string): string { + if (value.startsWith("https://upload.wikimedia.org/")) { value = value.substring(value.lastIndexOf("/") + 1) value = decodeURIComponent(value) - if (!value.startsWith("File:")) { - value = "File:" + value - } - return value } + if (value.startsWith("File:")) { + return value + } for (const prefix of WikimediaImageProvider.commonsPrefixes) { if (value.startsWith(prefix)) { let part = value.substr(prefix.length) @@ -139,6 +145,9 @@ export class WikimediaImageProvider extends ImageProvider { * * const result = await WikimediaImageProvider.singleton.ExtractUrls("wikimedia_commons", "File:Sèvres_-_square_madame_de_Pompadour_-_boîte_à_livres.jpg") * result[0].url_hd // => "https://commons.wikimedia.org/wiki/Special:FilePath/File%3AS%C3%A8vres_-_square_madame_de_Pompadour_-_bo%C3%AEte_%C3%A0_livres.jpg" + * + * const result = await WikimediaImageProvider.singleton.ExtractUrls("image", "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e2/Edinburgh_City_police_box_001.jpg/576px-Edinburgh_City_police_box_001.jpg") + * result[0].url_hd // => "https://commons.wikimedia.org/wiki/File:Edinburgh_City_police_box_001.jpg" */ public async ExtractUrls(key: string, value: string): undefined | Promise { const hasCommonsPrefix = WikimediaImageProvider.startsWithCommonsPrefix(value) diff --git a/src/Models/ThemeConfig/Conversion/FixImages.ts b/src/Models/ThemeConfig/Conversion/FixImages.ts index b2bc3ea33..63c45f36e 100644 --- a/src/Models/ThemeConfig/Conversion/FixImages.ts +++ b/src/Models/ThemeConfig/Conversion/FixImages.ts @@ -10,7 +10,7 @@ import { ConversionContext } from "./ConversionContext" export class ExtractImages extends Conversion< ThemeConfigJson, - { path: string; context: string }[] + { path: string; context: string; location: (string|number)[] }[] > { private static readonly layoutMetaPaths = metapaths.filter((mp) => { const typeHint = mp.hints.typehint @@ -24,11 +24,11 @@ export class ExtractImages extends Conversion< ) }) private static readonly tagRenderingMetaPaths = tagrenderingmetapaths - private _isOfficial: boolean + private readonly _isOfficial: boolean private _sharedTagRenderings: Set - constructor(isOfficial: boolean, sharedTagRenderings: Set) { - super("ExctractImages", "Extract all images from a layoutConfig using the meta paths.", []) + constructor(isOfficial: boolean, sharedTagRenderings: Set = new Set()) { + super("ExtractImages", "Extract all images from a themeConfig using the meta paths.") this._isOfficial = isOfficial this._sharedTagRenderings = sharedTagRenderings } @@ -111,8 +111,8 @@ export class ExtractImages extends Conversion< convert( json: ThemeConfigJson, context: ConversionContext - ): { path: string; context: string }[] { - const allFoundImages: { path: string; context: string }[] = [] + ): { path: string; context: string, location: (string | number)[] }[] { + const allFoundImages: { path: string; context: string, location: (string | number)[] }[] = [] for (const metapath of ExtractImages.layoutMetaPaths) { const mightBeTr = ExtractImages.mightBeTagRendering(metapath) @@ -143,7 +143,7 @@ export class ExtractImages extends Conversion< continue } - allFoundImages.push({ path: foundImage, context: context + "." + path }) + allFoundImages.push({ path: foundImage, context: context + "." + path, location: path }) } else { // This is a tagRendering. // Either every rendered value might be an icon @@ -177,6 +177,7 @@ export class ExtractImages extends Conversion< allFoundImages.push({ path: img.leaf, context: context + "." + path, + location: img.path }) } } @@ -191,6 +192,7 @@ export class ExtractImages extends Conversion< .map((path) => ({ path, context: context + "." + path, + location:img.path })) ) } @@ -211,12 +213,13 @@ export class ExtractImages extends Conversion< allFoundImages.push({ context: context.path.join(".") + "." + foundElement.path.join("."), path: foundElement.leaf, + location: foundElement.path }) } } } - const cleanedImages: { path: string; context: string }[] = [] + const cleanedImages: { path: string; context: string, location: (string | number)[] }[] = [] for (const foundImage of allFoundImages) { if (foundImage.path.startsWith("<") && foundImage.path.endsWith(">")) { @@ -225,7 +228,8 @@ export class ExtractImages extends Conversion< const images = Array.from(doc.getElementsByTagName("img")) const paths = images.map((i) => i.getAttribute("src")) cleanedImages.push( - ...paths.map((path) => ({ path, context: foundImage.context + " (in html)" })) + ...paths.map((path) => ({ path, context: foundImage.context + " (in html)", + location: foundImage.location })) ) continue } @@ -242,7 +246,7 @@ export class ExtractImages extends Conversion< ) ) for (const path of allPaths) { - cleanedImages.push({ path, context: foundImage.context }) + cleanedImages.push({ path, context: foundImage.context , location:foundImage.location}) } } @@ -255,9 +259,8 @@ export class FixImages extends DesugaringStep { constructor(knownImages: Set) { super( + "fixImages", "Walks over the entire theme and replaces images to the relative URL. Only works if the ID of the theme is an URL", - [], - "fixImages" ) this._knownImages = knownImages } diff --git a/src/Utils.ts b/src/Utils.ts index cd1138d1e..5775883d4 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -9,7 +9,7 @@ export class Utils { public static runningFromConsole = typeof window === "undefined" public static externalDownloadFunction: ( url: string, - headers?: any + headers?: any, ) => Promise<{ content: string } | { redirect: string }> public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. @@ -61,7 +61,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (Utils.runningFromConsole) { return } - DOMPurify.addHook("afterSanitizeAttributes", function (node) { + DOMPurify.addHook("afterSanitizeAttributes", function(node) { // set all elements owning target to target=_blank + add noopener noreferrer const target = node.getAttribute("target") if (target) { @@ -83,7 +83,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static ParseVisArgs>( specs: { name: string; defaultValue?: string }[], - args: string[] + args: string[], ): T { const parsed: Record = {} if (args.length > specs.length) { @@ -238,7 +238,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be object: any, name: string, init: () => any, - whenDone?: () => void + whenDone?: () => void, ) { Object.defineProperty(object, name, { enumerable: false, @@ -266,7 +266,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be object: any, name: string, init: () => Promise, - whenDone?: () => void + whenDone?: () => void, ) { Object.defineProperty(object, name, { enumerable: false, @@ -334,7 +334,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be */ public static DedupOnId( arr: T[], - toKey?: (t: T) => string | string[] + toKey?: (t: T) => string | string[], ): T[] { const uniq: T[] = [] const seen = new Set() @@ -461,7 +461,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static SubstituteKeys( txt: string | undefined, tags: Record | undefined, - useLang?: string + useLang?: string, ): string | undefined { if (txt === undefined) { return undefined @@ -493,7 +493,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key, "\nThe value is", - v + v, ) v = v.InnerConstructElement()?.textContent } @@ -614,7 +614,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (!Array.isArray(targetV)) { throw new Error( "Cannot concatenate: value to add is not an array: " + - JSON.stringify(targetV) + JSON.stringify(targetV), ) } if (Array.isArray(sourceV)) { @@ -622,9 +622,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } else { throw new Error( "Could not merge concatenate " + - JSON.stringify(sourceV) + - " and " + - JSON.stringify(targetV) + JSON.stringify(sourceV) + + " and " + + JSON.stringify(targetV), ) } } else { @@ -660,16 +660,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be /** * Walks the specified path into the object till the end. * - * If a list is encountered, this is transparently walked recursively on every object. + * If a list is encountered, the behaviour depends on the next breadcrumb: + * - if it is a string, the list is transparently walked recursively on every object. + * - it is is a number, that index will be taken + * * If 'null' or 'undefined' is encountered, this method stops * * The leaf objects are replaced in the object itself by the specified function. */ public static WalkPath( - path: string[], + path: (string | number)[], object: any, replaceLeaf: (leaf: any, travelledPath: string[]) => any, - travelledPath: string[] = [] + travelledPath: (string)[] = [], ): void { if (object == null) { return @@ -699,12 +702,28 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be return } if (Array.isArray(sub)) { - sub.forEach((el, i) => - Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, head, "" + i]) - ) + if (typeof path[1] === "number") { + const i = path[1] + if(path.length == 2){ + // We found the leaf + const leaf = sub[i] + if (leaf !== undefined) { + sub[i] = replaceLeaf(leaf, [...travelledPath, ""+head,""+i]) + if (sub[i] === undefined) { + delete sub[i] + } + } + }else{ + Utils.WalkPath(path.slice(2), sub[i], replaceLeaf, [...travelledPath, "" + head, "" + i]) + } + } else { + sub.forEach((el, i) => + Utils.WalkPath(path.slice(1), el, replaceLeaf, [...travelledPath, "" + head, "" + i]), + ) + } return } - Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, head]) + Utils.WalkPath(path.slice(1), sub, replaceLeaf, [...travelledPath, ""+head]) } /** @@ -717,7 +736,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be path: string[], object: any, collectedList: { leaf: any; path: string[] }[] = [], - travelledPath: string[] = [] + travelledPath: string[] = [], ): { leaf: any; path: string[] }[] { if (object === undefined || object === null) { return collectedList @@ -747,7 +766,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be if (Array.isArray(sub)) { sub.forEach((el, i) => - Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]) + Utils.CollectPath(path.slice(1), el, collectedList, [...travelledPath, "" + i]), ) return collectedList } @@ -791,7 +810,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be json: any, f: (v: object | number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, - path: string[] = [] + path: string[] = [], ) { if (json === undefined || json === null) { return f(json, path) @@ -830,7 +849,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be json: any, collect: (v: number | string | boolean | undefined, path: string[]) => any, isLeaf: (object) => boolean = undefined, - path = [] + path = [], ): void { if (json === undefined) { return @@ -875,7 +894,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static async download( url: string, - headers?: Record + headers?: Record, ): Promise { const result = await Utils.downloadAdvanced(url, headers) if (result["error"] !== undefined) { @@ -889,7 +908,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be headers?: Record, method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET", content?: string, - maxAttempts: number = 3 + maxAttempts: number = 3, ): Promise< | { content: string } | { redirect: string } @@ -913,7 +932,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be console.log( `Request to ${url} failed, Trying again in a moment. Attempt ${ i + 1 - }/${maxAttempts}` + }/${maxAttempts}`, ) await Utils.waitFor((i + 1) * 500) } @@ -927,7 +946,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be url: string, headers?: Record, method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET", - content?: string + content?: string, ): Promise< | { content: string } | { redirect: string } @@ -970,12 +989,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be xhr.onerror = (ev: ProgressEvent) => reject( "Could not get " + - url + - ", xhr status code is " + - xhr.status + - " (" + - xhr.statusText + - ")" + url + + ", xhr status code is " + + xhr.status + + " (" + + xhr.statusText + + ")", ) }) } @@ -983,7 +1002,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static upload( url: string, data: string | Blob, - headers?: Record + headers?: Record, ): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() @@ -1012,13 +1031,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be url: string, maxCacheTimeMs: number, headers?: Record, - dontCacheErrors: boolean = false + dontCacheErrors: boolean = false, ): Promise { const result = await Utils.downloadJsonCachedAdvanced( url, maxCacheTimeMs, headers, - dontCacheErrors + dontCacheErrors, ) if (result["content"]) { return result["content"] @@ -1031,7 +1050,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be maxCacheTimeMs: number, headers?: Record, dontCacheErrors = false, - maxAttempts = 3 + maxAttempts = 3, ): Promise< { content: T } | { error: string; url: string; statuscode?: number; errContent?: object } > { @@ -1043,10 +1062,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } const promise = /*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced( - url, - headers, - maxAttempts - ) + url, + headers, + maxAttempts, + ) Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() }) try { return await promise @@ -1060,12 +1079,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static async downloadJson( url: string, - headers?: Record + headers?: Record, ): Promise public static async downloadJson(url: string, headers?: Record): Promise public static async downloadJson( url: string, - headers?: Record + headers?: Record, ): Promise { const result = await Utils.downloadJsonAdvanced(url, headers) if (result["content"]) { @@ -1085,7 +1104,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static async downloadJsonAdvanced( url: string, headers?: Record, - maxAttempts = 3 + maxAttempts = 3, ): Promise< { content: T } | { error: string; url: string; statuscode?: number; errContent?: object } > { @@ -1098,7 +1117,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be Utils.Merge({ accept: "application/json" }, headers ?? {}), "GET", undefined, - maxAttempts + maxAttempts, ) if (result["error"] !== undefined) { return <{ error: string; url: string; statuscode?: number }>result @@ -1121,7 +1140,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be "due to", e, "\n", - e.stack + e.stack, ) return { error: "malformed", url } } @@ -1142,7 +1161,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | "{gpx=application/gpx+xml}" | "application/json" | "image/png" - } + }, ) { const element = document.createElement("a") let file @@ -1263,19 +1282,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static sortedByLevenshteinDistance( reference: string, - ts: ReadonlyArray + ts: ReadonlyArray, ): string[] public static sortedByLevenshteinDistance( reference: string, ts: ReadonlyArray, - getName: (t: T) => string + getName: (t: T) => string, ): T[] public static sortedByLevenshteinDistance( reference: string, ts: ReadonlyArray, - getName?: (t: T) => string + getName?: (t: T) => string, ): T[] { - getName ??= (str) => str; + getName ??= (str) => str const withDistance: [T, number][] = ts.map((t) => [ t, Utils.levenshteinDistance(getName(t), reference), @@ -1300,7 +1319,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be track[j][i] = Math.min( track[j][i - 1] + 1, // deletion track[j - 1][i] + 1, // insertion - track[j - 1][i - 1] + indicator // substitution + track[j - 1][i - 1] + indicator, // substitution ) } } @@ -1310,11 +1329,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static MapToObj(d: Map): Record public static MapToObj( d: Map, - onValue: (t: V, key: string) => T + onValue: (t: V, key: string) => T, ): Record public static MapToObj( d: Map, - onValue: (t: V, key: string) => T = undefined + onValue: (t: V, key: string) => T = undefined, ): Record { const o = {} const keys = Array.from(d.keys()) @@ -1332,7 +1351,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * Utils.TransposeMap({"a" : ["b", "c"], "x" : ["b", "y"]}) // => {"b" : ["a", "x"], "c" : ["a"], "y" : ["x"]} */ public static TransposeMap( - d: Record + d: Record, ): Record { const newD: Record = {} @@ -1355,7 +1374,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * {"a": "b", "c":"d"} // => {"b":"a", "d":"c"} */ public static transposeMapSimple( - d: Record + d: Record, ): Record { const inv = >{} for (const k in d) { @@ -1438,7 +1457,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } public static asDict( - tags: { key: string; value: string | number }[] + tags: { key: string; value: string | number }[], ): Map { const d = new Map() @@ -1451,7 +1470,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be public static asRecord( keys: K[], - f: (k: K) => V + f: (k: K) => V, ): Record { const results = >{} for (const key of keys) { @@ -1564,7 +1583,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be * */ public static splitIntoSubstitutionParts( - template: string + template: string, ): ({ message: string } | { subs: string })[] { const preparts = template.split("{") const spec: ({ message: string } | { subs: string })[] = [] @@ -1738,7 +1757,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } private static findParentWithScrolling( - element: HTMLBaseElement | HTMLDivElement + element: HTMLBaseElement | HTMLDivElement, ): HTMLBaseElement | HTMLDivElement { // Check if the element itself has scrolling if (element.scrollHeight > element.clientHeight) { @@ -1820,6 +1839,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be href = href.replaceAll(/ /g, "%20") return href } + private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u /** @@ -1855,6 +1875,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be for (const key of allKeys) { copy[key] = object[key] } - return copy + return copy } }