@versatiles/svg-renderer 0.8.0 → 1.1.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/README.md CHANGED
@@ -64,13 +64,23 @@ document.body.innerHTML = svg;
64
64
 
65
65
  The package includes an `SVGExportControl` that adds an export button to any MapLibre GL JS map.
66
66
 
67
+ With a bundler, import it from the `/maplibre` subpath:
68
+
69
+ ```typescript
70
+ import { SVGExportControl } from '@versatiles/svg-renderer/maplibre';
71
+
72
+ map.addControl(new SVGExportControl(), 'top-right');
73
+ ```
74
+
75
+ Or load it directly in the browser via the UMD bundle, which exposes a `VersaTilesSVG` global:
76
+
67
77
  ```html
68
78
  <!DOCTYPE html>
69
79
  <html>
70
80
  <head>
71
81
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" />
72
82
  <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
73
- <script src="https://unpkg.com/@versatiles/svg-renderer/dist/maplibre.umd.js"></script>
83
+ <script src="https://unpkg.com/@versatiles/svg-renderer/dist/maplibre-svg-export.umd.js"></script>
74
84
  </head>
75
85
  <body>
76
86
  <div id="map"></div>
@@ -87,7 +97,7 @@ The package includes an `SVGExportControl` that adds an export button to any Map
87
97
  </html>
88
98
  ```
89
99
 
90
- The control opens a panel where the user can set width, height, and scale, preview the SVG, download it, or open it in a new tab. Map interactions are disabled while the panel is open.
100
+ The control opens a panel where the user can set width and height, preview the SVG, download it, or open it in a new tab. Map interactions are disabled while the panel is open.
91
101
 
92
102
  Options:
93
103
 
@@ -95,7 +105,6 @@ Options:
95
105
  new SVGExportControl({
96
106
  defaultWidth: 1024, // default: 1024
97
107
  defaultHeight: 1024, // default: 1024
98
- defaultScale: 1, // default: 1
99
108
  });
100
109
  ```
101
110
 
@@ -145,53 +154,54 @@ subgraph 0["src"]
145
154
  2["index.ts"]
146
155
  subgraph 3["pipeline"]
147
156
  4["render.ts"]
148
- D["style_layer.ts"]
157
+ E["style_layer.ts"]
149
158
  end
150
159
  subgraph 5["sources"]
151
160
  6["index.ts"]
152
161
  7["geojson.ts"]
153
162
  9["raster.ts"]
154
- A["tiles.ts"]
155
- B["sprite.ts"]
156
- C["vector.ts"]
157
- N["merge.ts"]
163
+ A["base64.ts"]
164
+ B["tiles.ts"]
165
+ C["sprite.ts"]
166
+ D["vector.ts"]
158
167
  end
159
168
  8["geometry.ts"]
160
- subgraph E["renderer"]
161
- F["svg.ts"]
162
- G["color.ts"]
163
- H["svg_path.ts"]
164
- M["types.ts"]
169
+ subgraph F["renderer"]
170
+ G["svg.ts"]
171
+ H["color.ts"]
172
+ I["svg_path.ts"]
173
+ N["types.ts"]
165
174
  end
166
- subgraph I["maplibre"]
167
- J["control.ts"]
168
- K["panel_css.ts"]
169
- L["index.ts"]
175
+ subgraph J["maplibre"]
176
+ K["control.ts"]
177
+ L["panel_css.ts"]
178
+ M["index.ts"]
170
179
  end
171
180
  end
172
181
  1-->2
173
182
  2-->4
174
- 2-->F
183
+ 2-->G
175
184
  4-->6
176
- 4-->B
177
- 4-->D
185
+ 4-->C
186
+ 4-->E
178
187
  6-->7
179
188
  6-->9
180
- 6-->B
181
189
  6-->C
190
+ 6-->D
182
191
  7-->8
183
192
  9-->A
184
- A-->8
185
- C-->8
193
+ 9-->B
194
+ B-->8
186
195
  C-->A
187
- F-->G
188
- F-->H
189
- J-->2
190
- J-->K
191
- L-->2
192
- L-->J
193
- N-->8
194
-
195
- class 0,3,5,E,I subgraphs;
196
+ D-->8
197
+ D-->B
198
+ G-->H
199
+ G-->I
200
+ K-->2
201
+ K-->L
202
+ M-->2
203
+ M-->K
204
+
205
+ class 0,3,5,F,J subgraphs;
196
206
  classDef subgraphs fill-opacity:0.1, fill:#888, color:#888, stroke:#888;
197
207
  ```
