@seed-ship/mcp-ui-solid 6.9.0 → 6.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.10.0] - 2026-05-31
9
+
10
+ ### Security — map popups/tooltips are text-safe by default (XSS hardening)
11
+
12
+ Leaflet renders bound tooltip/popup strings as **HTML**, so `marker.tooltip`,
13
+ `marker.popup` and a GeoJSON `popup.template` were an **XSS vector** when the
14
+ payload is untrusted — the default for an LLM/connector-driven public package
15
+ (audit `docs/briefs/AUDIT-2026-05-30-visual-renderers-g6-ontology.md`, P1.2).
16
+
17
+ `<MapRenderer>` now treats that content as **plain text by default**:
18
+
19
+ - `marker.tooltip` / `marker.popup` are HTML-escaped before binding.
20
+ - a GeoJSON `popup.template` (raw HTML) is **ignored** on the default path;
21
+ the safe auto-generated popup (`titleField` / `fields`, values already
22
+ escaped) is used instead.
23
+ - substituted `{{prop}}` values in a template are escaped even on the
24
+ trusted path (they are data, not markup).
25
+
26
+ A **host** that renders `<MapRenderer>` with trusted data can restore rich
27
+ HTML popups via the new `allowHtmlPopups` prop. This is deliberately a
28
+ host-level prop, **not** a payload field — a malicious payload could just set
29
+ its own flag. The `<UIResourceRenderer>` path never sets it, so every
30
+ payload-driven map is text-safe.
31
+
32
+ **Behavior change:** if you previously relied on HTML inside
33
+ `marker.tooltip` / `marker.popup` / `popup.template`, pass `allowHtmlPopups`
34
+ to `<MapRenderer>` (or migrate that markup into the structured
35
+ `titleField` / `fields` popup config). Maps that pass plain-text popups —
36
+ the common case — are visually unchanged.
37
+
38
+ ### Tests
39
+
40
+ - `MapRenderer.security.test.ts` — 14 cases via new exported pure helpers
41
+ `popupSafeText` and `buildPopupContent`: escapes by default, passes raw
42
+ HTML only when opted in, template gated on the untrusted path, auto-popup
43
+ always escapes values.
44
+
8
45
  ## [6.9.0] - 2026-05-31
9
46
 
10
47
  ### Added — renderer fallback ladder (no silent blanks)
@@ -44,13 +44,13 @@ function buildStyleFn(style) {
44
44
  };
45
45
  };
46
46
  }
47
- function buildPopupContent(feature, popup) {
47
+ function buildPopupContent(feature, popup, allowHtml = false) {
48
48
  if (!popup || !(feature == null ? void 0 : feature.properties)) return null;
49
49
  const props = feature.properties;
50
- if (popup.template) {
50
+ if (allowHtml && popup.template) {
51
51
  return popup.template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
52
52
  const val = props[key];
53
- return val != null ? String(val) : "";
53
+ return val != null ? escapeHtml(String(val)) : "";
54
54
  });
55
55
  }
56
56
  const parts = [];
@@ -389,4 +389,5 @@ const MapRenderer = (props) => {
389
389
  });
390
390
  };
391
391
  exports.MapRenderer = MapRenderer;
