@ai-gui/plugin-map 0.4.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/dist/index.cjs ADDED
@@ -0,0 +1,877 @@
1
+ "use strict";
2
+
3
+ //#region src/css.ts
4
+ const mapCss = `
5
+ [data-aigui-map]{display:block;max-width:100%}
6
+ [data-aigui-map-canvas]{position:relative;width:100%;height:var(--aigui-map-height,360px);min-height:240px;overflow:hidden;background:#e8edf1;outline:none}
7
+ [data-aigui-map-canvas]:focus-visible{outline:3px solid #2563eb;outline-offset:2px}
8
+ [data-aigui-map-controls]{position:absolute;z-index:800;top:10px;right:10px;display:flex;gap:8px;flex-wrap:wrap}
9
+ [data-aigui-map-controls] button{min-width:44px;min-height:44px;border:1px solid #64748b;border-radius:6px;background:#fff;color:#111827;font:inherit;padding:8px 12px;cursor:pointer}
10
+ [data-aigui-map-summary]{margin-top:12px}
11
+ .leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}
12
+ .leaflet-container{overflow:hidden;-webkit-tap-highlight-color:transparent}.leaflet-container img,.leaflet-container svg{max-width:none!important;max-height:none!important}.leaflet-tile{visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-animated{transform-origin:0 0}.leaflet-zoom-hide{visibility:hidden}.leaflet-control-container{position:relative;z-index:800}.leaflet-control{position:relative;z-index:800;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control-zoom a{display:block;width:44px;height:44px;line-height:44px;text-align:center;background:#fff;color:#111;text-decoration:none}.leaflet-tooltip{position:absolute;padding:6px;background:#fff;border:1px solid #64748b;border-radius:4px;color:#111;white-space:nowrap;pointer-events:none}
13
+ @media (prefers-reduced-motion:reduce){[data-aigui-map] *{scroll-behavior:auto!important;transition:none!important;animation:none!important}}
14
+ `;
15
+
16
+ //#endregion
17
+ //#region src/mount.ts
18
+ const WORLD = [[-85.05112878, -180], [85.05112878, 180]];
19
+ const PAINT = {
20
+ default: {
21
+ color: "#334155",
22
+ fillColor: "#64748b",
23
+ fillOpacity: .25,
24
+ weight: 3
25
+ },
26
+ accent: {
27
+ color: "#1d4ed8",
28
+ fillColor: "#3b82f6",
29
+ fillOpacity: .28,
30
+ weight: 3
31
+ },
32
+ muted: {
33
+ color: "#64748b",
34
+ fillColor: "#94a3b8",
35
+ fillOpacity: .2,
36
+ weight: 2
37
+ },
38
+ positive: {
39
+ color: "#15803d",
40
+ fillColor: "#22c55e",
41
+ fillOpacity: .25,
42
+ weight: 3
43
+ },
44
+ warning: {
45
+ color: "#b45309",
46
+ fillColor: "#f59e0b",
47
+ fillOpacity: .28,
48
+ weight: 3
49
+ },
50
+ critical: {
51
+ color: "#b91c1c",
52
+ fillColor: "#ef4444",
53
+ fillOpacity: .28,
54
+ weight: 3
55
+ }
56
+ };
57
+ function mountMapDocument(host, document, options) {
58
+ if (!host || typeof host.replaceChildren !== "function") throw new TypeError("mountMapDocument requires an HTMLElement host.");
59
+ const canvas = globalThis.document.createElement("div");
60
+ canvas.setAttribute("data-aigui-map-canvas", "");
61
+ canvas.setAttribute("role", "region");
62
+ canvas.setAttribute("aria-label", document.ariaLabel ?? "Interactive map");
63
+ canvas.tabIndex = 0;
64
+ canvas.style.setProperty("--aigui-map-height", `${options.height}px`);
65
+ const controls = globalThis.document.createElement("div");
66
+ controls.setAttribute("data-aigui-map-controls", "");
67
+ const status = globalThis.document.createElement("div");
68
+ status.setAttribute("role", "status");
69
+ status.setAttribute("aria-live", "polite");
70
+ status.hidden = true;
71
+ canvas.append(controls, status);
72
+ host.replaceChildren(canvas);
73
+ let disposed = false;
74
+ let map$1;
75
+ let resize;
76
+ const listeners = [];
77
+ import("leaflet").then((module$1) => {
78
+ if (disposed) return;
79
+ const L = "default" in module$1 ? module$1.default : module$1;
80
+ map$1 = L.map(canvas, {
81
+ zoomControl: false,
82
+ attributionControl: false,
83
+ minZoom: options.minZoom,
84
+ maxZoom: options.maxZoom,
85
+ maxBounds: WORLD,
86
+ dragging: options.dragging,
87
+ touchZoom: options.touchZoom,
88
+ doubleClickZoom: options.doubleClickZoom,
89
+ scrollWheelZoom: options.scrollWheelZoom,
90
+ keyboard: options.keyboard
91
+ });
92
+ map$1.setMaxBounds(WORLD);
93
+ if (options.basemap) {
94
+ L.tileLayer(options.basemap.tileUrlTemplate, {
95
+ attribution: "",
96
+ minZoom: options.basemap.minZoom,
97
+ maxZoom: options.basemap.maxZoom,
98
+ maxNativeZoom: options.basemap.maxNativeZoom,
99
+ tileSize: options.basemap.tileSize
100
+ }).addTo(map$1);
101
+ canvas.appendChild(attribution(options.basemap.attribution));
102
+ }
103
+ const layers = [];
104
+ for (const layer of document.layers) addLayer(L, map$1, layer, layers);
105
+ const bounds = layers.length ? L.featureGroup(layers).getBounds() : void 0;
106
+ const reduced = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
107
+ const reset = () => {
108
+ if (!map$1) return;
109
+ if (document.view) map$1.setView(toLatLng(document.view.center), clamp(document.view.zoom, options.minZoom, options.maxZoom), { animate: !reduced });
110
+ else fit(map$1, bounds, options.maxFitZoom, reduced);
111
+ announce(status, "Map view reset.");
112
+ };
113
+ const fitData = () => {
114
+ if (map$1) {
115
+ fit(map$1, bounds, options.maxFitZoom, reduced);
116
+ announce(status, "Map fitted to data.");
117
+ }
118
+ };
119
+ if (document.view) reset();
120
+ else fitData();
121
+ if (options.controls.zoom) L.control.zoom({ position: "topleft" }).addTo(map$1);
122
+ if (options.controls.reset) listeners.push(button(controls, "Reset map view", reset));
123
+ if (options.controls.fit) listeners.push(button(controls, "Fit map data", fitData));
124
+ if (typeof ResizeObserver !== "undefined") {
125
+ resize = new ResizeObserver(() => map$1?.invalidateSize({ pan: false }));
126
+ resize.observe(canvas);
127
+ }
128
+ }).catch(() => {
129
+ if (!disposed) announce(status, "Map unavailable.");
130
+ });
131
+ return () => {
132
+ if (disposed) return;
133
+ disposed = true;
134
+ listeners.splice(0).forEach((remove) => remove());
135
+ resize?.disconnect();
136
+ map$1?.remove();
137
+ map$1 = void 0;
138
+ host.replaceChildren();
139
+ };
140
+ }
141
+ function addLayer(L, map$1, layer, layers) {
142
+ if (layer.type === "markers") for (const item of layer.items) {
143
+ const marker = L.circleMarker(toLatLng(item.position), {
144
+ ...paint(item.variant),
145
+ radius: 7
146
+ }).addTo(map$1);
147
+ marker.bindTooltip(tooltip([item.label, item.description]));
148
+ layers.push(marker);
149
+ }
150
+ else if (layer.type === "route") {
151
+ const route = L.polyline(layer.coordinates.map(toLatLng), paint(layer.variant)).addTo(map$1);
152
+ if (layer.label || layer.description) route.bindTooltip(tooltip([layer.label, layer.description]));
153
+ layers.push(route);
154
+ } else {
155
+ const geo = L.geoJSON(swapFeatureCollection(layer), {
156
+ style: paint(layer.variant),
157
+ pointToLayer: (_feature, latlng) => L.circleMarker(latlng, {
158
+ ...paint(layer.variant),
159
+ radius: 7
160
+ }),
161
+ onEachFeature: (feature, featureLayer) => {
162
+ const lines = geoTooltip(layer, feature);
163
+ if (lines.length) featureLayer.bindTooltip(tooltip(lines));
164
+ }
165
+ }).addTo(map$1);
166
+ layers.push(geo);
167
+ }
168
+ }
169
+ function geoTooltip(layer, feature) {
170
+ const result = [];
171
+ const label = featureLabel(layer, feature);
172
+ if (label) result.push(label);
173
+ for (const key of layer.tooltipProperties ?? []) {
174
+ const value = feature.properties?.[key];
175
+ if (value !== void 0 && value !== null) result.push(`${key}: ${String(value)}`);
176
+ }
177
+ return result;
178
+ }
179
+ function featureLabel(layer, feature) {
180
+ const value = layer.labelProperty ? feature.properties?.[layer.labelProperty] : void 0;
181
+ return value === void 0 || value === null ? void 0 : String(value);
182
+ }
183
+ function tooltip(lines) {
184
+ const node = globalThis.document.createElement("span");
185
+ node.textContent = lines.filter(Boolean).join("\n");
186
+ return node;
187
+ }
188
+ function paint(variant$1) {
189
+ return PAINT[variant$1 ?? "default"];
190
+ }
191
+ function toLatLng(position$1) {
192
+ return [position$1[1], position$1[0]];
193
+ }
194
+ function fit(map$1, bounds, maxZoom, reduced) {
195
+ if (bounds?.isValid()) map$1.fitBounds(bounds, {
196
+ padding: [24, 24],
197
+ maxZoom,
198
+ animate: !reduced
199
+ });
200
+ else map$1.setView([0, 0], 2, { animate: !reduced });
201
+ }
202
+ function button(host, label, action) {
203
+ const element = globalThis.document.createElement("button");
204
+ element.type = "button";
205
+ element.textContent = label;
206
+ element.setAttribute("aria-label", label);
207
+ element.addEventListener("click", action);
208
+ host.appendChild(element);
209
+ return () => element.removeEventListener("click", action);
210
+ }
211
+ function announce(status, text$1) {
212
+ status.hidden = false;
213
+ status.textContent = text$1;
214
+ }
215
+ function attribution(value) {
216
+ const node = globalThis.document.createElement("div");
217
+ node.setAttribute("data-aigui-map-attribution", "");
218
+ if (value.url) {
219
+ const link = globalThis.document.createElement("a");
220
+ link.href = value.url;
221
+ link.target = "_blank";
222
+ link.rel = "noopener noreferrer";
223
+ link.textContent = value.text;
224
+ node.appendChild(link);
225
+ } else node.textContent = value.text;
226
+ return node;
227
+ }
228
+ function clamp(value, min, max) {
229
+ return Math.min(max, Math.max(min, value));
230
+ }
231
+ function swapFeatureCollection(layer) {
232
+ return {
233
+ type: "FeatureCollection",
234
+ features: layer.data.features.map((feature) => ({
235
+ ...feature,
236
+ geometry: swapGeometry(feature.geometry)
237
+ }))
238
+ };
239
+ }
240
+ function swapGeometry(geometry) {
241
+ const swap = (value) => Array.isArray(value) && value.length === 2 && typeof value[0] === "number" ? [value[0], value[1]] : Array.isArray(value) ? value.map(swap) : value;
242
+ return {
243
+ type: geometry.type,
244
+ coordinates: swap(geometry.coordinates)
245
+ };
246
+ }
247
+
248
+ //#endregion
249
+ //#region src/options.ts
250
+ const OPTION_KEYS = set$1("height", "minZoom", "maxZoom", "maxFitZoom", "dragging", "touchZoom", "doubleClickZoom", "scrollWheelZoom", "keyboard", "controls", "basemap", "networkPolicy");
251
+ const CONTROL_KEYS = set$1("zoom", "reset", "fit");
252
+ const BASEMAP_KEYS = set$1("tileUrlTemplate", "attribution", "minZoom", "maxZoom", "maxNativeZoom", "tileSize");
253
+ const ATTRIBUTION_KEYS = set$1("text", "url");
254
+ const POLICY_KEYS = set$1("allowedTileOrigins");
255
+ function resolveMapOptions(options = {}) {
256
+ assertSafeOptions(options);
257
+ plain(options, "map options");
258
+ exact(options, OPTION_KEYS, "map options");
259
+ const height = integer(options.height, 240, 720, 360, "height");
260
+ const minZoom = integer(options.minZoom, 0, 22, 0, "minZoom");
261
+ const maxZoom = integer(options.maxZoom, 0, 22, 22, "maxZoom");
262
+ if (minZoom > maxZoom) fail("minZoom cannot exceed maxZoom.");
263
+ const maxFitZoom = integer(options.maxFitZoom, minZoom, maxZoom, maxZoom, "maxFitZoom");
264
+ const controls = options.controls ?? {};
265
+ plain(controls, "controls");
266
+ exact(controls, CONTROL_KEYS, "controls");
267
+ const basemap = options.basemap ?? false;
268
+ let validatedBasemap = false;
269
+ if (basemap !== false) {
270
+ plain(basemap, "basemap");
271
+ exact(basemap, BASEMAP_KEYS, "basemap");
272
+ plain(basemap.attribution, "basemap.attribution");
273
+ exact(basemap.attribution, ATTRIBUTION_KEYS, "basemap.attribution");
274
+ boundedText(basemap.attribution.text, "basemap.attribution.text");
275
+ if (basemap.attribution.url !== void 0) safeUrl(basemap.attribution.url, "basemap.attribution.url", false);
276
+ const tileURL = safeUrl(basemap.tileUrlTemplate, "basemap.tileUrlTemplate", true);
277
+ const tileMin = integer(basemap.minZoom, 0, 22, minZoom, "basemap.minZoom");
278
+ const tileMax = integer(basemap.maxZoom, 0, 22, maxZoom, "basemap.maxZoom");
279
+ if (tileMin > tileMax || tileMin < minZoom || tileMax > maxZoom) fail("Basemap zoom range must fit map zoom range.");
280
+ const maxNativeZoom = integer(basemap.maxNativeZoom, tileMin, tileMax, tileMax, "basemap.maxNativeZoom");
281
+ const tileSize = integer(basemap.tileSize, 128, 512, 256, "basemap.tileSize");
282
+ if (!options.networkPolicy) fail("networkPolicy.allowedTileOrigins is required for a basemap.");
283
+ plain(options.networkPolicy, "networkPolicy");
284
+ exact(options.networkPolicy, POLICY_KEYS, "networkPolicy");
285
+ if (!Array.isArray(options.networkPolicy.allowedTileOrigins) || options.networkPolicy.allowedTileOrigins.length === 0) fail("allowedTileOrigins must be a non-empty array.");
286
+ const origins = options.networkPolicy.allowedTileOrigins.map((origin) => normalizedOrigin(origin));
287
+ if (!origins.includes(tileURL.origin)) fail("Tile URL origin is not allowed by networkPolicy.");
288
+ validatedBasemap = {
289
+ tileUrlTemplate: basemap.tileUrlTemplate,
290
+ attribution: basemap.attribution,
291
+ minZoom: tileMin,
292
+ maxZoom: tileMax,
293
+ maxNativeZoom,
294
+ tileSize
295
+ };
296
+ } else if (options.networkPolicy !== void 0) {
297
+ plain(options.networkPolicy, "networkPolicy");
298
+ exact(options.networkPolicy, POLICY_KEYS, "networkPolicy");
299
+ if (!Array.isArray(options.networkPolicy.allowedTileOrigins)) fail("allowedTileOrigins must be an array.");
300
+ options.networkPolicy.allowedTileOrigins.forEach(normalizedOrigin);
301
+ }
302
+ return {
303
+ height,
304
+ minZoom,
305
+ maxZoom,
306
+ maxFitZoom,
307
+ dragging: bool(options.dragging, true, "dragging"),
308
+ touchZoom: bool(options.touchZoom, true, "touchZoom"),
309
+ doubleClickZoom: bool(options.doubleClickZoom, true, "doubleClickZoom"),
310
+ scrollWheelZoom: bool(options.scrollWheelZoom, false, "scrollWheelZoom"),
311
+ keyboard: bool(options.keyboard, true, "keyboard"),
312
+ controls: {
313
+ zoom: bool(controls.zoom, true, "controls.zoom"),
314
+ reset: bool(controls.reset, true, "controls.reset"),
315
+ fit: bool(controls.fit, true, "controls.fit")
316
+ },
317
+ basemap: validatedBasemap,
318
+ networkPolicy: options.networkPolicy
319
+ };
320
+ }
321
+ function safeUrl(value, name, template) {
322
+ if (typeof value !== "string" || value.length > 2048) fail(`${name} must be a bounded URL string.`);
323
+ let parsed;
324
+ try {
325
+ parsed = new URL(value);
326
+ } catch {
327
+ fail(`${name} must be an absolute URL.`);
328
+ }
329
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:" || parsed.username || parsed.password || parsed.hash) fail(`${name} must be a safe HTTP(S) URL.`);
330
+ const placeholders = [...value.matchAll(/\{([^}]+)\}/g)];
331
+ if (value.replace(/\{[^}]+\}/g, "").includes("{") || value.replace(/\{[^}]+\}/g, "").includes("}")) fail(`${name} contains malformed placeholders.`);
332
+ const authorityEnd = value.indexOf("/", value.indexOf("//") + 2);
333
+ for (const match of placeholders) if (![
334
+ "z",
335
+ "x",
336
+ "y",
337
+ "r"
338
+ ].includes(match[1]) || (match.index ?? 0) < authorityEnd) fail(`${name} contains an unsupported placeholder.`);
339
+ if (template && ![
340
+ "z",
341
+ "x",
342
+ "y"
343
+ ].every((key) => placeholders.some((match) => match[1] === key))) fail(`${name} must contain z, x, and y placeholders.`);
344
+ if (!template && placeholders.length) fail(`${name} cannot contain placeholders.`);
345
+ return parsed;
346
+ }
347
+ function normalizedOrigin(value) {
348
+ if (typeof value !== "string") fail("Allowed tile origins must be strings.");
349
+ let url;
350
+ try {
351
+ url = new URL(value);
352
+ } catch {
353
+ fail("Allowed tile origins must be absolute origins.");
354
+ }
355
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.pathname !== "/" || url.search || value !== url.origin) fail("Allowed tile origins must be exact normalized HTTP(S) origins.");
356
+ return url.origin;
357
+ }
358
+ function integer(value, min, max, fallback, name) {
359
+ if (value === void 0) return fallback;
360
+ if (!Number.isInteger(value) || value < min || value > max) fail(`${name} must be an integer from ${min} to ${max}.`);
361
+ return value;
362
+ }
363
+ function bool(value, fallback, name) {
364
+ if (value === void 0) return fallback;
365
+ if (typeof value !== "boolean") fail(`${name} must be boolean.`);
366
+ return value;
367
+ }
368
+ function boundedText(value, name) {
369
+ if (typeof value !== "string" || value.trim() === "" || value.length > 512) fail(`${name} must be a non-empty bounded string.`);
370
+ }
371
+ function plain(value, name) {
372
+ if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) fail(`${name} must be a plain object.`);
373
+ }
374
+ function exact(value, allowed, name) {
375
+ for (const key of Object.keys(value)) if (!allowed.has(key) || [
376
+ "__proto__",
377
+ "prototype",
378
+ "constructor"
379
+ ].includes(key)) fail(`${name}.${key} is not allowed.`);
380
+ }
381
+ function set$1(...keys) {
382
+ return new Set(keys);
383
+ }
384
+ function fail(message) {
385
+ throw new TypeError(message);
386
+ }
387
+ function assertSafeOptions(value) {
388
+ const active = new Set(), visited = new Set();
389
+ const walk = (current) => {
390
+ if (typeof current === "number" && !Number.isFinite(current)) fail("Map options must contain finite numbers.");
391
+ if (typeof current !== "object" || current === null) return;
392
+ if (active.has(current)) fail("Map options must not contain cycles.");
393
+ if (visited.has(current)) return;
394
+ if (!Array.isArray(current) && Object.getPrototypeOf(current) !== Object.prototype) fail("Map options must use plain objects.");
395
+ if (Array.isArray(current)) {
396
+ for (let index = 0; index < current.length; index++) if (!Object.hasOwn(current, index)) fail("Map option arrays must not be sparse.");
397
+ }
398
+ active.add(current);
399
+ visited.add(current);
400
+ for (const key of Object.keys(current)) {
401
+ if ([
402
+ "__proto__",
403
+ "prototype",
404
+ "constructor"
405
+ ].includes(key)) fail(`Dangerous option key "${key}" is not allowed.`);
406
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
407
+ if (!descriptor || descriptor.get || descriptor.set) fail("Map options must not contain accessors.");
408
+ walk(descriptor.value);
409
+ }
410
+ active.delete(current);
411
+ };
412
+ walk(value);
413
+ }
414
+
415
+ //#endregion
416
+ //#region src/prompt.ts
417
+ function mapPromptSpec() {
418
+ return [
419
+ "Maps (one complete fenced block maximum): ```map <strict MapDocument JSON>```.",
420
+ "MapDocument is exact: {\"version\":1,\"ariaLabel\"?:string,\"view\"?:{\"center\":[lon,lat],\"zoom\":0..22},\"layers\":[...]}.",
421
+ "Use maps, rather than ECharts, for inline GeoJSON, markers, routes, and user map navigation.",
422
+ "Layers are exact geojson FeatureCollections, marker items, or route coordinates; variants are default, accent, muted, positive, warning, or critical.",
423
+ "The model controls data only. The host controls navigation controls, basemap, and network policy.",
424
+ "Never include tile URLs, tokens, remote GeoJSON, geocoding, HTML, scripts, CSS, style expressions, images, callbacks, options, colors, or network fields."
425
+ ].join("\n");
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/errors.ts
430
+ var MapDocumentError = class extends Error {
431
+ issues;
432
+ constructor(issues) {
433
+ super(issues[0] ?? "Invalid map document.");
434
+ this.name = "MapDocumentError";
435
+ this.issues = [...issues];
436
+ }
437
+ };
438
+ var MapLimitError = class extends MapDocumentError {
439
+ constructor(message) {
440
+ super([message]);
441
+ this.name = "MapLimitError";
442
+ }
443
+ };
444
+
445
+ //#endregion
446
+ //#region src/limits.ts
447
+ const DEFAULT_MAP_LIMITS = Object.freeze({
448
+ sourceBytes: 256 * 1024,
449
+ layers: 16,
450
+ features: 500,
451
+ markers: 500,
452
+ coordinatePositions: 2e4,
453
+ routePositions: 5e3,
454
+ geometryDepth: 8,
455
+ properties: 16,
456
+ tooltipProperties: 8,
457
+ string: 512,
458
+ totalStrings: 64 * 1024,
459
+ id: 128
460
+ });
461
+
462
+ //#endregion
463
+ //#region src/validate.ts
464
+ const SAFE_ID = /^[A-Za-z][A-Za-z0-9_-]{0,127}$/;
465
+ const DANGEROUS_KEYS = new Set([
466
+ "__proto__",
467
+ "prototype",
468
+ "constructor"
469
+ ]);
470
+ const VARIANTS = new Set([
471
+ "default",
472
+ "accent",
473
+ "muted",
474
+ "positive",
475
+ "warning",
476
+ "critical"
477
+ ]);
478
+ const DOCUMENT_KEYS = set("version", "ariaLabel", "view", "layers");
479
+ const VIEW_KEYS = set("center", "zoom");
480
+ const LAYER_KEYS = {
481
+ geojson: set("id", "type", "data", "labelProperty", "tooltipProperties", "variant"),
482
+ markers: set("id", "type", "items"),
483
+ route: set("id", "type", "coordinates", "label", "description", "variant")
484
+ };
485
+ const FEATURE_COLLECTION_KEYS = set("type", "features");
486
+ const FEATURE_KEYS = set("type", "id", "properties", "geometry");
487
+ const GEOMETRY_KEYS = set("type", "coordinates");
488
+ const MARKER_KEYS = set("id", "position", "label", "description", "variant");
489
+ function parseMapDocument(source) {
490
+ if (typeof source !== "string") throw new MapDocumentError(["Map source must be a string."]);
491
+ if (new TextEncoder().encode(source).byteLength > DEFAULT_MAP_LIMITS.sourceBytes) throw new MapLimitError("Map source exceeds 256 KiB.");
492
+ let value;
493
+ try {
494
+ value = JSON.parse(source);
495
+ } catch {
496
+ throw new MapDocumentError(["Map source must be valid JSON."]);
497
+ }
498
+ return validateMapDocument(value);
499
+ }
500
+ function validateMapDocument(value) {
501
+ assertSafeGraph(value);
502
+ const issues = [];
503
+ if (!isPlainObject(value)) throw new MapDocumentError(["Map document must be a plain object."]);
504
+ rejectKeys(value, DOCUMENT_KEYS, "$", issues);
505
+ if (value.version !== 1) issues.push("$.version must be 1.");
506
+ const ctx = {
507
+ limits: DEFAULT_MAP_LIMITS,
508
+ issues,
509
+ ids: new Set(),
510
+ features: 0,
511
+ markers: 0,
512
+ positions: 0,
513
+ strings: 0
514
+ };
515
+ optionalString(value.ariaLabel, "$.ariaLabel", ctx);
516
+ validateView(value.view, ctx);
517
+ if (!Array.isArray(value.layers)) issues.push("$.layers must be an array.");
518
+ else {
519
+ if (value.layers.length > ctx.limits.layers) throw new MapLimitError(`Map exceeds ${ctx.limits.layers} layers.`);
520
+ value.layers.forEach((layer, index) => validateLayer(layer, `$.layers[${index}]`, ctx));
521
+ }
522
+ if (issues.length) throw new MapDocumentError(issues);
523
+ return value;
524
+ }
525
+ function validateView(value, ctx) {
526
+ if (value === void 0) return;
527
+ if (!isPlainObject(value)) {
528
+ ctx.issues.push("$.view must be a plain object.");
529
+ return;
530
+ }
531
+ rejectKeys(value, VIEW_KEYS, "$.view", ctx.issues);
532
+ position(value.center, "$.view.center", ctx);
533
+ if (!Number.isInteger(value.zoom) || value.zoom < 0 || value.zoom > 22) ctx.issues.push("$.view.zoom must be an integer from 0 to 22.");
534
+ }
535
+ function validateLayer(value, path, ctx) {
536
+ if (!isPlainObject(value)) {
537
+ ctx.issues.push(`${path} must be a plain object.`);
538
+ return;
539
+ }
540
+ const type = typeof value.type === "string" ? value.type : "";
541
+ const keys = LAYER_KEYS[type];
542
+ if (!keys) {
543
+ ctx.issues.push(`${path}.type is unsupported.`);
544
+ return;
545
+ }
546
+ rejectKeys(value, keys, path, ctx.issues);
547
+ id(value.id, `${path}.id`, ctx);
548
+ if (type === "geojson") validateGeoJSONLayer(value, path, ctx);
549
+ else if (type === "markers") validateMarkersLayer(value, path, ctx);
550
+ else validateRouteLayer(value, path, ctx);
551
+ }
552
+ function validateGeoJSONLayer(value, path, ctx) {
553
+ variant(value.variant, `${path}.variant`, ctx);
554
+ const labelProperty = optionalString(value.labelProperty, `${path}.labelProperty`, ctx);
555
+ if (labelProperty !== void 0 && DANGEROUS_KEYS.has(labelProperty)) ctx.issues.push(`${path}.labelProperty is dangerous.`);
556
+ if (value.tooltipProperties !== void 0) if (!Array.isArray(value.tooltipProperties) || value.tooltipProperties.length > ctx.limits.tooltipProperties) ctx.issues.push(`${path}.tooltipProperties must contain at most ${ctx.limits.tooltipProperties} strings.`);
557
+ else {
558
+ const seen = new Set();
559
+ value.tooltipProperties.forEach((item, index) => {
560
+ const key = requiredString(item, `${path}.tooltipProperties[${index}]`, ctx);
561
+ if (key && (DANGEROUS_KEYS.has(key) || seen.has(key))) ctx.issues.push(`${path}.tooltipProperties[${index}] must be safe and unique.`);
562
+ if (key) seen.add(key);
563
+ });
564
+ }
565
+ const data = value.data;
566
+ if (!isPlainObject(data)) {
567
+ ctx.issues.push(`${path}.data must be a FeatureCollection.`);
568
+ return;
569
+ }
570
+ rejectKeys(data, FEATURE_COLLECTION_KEYS, `${path}.data`, ctx.issues);
571
+ if (data.type !== "FeatureCollection" || !Array.isArray(data.features)) {
572
+ ctx.issues.push(`${path}.data must be an exact FeatureCollection.`);
573
+ return;
574
+ }
575
+ if (ctx.features + data.features.length > ctx.limits.features) throw new MapLimitError(`Map exceeds ${ctx.limits.features} features.`);
576
+ ctx.features += data.features.length;
577
+ data.features.forEach((feature, index) => validateFeature(feature, `${path}.data.features[${index}]`, ctx));
578
+ }
579
+ function validateFeature(value, path, ctx) {
580
+ if (!isPlainObject(value)) {
581
+ ctx.issues.push(`${path} must be a plain object.`);
582
+ return;
583
+ }
584
+ rejectKeys(value, FEATURE_KEYS, path, ctx.issues);
585
+ if (value.type !== "Feature") ctx.issues.push(`${path}.type must be Feature.`);
586
+ if (typeof value.id === "string") id(value.id, `${path}.id`, ctx);
587
+ else if (typeof value.id === "number" && Number.isFinite(value.id)) {
588
+ const key = `#${value.id}`;
589
+ if (ctx.ids.has(key)) ctx.issues.push(`${path}.id must be unique.`);
590
+ ctx.ids.add(key);
591
+ } else if (value.id !== void 0) ctx.issues.push(`${path}.id must be a safe string or finite number.`);
592
+ if (value.properties !== void 0) validateProperties(value.properties, `${path}.properties`, ctx);
593
+ validateGeometry(value.geometry, `${path}.geometry`, 1, ctx);
594
+ }
595
+ function validateProperties(value, path, ctx) {
596
+ if (!isPlainObject(value)) {
597
+ ctx.issues.push(`${path} must be a plain object.`);
598
+ return;
599
+ }
600
+ const entries = Object.entries(value);
601
+ if (entries.length > ctx.limits.properties) ctx.issues.push(`${path} exceeds ${ctx.limits.properties} properties.`);
602
+ for (const [key, item] of entries) {
603
+ if (DANGEROUS_KEYS.has(key) || key.length > ctx.limits.string) ctx.issues.push(`${path}.${key} is not a safe property.`);
604
+ countString(key, path, ctx);
605
+ if (!isScalar(item)) ctx.issues.push(`${path}.${key} must be a scalar.`);
606
+ else if (typeof item === "string") countString(item, `${path}.${key}`, ctx);
607
+ }
608
+ }
609
+ function validateGeometry(value, path, depth, ctx) {
610
+ if (depth > ctx.limits.geometryDepth) {
611
+ ctx.issues.push(`${path} exceeds geometry depth.`);
612
+ return;
613
+ }
614
+ if (!isPlainObject(value)) {
615
+ ctx.issues.push(`${path} must be a supported geometry.`);
616
+ return;
617
+ }
618
+ rejectKeys(value, GEOMETRY_KEYS, path, ctx.issues);
619
+ const geometry = value;
620
+ switch (geometry.type) {
621
+ case "Point":
622
+ position(geometry.coordinates, `${path}.coordinates`, ctx);
623
+ break;
624
+ case "MultiPoint":
625
+ positions(geometry.coordinates, `${path}.coordinates`, 0, ctx);
626
+ break;
627
+ case "LineString":
628
+ positions(geometry.coordinates, `${path}.coordinates`, 2, ctx);
629
+ break;
630
+ case "MultiLineString":
631
+ nestedLines(geometry.coordinates, `${path}.coordinates`, ctx);
632
+ break;
633
+ case "Polygon":
634
+ polygon(geometry.coordinates, `${path}.coordinates`, ctx);
635
+ break;
636
+ case "MultiPolygon": {
637
+ if (!Array.isArray(geometry.coordinates)) ctx.issues.push(`${path}.coordinates must be an array.`);
638
+ else geometry.coordinates.forEach((item, index) => polygon(item, `${path}.coordinates[${index}]`, ctx));
639
+ break;
640
+ }
641
+ default: ctx.issues.push(`${path}.type is unsupported.`);
642
+ }
643
+ }
644
+ function validateMarkersLayer(value, path, ctx) {
645
+ if (!Array.isArray(value.items)) {
646
+ ctx.issues.push(`${path}.items must be an array.`);
647
+ return;
648
+ }
649
+ if (ctx.markers + value.items.length > ctx.limits.markers) throw new MapLimitError(`Map exceeds ${ctx.limits.markers} markers.`);
650
+ ctx.markers += value.items.length;
651
+ value.items.forEach((item, index) => {
652
+ const itemPath = `${path}.items[${index}]`;
653
+ if (!isPlainObject(item)) {
654
+ ctx.issues.push(`${itemPath} must be a plain object.`);
655
+ return;
656
+ }
657
+ rejectKeys(item, MARKER_KEYS, itemPath, ctx.issues);
658
+ id(item.id, `${itemPath}.id`, ctx);
659
+ position(item.position, `${itemPath}.position`, ctx);
660
+ requiredString(item.label, `${itemPath}.label`, ctx);
661
+ optionalString(item.description, `${itemPath}.description`, ctx);
662
+ variant(item.variant, `${itemPath}.variant`, ctx);
663
+ });
664
+ }
665
+ function validateRouteLayer(value, path, ctx) {
666
+ if (Array.isArray(value.coordinates) && value.coordinates.length > ctx.limits.routePositions) throw new MapLimitError(`Route exceeds ${ctx.limits.routePositions} positions.`);
667
+ positions(value.coordinates, `${path}.coordinates`, 2, ctx);
668
+ optionalString(value.label, `${path}.label`, ctx);
669
+ optionalString(value.description, `${path}.description`, ctx);
670
+ variant(value.variant, `${path}.variant`, ctx);
671
+ }
672
+ function polygon(value, path, ctx) {
673
+ if (!Array.isArray(value) || value.length === 0) {
674
+ ctx.issues.push(`${path} must contain polygon rings.`);
675
+ return;
676
+ }
677
+ value.forEach((ring, index) => {
678
+ positions(ring, `${path}[${index}]`, 4, ctx);
679
+ if (Array.isArray(ring) && ring.length >= 2 && !samePosition(ring[0], ring[ring.length - 1])) ctx.issues.push(`${path}[${index}] must be closed.`);
680
+ });
681
+ }
682
+ function nestedLines(value, path, ctx) {
683
+ if (!Array.isArray(value)) ctx.issues.push(`${path} must be an array.`);
684
+ else value.forEach((line, index) => positions(line, `${path}[${index}]`, 2, ctx));
685
+ }
686
+ function positions(value, path, min, ctx) {
687
+ if (!Array.isArray(value) || value.length < min) {
688
+ ctx.issues.push(`${path} must contain at least ${min} positions.`);
689
+ return;
690
+ }
691
+ value.forEach((item, index) => position(item, `${path}[${index}]`, ctx));
692
+ }
693
+ function position(value, path, ctx) {
694
+ if (!Array.isArray(value) || value.length !== 2 || typeof value[0] !== "number" || typeof value[1] !== "number" || !Number.isFinite(value[0]) || !Number.isFinite(value[1])) {
695
+ ctx.issues.push(`${path} must be [longitude, latitude].`);
696
+ return false;
697
+ }
698
+ if (value[0] < -180 || value[0] > 180) ctx.issues.push(`${path}[0] must be from -180 to 180.`);
699
+ if (value[1] < -85.05112878 || value[1] > 85.05112878) ctx.issues.push(`${path}[1] must be within Web Mercator latitude bounds.`);
700
+ if (++ctx.positions > ctx.limits.coordinatePositions) throw new MapLimitError(`Map exceeds ${ctx.limits.coordinatePositions} coordinate positions.`);
701
+ return true;
702
+ }
703
+ function id(value, path, ctx) {
704
+ const text$1 = requiredString(value, path, ctx, ctx.limits.id);
705
+ if (!text$1) return;
706
+ if (!SAFE_ID.test(text$1)) ctx.issues.push(`${path} is not a safe id.`);
707
+ if (ctx.ids.has(text$1)) ctx.issues.push(`${path} must be unique.`);
708
+ ctx.ids.add(text$1);
709
+ }
710
+ function variant(value, path, ctx) {
711
+ if (value !== void 0 && !VARIANTS.has(value)) ctx.issues.push(`${path} is invalid.`);
712
+ }
713
+ function optionalString(value, path, ctx) {
714
+ if (value === void 0) return void 0;
715
+ return requiredString(value, path, ctx);
716
+ }
717
+ function requiredString(value, path, ctx, max = ctx.limits.string) {
718
+ if (typeof value !== "string" || value.trim() === "" || value.length > max) {
719
+ ctx.issues.push(`${path} must be a non-empty bounded string.`);
720
+ return void 0;
721
+ }
722
+ countString(value, path, ctx);
723
+ return value;
724
+ }
725
+ function countString(value, path, ctx) {
726
+ if (value.length > ctx.limits.string && !path.endsWith(".id")) ctx.issues.push(`${path} exceeds string limit.`);
727
+ ctx.strings += value.length;
728
+ if (ctx.strings > ctx.limits.totalStrings) throw new MapLimitError(`Map exceeds ${ctx.limits.totalStrings} total string characters.`);
729
+ }
730
+ function rejectKeys(value, allowed, path, issues) {
731
+ for (const key of Object.keys(value)) if (DANGEROUS_KEYS.has(key) || !allowed.has(key)) issues.push(`${path}.${key} is not allowed.`);
732
+ }
733
+ function samePosition(a, b) {
734
+ return Array.isArray(a) && Array.isArray(b) && a.length === 2 && b.length === 2 && a[0] === b[0] && a[1] === b[1];
735
+ }
736
+ function isScalar(value) {
737
+ return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
738
+ }
739
+ function isPlainObject(value) {
740
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
741
+ const prototype = Object.getPrototypeOf(value);
742
+ return prototype === Object.prototype || prototype === null;
743
+ }
744
+ function set(...values) {
745
+ return new Set(values);
746
+ }
747
+ function assertSafeGraph(value) {
748
+ const active = new Set(), visited = new Set();
749
+ const walk = (current) => {
750
+ if (typeof current === "number" && !Number.isFinite(current)) throw new MapDocumentError(["Map values must be finite."]);
751
+ if (typeof current !== "object" || current === null) return;
752
+ if (active.has(current)) throw new MapDocumentError(["Map values must not contain cycles."]);
753
+ if (visited.has(current)) return;
754
+ if (!Array.isArray(current) && !isPlainObject(current)) throw new MapDocumentError(["Map values must use plain objects."]);
755
+ if (Array.isArray(current)) {
756
+ for (let i = 0; i < current.length; i++) if (!Object.hasOwn(current, i)) throw new MapDocumentError(["Map arrays must not be sparse."]);
757
+ }
758
+ active.add(current);
759
+ visited.add(current);
760
+ for (const key of Object.keys(current)) {
761
+ if (DANGEROUS_KEYS.has(key)) throw new MapDocumentError([`Dangerous key "${key}" is not allowed.`]);
762
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
763
+ if (!descriptor || descriptor.get || descriptor.set) throw new MapDocumentError(["Map values must not contain accessors."]);
764
+ walk(descriptor.value);
765
+ }
766
+ active.delete(current);
767
+ };
768
+ walk(value);
769
+ }
770
+
771
+ //#endregion
772
+ //#region src/index.ts
773
+ function map(options = {}) {
774
+ const resolved = resolveMapOptions(options);
775
+ const outputs = new WeakMap();
776
+ const rejected = new WeakSet();
777
+ const render = (node) => {
778
+ const cached = outputs.get(node);
779
+ if (cached) return cached;
780
+ let output;
781
+ if (!node.complete) output = {
782
+ kind: "html",
783
+ html: "<div data-aigui-map-loading=\"\" data-block-type=\"map\"></div>"
784
+ };
785
+ else if (rejected.has(node)) output = invalidOutput();
786
+ else try {
787
+ const document = parseMapDocument(node.content ?? "");
788
+ output = renderMap(document, resolved);
789
+ } catch {
790
+ output = invalidOutput();
791
+ }
792
+ outputs.set(node, output);
793
+ return output;
794
+ };
795
+ return {
796
+ name: "map",
797
+ nodeRenderers: { map: render },
798
+ onASTCommit: (nodes) => {
799
+ let accepted = false;
800
+ for (const node of nodes) {
801
+ if (node.type !== "map" || !node.complete) continue;
802
+ if (accepted) rejected.add(node);
803
+ else accepted = true;
804
+ }
805
+ },
806
+ promptSpec: mapPromptSpec(),
807
+ css: mapCss
808
+ };
809
+ }
810
+ function invalidOutput() {
811
+ return {
812
+ kind: "html",
813
+ html: "<div data-aigui-map-invalid=\"\" role=\"alert\">Invalid map.</div>"
814
+ };
815
+ }
816
+ function renderMap(document, options) {
817
+ return {
818
+ kind: "element",
819
+ tag: "section",
820
+ props: {
821
+ "data-aigui-map": "",
822
+ "aria-label": document.ariaLabel ?? "Interactive map"
823
+ },
824
+ children: [{
825
+ kind: "mount",
826
+ mount: (host) => mountMapDocument(host, document, options)
827
+ }, mapSummary(document)]
828
+ };
829
+ }
830
+ function mapSummary(document) {
831
+ const items = [];
832
+ for (const layer of document.layers) if (layer.type === "markers") for (const marker of layer.items) items.push(summaryItem(marker.label, marker.description));
833
+ else if (layer.type === "route" && (layer.label || layer.description)) items.push(summaryItem(layer.label ?? "Route", layer.description));
834
+ else if (layer.type === "geojson" && layer.labelProperty) for (const feature of layer.data.features) {
835
+ const value = feature.properties?.[layer.labelProperty];
836
+ if (value !== void 0 && value !== null) items.push(summaryItem(String(value)));
837
+ }
838
+ if (!items.length) items.push(summaryItem("Map data is available in the interactive region."));
839
+ return {
840
+ kind: "element",
841
+ tag: "aside",
842
+ props: {
843
+ "data-aigui-map-summary": "",
844
+ "aria-label": "Map summary"
845
+ },
846
+ children: [{
847
+ kind: "element",
848
+ tag: "ul",
849
+ children: items
850
+ }]
851
+ };
852
+ }
853
+ function summaryItem(label, description) {
854
+ return {
855
+ kind: "element",
856
+ tag: "li",
857
+ children: [text(description ? `${label}: ${description}` : label)]
858
+ };
859
+ }
860
+ function text(value) {
861
+ return {
862
+ kind: "html",
863
+ html: value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&#39;")
864
+ };
865
+ }
866
+
867
+ //#endregion
868
+ exports.DEFAULT_MAP_LIMITS = DEFAULT_MAP_LIMITS
869
+ exports.MapDocumentError = MapDocumentError
870
+ exports.MapLimitError = MapLimitError
871
+ exports.map = map
872
+ exports.mapCss = mapCss
873
+ exports.mapPromptSpec = mapPromptSpec
874
+ exports.mountMapDocument = mountMapDocument
875
+ exports.parseMapDocument = parseMapDocument
876
+ exports.resolveMapOptions = resolveMapOptions
877
+ exports.validateMapDocument = validateMapDocument