allow HTML in wpt desc, and handle link field

This commit is contained in:
vcoppe
2024-08-09 16:22:32 +02:00
parent 646b77dbb7
commit 143dd71f4a
7 changed files with 184 additions and 11 deletions

View File

@@ -216,7 +216,7 @@ export class GPXFile extends GPXTreeNode<Track>{
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),

View File

@@ -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",

View File

@@ -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"
}
}
}

View File

@@ -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;
}
</script>
<div bind:this={popupElement} class="hidden">
{#if $currentPopupWaypoint}
<Card.Root class="border-none shadow-md text-base max-w-80 p-2">
<Card.Header class="p-0">
<Card.Title class="text-md">{$currentPopupWaypoint[0].name}</Card.Title>
<Card.Title class="text-md">
{#if $currentPopupWaypoint[0].link && $currentPopupWaypoint[0].link.attributes && $currentPopupWaypoint[0].link.attributes.href}
<a href={$currentPopupWaypoint[0].link.attributes.href} target="_blank">
{$currentPopupWaypoint[0].name ?? $currentPopupWaypoint[0].link.attributes.href}
<ExternalLink size="12" class="inline-block mb-1.5" />
</a>
{:else}
{$currentPopupWaypoint[0].name ?? $_('gpx.waypoint')}
{/if}
</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col p-0 text-sm">
<div class="flex flex-row items-center text-muted-foreground text-xs whitespace-nowrap">
@@ -52,10 +75,10 @@
{/if}
</div>
{#if $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{$currentPopupWaypoint[0].desc}</span>
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].desc)}</span>
{/if}
{#if $currentPopupWaypoint[0].cmt && $currentPopupWaypoint[0].cmt !== $currentPopupWaypoint[0].desc}
<span class="whitespace-pre-wrap">{$currentPopupWaypoint[0].cmt}</span>
<span class="whitespace-pre-wrap">{@html sanitize($currentPopupWaypoint[0].cmt)}</span>
{/if}
{#if $currentTool === Tool.WAYPOINT}
<Button
@@ -73,3 +96,10 @@
</Card.Root>
{/if}
</div>
<style lang="postcss">
div :global(a) {
@apply text-blue-500 dark:text-blue-300;
@apply hover:underline;
}
</style>

View File

@@ -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}
</Select.Content>
</Select.Root>
<Label for="link">{$_('toolbar.waypoint.link')}</Label>
<Input bind:value={link} id="link" class="h-8" />
<div class="flex flex-row gap-2">
<div>
<Label for="latitude">{$_('toolbar.waypoint.latitude')}</Label>

View File

@@ -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;
});

View File

@@ -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",