392
+ exports.buildPopupContent = buildPopupContent;
392
393
  //# sourceMappingURL=MapRenderer.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"MapRenderer.cjs","sources":["../../src/components/MapRenderer.tsx"],"sourcesContent":["/**\n * MapRenderer - Interactive map Component\n * Sprint 6: Markers + clustering\n * v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles\n */\n\nimport { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';\nimport { isServer } from 'solid-js/web';\nimport type {\n UIComponent,\n MapComponentParams,\n MapClusterOptions,\n MapGeoJSONStyle,\n MapPopupConfig,\n MapLayer,\n MapPMTilesConfig,\n} from '../types';\nimport { ExpandableWrapper, useExpanded } from './ExpandableWrapper';\nimport { DegradedFallback } from './DegradedFallback';\nimport { mapToDegradedTable } from '../utils/degraded-projections';\nimport { useTelemetry } from '../context/MCPUITelemetryContext';\n\n// Lazy load leaflet (it doesn't support SSR well)\nlet L: any = null;\n// Track if marker cluster CSS has been loaded\nlet clusterCssLoaded = false;\n\nexport interface MapRendererProps {\n /**\n * UIComponent containing map params\n */\n component?: UIComponent;\n\n /**\n * Direct map params\n */\n params?: MapComponentParams;\n\n /**\n * Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).\n * @see ExpandableWrapperProps.toolbarVariant\n */\n toolbarVariant?: 'hover' | 'always-visible';\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Resolve choropleth color for a feature based on property value and scale stops.\n */\nfunction getChoroplethColor(\n value: unknown,\n scale: Array<[number, string]>,\n fallback: string\n): string {\n if (value == null || typeof value !== 'number' || !isFinite(value)) return fallback;\n\n // Scale is sorted ascending: [[0, '#eff3ff'], [100, '#084594']]\n if (scale.length === 0) return fallback;\n if (value <= scale[0][0]) return scale[0][1];\n if (value >= scale[scale.length - 1][0]) return scale[scale.length - 1][1];\n\n // Find surrounding stops and interpolate (use upper bracket color)\n for (let i = 1; i < scale.length; i++) {\n if (value <= scale[i][0]) return scale[i][1];\n }\n return scale[scale.length - 1][1];\n}\n\n/**\n * Build a Leaflet style function from MapGeoJSONStyle config.\n */\nfunction buildStyleFn(\n style: MapGeoJSONStyle | undefined\n): (feature: any) => Record<string, unknown> {\n if (!style) {\n return () => ({\n fillColor: '#3388ff',\n fillOpacity: 0.6,\n color: '#333',\n weight: 1,\n opacity: 1,\n });\n }\n\n return (feature: any) => {\n let fillColor = style.fillColor || '#3388ff';\n\n // Choropleth: override fillColor based on feature property\n if (style.choroplethField && style.choroplethScale && feature?.properties) {\n const val = feature.properties[style.choroplethField];\n fillColor = getChoroplethColor(\n val,\n style.choroplethScale,\n style.choroplethFallback || '#ccc'\n );\n }\n\n return {\n fillColor,\n fillOpacity: style.fillOpacity ?? 0.6,\n color: style.strokeColor || '#333',\n weight: style.strokeWeight ?? 1,\n opacity: style.strokeOpacity ?? 1,\n };\n };\n}\n\n/**\n * Build popup HTML from a feature's properties using popup config.\n */\nfunction buildPopupContent(feature: any, popup: MapPopupConfig | undefined): string | null {\n if (!popup || !feature?.properties) return null;\n const props = feature.properties;\n\n // Custom template\n if (popup.template) {\n return popup.template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => {\n const val = props[key];\n return val != null ? String(val) : '';\n });\n }\n\n // Auto-generated popup\n const parts: string[] = [];\n\n if (popup.titleField && props[popup.titleField] != null) {\n parts.push(`<strong>${escapeHtml(String(props[popup.titleField]))}</strong>`);\n }\n\n const fields = popup.fields || Object.keys(props).slice(0, 8);\n for (const key of fields) {\n if (key === popup.titleField) continue;\n const val = props[key];\n if (val == null) continue;\n const formatted = typeof val === 'number' ? val.toLocaleString('fr-FR') : String(val);\n parts.push(\n `<span style=\"color:#666;font-size:11px\">${escapeHtml(key)}</span>: ${escapeHtml(formatted)}`\n );\n }\n\n return parts.join('<br/>');\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * Add a GeoJSON layer to the map with style and popup support.\n * Returns the layer for bounds calculation.\n */\nfunction addGeoJSONLayer(\n mapInst: any,\n leaflet: any,\n geojson: unknown,\n style?: MapGeoJSONStyle,\n popup?: MapPopupConfig\n): any {\n const styleFn = buildStyleFn(style);\n\n const layer = leaflet.geoJSON(geojson, {\n style: styleFn,\n pointToLayer: (feature: any, latlng: any) => {\n // Render points as circle markers for consistency\n const s = styleFn(feature);\n return leaflet.circleMarker(latlng, {\n radius: 6,\n fillColor: s.fillColor,\n fillOpacity: s.fillOpacity,\n color: s.color,\n weight: s.weight,\n opacity: s.opacity,\n });\n },\n onEachFeature: (feature: any, featureLayer: any) => {\n const html = buildPopupContent(feature, popup);\n if (html) {\n featureLayer.bindPopup(html, { maxWidth: 300 });\n }\n },\n });\n\n layer.addTo(mapInst);\n return layer;\n}\n\n// ─── Component ──────────────────────────────────────────────\n\n/**\n * Build a GeoJSON FeatureCollection from the map's `markers` (and any\n * inlined GeoJSON layers, when present). Used by the \"Copy data\" button\n * shipped via `<ExpandableWrapper>` (v6.2.0). Best-effort — clusters,\n * tile layers, and choropleth-only data don't get round-tripped.\n */\nfunction mapToGeoJSON(p: MapComponentParams | undefined): string {\n if (!p) return '{\"type\":\"FeatureCollection\",\"features\":[]}';\n const features: any[] = [];\n for (const marker of p.markers ?? []) {\n const pos: any = marker.position as any;\n // Accept both [lat, lng] tuple and {lat, lng} object shapes (v5.0.2 spec)\n const lat = Array.isArray(pos) ? pos[0] : pos?.lat;\n const lng = Array.isArray(pos) ? pos[1] : pos?.lng;\n if (typeof lat !== 'number' || typeof lng !== 'number') continue;\n features.push({\n type: 'Feature',\n geometry: { type: 'Point', coordinates: [lng, lat] },\n properties: {\n ...(marker.tooltip ? { tooltip: marker.tooltip } : {}),\n ...(marker.popup ? { popup: marker.popup } : {}),\n },\n });\n }\n return JSON.stringify({ type: 'FeatureCollection', features }, null, 2);\n}\n\nexport const MapRenderer: Component<MapRendererProps> = (props) => {\n let mapContainer: HTMLDivElement | undefined;\n let mapInstance: any = null;\n const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const isExpanded = useExpanded();\n const telemetry = useTelemetry();\n\n const params = () => props.params || (props.component?.params as MapComponentParams);\n\n // v6.2.0 — Leaflet has to be told to re-measure when its container\n // resizes (e.g. transitioning to fullscreen via ExpandableWrapper).\n // We give the DOM a tick to settle the new dimensions, then ask\n // Leaflet to reflow tiles.\n createEffect(() => {\n const expanded = isExpanded();\n if (!mapInstance) return;\n // Read the signal so the effect re-runs on toggle ; the value is\n // observed for its side effects on layout.\n void expanded;\n setTimeout(() => mapInstance?.invalidateSize?.(), 100);\n });\n\n // Initialize Map\n createEffect(async () => {\n if (isServer) return; // Don't run on server\n\n if (!L) {\n try {\n const module = await import('leaflet');\n L = module.default || module;\n await import('leaflet/dist/leaflet.css'); // Import CSS\n setIsLeafletLoaded(true);\n } catch (e) {\n console.warn('Failed to load leaflet', e);\n setError('Map library could not be loaded.');\n return;\n }\n } else {\n setIsLeafletLoaded(true);\n }\n\n if (isLeafletLoaded() && mapContainer && !mapInstance) {\n const p = params();\n const center = p?.center || [51.505, -0.09]; // Default to London\n const zoom = p?.zoom || 13;\n\n mapInstance = L.map(mapContainer, {\n zoomControl: p?.zoomControl !== false,\n scrollWheelZoom: p?.scrollWheelZoom !== false,\n attributionControl: false,\n }).setView(center, zoom);\n\n // Add OpenStreetMap tile layer\n const tileLayerUrl = p?.tileLayer || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n L.tileLayer(tileLayerUrl, {\n attribution:\n p?.attribution ||\n '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',\n }).addTo(mapInstance);\n\n if (p?.attribution !== '') {\n L.control.attribution({ prefix: false }).addTo(mapInstance);\n }\n\n // Fix marker icons (Leaflet issue with bundlers)\n delete (L.Icon.Default.prototype as any)._getIconUrl;\n L.Icon.Default.mergeOptions({\n iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',\n iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',\n shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',\n });\n }\n\n // Update markers and view\n if (mapInstance && L) {\n try {\n const p = params();\n const allBoundsLayers: any[] = [];\n\n // Clear existing layers (markers, cluster groups, GeoJSON)\n mapInstance.eachLayer((layer: any) => {\n if (\n layer instanceof L.Marker ||\n layer instanceof L.GeoJSON ||\n layer instanceof L.CircleMarker ||\n layer._group ||\n layer._featureGroup\n ) {\n mapInstance.removeLayer(layer);\n }\n });\n\n // ─── Markers (legacy) ────────────────────────\n const markers: any[] = [];\n const shouldCluster = p?.clustering && p?.markers && p.markers.length > 0;\n\n if (shouldCluster) {\n try {\n await import('leaflet.markercluster');\n if (!clusterCssLoaded) {\n await import('leaflet.markercluster/dist/MarkerCluster.css');\n await import('leaflet.markercluster/dist/MarkerCluster.Default.css');\n clusterCssLoaded = true;\n }\n const clusterOpts: MapClusterOptions =\n typeof p.clustering === 'object' ? p.clustering : {};\n const clusterGroup = (L as any).markerClusterGroup({\n maxClusterRadius: clusterOpts.maxClusterRadius ?? 80,\n spiderfyOnMaxZoom: clusterOpts.spiderfyOnMaxZoom ?? true,\n showCoverageOnHover: clusterOpts.showCoverageOnHover ?? true,\n disableClusteringAtZoom: clusterOpts.disableClusteringAtZoom,\n animate: clusterOpts.animateAddingMarkers ?? true,\n });\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n clusterGroup.addLayer(m);\n markers.push(m);\n });\n mapInstance.addLayer(clusterGroup);\n } catch {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n } else {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n\n if (markers.length > 0) {\n allBoundsLayers.push(...markers);\n }\n\n // ─── GeoJSON (v3.1.0) ───────────────────────\n if (p?.geojson) {\n const geoLayer = addGeoJSONLayer(mapInstance, L, p.geojson, p.geojsonStyle, p.popup);\n allBoundsLayers.push(geoLayer);\n }\n\n // ─── Named layers (v3.1.0) ──────────────────\n if (p?.layers && p.layers.length > 0) {\n const overlays: Record<string, any> = {};\n\n for (const layerDef of p.layers) {\n const geoLayer = addGeoJSONLayer(\n mapInstance,\n L,\n layerDef.geojson,\n layerDef.style || p?.geojsonStyle,\n layerDef.popup || p?.popup\n );\n\n overlays[layerDef.name] = geoLayer;\n allBoundsLayers.push(geoLayer);\n\n // Respect initial visibility\n if (layerDef.visible === false) {\n mapInstance.removeLayer(geoLayer);\n }\n }\n\n // Add layer control if multiple layers\n if (Object.keys(overlays).length > 1) {\n L.control.layers(null, overlays, { collapsed: true }).addTo(mapInstance);\n }\n }\n\n // ─── PMTiles (v3.1.0) ────────────────────────\n if (p?.pmtiles) {\n try {\n // @ts-ignore — optional peer dependency, may not be installed\n const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet');\n const pmConfig = p.pmtiles;\n\n const paintRules =\n pmConfig.paintRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any)[\n rule.symbolizer === 'polygon'\n ? 'PolygonSymbolizer'\n : rule.symbolizer === 'line'\n ? 'LineSymbolizer'\n : 'CircleSymbolizer'\n ]({\n fill: rule.color || '#3388ff',\n stroke: rule.color || '#333',\n width: rule.width ?? 1,\n opacity: rule.opacity ?? 0.6,\n }),\n })) || [];\n\n const labelRules =\n pmConfig.labelRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any).TextSymbolizer({\n label_props: [rule.textField],\n fontSize: rule.fontSize ?? 12,\n }),\n })) || [];\n\n const pmLayer = (protomaps as any).leafletLayer({\n url: pmConfig.url,\n attribution: pmConfig.attribution,\n paintRules,\n labelRules,\n maxZoom: pmConfig.maxZoom,\n minZoom: pmConfig.minZoom,\n });\n\n pmLayer.addTo(mapInstance);\n } catch (e) {\n console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e);\n }\n }\n\n // ─── Fit bounds ─────────────────────────────\n if (p?.fitBounds && allBoundsLayers.length > 0) {\n const group = L.featureGroup(allBoundsLayers);\n const bounds = group.getBounds();\n if (bounds.isValid()) {\n mapInstance.fitBounds(bounds.pad(0.1));\n }\n } else if (p?.center) {\n mapInstance.setView(p.center, p.zoom || mapInstance.getZoom());\n }\n } catch (err) {\n // Fallback ladder (P2.5): a Leaflet drawing failure degrades to\n // the coordinate table below instead of a blank/partial map.\n const message = err instanceof Error ? err.message : 'Failed to render map';\n setError(message);\n telemetry?.dispatch({\n type: 'render:error',\n errorMessage: message,\n id: props.component?.id ?? '',\n componentType: 'map',\n ts: Date.now(),\n });\n }\n }\n });\n\n // Cleanup\n onCleanup(() => {\n if (mapInstance) {\n mapInstance.remove();\n mapInstance = null;\n }\n });\n\n return (\n <ExpandableWrapper\n title={'Map'}\n copyData={mapToGeoJSON(params())}\n copyLabel=\"Copy markers as GeoJSON\"\n toolbarVariant={props.toolbarVariant}\n >\n <div\n class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''} ${\n isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''\n }`}\n >\n <Show when={error()}>\n {/* Fallback ladder (P2.5): degrade to a coordinate table\n rather than a bare error string. */}\n <div class=\"p-3\">\n <DegradedFallback\n message={`Map rendering failed: ${error()}`}\n caption=\"Showing the map data as a coordinate table — the interactive map is unavailable.\"\n {...mapToDegradedTable(params() ?? {})}\n />\n </div>\n </Show>\n <Show when={!error()}>\n <div\n ref={mapContainer}\n style={\n isExpanded()\n ? { height: '100%', width: '100%', 'z-index': 0 }\n : { height: params()?.height || '400px', width: '100%', 'z-index': 0 }\n }\n class={`relative z-0 ${isExpanded() ? 'flex-1 min-h-0' : ''}`}\n />\n </Show>\n </div>\n </ExpandableWrapper>\n );\n};\n"],"names":["L","clusterCssLoaded","getChoroplethColor","value","scale","fallback","isFinite","length","i","buildStyleFn","style","fillColor","fillOpacity","color","weight","opacity","feature","choroplethField","choroplethScale","properties","val","choroplethFallback","strokeColor","strokeWeight","strokeOpacity","buildPopupContent","popup","props","template","replace","_","key","String","parts","titleField","push","escapeHtml","fields","Object","keys","slice","formatted","toLocaleString","join","str","addGeoJSONLayer","mapInst","leaflet","geojson","styleFn","layer","geoJSON","pointToLayer","latlng","s","circleMarker","radius","onEachFeature","featureLayer","html","bindPopup","maxWidth","addTo","mapToGeoJSON","p","features","marker","markers","pos","position","lat","Array","isArray","lng","type","geometry","coordinates","tooltip","JSON","stringify","MapRenderer","mapContainer","mapInstance","isLeafletLoaded","setIsLeafletLoaded","createSignal","error","setError","isExpanded","useExpanded","telemetry","useTelemetry","params","component","createEffect","setTimeout","invalidateSize","isServer","module","default","e","console","warn","center","zoom","map","zoomControl","scrollWheelZoom","attributionControl","setView","tileLayerUrl","tileLayer","attribution","control","prefix","Icon","Default","prototype","_getIconUrl","mergeOptions","iconRetinaUrl","iconUrl","shadowUrl","allBoundsLayers","eachLayer","Marker","GeoJSON","CircleMarker","_group","_featureGroup","removeLayer","shouldCluster","clustering","clusterOpts","clusterGroup","markerClusterGroup","maxClusterRadius","spiderfyOnMaxZoom","showCoverageOnHover","disableClusteringAtZoom","animate","animateAddingMarkers","forEach","m","bindTooltip","addLayer","geoLayer","geojsonStyle","layers","overlays","layerDef","name","visible","collapsed","pmtiles","protomaps","pmConfig","paintRules","rule","dataLayer","symbolizer","fill","stroke","width","labelRules","TextSymbolizer","label_props","textField","fontSize","pmLayer","leafletLayer","url","maxZoom","minZoom","fitBounds","group","featureGroup","bounds","getBounds","isValid","pad","getZoom","err","message","Error","dispatch","errorMessage","id","componentType","ts","Date","now","onCleanup","remove","_$createComponent","ExpandableWrapper","title","copyData","copyLabel","toolbarVariant","children","_el$","_$getNextElement","_tmpl$3","_el$4","firstChild","_el$5","_co$","_$getNextMarker","nextSibling","_el$6","_el$7","_co$2","_$insert","Show","when","_el$2","_tmpl$","DegradedFallback","_$mergeProps","caption","mapToDegradedTable","_el$3","_tmpl$2","_ref$","_$use","_$effect","_p$","_v$","height","_v$2","_$style","t","_$className","undefined","className"],"mappings":"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"}