package/dist/index.cjs CHANGED
@@ -449,19 +449,28 @@ var v8_default = {
449
449
  },
450
450
  "property-type": "data-constant"
451
451
  } },
452
- "layout_fill-extrusion": { "visibility": {
453
- "type": "enum",
454
- "values": {
455
- "visible": {},
456
- "none": {}
457
- },
458
- "default": "visible",
459
- "expression": {
460
- "interpolated": false,
461
- "parameters": ["global-state"]
452
+ "layout_fill-extrusion": {
453
+ "visibility": {
454
+ "type": "enum",
455
+ "values": {
456
+ "visible": {},
457
+ "none": {}
458
+ },
459
+ "default": "visible",
460
+ "expression": {
461
+ "interpolated": false,
462
+ "parameters": ["global-state"]
463
+ },
464
+ "property-type": "data-constant"
462
465
  },
463
- "property-type": "data-constant"
464
- } },
466
+ "fill-extrusion-rounded-corner-distance": {
467
+ "type": "number",
468
+ "default": 0,
469
+ "minimum": 0,
470
+ "units": "meters",
471
+ "property-type": "constant"
472
+ }
473
+ },
465
474
  layout_line: {
466
475
  "line-cap": {
467
476
  "type": "enum",
@@ -4499,7 +4508,11 @@ var Coercion = class Coercion {
4499
4508
  }
4500
4509
  case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4501
4510
  case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4502
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4511
+ case "projectionDefinition": {
4512
+ const input = this.args[0].evaluate(ctx);
4513
+ if (ProjectionDefinition.parse(input)) return input;
4514
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4515
+ }
4503
4516
  default: return valueToString(this.args[0].evaluate(ctx));
4504
4517
  }
4505
4518
  }
@@ -4602,7 +4615,11 @@ var ParsingContext = class ParsingContext {
4602
4615
  const expected = this.expectedType;
4603
4616
  const actual = parsed.type;
4604
4617
  if ((expected.kind === "string" || expected.kind === "number" || expected.kind === "boolean" || expected.kind === "object" || expected.kind === "array") && actual.kind === "value") parsed = annotate(parsed, expected, options.typeAnnotation || "assert");
4605
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4618
+ else if ("projectionDefinition" === expected.kind && [
4619
+ "string",
4620
+ "array",
4621
+ "value"
4622
+ ].includes(actual.kind) || [
4606
4623
  "color",
4607
4624
  "formatted",
4608
4625
  "resolvedImage"
@@ -10027,6 +10044,34 @@ function loadGeoJSONSource(options) {
10027
10044
  }
10028
10045
  }
10029
10046
 
10047
+ /**
10048
+ * Base64-encode an ArrayBuffer (standard alphabet, with padding).
10049
+ *
10050
+ * Three paths, all producing identical output:
10051
+ * 1. `Uint8Array.prototype.toBase64` — the native method (Node 22+, modern
10052
+ * browsers). Fastest and simplest; used when present.
10053
+ * 2. Node's `Buffer` — for Node versions without the native method.
10054
+ * 3. Chunked `btoa(String.fromCharCode(...))` — the browser fallback. It must
10055
+ * chunk: spreading a whole tile/sprite PNG at once passes one argument per
10056
+ * byte, overflowing the engine's argument-count limit (Safari ~64K, V8
10057
+ * higher but finite) and throwing `RangeError: Maximum call stack size
10058
+ * exceeded` for buffers above ~64 KB. 32 KB slices stay well under the limit.
10059
+ */
10060
+ function arrayBufferToBase64(buffer) {
10061
+ const bytes = new Uint8Array(buffer);
10062
+ const toBase64 = bytes.toBase64;
10063
+ if (typeof toBase64 === 'function')
10064
+ return toBase64.call(bytes);
10065
+ if (typeof Buffer !== 'undefined')
10066
+ return Buffer.from(buffer).toString('base64');
10067
+ const CHUNK_SIZE = 0x8000; // 32 KB, well below the argument-count limit
10068
+ let binary = '';
10069
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
10070
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
10071
+ }
10072
+ return btoa(binary);
10073
+ }
10074
+
10030
10075
  async function getRasterTiles(job, sourceName) {
10031
10076
  const { width, height } = job.renderer;
10032
10077
  const { zoom, center } = job.view;
@@ -10040,10 +10085,7 @@ async function getRasterTiles(job, sourceName) {
10040
10085
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
10041
10086
  if (!tile)
10042
10087
  return null;
10043
- const base64 = typeof Buffer !== 'undefined'
10044
- ? Buffer.from(tile.buffer).toString('base64')
10045
- : btoa(String.fromCharCode(...new Uint8Array(tile.buffer)));
10046
- const dataUri = `data:${tile.contentType};base64,${base64}`;
10088
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10047
10089
  return {
10048
10090
  x: offsetX,
10049
10091
  y: offsetY,
@@ -10089,10 +10131,7 @@ async function loadSpriteAtlas(style) {
10089
10131
  const { jsonResponse, imageResponse } = spritePair;
10090
10132
  const json = (await jsonResponse.json());
10091
10133
  const imageBuffer = await imageResponse.arrayBuffer();
10092
- const base64 = typeof Buffer !== 'undefined'
10093
- ? Buffer.from(imageBuffer).toString('base64')
10094
- : btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
10095
- const sheetDataUri = `data:image/png;base64,${base64}`;
10134
+ const sheetDataUri = `data:image/png;base64,${arrayBufferToBase64(imageBuffer)}`;
10096
10135
  // Estimate sheet dimensions from sprite entries
10097
10136
  let sheetWidth = 0;
10098
10137
  let sheetHeight = 0;
package/dist/index.js CHANGED
@@ -447,19 +447,28 @@ var v8_default = {
447
447
  },
448
448
  "property-type": "data-constant"
449
449
  } },
450
- "layout_fill-extrusion": { "visibility": {
451
- "type": "enum",
452
- "values": {
453
- "visible": {},
454
- "none": {}
455
- },
456
- "default": "visible",
457
- "expression": {
458
- "interpolated": false,
459
- "parameters": ["global-state"]
450
+ "layout_fill-extrusion": {
451
+ "visibility": {
452
+ "type": "enum",
453
+ "values": {
454
+ "visible": {},
455
+ "none": {}
456
+ },
457
+ "default": "visible",
458
+ "expression": {
459
+ "interpolated": false,
460
+ "parameters": ["global-state"]
461
+ },
462
+ "property-type": "data-constant"
460
463
  },
461
- "property-type": "data-constant"
462
- } },
464
+ "fill-extrusion-rounded-corner-distance": {
465
+ "type": "number",
466
+ "default": 0,
467
+ "minimum": 0,
468
+ "units": "meters",
469
+ "property-type": "constant"
470
+ }
471
+ },
463
472
  layout_line: {
464
473
  "line-cap": {
465
474
  "type": "enum",
@@ -4497,7 +4506,11 @@ var Coercion = class Coercion {
4497
4506
  }
4498
4507
  case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4499
4508
  case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4500
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4509
+ case "projectionDefinition": {
4510
+ const input = this.args[0].evaluate(ctx);
4511
+ if (ProjectionDefinition.parse(input)) return input;
4512
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4513
+ }
4501
4514
  default: return valueToString(this.args[0].evaluate(ctx));
4502
4515
  }
4503
4516
  }
@@ -4600,7 +4613,11 @@ var ParsingContext = class ParsingContext {
4600
4613
  const expected = this.expectedType;
4601
4614
  const actual = parsed.type;
4602
4615
  if ((expected.kind === "string" || expected.kind === "number" || expected.kind === "boolean" || expected.kind === "object" || expected.kind === "array") && actual.kind === "value") parsed = annotate(parsed, expected, options.typeAnnotation || "assert");
4603
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4616
+ else if ("projectionDefinition" === expected.kind && [
4617
+ "string",
4618
+ "array",
4619
+ "value"
4620
+ ].includes(actual.kind) || [
4604
4621
  "color",
4605
4622
  "formatted",
4606
4623
  "resolvedImage"
@@ -10025,6 +10042,34 @@ function loadGeoJSONSource(options) {
10025
10042
  }
10026
10043
  }
10027
10044
 
10045
+ /**
10046
+ * Base64-encode an ArrayBuffer (standard alphabet, with padding).
10047
+ *
10048
+ * Three paths, all producing identical output:
10049
+ * 1. `Uint8Array.prototype.toBase64` — the native method (Node 22+, modern
10050
+ * browsers). Fastest and simplest; used when present.
10051
+ * 2. Node's `Buffer` — for Node versions without the native method.
10052
+ * 3. Chunked `btoa(String.fromCharCode(...))` — the browser fallback. It must
10053
+ * chunk: spreading a whole tile/sprite PNG at once passes one argument per
10054
+ * byte, overflowing the engine's argument-count limit (Safari ~64K, V8
10055
+ * higher but finite) and throwing `RangeError: Maximum call stack size
10056
+ * exceeded` for buffers above ~64 KB. 32 KB slices stay well under the limit.
10057
+ */
10058
+ function arrayBufferToBase64(buffer) {
10059
+ const bytes = new Uint8Array(buffer);
10060
+ const toBase64 = bytes.toBase64;
10061
+ if (typeof toBase64 === 'function')
10062
+ return toBase64.call(bytes);
10063
+ if (typeof Buffer !== 'undefined')
10064
+ return Buffer.from(buffer).toString('base64');
10065
+ const CHUNK_SIZE = 0x8000; // 32 KB, well below the argument-count limit
10066
+ let binary = '';
10067
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
10068
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
10069
+ }
10070
+ return btoa(binary);
10071
+ }
10072
+
10028
10073
  async function getRasterTiles(job, sourceName) {
10029
10074
  const { width, height } = job.renderer;
10030
10075
  const { zoom, center } = job.view;
@@ -10038,10 +10083,7 @@ async function getRasterTiles(job, sourceName) {
10038
10083
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
10039
10084
  if (!tile)
10040
10085
  return null;
10041
- const base64 = typeof Buffer !== 'undefined'
10042
- ? Buffer.from(tile.buffer).toString('base64')
10043
- : btoa(String.fromCharCode(...new Uint8Array(tile.buffer)));
10044
- const dataUri = `data:${tile.contentType};base64,${base64}`;
10086
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10045
10087
  return {
10046
10088
  x: offsetX,
10047
10089
  y: offsetY,
@@ -10087,10 +10129,7 @@ async function loadSpriteAtlas(style) {
10087
10129
  const { jsonResponse, imageResponse } = spritePair;
10088
10130
  const json = (await jsonResponse.json());
10089
10131
  const imageBuffer = await imageResponse.arrayBuffer();
10090
- const base64 = typeof Buffer !== 'undefined'
10091
- ? Buffer.from(imageBuffer).toString('base64')
10092
- : btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
10093
- const sheetDataUri = `data:image/png;base64,${base64}`;
10132
+ const sheetDataUri = `data:image/png;base64,${arrayBufferToBase64(imageBuffer)}`;
10094
10133
  // Estimate sheet dimensions from sprite entries
10095
10134
  let sheetWidth = 0;
10096
10135
  let sheetHeight = 0;
@@ -2,7 +2,9 @@ import { IControl, Map } from 'maplibre-gl';
2
2
  import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
3
3
 
4
4
  interface SVGExportControlOptions {
5
+ /** Initial export width in px. Defaults to the current map viewport width. */
5
6
  defaultWidth?: number;
7
+ /** Initial export height in px. Defaults to the current map viewport height. */
6
8
  defaultHeight?: number;
7
9
  }
8
10
  declare class SVGExportControl implements IControl {
@@ -628,19 +628,28 @@ var v8_default = {
628
628
  },
629
629
  "property-type": "data-constant"
630
630
  } },
631
- "layout_fill-extrusion": { "visibility": {
632
- "type": "enum",
633
- "values": {
634
- "visible": {},
635
- "none": {}
636
- },
637
- "default": "visible",
638
- "expression": {
639
- "interpolated": false,
640
- "parameters": ["global-state"]
631
+ "layout_fill-extrusion": {
632
+ "visibility": {
633
+ "type": "enum",
634
+ "values": {
635
+ "visible": {},
636
+ "none": {}
637
+ },
638
+ "default": "visible",
639
+ "expression": {
640
+ "interpolated": false,
641
+ "parameters": ["global-state"]
642
+ },
643
+ "property-type": "data-constant"
641
644
  },
642
- "property-type": "data-constant"
643
- } },
645
+ "fill-extrusion-rounded-corner-distance": {
646
+ "type": "number",
647
+ "default": 0,
648
+ "minimum": 0,
649
+ "units": "meters",
650
+ "property-type": "constant"
651
+ }
652
+ },
644
653
  layout_line: {
645
654
  "line-cap": {
646
655
  "type": "enum",
@@ -4678,7 +4687,11 @@ var Coercion = class Coercion {
4678
4687
  }
4679
4688
  case "formatted": return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
4680
4689
  case "resolvedImage": return ResolvedImage.fromString(valueToString(this.args[0].evaluate(ctx)));
4681
- case "projectionDefinition": return this.args[0].evaluate(ctx);
4690
+ case "projectionDefinition": {
4691
+ const input = this.args[0].evaluate(ctx);
4692
+ if (ProjectionDefinition.parse(input)) return input;
4693
+ throw new RuntimeError(`Could not parse projectionDefinition from value '${typeof input === "string" ? input : JSON.stringify(input)}'`, this.key);
4694
+ }
4682
4695
  default: return valueToString(this.args[0].evaluate(ctx));
4683
4696
  }
4684
4697
  }
@@ -4781,7 +4794,11 @@ var ParsingContext = class ParsingContext {
4781
4794
  const expected = this.expectedType;
4782
4795
  const actual = parsed.type;
4783
4796
  if ((expected.kind === "string" || expected.kind === "number" || expected.kind === "boolean" || expected.kind === "object" || expected.kind === "array") && actual.kind === "value") parsed = annotate(parsed, expected, options.typeAnnotation || "assert");
4784
- else if ("projectionDefinition" === expected.kind && ["string", "array"].includes(actual.kind) || [
4797
+ else if ("projectionDefinition" === expected.kind && [
4798
+ "string",
4799
+ "array",
4800
+ "value"
4801
+ ].includes(actual.kind) || [
4785
4802
  "color",
4786
4803
  "formatted",
4787
4804
  "resolvedImage"
@@ -10206,6 +10223,34 @@ function loadGeoJSONSource(options) {
10206
10223
  }
10207
10224
  }
10208
10225
 
10226
+ /**
10227
+ * Base64-encode an ArrayBuffer (standard alphabet, with padding).
10228
+ *
10229
+ * Three paths, all producing identical output:
10230
+ * 1. `Uint8Array.prototype.toBase64` — the native method (Node 22+, modern
10231
+ * browsers). Fastest and simplest; used when present.
10232
+ * 2. Node's `Buffer` — for Node versions without the native method.
10233
+ * 3. Chunked `btoa(String.fromCharCode(...))` — the browser fallback. It must
10234
+ * chunk: spreading a whole tile/sprite PNG at once passes one argument per
10235
+ * byte, overflowing the engine's argument-count limit (Safari ~64K, V8
10236
+ * higher but finite) and throwing `RangeError: Maximum call stack size
10237
+ * exceeded` for buffers above ~64 KB. 32 KB slices stay well under the limit.
10238
+ */
10239
+ function arrayBufferToBase64(buffer) {
10240
+ const bytes = new Uint8Array(buffer);
10241
+ const toBase64 = bytes.toBase64;
10242
+ if (typeof toBase64 === 'function')
10243
+ return toBase64.call(bytes);
10244
+ if (typeof Buffer !== 'undefined')
10245
+ return Buffer.from(buffer).toString('base64');
10246
+ const CHUNK_SIZE = 0x8000; // 32 KB, well below the argument-count limit
10247
+ let binary = '';
10248
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
10249
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE));
10250
+ }
10251
+ return btoa(binary);
10252
+ }
10253
+
10209
10254
  async function getRasterTiles(job, sourceName) {
10210
10255
  const { width, height } = job.renderer;
10211
10256
  const { zoom, center } = job.view;
@@ -10219,10 +10264,7 @@ async function getRasterTiles(job, sourceName) {
10219
10264
  const tile = await getTile(sourceUrl, zoomLevel, x, y);
10220
10265
  if (!tile)
10221
10266
  return null;
10222
- const base64 = typeof Buffer !== 'undefined'
10223
- ? Buffer.from(tile.buffer).toString('base64')
10224
- : btoa(String.fromCharCode(...new Uint8Array(tile.buffer)));
10225
- const dataUri = `data:${tile.contentType};base64,${base64}`;
10267
+ const dataUri = `data:${tile.contentType};base64,${arrayBufferToBase64(tile.buffer)}`;
10226
10268
  return {
10227
10269
  x: offsetX,
10228
10270
  y: offsetY,
@@ -10268,10 +10310,7 @@ async function loadSpriteAtlas(style) {
10268
10310
  const { jsonResponse, imageResponse } = spritePair;
10269
10311
  const json = (await jsonResponse.json());
10270
10312
  const imageBuffer = await imageResponse.arrayBuffer();
10271
- const base64 = typeof Buffer !== 'undefined'
10272
- ? Buffer.from(imageBuffer).toString('base64')
10273
- : btoa(String.fromCharCode(...new Uint8Array(imageBuffer)));
10274
- const sheetDataUri = `data:image/png;base64,${base64}`;
10313
+ const sheetDataUri = `data:image/png;base64,${arrayBufferToBase64(imageBuffer)}`;
10275
10314
  // Estimate sheet dimensions from sprite entries
10276
10315
  let sheetWidth = 0;
10277
10316
  let sheetHeight = 0;
@@ -10725,7 +10764,12 @@ const ALLOWED_TAGS = new Set(['a', 'b', 'i', 'em', 'strong', 'span']);
10725
10764
  function sanitizeHTML(html) {
10726
10765
  const parser = new DOMParser();
10727
10766
  const doc = parser.parseFromString(html, 'text/html');
10728
- return sanitizeNode(doc.body).textContent ?? '';
10767
+ // Serialize the sanitized tree back to HTML so allowed tags and safe links
10768
+ // survive. The result is later assigned via innerHTML; every text node is
10769
+ // re-escaped by the serializer, so it is safe to concatenate and inject.
10770
+ const container = document.createElement('div');
10771
+ container.append(sanitizeNode(doc.body));
10772
+ return container.innerHTML;
10729
10773
  }
10730
10774
  function sanitizeNode(node) {
10731
10775
  if (node.nodeType === Node.TEXT_NODE) {
@@ -10764,10 +10808,7 @@ class SVGExportControl {
10764
10808
  renderGeneration = 0;
10765
10809
  options;
10766
10810
  constructor(options) {
10767
- this.options = {
10768
- defaultWidth: options?.defaultWidth ?? 1024,
10769
- defaultHeight: options?.defaultHeight ?? 1024,
10770
- };
10811
+ this.options = { ...options };
10771
10812
  }
10772
10813
  onAdd(map) {
10773
10814
  this.map = map;
@@ -10797,6 +10838,12 @@ class SVGExportControl {
10797
10838
  if (this.panel || !this.map)
10798
10839
  return;
10799
10840
  const mapContainer = this.map.getContainer();
10841
+ // Seed the size inputs from the current map viewport so the default export
10842
+ // matches what the user sees on screen. Explicit constructor options win;
10843
+ // the 1024 fallback covers environments that report no layout size
10844
+ // (clientWidth 0). Inputs stay independent — the user can change either.
10845
+ const defaultWidth = this.options.defaultWidth ?? (mapContainer.clientWidth || 1024);
10846
+ const defaultHeight = this.options.defaultHeight ?? (mapContainer.clientHeight || 1024);
10800
10847
  this.panel = document.createElement('div');
10801
10848
  this.panel.className = 'svg-export-panel';
10802
10849
  this.panel.innerHTML = `
@@ -10811,8 +10858,8 @@ class SVGExportControl {
10811
10858
  </div>
10812
10859
  <div class="panel-inputs">
10813
10860
  <div class="grid">
10814
- <label>Width<input type="number" class="input-width" value="${String(this.options.defaultWidth)}" min="1" max="8192"></label>
10815
- <label>Height<input type="number" class="input-height" value="${String(this.options.defaultHeight)}" min="1" max="8192"></label>
10861
+ <label>Width<input type="number" class="input-width" value="${String(defaultWidth)}" min="1" max="8192"></label>
10862
+ <label>Height<input type="number" class="input-height" value="${String(defaultHeight)}" min="1" max="8192"></label>
10816
10863
  </div>
10817
10864
  <label class="label-checkbox"><input type="checkbox" class="input-labels"> Include labels and icons (buggy)</label>
10818
10865
  </div>
@@ -10978,4 +11025,4 @@ class SVGExportControl {
10978
11025
  }
10979
11026
 
10980
11027
  export { SVGExportControl, renderToSVG };
10981
- //# sourceMappingURL=maplibre.js.map
11028
+ //# sourceMappingURL=maplibre-svg-export.js.map