styled-map-package-api 5.0.0-pre.3 → 5.0.0-pre.4
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 +94 -0
- package/dist/download.d.ts +11 -21
- package/dist/fallbacks.d.ts +32 -0
- package/dist/from-mbtiles.d.ts +1 -3
- package/dist/index.d.ts +11 -24
- package/dist/reader.d.ts +28 -12
- package/dist/server.d.ts +23 -14
- package/dist/style-downloader.d.ts +13 -19
- package/dist/tile-downloader.d.ts +13 -23
- package/dist/types.d.ts +61 -0
- package/dist/utils/errors.d.ts +2 -4
- package/dist/utils/fetch.d.ts +3 -8
- package/dist/utils/file-formats.d.ts +3 -10
- package/dist/utils/geo.d.ts +17 -9
- package/dist/utils/mapbox.d.ts +8 -10
- package/dist/utils/misc.d.ts +3 -5
- package/dist/utils/streams.d.ts +6 -10
- package/dist/utils/style.d.ts +27 -16
- package/dist/utils/templates.d.ts +30 -25
- package/dist/validator.d.ts +66 -0
- package/dist/writer.d.ts +157 -4
- package/lib/download.js +125 -0
- package/lib/fallbacks.js +157 -0
- package/lib/from-mbtiles.js +131 -0
- package/lib/index.js +12 -0
- package/lib/reader.js +360 -0
- package/lib/server.js +222 -0
- package/lib/style-downloader.js +369 -0
- package/lib/tile-downloader.js +189 -0
- package/lib/types.ts +99 -0
- package/lib/utils/errors.js +24 -0
- package/lib/utils/fetch.js +104 -0
- package/lib/utils/file-formats.js +92 -0
- package/lib/utils/geo.js +97 -0
- package/lib/utils/mapbox.js +155 -0
- package/{dist/utils/misc.d.cts → lib/utils/misc.js} +9 -5
- package/lib/utils/streams.js +101 -0
- package/lib/utils/style.js +206 -0
- package/lib/utils/templates.js +165 -0
- package/lib/validator.js +789 -0
- package/lib/writer.js +652 -0
- package/package.json +30 -78
- package/dist/download.cjs +0 -100
- package/dist/download.d.cts +0 -63
- package/dist/download.js +0 -76
- package/dist/from-mbtiles.cjs +0 -127
- package/dist/from-mbtiles.d.cts +0 -14
- package/dist/from-mbtiles.js +0 -103
- package/dist/index.cjs +0 -46
- package/dist/index.d.cts +0 -24
- package/dist/index.js +0 -16
- package/dist/reader.cjs +0 -287
- package/dist/reader.d.cts +0 -67
- package/dist/reader.js +0 -259
- package/dist/server.cjs +0 -73
- package/dist/server.d.cts +0 -45
- package/dist/server.js +0 -49
- package/dist/style-downloader.cjs +0 -314
- package/dist/style-downloader.d.cts +0 -118
- package/dist/style-downloader.js +0 -290
- package/dist/tile-downloader.cjs +0 -156
- package/dist/tile-downloader.d.cts +0 -82
- package/dist/tile-downloader.js +0 -124
- package/dist/types-Bhn0-Ldk.d.cts +0 -201
- package/dist/types-Bhn0-Ldk.d.ts +0 -201
- package/dist/utils/errors.cjs +0 -41
- package/dist/utils/errors.d.cts +0 -18
- package/dist/utils/errors.js +0 -16
- package/dist/utils/fetch.cjs +0 -97
- package/dist/utils/fetch.d.cts +0 -50
- package/dist/utils/fetch.js +0 -63
- package/dist/utils/file-formats.cjs +0 -96
- package/dist/utils/file-formats.d.cts +0 -32
- package/dist/utils/file-formats.js +0 -70
- package/dist/utils/geo.cjs +0 -84
- package/dist/utils/geo.d.cts +0 -46
- package/dist/utils/geo.js +0 -56
- package/dist/utils/mapbox.cjs +0 -121
- package/dist/utils/mapbox.d.cts +0 -43
- package/dist/utils/mapbox.js +0 -91
- package/dist/utils/misc.cjs +0 -39
- package/dist/utils/misc.js +0 -13
- package/dist/utils/streams.cjs +0 -99
- package/dist/utils/streams.d.cts +0 -49
- package/dist/utils/streams.js +0 -73
- package/dist/utils/style.cjs +0 -126
- package/dist/utils/style.d.cts +0 -66
- package/dist/utils/style.js +0 -98
- package/dist/utils/templates.cjs +0 -124
- package/dist/utils/templates.d.cts +0 -79
- package/dist/utils/templates.js +0 -85
- package/dist/writer.cjs +0 -539
- package/dist/writer.d.cts +0 -4
- package/dist/writer.js +0 -516
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { expressions, validateStyleMin } from '@maplibre/maplibre-gl-style-spec'
|
|
2
|
+
|
|
3
|
+
/** @import {StyleSpecification, ExpressionSpecification, ValidationError} from '@maplibre/maplibre-gl-style-spec' */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* For a given style, replace all font stacks (`text-field` properties) with the
|
|
7
|
+
* provided fonts. If no matching font is found, the first font in the stack is
|
|
8
|
+
* used.
|
|
9
|
+
*
|
|
10
|
+
* *Modifies the input style object*
|
|
11
|
+
*
|
|
12
|
+
* @param {StyleSpecification} style
|
|
13
|
+
* @param {string[]} fonts
|
|
14
|
+
*/
|
|
15
|
+
export function replaceFontStacks(style, fonts) {
|
|
16
|
+
const mappedLayers = mapFontStacks(style.layers, (fontStack) => {
|
|
17
|
+
let match
|
|
18
|
+
for (const font of fontStack) {
|
|
19
|
+
if (fonts.includes(font)) {
|
|
20
|
+
match = font
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [match || fonts[0]]
|
|
25
|
+
})
|
|
26
|
+
style.layers = mappedLayers
|
|
27
|
+
return style
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* From given style layers, create a new style by calling the provided callback
|
|
32
|
+
* function on every font stack defined in the style.
|
|
33
|
+
*
|
|
34
|
+
* @param {StyleSpecification['layers']} layers
|
|
35
|
+
* @param {(fontStack: string[]) => string[]} callbackFn
|
|
36
|
+
* @returns {StyleSpecification['layers']}
|
|
37
|
+
*/
|
|
38
|
+
export function mapFontStacks(layers, callbackFn) {
|
|
39
|
+
return layers.map((layer) => {
|
|
40
|
+
if (layer.type !== 'symbol' || !layer.layout || !layer.layout['text-font'])
|
|
41
|
+
return layer
|
|
42
|
+
const textFont = layer.layout['text-font']
|
|
43
|
+
let mappedValue
|
|
44
|
+
if (isExpression(textFont)) {
|
|
45
|
+
mappedValue = mapArrayExpressionValue(textFont, callbackFn)
|
|
46
|
+
} else if (Array.isArray(textFont)) {
|
|
47
|
+
mappedValue = callbackFn(textFont)
|
|
48
|
+
} else {
|
|
49
|
+
// Deprecated property function, unsupported, but within this module
|
|
50
|
+
// functions will have been migrated to expressions anyway.
|
|
51
|
+
console.warn(
|
|
52
|
+
'Deprecated function definitions are not supported, font stack has not been transformed.',
|
|
53
|
+
)
|
|
54
|
+
console.dir(textFont, { depth: null })
|
|
55
|
+
return layer
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...layer,
|
|
59
|
+
layout: {
|
|
60
|
+
...layer.layout,
|
|
61
|
+
'text-font': mappedValue,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* See https://github.com/maplibre/maplibre-style-spec/blob/c2f01dbaa6c5fb8409126258b9464b450018e939/src/expression/index.ts#L128
|
|
69
|
+
*
|
|
70
|
+
* @param {unknown} value
|
|
71
|
+
* @returns {value is ExpressionSpecification}
|
|
72
|
+
*/
|
|
73
|
+
function isExpression(value) {
|
|
74
|
+
return (
|
|
75
|
+
Array.isArray(value) &&
|
|
76
|
+
value.length > 0 &&
|
|
77
|
+
typeof value[0] === 'string' &&
|
|
78
|
+
value[0] in expressions
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* For an expression whose value is an array, map the array to a new array using
|
|
84
|
+
* the given callbackFn.
|
|
85
|
+
*
|
|
86
|
+
* @param {ExpressionSpecification} expression
|
|
87
|
+
* @param {(value: string[]) => string[]} callbackFn
|
|
88
|
+
* @returns {ExpressionSpecification}
|
|
89
|
+
*/
|
|
90
|
+
function mapArrayExpressionValue(expression, callbackFn) {
|
|
91
|
+
// This only works for properties whose value is an array, because it relies
|
|
92
|
+
// on the style specification that array values must be declared with the
|
|
93
|
+
// `literal` expression.
|
|
94
|
+
if (expression[0] === 'literal' && Array.isArray(expression[1])) {
|
|
95
|
+
return ['literal', callbackFn(expression[1])]
|
|
96
|
+
} else {
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
return [
|
|
99
|
+
expression[0],
|
|
100
|
+
...expression.slice(1).map(
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
(x) => {
|
|
103
|
+
if (isExpression(x)) {
|
|
104
|
+
return mapArrayExpressionValue(x, callbackFn)
|
|
105
|
+
} else {
|
|
106
|
+
return x
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* PBF glyph ranges rendered client-side by MapLibre GL via
|
|
116
|
+
* `localIdeographFontFamily` (enabled by default as 'sans-serif'). SMP files
|
|
117
|
+
* do not need to include glyph data for these ranges. Each entry is a
|
|
118
|
+
* half-open interval [start, end) of PBF range start codepoints.
|
|
119
|
+
*
|
|
120
|
+
* Based on `codePointUsesLocalIdeographFontFamily()` in MapLibre GL JS.
|
|
121
|
+
* Only ranges where the ENTIRE 256-codepoint PBF range is locally rendered
|
|
122
|
+
* are listed here; partially-local ranges are conservatively kept as required.
|
|
123
|
+
*/
|
|
124
|
+
export const LOCAL_GLYPH_RANGES = [
|
|
125
|
+
[0x3000, 0x3400], // CJK Symbols, Hiragana, Katakana, Bopomofo, CJK Strokes, Enclosed CJK, CJK Compat
|
|
126
|
+
[0x3400, 0x4e00], // CJK Unified Ideographs Extension A
|
|
127
|
+
[0x4e00, 0xa000], // CJK Unified Ideographs
|
|
128
|
+
[0xa000, 0xa400], // Yi Syllables + Yi Radicals
|
|
129
|
+
[0xac00, 0xd800], // Hangul Syllables + Hangul Jamo Extended-B
|
|
130
|
+
[0xf900, 0xfb00], // CJK Compatibility Ideographs
|
|
131
|
+
[0xff00, 0x10000], // Halfwidth and Fullwidth Forms
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check whether a PBF glyph range (identified by its start codepoint) is
|
|
136
|
+
* rendered client-side by MapLibre GL and does not need a server-side PBF file.
|
|
137
|
+
* @param {number} rangeStart
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
export function isLocallyRenderedRange(rangeStart) {
|
|
141
|
+
return LOCAL_GLYPH_RANGES.some(
|
|
142
|
+
([start, end]) => rangeStart >= start && rangeStart < end,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @typedef {object} TileJSONPartial
|
|
148
|
+
* @property {string[]} tiles
|
|
149
|
+
* @property {string} [description]
|
|
150
|
+
* @property {string} [attribution]
|
|
151
|
+
* @property {object[]} [vector_layers]
|
|
152
|
+
* @property {import('./geo.js').BBox} [bounds]
|
|
153
|
+
* @property {number} [maxzoom]
|
|
154
|
+
* @property {number} [minzoom]
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
*
|
|
159
|
+
* @param {unknown} tilejson
|
|
160
|
+
* @returns {asserts tilejson is TileJSONPartial}
|
|
161
|
+
*/
|
|
162
|
+
export function assertTileJSON(tilejson) {
|
|
163
|
+
if (typeof tilejson !== 'object' || tilejson === null) {
|
|
164
|
+
throw new Error('Invalid TileJSON')
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
!('tiles' in tilejson) ||
|
|
168
|
+
!Array.isArray(tilejson.tiles) ||
|
|
169
|
+
tilejson.tiles.length === 0 ||
|
|
170
|
+
tilejson.tiles.some((tile) => typeof tile !== 'string')
|
|
171
|
+
) {
|
|
172
|
+
throw new Error('Invalid TileJSON: missing or invalid tiles property')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const validateStyle =
|
|
177
|
+
/** @type {{ (style: unknown): style is StyleSpecification, errors: ValidationError[] }} */ (
|
|
178
|
+
(style) => {
|
|
179
|
+
validateStyle.errors = validateStyleMin(
|
|
180
|
+
/** @type {StyleSpecification} */ (style),
|
|
181
|
+
)
|
|
182
|
+
if (validateStyle.errors.length) return false
|
|
183
|
+
return true
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check whether a source is already inlined (e.g. does not reference a TileJSON or GeoJSON url)
|
|
189
|
+
*
|
|
190
|
+
* @param {import('@maplibre/maplibre-gl-style-spec').SourceSpecification} source
|
|
191
|
+
* @returns {source is import('../types.js').InlinedSource}
|
|
192
|
+
*/
|
|
193
|
+
export function isInlinedSource(source) {
|
|
194
|
+
if (source.type === 'geojson') {
|
|
195
|
+
return typeof source.data === 'object'
|
|
196
|
+
} else if (
|
|
197
|
+
source.type === 'vector' ||
|
|
198
|
+
source.type === 'raster' ||
|
|
199
|
+
source.type === 'raster-dem'
|
|
200
|
+
) {
|
|
201
|
+
return 'tiles' in source
|
|
202
|
+
} else {
|
|
203
|
+
// Video and image sources are not strictly "inlined", but we treat them as such.
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export const URI_SCHEME = 'smp' // "Styled Map Package"
|
|
2
|
+
export const URI_BASE = URI_SCHEME + '://maps.v1/'
|
|
3
|
+
|
|
4
|
+
// These constants determine the file format structure
|
|
5
|
+
export const VERSION_FILE = 'VERSION'
|
|
6
|
+
export const FORMAT_VERSION = '1.0'
|
|
7
|
+
export const STYLE_FILE = 'style.json'
|
|
8
|
+
export const SOURCES_FOLDER = 's'
|
|
9
|
+
const SPRITES_FOLDER = 'sprites'
|
|
10
|
+
export const FONTS_FOLDER = 'fonts'
|
|
11
|
+
|
|
12
|
+
// This must include placeholders `{z}`, `{x}`, `{y}`, since these are used to
|
|
13
|
+
// define the tile URL, and this is a TileJSON standard.
|
|
14
|
+
// The folder here is just `s` to minimize bytes used for filenames, which are
|
|
15
|
+
// included in the header of every tile in the zip file.
|
|
16
|
+
const TILE_FILE = SOURCES_FOLDER + '/{sourceId}/{z}/{x}/{y}{ext}'
|
|
17
|
+
// The pixel ratio and ext placeholders must be at the end of the string with no
|
|
18
|
+
// data between them, because this is the format defined in the MapLibre style spec.
|
|
19
|
+
const SPRITE_FILE = SPRITES_FOLDER + '/{id}/sprite{pixelRatio}{ext}'
|
|
20
|
+
// This must include placeholders `{fontstack}` and `{range}`, since these are
|
|
21
|
+
// part of the MapLibre style spec.
|
|
22
|
+
const GLYPH_FILE = FONTS_FOLDER + '/{fontstack}/{range}.pbf.gz'
|
|
23
|
+
export const GLYPH_URI = URI_BASE + GLYPH_FILE
|
|
24
|
+
|
|
25
|
+
const pathToResouceType = /** @type {const} */ ({
|
|
26
|
+
[TILE_FILE.split('/')[0] + '/']: 'tile',
|
|
27
|
+
[SPRITE_FILE.split('/')[0] + '/']: 'sprite',
|
|
28
|
+
[GLYPH_FILE.split('/')[0] + '/']: 'glyph',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} path
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
export function getResourceType(path) {
|
|
36
|
+
if (path === 'style.json') return 'style'
|
|
37
|
+
for (const [prefix, type] of Object.entries(pathToResouceType)) {
|
|
38
|
+
if (path.startsWith(prefix)) return type
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Unknown resource type for path: ${path}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Determine the content type of a file based on its extension.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} path
|
|
47
|
+
*/
|
|
48
|
+
export function getContentType(path) {
|
|
49
|
+
if (path.endsWith('.json') || path.endsWith('.geojson'))
|
|
50
|
+
return 'application/json; charset=utf-8'
|
|
51
|
+
if (path.endsWith('.pbf.gz') || path.endsWith('.pbf'))
|
|
52
|
+
return 'application/x-protobuf'
|
|
53
|
+
if (path.endsWith('.png')) return 'image/png'
|
|
54
|
+
if (path.endsWith('.jpg')) return 'image/jpeg'
|
|
55
|
+
if (path.endsWith('.webp')) return 'image/webp'
|
|
56
|
+
if (path.endsWith('.mvt.gz') || path.endsWith('.mvt'))
|
|
57
|
+
return 'application/vnd.mapbox-vector-tile'
|
|
58
|
+
throw new Error(`Unknown content type for path: ${path}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the filename for a tile, given the TileInfo
|
|
63
|
+
*
|
|
64
|
+
* @param {import("type-fest").SetRequired<import("../writer.js").TileInfo, 'format'>} tileInfo
|
|
65
|
+
* @returns
|
|
66
|
+
*/
|
|
67
|
+
export function getTileFilename({ sourceId, z, x, y, format }) {
|
|
68
|
+
const ext = '.' + format + (format === 'mvt' ? '.gz' : '')
|
|
69
|
+
return replaceVariables(TILE_FILE, { sourceId, z, x, y, ext })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get a filename for a sprite file, given the sprite id, pixel ratio and extension
|
|
74
|
+
*
|
|
75
|
+
* @param {{ id: string, pixelRatio: number, ext: '.json' | '.png'}} spriteInfo
|
|
76
|
+
*/
|
|
77
|
+
export function getSpriteFilename({ id, pixelRatio, ext }) {
|
|
78
|
+
return replaceVariables(SPRITE_FILE, {
|
|
79
|
+
id,
|
|
80
|
+
pixelRatio: getPixelRatioString(pixelRatio),
|
|
81
|
+
ext,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the filename for a glyph file, given the fontstack and range
|
|
87
|
+
*
|
|
88
|
+
* @param {object} options
|
|
89
|
+
* @param {string} options.fontstack
|
|
90
|
+
* @param {import("../writer.js").GlyphRange} options.range
|
|
91
|
+
*/
|
|
92
|
+
export function getGlyphFilename({ fontstack, range }) {
|
|
93
|
+
return replaceVariables(GLYPH_FILE, { fontstack, range })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the URI template for the sprites in the style
|
|
98
|
+
*/
|
|
99
|
+
export function getSpriteUri(id = 'default') {
|
|
100
|
+
return (
|
|
101
|
+
URI_BASE + replaceVariables(SPRITE_FILE, { id, pixelRatio: '', ext: '' })
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the URI template for tiles in the style
|
|
107
|
+
*
|
|
108
|
+
* @param {object} opts
|
|
109
|
+
* @param {string} opts.sourceId
|
|
110
|
+
* @param {import("../writer.js").TileFormat} opts.format
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
export function getTileUri({ sourceId, format }) {
|
|
114
|
+
const ext = '.' + format + (format === 'mvt' ? '.gz' : '')
|
|
115
|
+
return (
|
|
116
|
+
URI_BASE + TILE_FILE.replace('{sourceId}', sourceId).replace('{ext}', ext)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {number} pixelRatio
|
|
122
|
+
*/
|
|
123
|
+
function getPixelRatioString(pixelRatio) {
|
|
124
|
+
return pixelRatio === 1 ? '' : `@${pixelRatio}x`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Replaces variables in a string with values provided in an object. Variables
|
|
129
|
+
* in the string are denoted by curly braces, e.g., {variableName}.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} template - The string containing variables wrapped in curly braces.
|
|
132
|
+
* @param {Record<string, string | number>} variables - An object where the keys correspond to variable names and values correspond to the replacement values.
|
|
133
|
+
* @returns {string} The string with the variables replaced by their corresponding values.
|
|
134
|
+
*/
|
|
135
|
+
export function replaceVariables(template, variables) {
|
|
136
|
+
return template.replace(/{(.*?)}/g, (match, varName) => {
|
|
137
|
+
return varName in variables ? String(variables[varName]) : match
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Inverse of {@link replaceVariables}. Converts a template string into a RegExp
|
|
143
|
+
* that captures the placeholder values. Only placeholders present in the
|
|
144
|
+
* `placeholders` map become named capture groups; unknown placeholders are
|
|
145
|
+
* treated as literal text and escaped.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} template - The template string with `{name}` placeholders.
|
|
148
|
+
* @param {Record<string, string>} placeholders - Map of placeholder
|
|
149
|
+
* name → regex pattern (e.g. `{ z: '\\d+' }`).
|
|
150
|
+
* @returns {RegExp} A RegExp with named capture groups for each known placeholder.
|
|
151
|
+
*/
|
|
152
|
+
export function templateToRegex(template, placeholders) {
|
|
153
|
+
const parts = template.split(/\{(\w+)\}/)
|
|
154
|
+
let pattern = ''
|
|
155
|
+
for (let i = 0; i < parts.length; i++) {
|
|
156
|
+
if (i % 2 === 0) {
|
|
157
|
+
pattern += parts[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
158
|
+
} else if (parts[i] in placeholders) {
|
|
159
|
+
pattern += `(?<${parts[i]}>${placeholders[parts[i]]})`
|
|
160
|
+
} else {
|
|
161
|
+
pattern += `\\{${parts[i]}\\}`
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return new RegExp(`^${pattern}$`)
|
|
165
|
+
}
|