1
+ {"version":3,"file":"MapRenderer.cjs","sources":["../../src/components/MapRenderer.tsx"],"sourcesContent":["/**\n * MapRenderer - Interactive map Component\n * Sprint 6: Markers + clustering\n * v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles\n */\n\nimport { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';\nimport { isServer } from 'solid-js/web';\nimport type {\n UIComponent,\n MapComponentParams,\n MapClusterOptions,\n MapGeoJSONStyle,\n MapPopupConfig,\n MapLayer,\n MapPMTilesConfig,\n} from '../types';\nimport { ExpandableWrapper, useExpanded } from './ExpandableWrapper';\nimport { DegradedFallback } from './DegradedFallback';\nimport { mapToDegradedTable } from '../utils/degraded-projections';\nimport { useTelemetry } from '../context/MCPUITelemetryContext';\n\n// Lazy load leaflet (it doesn't support SSR well)\nlet L: any = null;\n// Track if marker cluster CSS has been loaded\nlet clusterCssLoaded = false;\n\nexport interface MapRendererProps {\n /**\n * UIComponent containing map params\n */\n component?: UIComponent;\n\n /**\n * Direct map params\n */\n params?: MapComponentParams;\n\n /**\n * Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).\n * @see ExpandableWrapperProps.toolbarVariant\n */\n toolbarVariant?: 'hover' | 'always-visible';\n\n /**\n * Trust marker / popup content as raw HTML (v6.10.0, audit P1.2).\n *\n * Leaflet renders bound tooltip/popup strings as HTML, so `marker.tooltip`,\n * `marker.popup` and a GeoJSON `popup.template` are XSS vectors when the\n * payload is untrusted (the default for an LLM/connector-driven public\n * package). **Default `false`** → those are escaped as plain text, and\n * `popup.template` is ignored in favour of the safe auto-generated popup.\n *\n * This is a **host-level** trust decision, deliberately NOT a payload field\n * (a malicious payload could just set its own flag). The\n * `<UIResourceRenderer>` path never sets it, so payload-driven maps are\n * always text-safe. A host rendering `<MapRenderer>` directly with trusted\n * data may set `allowHtmlPopups` to restore rich HTML popups. The\n * auto-generated popup (`titleField` / `fields`) is safe-by-construction\n * and unaffected either way.\n */\n allowHtmlPopups?: boolean;\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Resolve choropleth color for a feature based on property value and scale stops.\n */\nfunction getChoroplethColor(\n value: unknown,\n scale: Array<[number, string]>,\n fallback: string\n): string {\n if (value == null || typeof value !== 'number' || !isFinite(value)) return fallback;\n\n // Scale is sorted ascending: [[0, '#eff3ff'], [100, '#084594']]\n if (scale.length === 0) return fallback;\n if (value <= scale[0][0]) return scale[0][1];\n if (value >= scale[scale.length - 1][0]) return scale[scale.length - 1][1];\n\n // Find surrounding stops and interpolate (use upper bracket color)\n for (let i = 1; i < scale.length; i++) {\n if (value <= scale[i][0]) return scale[i][1];\n }\n return scale[scale.length - 1][1];\n}\n\n/**\n * Build a Leaflet style function from MapGeoJSONStyle config.\n */\nfunction buildStyleFn(\n style: MapGeoJSONStyle | undefined\n): (feature: any) => Record<string, unknown> {\n if (!style) {\n return () => ({\n fillColor: '#3388ff',\n fillOpacity: 0.6,\n color: '#333',\n weight: 1,\n opacity: 1,\n });\n }\n\n return (feature: any) => {\n let fillColor = style.fillColor || '#3388ff';\n\n // Choropleth: override fillColor based on feature property\n if (style.choroplethField && style.choroplethScale && feature?.properties) {\n const val = feature.properties[style.choroplethField];\n fillColor = getChoroplethColor(\n val,\n style.choroplethScale,\n style.choroplethFallback || '#ccc'\n );\n }\n\n return {\n fillColor,\n fillOpacity: style.fillOpacity ?? 0.6,\n color: style.strokeColor || '#333',\n weight: style.strokeWeight ?? 1,\n opacity: style.strokeOpacity ?? 1,\n };\n };\n}\n\n/**\n * Build popup HTML from a feature's properties using popup config.\n *\n * `allowHtml` (audit P1.2) gates the raw-HTML `popup.template`: it is honored\n * only when the host trusts the payload. On the default (untrusted) path the\n * template is ignored and we fall through to the auto-generated popup, whose\n * structure is renderer-authored and whose values are always escaped — safe\n * by construction. Even in the trusted path, substituted `{{prop}}` values\n * are escaped (they are data, not markup).\n */\nexport function buildPopupContent(\n feature: any,\n popup: MapPopupConfig | undefined,\n allowHtml = false\n): string | null {\n if (!popup || !feature?.properties) return null;\n const props = feature.properties;\n\n // Custom template — raw HTML, so trusted-host only.\n if (allowHtml && popup.template) {\n return popup.template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => {\n const val = props[key];\n return val != null ? escapeHtml(String(val)) : '';\n });\n }\n\n // Auto-generated popup\n const parts: string[] = [];\n\n if (popup.titleField && props[popup.titleField] != null) {\n parts.push(`<strong>${escapeHtml(String(props[popup.titleField]))}</strong>`);\n }\n\n const fields = popup.fields || Object.keys(props).slice(0, 8);\n for (const key of fields) {\n if (key === popup.titleField) continue;\n const val = props[key];\n if (val == null) continue;\n const formatted = typeof val === 'number' ? val.toLocaleString('fr-FR') : String(val);\n parts.push(\n `<span style=\"color:#666;font-size:11px\">${escapeHtml(key)}</span>: ${escapeHtml(formatted)}`\n );\n }\n\n return parts.join('<br/>');\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * Resolve a marker's tooltip/popup string for binding (audit P1.2). Leaflet\n * renders bound strings as HTML; `marker.tooltip` / `marker.popup` come from\n * the (untrusted) payload, so they are escaped to plain text unless the host\n * has opted into raw HTML via `allowHtmlPopups`. Pure + exported for tests.\n */\nexport function popupSafeText(value: string | undefined, allowHtml = false): string | undefined {\n if (value == null) return undefined;\n return allowHtml ? value : escapeHtml(value);\n}\n\n/** Bind a marker's tooltip + popup, escaping by default (see popupSafeText). */\nfunction bindMarkerContent(\n m: any,\n marker: { tooltip?: string; popup?: string },\n allowHtml: boolean\n): void {\n const tooltip = popupSafeText(marker.tooltip, allowHtml);\n if (tooltip) m.bindTooltip(tooltip);\n const popup = popupSafeText(marker.popup, allowHtml);\n if (popup) m.bindPopup(popup);\n}\n\n/**\n * Add a GeoJSON layer to the map with style and popup support.\n * Returns the layer for bounds calculation.\n */\nfunction addGeoJSONLayer(\n mapInst: any,\n leaflet: any,\n geojson: unknown,\n style?: MapGeoJSONStyle,\n popup?: MapPopupConfig\n): any {\n const styleFn = buildStyleFn(style);\n\n const layer = leaflet.geoJSON(geojson, {\n style: styleFn,\n pointToLayer: (feature: any, latlng: any) => {\n // Render points as circle markers for consistency\n const s = styleFn(feature);\n return leaflet.circleMarker(latlng, {\n radius: 6,\n fillColor: s.fillColor,\n fillOpacity: s.fillOpacity,\n color: s.color,\n weight: s.weight,\n opacity: s.opacity,\n });\n },\n onEachFeature: (feature: any, featureLayer: any) => {\n const html = buildPopupContent(feature, popup);\n if (html) {\n featureLayer.bindPopup(html, { maxWidth: 300 });\n }\n },\n });\n\n layer.addTo(mapInst);\n return layer;\n}\n\n// ─── Component ──────────────────────────────────────────────\n\n/**\n * Build a GeoJSON FeatureCollection from the map's `markers` (and any\n * inlined GeoJSON layers, when present). Used by the \"Copy data\" button\n * shipped via `<ExpandableWrapper>` (v6.2.0). Best-effort — clusters,\n * tile layers, and choropleth-only data don't get round-tripped.\n */\nfunction mapToGeoJSON(p: MapComponentParams | undefined): string {\n if (!p) return '{\"type\":\"FeatureCollection\",\"features\":[]}';\n const features: any[] = [];\n for (const marker of p.markers ?? []) {\n const pos: any = marker.position as any;\n // Accept both [lat, lng] tuple and {lat, lng} object shapes (v5.0.2 spec)\n const lat = Array.isArray(pos) ? pos[0] : pos?.lat;\n const lng = Array.isArray(pos) ? pos[1] : pos?.lng;\n if (typeof lat !== 'number' || typeof lng !== 'number') continue;\n features.push({\n type: 'Feature',\n geometry: { type: 'Point', coordinates: [lng, lat] },\n properties: {\n ...(marker.tooltip ? { tooltip: marker.tooltip } : {}),\n ...(marker.popup ? { popup: marker.popup } : {}),\n },\n });\n }\n return JSON.stringify({ type: 'FeatureCollection', features }, null, 2);\n}\n\nexport const MapRenderer: Component<MapRendererProps> = (props) => {\n let mapContainer: HTMLDivElement | undefined;\n let mapInstance: any = null;\n const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const isExpanded = useExpanded();\n const telemetry = useTelemetry();\n // Host-level trust for raw HTML in marker/popup content (audit P1.2).\n // Default false → tooltip/popup escaped, GeoJSON `popup.template` ignored.\n const allowHtml = () => props.allowHtmlPopups === true;\n\n const params = () => props.params || (props.component?.params as MapComponentParams);\n\n // v6.2.0 — Leaflet has to be told to re-measure when its container\n // resizes (e.g. transitioning to fullscreen via ExpandableWrapper).\n // We give the DOM a tick to settle the new dimensions, then ask\n // Leaflet to reflow tiles.\n createEffect(() => {\n const expanded = isExpanded();\n if (!mapInstance) return;\n // Read the signal so the effect re-runs on toggle ; the value is\n // observed for its side effects on layout.\n void expanded;\n setTimeout(() => mapInstance?.invalidateSize?.(), 100);\n });\n\n // Initialize Map\n createEffect(async () => {\n if (isServer) return; // Don't run on server\n\n if (!L) {\n try {\n const module = await import('leaflet');\n L = module.default || module;\n await import('leaflet/dist/leaflet.css'); // Import CSS\n setIsLeafletLoaded(true);\n } catch (e) {\n console.warn('Failed to load leaflet', e);\n setError('Map library could not be loaded.');\n return;\n }\n } else {\n setIsLeafletLoaded(true);\n }\n\n if (isLeafletLoaded() && mapContainer && !mapInstance) {\n const p = params();\n const center = p?.center || [51.505, -0.09]; // Default to London\n const zoom = p?.zoom || 13;\n\n mapInstance = L.map(mapContainer, {\n zoomControl: p?.zoomControl !== false,\n scrollWheelZoom: p?.scrollWheelZoom !== false,\n attributionControl: false,\n }).setView(center, zoom);\n\n // Add OpenStreetMap tile layer\n const tileLayerUrl = p?.tileLayer || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n L.tileLayer(tileLayerUrl, {\n attribution:\n p?.attribution ||\n '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',\n }).addTo(mapInstance);\n\n if (p?.attribution !== '') {\n L.control.attribution({ prefix: false }).addTo(mapInstance);\n }\n\n // Fix marker icons (Leaflet issue with bundlers)\n delete (L.Icon.Default.prototype as any)._getIconUrl;\n L.Icon.Default.mergeOptions({\n iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',\n iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',\n shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',\n });\n }\n\n // Update markers and view\n if (mapInstance && L) {\n try {\n const p = params();\n const allBoundsLayers: any[] = [];\n\n // Clear existing layers (markers, cluster groups, GeoJSON)\n mapInstance.eachLayer((layer: any) => {\n if (\n layer instanceof L.Marker ||\n layer instanceof L.GeoJSON ||\n layer instanceof L.CircleMarker ||\n layer._group ||\n layer._featureGroup\n ) {\n mapInstance.removeLayer(layer);\n }\n });\n\n // ─── Markers (legacy) ────────────────────────\n const markers: any[] = [];\n const shouldCluster = p?.clustering && p?.markers && p.markers.length > 0;\n\n if (shouldCluster) {\n try {\n await import('leaflet.markercluster');\n if (!clusterCssLoaded) {\n await import('leaflet.markercluster/dist/MarkerCluster.css');\n await import('leaflet.markercluster/dist/MarkerCluster.Default.css');\n clusterCssLoaded = true;\n }\n const clusterOpts: MapClusterOptions =\n typeof p.clustering === 'object' ? p.clustering : {};\n const clusterGroup = (L as any).markerClusterGroup({\n maxClusterRadius: clusterOpts.maxClusterRadius ?? 80,\n spiderfyOnMaxZoom: clusterOpts.spiderfyOnMaxZoom ?? true,\n showCoverageOnHover: clusterOpts.showCoverageOnHover ?? true,\n disableClusteringAtZoom: clusterOpts.disableClusteringAtZoom,\n animate: clusterOpts.animateAddingMarkers ?? true,\n });\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n clusterGroup.addLayer(m);\n markers.push(m);\n });\n mapInstance.addLayer(clusterGroup);\n } catch {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n } else {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n\n if (markers.length > 0) {\n allBoundsLayers.push(...markers);\n }\n\n // ─── GeoJSON (v3.1.0) ───────────────────────\n if (p?.geojson) {\n const geoLayer = addGeoJSONLayer(mapInstance, L, p.geojson, p.geojsonStyle, p.popup);\n allBoundsLayers.push(geoLayer);\n }\n\n // ─── Named layers (v3.1.0) ──────────────────\n if (p?.layers && p.layers.length > 0) {\n const overlays: Record<string, any> = {};\n\n for (const layerDef of p.layers) {\n const geoLayer = addGeoJSONLayer(\n mapInstance,\n L,\n layerDef.geojson,\n layerDef.style || p?.geojsonStyle,\n layerDef.popup || p?.popup\n );\n\n overlays[layerDef.name] = geoLayer;\n allBoundsLayers.push(geoLayer);\n\n // Respect initial visibility\n if (layerDef.visible === false) {\n mapInstance.removeLayer(geoLayer);\n }\n }\n\n // Add layer control if multiple layers\n if (Object.keys(overlays).length > 1) {\n L.control.layers(null, overlays, { collapsed: true }).addTo(mapInstance);\n }\n }\n\n // ─── PMTiles (v3.1.0) ────────────────────────\n if (p?.pmtiles) {\n try {\n // @ts-ignore — optional peer dependency, may not be installed\n const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet');\n const pmConfig = p.pmtiles;\n\n const paintRules =\n pmConfig.paintRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any)[\n rule.symbolizer === 'polygon'\n ? 'PolygonSymbolizer'\n : rule.symbolizer === 'line'\n ? 'LineSymbolizer'\n : 'CircleSymbolizer'\n ]({\n fill: rule.color || '#3388ff',\n stroke: rule.color || '#333',\n width: rule.width ?? 1,\n opacity: rule.opacity ?? 0.6,\n }),\n })) || [];\n\n const labelRules =\n pmConfig.labelRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any).TextSymbolizer({\n label_props: [rule.textField],\n fontSize: rule.fontSize ?? 12,\n }),\n })) || [];\n\n const pmLayer = (protomaps as any).leafletLayer({\n url: pmConfig.url,\n attribution: pmConfig.attribution,\n paintRules,\n labelRules,\n maxZoom: pmConfig.maxZoom,\n minZoom: pmConfig.minZoom,\n });\n\n pmLayer.addTo(mapInstance);\n } catch (e) {\n console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e);\n }\n }\n\n // ─── Fit bounds ─────────────────────────────\n if (p?.fitBounds && allBoundsLayers.length > 0) {\n const group = L.featureGroup(allBoundsLayers);\n const bounds = group.getBounds();\n if (bounds.isValid()) {\n mapInstance.fitBounds(bounds.pad(0.1));\n }\n } else if (p?.center) {\n mapInstance.setView(p.center, p.zoom || mapInstance.getZoom());\n }\n } catch (err) {\n // Fallback ladder (P2.5): a Leaflet drawing failure degrades to\n // the coordinate table below instead of a blank/partial map.\n const message = err instanceof Error ? err.message : 'Failed to render map';\n setError(message);\n telemetry?.dispatch({\n type: 'render:error',\n errorMessage: message,\n id: props.component?.id ?? '',\n componentType: 'map',\n ts: Date.now(),\n });\n }\n }\n });\n\n // Cleanup\n onCleanup(() => {\n if (mapInstance) {\n mapInstance.remove();\n mapInstance = null;\n }\n });\n\n return (\n <ExpandableWrapper\n title={'Map'}\n copyData={mapToGeoJSON(params())}\n copyLabel=\"Copy markers as GeoJSON\"\n toolbarVariant={props.toolbarVariant}\n >\n <div\n class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''} ${\n isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''\n }`}\n >\n <Show when={error()}>\n {/* Fallback ladder (P2.5): degrade to a coordinate table\n rather than a bare error string. */}\n <div class=\"p-3\">\n <DegradedFallback\n message={`Map rendering failed: ${error()}`}\n caption=\"Showing the map data as a coordinate table — the interactive map is unavailable.\"\n {...mapToDegradedTable(params() ?? {})}\n />\n </div>\n </Show>\n <Show when={!error()}>\n <div\n ref={mapContainer}\n style={\n isExpanded()\n ? { height: '100%', width: '100%', 'z-index': 0 }\n : { height: params()?.height || '400px', width: '100%', 'z-index': 0 }\n }\n class={`relative z-0 ${isExpanded() ? 'flex-1 min-h-0' : ''}`}\n />\n </Show>\n </div>\n </ExpandableWrapper>\n );\n};\n"],"names":["L","clusterCssLoaded","getChoroplethColor","value","scale","fallback","isFinite","length","i","buildStyleFn","style","fillColor","fillOpacity","color","weight","opacity","feature","choroplethField","choroplethScale","properties","val","choroplethFallback","strokeColor","strokeWeight","strokeOpacity","buildPopupContent","popup","allowHtml","props","template","replace","_","key","escapeHtml","String","parts","titleField","push","fields","Object","keys","slice","formatted","toLocaleString","join","str","addGeoJSONLayer","mapInst","leaflet","geojson","styleFn","layer","geoJSON","pointToLayer","latlng","s","circleMarker","radius","onEachFeature","featureLayer","html","bindPopup","maxWidth","addTo","mapToGeoJSON","p","features","marker","markers","pos","position","lat","Array","isArray","lng","type","geometry","coordinates","tooltip","JSON","stringify","MapRenderer","mapContainer","mapInstance","isLeafletLoaded","setIsLeafletLoaded","createSignal","error","setError","isExpanded","useExpanded","telemetry","useTelemetry","params","component","createEffect","setTimeout","invalidateSize","isServer","module","default","e","console","warn","center","zoom","map","zoomControl","scrollWheelZoom","attributionControl","setView","tileLayerUrl","tileLayer","attribution","control","prefix","Icon","Default","prototype","_getIconUrl","mergeOptions","iconRetinaUrl","iconUrl","shadowUrl","allBoundsLayers","eachLayer","Marker","GeoJSON","CircleMarker","_group","_featureGroup","removeLayer","shouldCluster","clustering","clusterOpts","clusterGroup","markerClusterGroup","maxClusterRadius","spiderfyOnMaxZoom","showCoverageOnHover","disableClusteringAtZoom","animate","animateAddingMarkers","forEach","m","bindTooltip","addLayer","geoLayer","geojsonStyle","layers","overlays","layerDef","name","visible","collapsed","pmtiles","protomaps","pmConfig","paintRules","rule","dataLayer","symbolizer","fill","stroke","width","labelRules","TextSymbolizer","label_props","textField","fontSize","pmLayer","leafletLayer","url","maxZoom","minZoom","fitBounds","group","featureGroup","bounds","getBounds","isValid","pad","getZoom","err","message","Error","dispatch","errorMessage","id","componentType","ts","Date","now","onCleanup","remove","_$createComponent","ExpandableWrapper","title","copyData","copyLabel","toolbarVariant","children","_el$","_$getNextElement","_tmpl$3","_el$4","firstChild","_el$5","_co$","_$getNextMarker","nextSibling","_el$6","_el$7","_co$2","_$insert","Show","when","_el$2","_tmpl$","DegradedFallback","_$mergeProps","caption","mapToDegradedTable","_el$3","_tmpl$2","_ref$","_$use","_$effect","_p$","_v$","height","_v$2","_$style","t","_$className","undefined","className"],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  * v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles
5
5
  */
6
6
  import { Component } from 'solid-js';
7
- import type { UIComponent, MapComponentParams } from '../types';
7
+ import type { UIComponent, MapComponentParams, MapPopupConfig } from '../types';
8
8
  export interface MapRendererProps {
9
9
  /**
10
10
  * UIComponent containing map params
@@ -19,6 +19,42 @@ export interface MapRendererProps {
19
19
  * @see ExpandableWrapperProps.toolbarVariant
20
20
  */
21
21
  toolbarVariant?: 'hover' | 'always-visible';
22
+ /**
23
+ * Trust marker / popup content as raw HTML (v6.10.0, audit P1.2).
24
+ *
25
+ * Leaflet renders bound tooltip/popup strings as HTML, so `marker.tooltip`,
26
+ * `marker.popup` and a GeoJSON `popup.template` are XSS vectors when the
27
+ * payload is untrusted (the default for an LLM/connector-driven public
28
+ * package). **Default `false`** → those are escaped as plain text, and
29
+ * `popup.template` is ignored in favour of the safe auto-generated popup.
30
+ *
31
+ * This is a **host-level** trust decision, deliberately NOT a payload field
32
+ * (a malicious payload could just set its own flag). The
33
+ * `<UIResourceRenderer>` path never sets it, so payload-driven maps are
34
+ * always text-safe. A host rendering `<MapRenderer>` directly with trusted
35
+ * data may set `allowHtmlPopups` to restore rich HTML popups. The
36
+ * auto-generated popup (`titleField` / `fields`) is safe-by-construction
37
+ * and unaffected either way.
38
+ */
39
+ allowHtmlPopups?: boolean;
22
40
  }
41
+ /**
42
+ * Build popup HTML from a feature's properties using popup config.
43
+ *
44
+ * `allowHtml` (audit P1.2) gates the raw-HTML `popup.template`: it is honored
45
+ * only when the host trusts the payload. On the default (untrusted) path the
46
+ * template is ignored and we fall through to the auto-generated popup, whose
47
+ * structure is renderer-authored and whose values are always escaped — safe
48
+ * by construction. Even in the trusted path, substituted `{{prop}}` values
49
+ * are escaped (they are data, not markup).
50
+ */
51
+ export declare function buildPopupContent(feature: any, popup: MapPopupConfig | undefined, allowHtml?: boolean): string | null;
52
+ /**
53
+ * Resolve a marker's tooltip/popup string for binding (audit P1.2). Leaflet
54
+ * renders bound strings as HTML; `marker.tooltip` / `marker.popup` come from
55
+ * the (untrusted) payload, so they are escaped to plain text unless the host
56
+ * has opted into raw HTML via `allowHtmlPopups`. Pure + exported for tests.
57
+ */
58
+ export declare function popupSafeText(value: string | undefined, allowHtml?: boolean): string | undefined;
23
59
  export declare const MapRenderer: Component<MapRendererProps>;
24
60
  //# sourceMappingURL=MapRenderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MapRenderer.d.ts","sourceRoot":"","sources":["../../src/components/MapRenderer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAA+C,MAAM,UAAU,CAAC;AAElF,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAMnB,MAAM,UAAU,CAAC;AAWlB,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAE5B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAC7C;AAiLD,eAAO,MAAM,WAAW,EAAE,SAAS,CAAC,gBAAgB,CAwSnD,CAAC"}
1
+ {"version":3,"file":"MapRenderer.d.ts","sourceRoot":"","sources":["../../src/components/MapRenderer.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAA+C,MAAM,UAAU,CAAC;AAElF,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAGlB,cAAc,EAGf,MAAM,UAAU,CAAC;AAWlB,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAE5B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAE5C;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAiED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,cAAc,GAAG,SAAS,EACjC,SAAS,UAAQ,GAChB,MAAM,GAAG,IAAI,CA+Bf;AAUD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,UAAQ,GAAG,MAAM,GAAG,SAAS,CAG9F;AAkFD,eAAO,MAAM,WAAW,EAAE,SAAS,CAAC,gBAAgB,CA2SnD,CAAC"}
@@ -42,13 +42,13 @@ function buildStyleFn(style2) {
42
42
  };
43
43
  };
44
44
  }
