diff --git a/gpx/src/gpx.ts b/gpx/src/gpx.ts index a69a07ac..54612f18 100644 --- a/gpx/src/gpx.ts +++ b/gpx/src/gpx.ts @@ -216,7 +216,7 @@ export class GPXFile extends GPXTreeNode{ let file: GPXFileType = { attributes: cloneJSON(this.attributes), metadata: {}, - wpt: this.wpt, + wpt: this.wpt.map((wpt) => wpt.toWaypointType(exclude)), trk: this.trk.map((track) => track.toTrackType(exclude)), rte: [], }; @@ -1107,6 +1107,23 @@ export class Waypoint { return this.attributes.lon; } + toWaypointType(exclude: string[] = []): WaypointType { + let wpt: WaypointType = { + attributes: this.attributes, + ele: this.ele, + name: this.name, + cmt: this.cmt, + desc: this.desc, + link: this.link, + sym: this.sym, + type: this.type, + }; + if (!exclude.includes('time')) { + wpt = { ...wpt, time: this.time }; + } + return wpt; + } + clone(): Waypoint { return new Waypoint({ attributes: cloneJSON(this.attributes), diff --git a/website/package-lock.json b/website/package-lock.json index c3d5713e..f3a09880 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -23,6 +23,7 @@ "mapbox-gl": "^3.4.0", "mapillary-js": "^4.1.2", "mode-watcher": "^0.3.1", + "sanitize-html": "^2.13.0", "sortablejs": "^1.15.2", "svelte-i18n": "^4.0.0", "svelte-sonner": "^0.3.24", @@ -40,6 +41,7 @@ "@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/mapbox-gl": "^3.1.0", "@types/node": "^20.14.6", + "@types/sanitize-html": "^2.11.0", "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", @@ -2039,6 +2041,15 @@ "@types/node": "*" } }, + "node_modules/@types/sanitize-html": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.11.0.tgz", + "integrity": "sha512-7oxPGNQHXLHE48r/r/qjn7q0hlrs3kL7oZnGj0Wf/h9tj/6ibFyRkNbsDxaBBZ4XUZ0Dx5LGCyDJ04ytSofacQ==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/sortablejs": { "version": "1.15.8", "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", @@ -3060,11 +3071,62 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", @@ -3094,6 +3156,17 @@ "once": "^1.4.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/error/-/error-4.4.0.tgz", @@ -3233,7 +3306,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -3925,6 +3997,24 @@ "node": ">=10" } }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -4154,6 +4244,14 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -4918,6 +5016,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5777,6 +5880,19 @@ "rimraf": "bin.js" } }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", diff --git a/website/package.json b/website/package.json index 8627e62f..18fa5222 100644 --- a/website/package.json +++ b/website/package.json @@ -23,6 +23,7 @@ "@types/mapbox__mapbox-gl-geocoder": "^5.0.0", "@types/mapbox-gl": "^3.1.0", "@types/node": "^20.14.6", + "@types/sanitize-html": "^2.11.0", "@types/sortablejs": "^1.15.8", "@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/parser": "^7.13.1", @@ -61,10 +62,11 @@ "mapbox-gl": "^3.4.0", "mapillary-js": "^4.1.2", "mode-watcher": "^0.3.1", + "sanitize-html": "^2.13.0", "sortablejs": "^1.15.2", "svelte-i18n": "^4.0.0", "svelte-sonner": "^0.3.24", "tailwind-merge": "^2.3.0", "tailwind-variants": "^0.2.1" } -} \ No newline at end of file +} diff --git a/website/src/lib/components/gpx-layer/WaypointPopup.svelte b/website/src/lib/components/gpx-layer/WaypointPopup.svelte index 2cd3699e..f570e262 100644 --- a/website/src/lib/components/gpx-layer/WaypointPopup.svelte +++ b/website/src/lib/components/gpx-layer/WaypointPopup.svelte @@ -4,11 +4,12 @@ import Shortcut from '$lib/components/Shortcut.svelte'; import { waypointPopup, currentPopupWaypoint, deleteWaypoint } from './WaypointPopup'; import WithUnits from '$lib/components/WithUnits.svelte'; - import { Dot, Trash2 } from 'lucide-svelte'; + import { Dot, ExternalLink, Trash2 } from 'lucide-svelte'; import { onMount } from 'svelte'; import { Tool, currentTool } from '$lib/stores'; import { getSymbolKey, symbols } from '$lib/assets/symbols'; import { _ } from 'svelte-i18n'; + import sanitizeHtml from 'sanitize-html'; let popupElement: HTMLDivElement; @@ -18,13 +19,35 @@ }); $: symbolKey = $currentPopupWaypoint ? getSymbolKey($currentPopupWaypoint[0].sym) : undefined; + + function sanitize(text: string | undefined): string { + if (text === undefined) { + return ''; + } + let sanitized = sanitizeHtml(text, { + allowedTags: ['a', 'br'], + allowedAttributes: { + a: ['href', 'target'] + } + }).trim(); + return sanitized; + } + + diff --git a/website/src/lib/components/toolbar/tools/Waypoint.svelte b/website/src/lib/components/toolbar/tools/Waypoint.svelte index f3b16274..309b129a 100644 --- a/website/src/lib/components/toolbar/tools/Waypoint.svelte +++ b/website/src/lib/components/toolbar/tools/Waypoint.svelte @@ -25,6 +25,7 @@ let name: string; let description: string; + let link: string; let longitude: number; let latitude: number; @@ -67,6 +68,7 @@ ) { description += '\n\n' + $selectedWaypoint[0].cmt; } + link = $selectedWaypoint[0].link?.attributes?.href ?? ''; let symbol = $selectedWaypoint[0].sym ?? ''; let symbolKey = getSymbolKey(symbol); if (symbolKey) { @@ -94,6 +96,7 @@ function resetWaypointData() { name = ''; description = ''; + link = ''; selectedSymbol = { value: '', label: '' @@ -133,10 +136,11 @@ lat: latitude, lon: longitude }, - name, - desc: description, - cmt: description, - sym: selectedSymbol.value + name: name.length > 0 ? name : undefined, + desc: description.length > 0 ? description : undefined, + cmt: description.length > 0 ? description : undefined, + link: link.length > 0 ? { attributes: { href: link } } : undefined, + sym: selectedSymbol.value.length > 0 ? selectedSymbol.value : undefined }, $selectedWaypoint ? new ListWaypointItem($selectedWaypoint[1], $selectedWaypoint[0]._data.index) @@ -200,6 +204,8 @@ {/each} + +
diff --git a/website/src/lib/db.ts b/website/src/lib/db.ts index 8b6edcd3..6753eb4f 100644 --- a/website/src/lib/db.ts +++ b/website/src/lib/db.ts @@ -927,6 +927,7 @@ export const dbUtils = { wpt.desc = waypoint.desc; wpt.cmt = waypoint.cmt; wpt.sym = waypoint.sym; + wpt.link = waypoint.link; wpt.setCoordinates(waypoint.attributes); wpt.ele = ele; }); diff --git a/website/src/locales/en.json b/website/src/locales/en.json index b521ae24..f9ff3f2d 100644 --- a/website/src/locales/en.json +++ b/website/src/locales/en.json @@ -183,6 +183,7 @@ "waypoint": { "tooltip": "Create and edit points of interest", "icon": "Icon", + "link": "Link", "longitude": "Longitude", "latitude": "Latitude", "create": "Create point of interest",