45
- function buildPopupContent(feature, popup) {
45
+ function buildPopupContent(feature, popup, allowHtml = false) {
46
46
  if (!popup || !(feature == null ? void 0 : feature.properties)) return null;
47
47
  const props = feature.properties;
48
- if (popup.template) {
48
+ if (allowHtml && popup.template) {
49
49
  return popup.template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
50
50
  const val = props[key];
51
- return val != null ? String(val) : "";
51
+ return val != null ? escapeHtml(String(val)) : "";
52
52
  });
53
53
  }
54
54
  const parts = [];
@@ -387,6 +387,7 @@ const MapRenderer = (props) => {
387
387
  });
388
388
  };
389
389
  export {
390
- MapRenderer
390
+ MapRenderer,
391
+ buildPopupContent
391
392
  };
392
393
  //# sourceMappingURL=MapRenderer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"MapRenderer.js","sources":["../../src/components/MapRenderer.tsx"],"sourcesContent":["/**\n * MapRenderer - Interactive map Component\n * Sprint 6: Markers + clustering\n * v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles\n */\n\nimport { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';\nimport { isServer } from 'solid-js/web';\nimport type {\n UIComponent,\n MapComponentParams,\n MapClusterOptions,\n MapGeoJSONStyle,\n MapPopupConfig,\n MapLayer,\n MapPMTilesConfig,\n} from '../types';\nimport { ExpandableWrapper, useExpanded } from './ExpandableWrapper';\nimport { DegradedFallback } from './DegradedFallback';\nimport { mapToDegradedTable } from '../utils/degraded-projections';\nimport { useTelemetry } from '../context/MCPUITelemetryContext';\n\n// Lazy load leaflet (it doesn't support SSR well)\nlet L: any = null;\n// Track if marker cluster CSS has been loaded\nlet clusterCssLoaded = false;\n\nexport interface MapRendererProps {\n /**\n * UIComponent containing map params\n */\n component?: UIComponent;\n\n /**\n * Direct map params\n */\n params?: MapComponentParams;\n\n /**\n * Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).\n * @see ExpandableWrapperProps.toolbarVariant\n */\n toolbarVariant?: 'hover' | 'always-visible';\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Resolve choropleth color for a feature based on property value and scale stops.\n */\nfunction getChoroplethColor(\n value: unknown,\n scale: Array<[number, string]>,\n fallback: string\n): string {\n if (value == null || typeof value !== 'number' || !isFinite(value)) return fallback;\n\n // Scale is sorted ascending: [[0, '#eff3ff'], [100, '#084594']]\n if (scale.length === 0) return fallback;\n if (value <= scale[0][0]) return scale[0][1];\n if (value >= scale[scale.length - 1][0]) return scale[scale.length - 1][1];\n\n // Find surrounding stops and interpolate (use upper bracket color)\n for (let i = 1; i < scale.length; i++) {\n if (value <= scale[i][0]) return scale[i][1];\n }\n return scale[scale.length - 1][1];\n}\n\n/**\n * Build a Leaflet style function from MapGeoJSONStyle config.\n */\nfunction buildStyleFn(\n style: MapGeoJSONStyle | undefined\n): (feature: any) => Record<string, unknown> {\n if (!style) {\n return () => ({\n fillColor: '#3388ff',\n fillOpacity: 0.6,\n color: '#333',\n weight: 1,\n opacity: 1,\n });\n }\n\n return (feature: any) => {\n let fillColor = style.fillColor || '#3388ff';\n\n // Choropleth: override fillColor based on feature property\n if (style.choroplethField && style.choroplethScale && feature?.properties) {\n const val = feature.properties[style.choroplethField];\n fillColor = getChoroplethColor(\n val,\n style.choroplethScale,\n style.choroplethFallback || '#ccc'\n );\n }\n\n return {\n fillColor,\n fillOpacity: style.fillOpacity ?? 0.6,\n color: style.strokeColor || '#333',\n weight: style.strokeWeight ?? 1,\n opacity: style.strokeOpacity ?? 1,\n };\n };\n}\n\n/**\n * Build popup HTML from a feature's properties using popup config.\n */\nfunction buildPopupContent(feature: any, popup: MapPopupConfig | undefined): string | null {\n if (!popup || !feature?.properties) return null;\n const props = feature.properties;\n\n // Custom template\n if (popup.template) {\n return popup.template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => {\n const val = props[key];\n return val != null ? String(val) : '';\n });\n }\n\n // Auto-generated popup\n const parts: string[] = [];\n\n if (popup.titleField && props[popup.titleField] != null) {\n parts.push(`<strong>${escapeHtml(String(props[popup.titleField]))}</strong>`);\n }\n\n const fields = popup.fields || Object.keys(props).slice(0, 8);\n for (const key of fields) {\n if (key === popup.titleField) continue;\n const val = props[key];\n if (val == null) continue;\n const formatted = typeof val === 'number' ? val.toLocaleString('fr-FR') : String(val);\n parts.push(\n `<span style=\"color:#666;font-size:11px\">${escapeHtml(key)}</span>: ${escapeHtml(formatted)}`\n );\n }\n\n return parts.join('<br/>');\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * Add a GeoJSON layer to the map with style and popup support.\n * Returns the layer for bounds calculation.\n */\nfunction addGeoJSONLayer(\n mapInst: any,\n leaflet: any,\n geojson: unknown,\n style?: MapGeoJSONStyle,\n popup?: MapPopupConfig\n): any {\n const styleFn = buildStyleFn(style);\n\n const layer = leaflet.geoJSON(geojson, {\n style: styleFn,\n pointToLayer: (feature: any, latlng: any) => {\n // Render points as circle markers for consistency\n const s = styleFn(feature);\n return leaflet.circleMarker(latlng, {\n radius: 6,\n fillColor: s.fillColor,\n fillOpacity: s.fillOpacity,\n color: s.color,\n weight: s.weight,\n opacity: s.opacity,\n });\n },\n onEachFeature: (feature: any, featureLayer: any) => {\n const html = buildPopupContent(feature, popup);\n if (html) {\n featureLayer.bindPopup(html, { maxWidth: 300 });\n }\n },\n });\n\n layer.addTo(mapInst);\n return layer;\n}\n\n// ─── Component ──────────────────────────────────────────────\n\n/**\n * Build a GeoJSON FeatureCollection from the map's `markers` (and any\n * inlined GeoJSON layers, when present). Used by the \"Copy data\" button\n * shipped via `<ExpandableWrapper>` (v6.2.0). Best-effort — clusters,\n * tile layers, and choropleth-only data don't get round-tripped.\n */\nfunction mapToGeoJSON(p: MapComponentParams | undefined): string {\n if (!p) return '{\"type\":\"FeatureCollection\",\"features\":[]}';\n const features: any[] = [];\n for (const marker of p.markers ?? []) {\n const pos: any = marker.position as any;\n // Accept both [lat, lng] tuple and {lat, lng} object shapes (v5.0.2 spec)\n const lat = Array.isArray(pos) ? pos[0] : pos?.lat;\n const lng = Array.isArray(pos) ? pos[1] : pos?.lng;\n if (typeof lat !== 'number' || typeof lng !== 'number') continue;\n features.push({\n type: 'Feature',\n geometry: { type: 'Point', coordinates: [lng, lat] },\n properties: {\n ...(marker.tooltip ? { tooltip: marker.tooltip } : {}),\n ...(marker.popup ? { popup: marker.popup } : {}),\n },\n });\n }\n return JSON.stringify({ type: 'FeatureCollection', features }, null, 2);\n}\n\nexport const MapRenderer: Component<MapRendererProps> = (props) => {\n let mapContainer: HTMLDivElement | undefined;\n let mapInstance: any = null;\n const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const isExpanded = useExpanded();\n const telemetry = useTelemetry();\n\n const params = () => props.params || (props.component?.params as MapComponentParams);\n\n // v6.2.0 — Leaflet has to be told to re-measure when its container\n // resizes (e.g. transitioning to fullscreen via ExpandableWrapper).\n // We give the DOM a tick to settle the new dimensions, then ask\n // Leaflet to reflow tiles.\n createEffect(() => {\n const expanded = isExpanded();\n if (!mapInstance) return;\n // Read the signal so the effect re-runs on toggle ; the value is\n // observed for its side effects on layout.\n void expanded;\n setTimeout(() => mapInstance?.invalidateSize?.(), 100);\n });\n\n // Initialize Map\n createEffect(async () => {\n if (isServer) return; // Don't run on server\n\n if (!L) {\n try {\n const module = await import('leaflet');\n L = module.default || module;\n await import('leaflet/dist/leaflet.css'); // Import CSS\n setIsLeafletLoaded(true);\n } catch (e) {\n console.warn('Failed to load leaflet', e);\n setError('Map library could not be loaded.');\n return;\n }\n } else {\n setIsLeafletLoaded(true);\n }\n\n if (isLeafletLoaded() && mapContainer && !mapInstance) {\n const p = params();\n const center = p?.center || [51.505, -0.09]; // Default to London\n const zoom = p?.zoom || 13;\n\n mapInstance = L.map(mapContainer, {\n zoomControl: p?.zoomControl !== false,\n scrollWheelZoom: p?.scrollWheelZoom !== false,\n attributionControl: false,\n }).setView(center, zoom);\n\n // Add OpenStreetMap tile layer\n const tileLayerUrl = p?.tileLayer || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n L.tileLayer(tileLayerUrl, {\n attribution:\n p?.attribution ||\n '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',\n }).addTo(mapInstance);\n\n if (p?.attribution !== '') {\n L.control.attribution({ prefix: false }).addTo(mapInstance);\n }\n\n // Fix marker icons (Leaflet issue with bundlers)\n delete (L.Icon.Default.prototype as any)._getIconUrl;\n L.Icon.Default.mergeOptions({\n iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',\n iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',\n shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',\n });\n }\n\n // Update markers and view\n if (mapInstance && L) {\n try {\n const p = params();\n const allBoundsLayers: any[] = [];\n\n // Clear existing layers (markers, cluster groups, GeoJSON)\n mapInstance.eachLayer((layer: any) => {\n if (\n layer instanceof L.Marker ||\n layer instanceof L.GeoJSON ||\n layer instanceof L.CircleMarker ||\n layer._group ||\n layer._featureGroup\n ) {\n mapInstance.removeLayer(layer);\n }\n });\n\n // ─── Markers (legacy) ────────────────────────\n const markers: any[] = [];\n const shouldCluster = p?.clustering && p?.markers && p.markers.length > 0;\n\n if (shouldCluster) {\n try {\n await import('leaflet.markercluster');\n if (!clusterCssLoaded) {\n await import('leaflet.markercluster/dist/MarkerCluster.css');\n await import('leaflet.markercluster/dist/MarkerCluster.Default.css');\n clusterCssLoaded = true;\n }\n const clusterOpts: MapClusterOptions =\n typeof p.clustering === 'object' ? p.clustering : {};\n const clusterGroup = (L as any).markerClusterGroup({\n maxClusterRadius: clusterOpts.maxClusterRadius ?? 80,\n spiderfyOnMaxZoom: clusterOpts.spiderfyOnMaxZoom ?? true,\n showCoverageOnHover: clusterOpts.showCoverageOnHover ?? true,\n disableClusteringAtZoom: clusterOpts.disableClusteringAtZoom,\n animate: clusterOpts.animateAddingMarkers ?? true,\n });\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n clusterGroup.addLayer(m);\n markers.push(m);\n });\n mapInstance.addLayer(clusterGroup);\n } catch {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n } else {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n\n if (markers.length > 0) {\n allBoundsLayers.push(...markers);\n }\n\n // ─── GeoJSON (v3.1.0) ───────────────────────\n if (p?.geojson) {\n const geoLayer = addGeoJSONLayer(mapInstance, L, p.geojson, p.geojsonStyle, p.popup);\n allBoundsLayers.push(geoLayer);\n }\n\n // ─── Named layers (v3.1.0) ──────────────────\n if (p?.layers && p.layers.length > 0) {\n const overlays: Record<string, any> = {};\n\n for (const layerDef of p.layers) {\n const geoLayer = addGeoJSONLayer(\n mapInstance,\n L,\n layerDef.geojson,\n layerDef.style || p?.geojsonStyle,\n layerDef.popup || p?.popup\n );\n\n overlays[layerDef.name] = geoLayer;\n allBoundsLayers.push(geoLayer);\n\n // Respect initial visibility\n if (layerDef.visible === false) {\n mapInstance.removeLayer(geoLayer);\n }\n }\n\n // Add layer control if multiple layers\n if (Object.keys(overlays).length > 1) {\n L.control.layers(null, overlays, { collapsed: true }).addTo(mapInstance);\n }\n }\n\n // ─── PMTiles (v3.1.0) ────────────────────────\n if (p?.pmtiles) {\n try {\n // @ts-ignore — optional peer dependency, may not be installed\n const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet');\n const pmConfig = p.pmtiles;\n\n const paintRules =\n pmConfig.paintRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any)[\n rule.symbolizer === 'polygon'\n ? 'PolygonSymbolizer'\n : rule.symbolizer === 'line'\n ? 'LineSymbolizer'\n : 'CircleSymbolizer'\n ]({\n fill: rule.color || '#3388ff',\n stroke: rule.color || '#333',\n width: rule.width ?? 1,\n opacity: rule.opacity ?? 0.6,\n }),\n })) || [];\n\n const labelRules =\n pmConfig.labelRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any).TextSymbolizer({\n label_props: [rule.textField],\n fontSize: rule.fontSize ?? 12,\n }),\n })) || [];\n\n const pmLayer = (protomaps as any).leafletLayer({\n url: pmConfig.url,\n attribution: pmConfig.attribution,\n paintRules,\n labelRules,\n maxZoom: pmConfig.maxZoom,\n minZoom: pmConfig.minZoom,\n });\n\n pmLayer.addTo(mapInstance);\n } catch (e) {\n console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e);\n }\n }\n\n // ─── Fit bounds ─────────────────────────────\n if (p?.fitBounds && allBoundsLayers.length > 0) {\n const group = L.featureGroup(allBoundsLayers);\n const bounds = group.getBounds();\n if (bounds.isValid()) {\n mapInstance.fitBounds(bounds.pad(0.1));\n }\n } else if (p?.center) {\n mapInstance.setView(p.center, p.zoom || mapInstance.getZoom());\n }\n } catch (err) {\n // Fallback ladder (P2.5): a Leaflet drawing failure degrades to\n // the coordinate table below instead of a blank/partial map.\n const message = err instanceof Error ? err.message : 'Failed to render map';\n setError(message);\n telemetry?.dispatch({\n type: 'render:error',\n errorMessage: message,\n id: props.component?.id ?? '',\n componentType: 'map',\n ts: Date.now(),\n });\n }\n }\n });\n\n // Cleanup\n onCleanup(() => {\n if (mapInstance) {\n mapInstance.remove();\n mapInstance = null;\n }\n });\n\n return (\n <ExpandableWrapper\n title={'Map'}\n copyData={mapToGeoJSON(params())}\n copyLabel=\"Copy markers as GeoJSON\"\n toolbarVariant={props.toolbarVariant}\n >\n <div\n class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''} ${\n isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''\n }`}\n >\n <Show when={error()}>\n {/* Fallback ladder (P2.5): degrade to a coordinate table\n rather than a bare error string. */}\n <div class=\"p-3\">\n <DegradedFallback\n message={`Map rendering failed: ${error()}`}\n caption=\"Showing the map data as a coordinate table — the interactive map is unavailable.\"\n {...mapToDegradedTable(params() ?? {})}\n />\n </div>\n </Show>\n <Show when={!error()}>\n <div\n ref={mapContainer}\n style={\n isExpanded()\n ? { height: '100%', width: '100%', 'z-index': 0 }\n : { height: params()?.height || '400px', width: '100%', 'z-index': 0 }\n }\n class={`relative z-0 ${isExpanded() ? 'flex-1 min-h-0' : ''}`}\n />\n </Show>\n </div>\n </ExpandableWrapper>\n );\n};\n"],"names":["L","clusterCssLoaded","getChoroplethColor","value","scale","fallback","isFinite","length","i","buildStyleFn","style","fillColor","fillOpacity","color","weight","opacity","feature","choroplethField","choroplethScale","properties","val","choroplethFallback","strokeColor","strokeWeight","strokeOpacity","buildPopupContent","popup","props","template","replace","_","key","String","parts","titleField","push","escapeHtml","fields","Object","keys","slice","formatted","toLocaleString","join","str","addGeoJSONLayer","mapInst","leaflet","geojson","styleFn","layer","geoJSON","pointToLayer","latlng","s","circleMarker","radius","onEachFeature","featureLayer","html","bindPopup","maxWidth","addTo","mapToGeoJSON","p","features","marker","markers","pos","position","lat","Array","isArray","lng","type","geometry","coordinates","tooltip","JSON","stringify","MapRenderer","mapContainer","mapInstance","isLeafletLoaded","setIsLeafletLoaded","createSignal","error","setError","isExpanded","useExpanded","telemetry","useTelemetry","params","component","createEffect","setTimeout","invalidateSize","isServer","module","default","e","console","warn","center","zoom","map","zoomControl","scrollWheelZoom","attributionControl","setView","tileLayerUrl","tileLayer","attribution","control","prefix","Icon","Default","prototype","_getIconUrl","mergeOptions","iconRetinaUrl","iconUrl","shadowUrl","allBoundsLayers","eachLayer","Marker","GeoJSON","CircleMarker","_group","_featureGroup","removeLayer","shouldCluster","clustering","clusterOpts","clusterGroup","markerClusterGroup","maxClusterRadius","spiderfyOnMaxZoom","showCoverageOnHover","disableClusteringAtZoom","animate","animateAddingMarkers","forEach","m","bindTooltip","addLayer","geoLayer","geojsonStyle","layers","overlays","layerDef","name","visible","collapsed","pmtiles","protomaps","pmConfig","paintRules","rule","dataLayer","symbolizer","fill","stroke","width","labelRules","TextSymbolizer","label_props","textField","fontSize","pmLayer","leafletLayer","url","maxZoom","minZoom","fitBounds","group","featureGroup","bounds","getBounds","isValid","pad","getZoom","err","message","Error","dispatch","errorMessage","id","componentType","ts","Date","now","onCleanup","remove","_$createComponent","ExpandableWrapper","title","copyData","copyLabel","toolbarVariant","children","_el$","_$getNextElement","_tmpl$3","_el$4","firstChild","_el$5","_co$","_$getNextMarker","nextSibling","_el$6","_el$7","_co$2","_$insert","Show","when","_el$2","_tmpl$","DegradedFallback","_$mergeProps","caption","mapToDegradedTable","_el$3","_tmpl$2","_ref$","_$use","_$effect","_p$","_v$","height","_v$2","_$style","t","_$className","undefined","className"],"mappings":"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"}
1
+ {"version":3,"file":"MapRenderer.js","sources":["../../src/components/MapRenderer.tsx"],"sourcesContent":["/**\n * MapRenderer - Interactive map Component\n * Sprint 6: Markers + clustering\n * v3.1.0: GeoJSON, choropleth, popups, multi-layer, PMTiles\n */\n\nimport { Component, createEffect, onCleanup, createSignal, Show } from 'solid-js';\nimport { isServer } from 'solid-js/web';\nimport type {\n UIComponent,\n MapComponentParams,\n MapClusterOptions,\n MapGeoJSONStyle,\n MapPopupConfig,\n MapLayer,\n MapPMTilesConfig,\n} from '../types';\nimport { ExpandableWrapper, useExpanded } from './ExpandableWrapper';\nimport { DegradedFallback } from './DegradedFallback';\nimport { mapToDegradedTable } from '../utils/degraded-projections';\nimport { useTelemetry } from '../context/MCPUITelemetryContext';\n\n// Lazy load leaflet (it doesn't support SSR well)\nlet L: any = null;\n// Track if marker cluster CSS has been loaded\nlet clusterCssLoaded = false;\n\nexport interface MapRendererProps {\n /**\n * UIComponent containing map params\n */\n component?: UIComponent;\n\n /**\n * Direct map params\n */\n params?: MapComponentParams;\n\n /**\n * Forwarded to the underlying `<ExpandableWrapper>` (v6.3.1).\n * @see ExpandableWrapperProps.toolbarVariant\n */\n toolbarVariant?: 'hover' | 'always-visible';\n\n /**\n * Trust marker / popup content as raw HTML (v6.10.0, audit P1.2).\n *\n * Leaflet renders bound tooltip/popup strings as HTML, so `marker.tooltip`,\n * `marker.popup` and a GeoJSON `popup.template` are XSS vectors when the\n * payload is untrusted (the default for an LLM/connector-driven public\n * package). **Default `false`** → those are escaped as plain text, and\n * `popup.template` is ignored in favour of the safe auto-generated popup.\n *\n * This is a **host-level** trust decision, deliberately NOT a payload field\n * (a malicious payload could just set its own flag). The\n * `<UIResourceRenderer>` path never sets it, so payload-driven maps are\n * always text-safe. A host rendering `<MapRenderer>` directly with trusted\n * data may set `allowHtmlPopups` to restore rich HTML popups. The\n * auto-generated popup (`titleField` / `fields`) is safe-by-construction\n * and unaffected either way.\n */\n allowHtmlPopups?: boolean;\n}\n\n// ─── Helpers ────────────────────────────────────────────────\n\n/**\n * Resolve choropleth color for a feature based on property value and scale stops.\n */\nfunction getChoroplethColor(\n value: unknown,\n scale: Array<[number, string]>,\n fallback: string\n): string {\n if (value == null || typeof value !== 'number' || !isFinite(value)) return fallback;\n\n // Scale is sorted ascending: [[0, '#eff3ff'], [100, '#084594']]\n if (scale.length === 0) return fallback;\n if (value <= scale[0][0]) return scale[0][1];\n if (value >= scale[scale.length - 1][0]) return scale[scale.length - 1][1];\n\n // Find surrounding stops and interpolate (use upper bracket color)\n for (let i = 1; i < scale.length; i++) {\n if (value <= scale[i][0]) return scale[i][1];\n }\n return scale[scale.length - 1][1];\n}\n\n/**\n * Build a Leaflet style function from MapGeoJSONStyle config.\n */\nfunction buildStyleFn(\n style: MapGeoJSONStyle | undefined\n): (feature: any) => Record<string, unknown> {\n if (!style) {\n return () => ({\n fillColor: '#3388ff',\n fillOpacity: 0.6,\n color: '#333',\n weight: 1,\n opacity: 1,\n });\n }\n\n return (feature: any) => {\n let fillColor = style.fillColor || '#3388ff';\n\n // Choropleth: override fillColor based on feature property\n if (style.choroplethField && style.choroplethScale && feature?.properties) {\n const val = feature.properties[style.choroplethField];\n fillColor = getChoroplethColor(\n val,\n style.choroplethScale,\n style.choroplethFallback || '#ccc'\n );\n }\n\n return {\n fillColor,\n fillOpacity: style.fillOpacity ?? 0.6,\n color: style.strokeColor || '#333',\n weight: style.strokeWeight ?? 1,\n opacity: style.strokeOpacity ?? 1,\n };\n };\n}\n\n/**\n * Build popup HTML from a feature's properties using popup config.\n *\n * `allowHtml` (audit P1.2) gates the raw-HTML `popup.template`: it is honored\n * only when the host trusts the payload. On the default (untrusted) path the\n * template is ignored and we fall through to the auto-generated popup, whose\n * structure is renderer-authored and whose values are always escaped — safe\n * by construction. Even in the trusted path, substituted `{{prop}}` values\n * are escaped (they are data, not markup).\n */\nexport function buildPopupContent(\n feature: any,\n popup: MapPopupConfig | undefined,\n allowHtml = false\n): string | null {\n if (!popup || !feature?.properties) return null;\n const props = feature.properties;\n\n // Custom template — raw HTML, so trusted-host only.\n if (allowHtml && popup.template) {\n return popup.template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => {\n const val = props[key];\n return val != null ? escapeHtml(String(val)) : '';\n });\n }\n\n // Auto-generated popup\n const parts: string[] = [];\n\n if (popup.titleField && props[popup.titleField] != null) {\n parts.push(`<strong>${escapeHtml(String(props[popup.titleField]))}</strong>`);\n }\n\n const fields = popup.fields || Object.keys(props).slice(0, 8);\n for (const key of fields) {\n if (key === popup.titleField) continue;\n const val = props[key];\n if (val == null) continue;\n const formatted = typeof val === 'number' ? val.toLocaleString('fr-FR') : String(val);\n parts.push(\n `<span style=\"color:#666;font-size:11px\">${escapeHtml(key)}</span>: ${escapeHtml(formatted)}`\n );\n }\n\n return parts.join('<br/>');\n}\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\n/**\n * Resolve a marker's tooltip/popup string for binding (audit P1.2). Leaflet\n * renders bound strings as HTML; `marker.tooltip` / `marker.popup` come from\n * the (untrusted) payload, so they are escaped to plain text unless the host\n * has opted into raw HTML via `allowHtmlPopups`. Pure + exported for tests.\n */\nexport function popupSafeText(value: string | undefined, allowHtml = false): string | undefined {\n if (value == null) return undefined;\n return allowHtml ? value : escapeHtml(value);\n}\n\n/** Bind a marker's tooltip + popup, escaping by default (see popupSafeText). */\nfunction bindMarkerContent(\n m: any,\n marker: { tooltip?: string; popup?: string },\n allowHtml: boolean\n): void {\n const tooltip = popupSafeText(marker.tooltip, allowHtml);\n if (tooltip) m.bindTooltip(tooltip);\n const popup = popupSafeText(marker.popup, allowHtml);\n if (popup) m.bindPopup(popup);\n}\n\n/**\n * Add a GeoJSON layer to the map with style and popup support.\n * Returns the layer for bounds calculation.\n */\nfunction addGeoJSONLayer(\n mapInst: any,\n leaflet: any,\n geojson: unknown,\n style?: MapGeoJSONStyle,\n popup?: MapPopupConfig\n): any {\n const styleFn = buildStyleFn(style);\n\n const layer = leaflet.geoJSON(geojson, {\n style: styleFn,\n pointToLayer: (feature: any, latlng: any) => {\n // Render points as circle markers for consistency\n const s = styleFn(feature);\n return leaflet.circleMarker(latlng, {\n radius: 6,\n fillColor: s.fillColor,\n fillOpacity: s.fillOpacity,\n color: s.color,\n weight: s.weight,\n opacity: s.opacity,\n });\n },\n onEachFeature: (feature: any, featureLayer: any) => {\n const html = buildPopupContent(feature, popup);\n if (html) {\n featureLayer.bindPopup(html, { maxWidth: 300 });\n }\n },\n });\n\n layer.addTo(mapInst);\n return layer;\n}\n\n// ─── Component ──────────────────────────────────────────────\n\n/**\n * Build a GeoJSON FeatureCollection from the map's `markers` (and any\n * inlined GeoJSON layers, when present). Used by the \"Copy data\" button\n * shipped via `<ExpandableWrapper>` (v6.2.0). Best-effort — clusters,\n * tile layers, and choropleth-only data don't get round-tripped.\n */\nfunction mapToGeoJSON(p: MapComponentParams | undefined): string {\n if (!p) return '{\"type\":\"FeatureCollection\",\"features\":[]}';\n const features: any[] = [];\n for (const marker of p.markers ?? []) {\n const pos: any = marker.position as any;\n // Accept both [lat, lng] tuple and {lat, lng} object shapes (v5.0.2 spec)\n const lat = Array.isArray(pos) ? pos[0] : pos?.lat;\n const lng = Array.isArray(pos) ? pos[1] : pos?.lng;\n if (typeof lat !== 'number' || typeof lng !== 'number') continue;\n features.push({\n type: 'Feature',\n geometry: { type: 'Point', coordinates: [lng, lat] },\n properties: {\n ...(marker.tooltip ? { tooltip: marker.tooltip } : {}),\n ...(marker.popup ? { popup: marker.popup } : {}),\n },\n });\n }\n return JSON.stringify({ type: 'FeatureCollection', features }, null, 2);\n}\n\nexport const MapRenderer: Component<MapRendererProps> = (props) => {\n let mapContainer: HTMLDivElement | undefined;\n let mapInstance: any = null;\n const [isLeafletLoaded, setIsLeafletLoaded] = createSignal(false);\n const [error, setError] = createSignal<string | null>(null);\n const isExpanded = useExpanded();\n const telemetry = useTelemetry();\n // Host-level trust for raw HTML in marker/popup content (audit P1.2).\n // Default false → tooltip/popup escaped, GeoJSON `popup.template` ignored.\n const allowHtml = () => props.allowHtmlPopups === true;\n\n const params = () => props.params || (props.component?.params as MapComponentParams);\n\n // v6.2.0 — Leaflet has to be told to re-measure when its container\n // resizes (e.g. transitioning to fullscreen via ExpandableWrapper).\n // We give the DOM a tick to settle the new dimensions, then ask\n // Leaflet to reflow tiles.\n createEffect(() => {\n const expanded = isExpanded();\n if (!mapInstance) return;\n // Read the signal so the effect re-runs on toggle ; the value is\n // observed for its side effects on layout.\n void expanded;\n setTimeout(() => mapInstance?.invalidateSize?.(), 100);\n });\n\n // Initialize Map\n createEffect(async () => {\n if (isServer) return; // Don't run on server\n\n if (!L) {\n try {\n const module = await import('leaflet');\n L = module.default || module;\n await import('leaflet/dist/leaflet.css'); // Import CSS\n setIsLeafletLoaded(true);\n } catch (e) {\n console.warn('Failed to load leaflet', e);\n setError('Map library could not be loaded.');\n return;\n }\n } else {\n setIsLeafletLoaded(true);\n }\n\n if (isLeafletLoaded() && mapContainer && !mapInstance) {\n const p = params();\n const center = p?.center || [51.505, -0.09]; // Default to London\n const zoom = p?.zoom || 13;\n\n mapInstance = L.map(mapContainer, {\n zoomControl: p?.zoomControl !== false,\n scrollWheelZoom: p?.scrollWheelZoom !== false,\n attributionControl: false,\n }).setView(center, zoom);\n\n // Add OpenStreetMap tile layer\n const tileLayerUrl = p?.tileLayer || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';\n L.tileLayer(tileLayerUrl, {\n attribution:\n p?.attribution ||\n '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors',\n }).addTo(mapInstance);\n\n if (p?.attribution !== '') {\n L.control.attribution({ prefix: false }).addTo(mapInstance);\n }\n\n // Fix marker icons (Leaflet issue with bundlers)\n delete (L.Icon.Default.prototype as any)._getIconUrl;\n L.Icon.Default.mergeOptions({\n iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',\n iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',\n shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',\n });\n }\n\n // Update markers and view\n if (mapInstance && L) {\n try {\n const p = params();\n const allBoundsLayers: any[] = [];\n\n // Clear existing layers (markers, cluster groups, GeoJSON)\n mapInstance.eachLayer((layer: any) => {\n if (\n layer instanceof L.Marker ||\n layer instanceof L.GeoJSON ||\n layer instanceof L.CircleMarker ||\n layer._group ||\n layer._featureGroup\n ) {\n mapInstance.removeLayer(layer);\n }\n });\n\n // ─── Markers (legacy) ────────────────────────\n const markers: any[] = [];\n const shouldCluster = p?.clustering && p?.markers && p.markers.length > 0;\n\n if (shouldCluster) {\n try {\n await import('leaflet.markercluster');\n if (!clusterCssLoaded) {\n await import('leaflet.markercluster/dist/MarkerCluster.css');\n await import('leaflet.markercluster/dist/MarkerCluster.Default.css');\n clusterCssLoaded = true;\n }\n const clusterOpts: MapClusterOptions =\n typeof p.clustering === 'object' ? p.clustering : {};\n const clusterGroup = (L as any).markerClusterGroup({\n maxClusterRadius: clusterOpts.maxClusterRadius ?? 80,\n spiderfyOnMaxZoom: clusterOpts.spiderfyOnMaxZoom ?? true,\n showCoverageOnHover: clusterOpts.showCoverageOnHover ?? true,\n disableClusteringAtZoom: clusterOpts.disableClusteringAtZoom,\n animate: clusterOpts.animateAddingMarkers ?? true,\n });\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n clusterGroup.addLayer(m);\n markers.push(m);\n });\n mapInstance.addLayer(clusterGroup);\n } catch {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n } else {\n p?.markers?.forEach((marker) => {\n const m = L.marker(marker.position).addTo(mapInstance);\n if (marker.tooltip) m.bindTooltip(marker.tooltip);\n if (marker.popup) m.bindPopup(marker.popup);\n markers.push(m);\n });\n }\n\n if (markers.length > 0) {\n allBoundsLayers.push(...markers);\n }\n\n // ─── GeoJSON (v3.1.0) ───────────────────────\n if (p?.geojson) {\n const geoLayer = addGeoJSONLayer(mapInstance, L, p.geojson, p.geojsonStyle, p.popup);\n allBoundsLayers.push(geoLayer);\n }\n\n // ─── Named layers (v3.1.0) ──────────────────\n if (p?.layers && p.layers.length > 0) {\n const overlays: Record<string, any> = {};\n\n for (const layerDef of p.layers) {\n const geoLayer = addGeoJSONLayer(\n mapInstance,\n L,\n layerDef.geojson,\n layerDef.style || p?.geojsonStyle,\n layerDef.popup || p?.popup\n );\n\n overlays[layerDef.name] = geoLayer;\n allBoundsLayers.push(geoLayer);\n\n // Respect initial visibility\n if (layerDef.visible === false) {\n mapInstance.removeLayer(geoLayer);\n }\n }\n\n // Add layer control if multiple layers\n if (Object.keys(overlays).length > 1) {\n L.control.layers(null, overlays, { collapsed: true }).addTo(mapInstance);\n }\n }\n\n // ─── PMTiles (v3.1.0) ────────────────────────\n if (p?.pmtiles) {\n try {\n // @ts-ignore — optional peer dependency, may not be installed\n const protomaps = await import(/* @vite-ignore */ 'protomaps-leaflet');\n const pmConfig = p.pmtiles;\n\n const paintRules =\n pmConfig.paintRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any)[\n rule.symbolizer === 'polygon'\n ? 'PolygonSymbolizer'\n : rule.symbolizer === 'line'\n ? 'LineSymbolizer'\n : 'CircleSymbolizer'\n ]({\n fill: rule.color || '#3388ff',\n stroke: rule.color || '#333',\n width: rule.width ?? 1,\n opacity: rule.opacity ?? 0.6,\n }),\n })) || [];\n\n const labelRules =\n pmConfig.labelRules?.map((rule) => ({\n dataLayer: rule.dataLayer,\n symbolizer: new (protomaps as any).TextSymbolizer({\n label_props: [rule.textField],\n fontSize: rule.fontSize ?? 12,\n }),\n })) || [];\n\n const pmLayer = (protomaps as any).leafletLayer({\n url: pmConfig.url,\n attribution: pmConfig.attribution,\n paintRules,\n labelRules,\n maxZoom: pmConfig.maxZoom,\n minZoom: pmConfig.minZoom,\n });\n\n pmLayer.addTo(mapInstance);\n } catch (e) {\n console.warn('[MCP-UI] Failed to load protomaps-leaflet for PMTiles:', e);\n }\n }\n\n // ─── Fit bounds ─────────────────────────────\n if (p?.fitBounds && allBoundsLayers.length > 0) {\n const group = L.featureGroup(allBoundsLayers);\n const bounds = group.getBounds();\n if (bounds.isValid()) {\n mapInstance.fitBounds(bounds.pad(0.1));\n }\n } else if (p?.center) {\n mapInstance.setView(p.center, p.zoom || mapInstance.getZoom());\n }\n } catch (err) {\n // Fallback ladder (P2.5): a Leaflet drawing failure degrades to\n // the coordinate table below instead of a blank/partial map.\n const message = err instanceof Error ? err.message : 'Failed to render map';\n setError(message);\n telemetry?.dispatch({\n type: 'render:error',\n errorMessage: message,\n id: props.component?.id ?? '',\n componentType: 'map',\n ts: Date.now(),\n });\n }\n }\n });\n\n // Cleanup\n onCleanup(() => {\n if (mapInstance) {\n mapInstance.remove();\n mapInstance = null;\n }\n });\n\n return (\n <ExpandableWrapper\n title={'Map'}\n copyData={mapToGeoJSON(params())}\n copyLabel=\"Copy markers as GeoJSON\"\n toolbarVariant={props.toolbarVariant}\n >\n <div\n class={`w-full bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden ${params()?.className || ''} ${\n isExpanded() ? 'flex-1 min-h-0 flex flex-col' : ''\n }`}\n >\n <Show when={error()}>\n {/* Fallback ladder (P2.5): degrade to a coordinate table\n rather than a bare error string. */}\n <div class=\"p-3\">\n <DegradedFallback\n message={`Map rendering failed: ${error()}`}\n caption=\"Showing the map data as a coordinate table — the interactive map is unavailable.\"\n {...mapToDegradedTable(params() ?? {})}\n />\n </div>\n </Show>\n <Show when={!error()}>\n <div\n ref={mapContainer}\n style={\n isExpanded()\n ? { height: '100%', width: '100%', 'z-index': 0 }\n : { height: params()?.height || '400px', width: '100%', 'z-index': 0 }\n }\n class={`relative z-0 ${isExpanded() ? 'flex-1 min-h-0' : ''}`}\n />\n </Show>\n </div>\n </ExpandableWrapper>\n );\n};\n"],"names":["L","clusterCssLoaded","getChoroplethColor","value","scale","fallback","isFinite","length","i","buildStyleFn","style","fillColor","fillOpacity","color","weight","opacity","feature","choroplethField","choroplethScale","properties","val","choroplethFallback","strokeColor","strokeWeight","strokeOpacity","buildPopupContent","popup","allowHtml","props","template","replace","_","key","escapeHtml","String","parts","titleField","push","fields","Object","keys","slice","formatted","toLocaleString","join","str","addGeoJSONLayer","mapInst","leaflet","geojson","styleFn","layer","geoJSON","pointToLayer","latlng","s","circleMarker","radius","onEachFeature","featureLayer","html","bindPopup","maxWidth","addTo","mapToGeoJSON","p","features","marker","markers","pos","position","lat","Array","isArray","lng","type","geometry","coordinates","tooltip","JSON","stringify","MapRenderer","mapContainer","mapInstance","isLeafletLoaded","setIsLeafletLoaded","createSignal","error","setError","isExpanded","useExpanded","telemetry","useTelemetry","params","component","createEffect","setTimeout","invalidateSize","isServer","module","default","e","console","warn","center","zoom","map","zoomControl","scrollWheelZoom","attributionControl","setView","tileLayerUrl","tileLayer","attribution","control","prefix","Icon","Default","prototype","_getIconUrl","mergeOptions","iconRetinaUrl","iconUrl","shadowUrl","allBoundsLayers","eachLayer","Marker","GeoJSON","CircleMarker","_group","_featureGroup","removeLayer","shouldCluster","clustering","clusterOpts","clusterGroup","markerClusterGroup","maxClusterRadius","spiderfyOnMaxZoom","showCoverageOnHover","disableClusteringAtZoom","animate","animateAddingMarkers","forEach","m","bindTooltip","addLayer","geoLayer","geojsonStyle","layers","overlays","layerDef","name","visible","collapsed","pmtiles","protomaps","pmConfig","paintRules","rule","dataLayer","symbolizer","fill","stroke","width","labelRules","TextSymbolizer","label_props","textField","fontSize","pmLayer","leafletLayer","url","maxZoom","minZoom","fitBounds","group","featureGroup","bounds","getBounds","isValid","pad","getZoom","err","message","Error","dispatch","errorMessage","id","componentType","ts","Date","now","onCleanup","remove","_$createComponent","ExpandableWrapper","title","copyData","copyLabel","toolbarVariant","children","_el$","_$getNextElement","_tmpl$3","_el$4","firstChild","_el$5","_co$","_$getNextMarker","nextSibling","_el$6","_el$7","_co$2","_$insert","Show","when","_el$2","_tmpl$","DegradedFallback","_$mergeProps","caption","mapToDegradedTable","_el$3","_tmpl$2","_ref$","_$use","_$effect","_p$","_v$","height","_v$2","_$style","t","_$className","undefined","className"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-ship/mcp-ui-solid",
3
- "version": "6.9.0",
3
+ "version": "6.10.0",
4
4
  "description": "SolidJS components for rendering MCP-generated UI resources",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -0,0 +1,83 @@
1
+ /**
2
+ * MapRenderer XSS hardening tests (audit P1.2, v6.10.0).
3
+ *
4
+ * Leaflet renders bound tooltip/popup strings as HTML, so untrusted payload
5
+ * content (`marker.tooltip`, `marker.popup`, GeoJSON `popup.template`) is an
6
+ * XSS vector. These lock the safe-by-default behavior and the host opt-in,
7
+ * via the pure exported helpers (the Leaflet binding itself is a thin wrapper
8
+ * and not exercisable in jsdom).
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { popupSafeText, buildPopupContent } from './MapRenderer';
13
+
14
+ const XSS = '<img src=x onerror=alert(1)>';
15
+
16
+ describe('popupSafeText — marker tooltip/popup (P1.2)', () => {
17
+ it('escapes HTML by default (untrusted payload path)', () => {
18
+ const out = popupSafeText(XSS);
19
+ expect(out).toBe('&lt;img src=x onerror=alert(1)&gt;');
20
+ expect(out).not.toContain('<img');
21
+ });
22
+
23
+ it('escapes < > & " so no tag can be injected', () => {
24
+ expect(popupSafeText('a & b <c> "d"')).toBe('a &amp; b &lt;c&gt; &quot;d&quot;');
25
+ });
26
+
27
+ it('passes raw HTML through when the host opts in (trusted)', () => {
28
+ expect(popupSafeText(XSS, true)).toBe(XSS);
29
+ });
30
+
31
+ it('returns undefined for absent content', () => {
32
+ expect(popupSafeText(undefined)).toBeUndefined();
33
+ expect(popupSafeText(undefined, true)).toBeUndefined();
34
+ });
35
+
36
+ it('leaves plain text visually unchanged', () => {
37
+ expect(popupSafeText('Paris')).toBe('Paris');
38
+ });
39
+ });
40
+
41
+ describe('buildPopupContent — GeoJSON popup (P1.2)', () => {
42
+ const feature = (props: Record<string, unknown>) => ({ properties: props });
43
+
44
+ it('ignores a raw `popup.template` on the default (untrusted) path', () => {
45
+ const html = buildPopupContent(
46
+ feature({ name: 'Zone' }),
47
+ { template: `<b>{{name}}</b><img src=x onerror=alert(1)>` }
48
+ // allowHtml defaults to false
49
+ );
50
+ // Template skipped → falls through to the auto popup (no <img>, no <b>).
51
+ expect(html).not.toContain('<img');
52
+ expect(html).not.toContain('<b>');
53
+ });
54
+
55
+ it('honors `popup.template` when the host opts in, but escapes substituted values', () => {
56
+ const html = buildPopupContent(
57
+ feature({ name: '<script>evil</script>' }),
58
+ { template: '<b>{{name}}</b>' },
59
+ true
60
+ );
61
+ // The authored structural HTML stays; the data value is escaped.
62
+ expect(html).toContain('<b>');
63
+ expect(html).toContain('&lt;script&gt;');
64
+ expect(html).not.toContain('<script>');
65
+ });
66
+
67
+ it('auto-generated popup always escapes values (safe by construction)', () => {
68
+ const html = buildPopupContent(
69
+ feature({ title: XSS, count: 5 }),
70
+ { titleField: 'title', fields: ['count'] }
71
+ // default untrusted path
72
+ );
73
+ expect(html).not.toContain('<img');
74
+ expect(html).toContain('&lt;img');
75
+ // structural HTML authored by the renderer is present
76
+ expect(html).toContain('<strong>');
77
+ });
78
+
79
+ it('returns null when there is no popup config or no properties', () => {
80
+ expect(buildPopupContent({ properties: { a: 1 } }, undefined)).toBeNull();
81
+ expect(buildPopupContent({}, { titleField: 'a' })).toBeNull();
82
+ });
83
+ });