@versatiles/style 5.8.4 → 5.9.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 +24 -18
- package/dist/index.d.ts +20 -14
- package/dist/index.js +151 -83
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/src/color/hsl.test.ts +1 -2
- package/src/color/hsl.ts +0 -8
- package/src/color/hsv.ts +1 -1
- package/src/color/index.test.ts +89 -1
- package/src/color/index.ts +3 -1
- package/src/color/random.test.ts +54 -0
- package/src/color/random.ts +3 -1
- package/src/color/rgb.test.ts +5 -2
- package/src/color/rgb.ts +0 -9
- package/src/color/utils.ts +1 -1
- package/src/guess_style/guess_style.ts +3 -1
- package/src/index.test.ts +1 -1
- package/src/index.ts +12 -2
- package/src/lib/utils.test.ts +84 -1
- package/src/lib/utils.ts +10 -19
- package/src/style_builder/decorator.ts +18 -11
- package/src/style_builder/recolor.ts +18 -11
- package/src/style_builder/style_builder.ts +6 -3
- package/src/style_builder/types.ts +2 -43
- package/src/styles/index.ts +2 -0
- package/src/styles/satellite.test.ts +146 -0
- package/src/styles/satellite.ts +106 -0
- package/src/types/tilejson.test.ts +10 -10
- package/src/types/tilejson.ts +46 -24
- package/src/types/vector_layer.test.ts +1 -1
- package/src/types/vector_layer.ts +2 -4
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versatiles/style",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.9.0",
|
|
4
4
|
"description": "Generate StyleJSON for MapLibre",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"devEngines": {
|
|
8
8
|
"runtime": {
|
|
9
9
|
"name": "node",
|
|
10
|
-
"version": ">=20.0.0
|
|
10
|
+
"version": ">= 20.0.0"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"src/*"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@maplibre/maplibre-gl-native": "^6.
|
|
52
|
-
"@maplibre/maplibre-gl-style-spec": "^24.
|
|
51
|
+
"@maplibre/maplibre-gl-native": "^6.3.0",
|
|
52
|
+
"@maplibre/maplibre-gl-style-spec": "^24.4.1",
|
|
53
53
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
54
54
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
55
55
|
"@rollup/plugin-terser": "^0.4.4",
|
|
@@ -57,26 +57,26 @@
|
|
|
57
57
|
"@types/bin-pack": "^1.0.3",
|
|
58
58
|
"@types/brace-expansion": "^1.1.2",
|
|
59
59
|
"@types/inquirer": "^9.0.9",
|
|
60
|
-
"@types/node": "^
|
|
60
|
+
"@types/node": "^25.2.1",
|
|
61
61
|
"@types/tar-stream": "^3.1.4",
|
|
62
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
63
|
-
"@typescript-eslint/parser": "^8.
|
|
64
|
-
"@versatiles/release-tool": "^2.
|
|
65
|
-
"@vitest/coverage-v8": "^4.0.
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
63
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
64
|
+
"@versatiles/release-tool": "^2.7.0",
|
|
65
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
66
66
|
"bin-pack": "^1.0.2",
|
|
67
|
-
"esbuild": "^0.27.
|
|
68
|
-
"eslint": "^9.39.
|
|
67
|
+
"esbuild": "^0.27.3",
|
|
68
|
+
"eslint": "^9.39.2",
|
|
69
69
|
"husky": "^9.1.7",
|
|
70
|
-
"inquirer": "^13.
|
|
71
|
-
"prettier": "^3.
|
|
72
|
-
"rollup": "^4.
|
|
70
|
+
"inquirer": "^13.2.2",
|
|
71
|
+
"prettier": "^3.8.1",
|
|
72
|
+
"rollup": "^4.57.1",
|
|
73
73
|
"rollup-plugin-dts": "^6.3.0",
|
|
74
74
|
"rollup-plugin-sourcemaps2": "^0.5.4",
|
|
75
75
|
"sharp": "^0.34.5",
|
|
76
76
|
"tar-stream": "^3.1.7",
|
|
77
77
|
"tsx": "^4.21.0",
|
|
78
78
|
"typescript": "^5.9.3",
|
|
79
|
-
"typescript-eslint": "^8.
|
|
80
|
-
"vitest": "^4.0.
|
|
79
|
+
"typescript-eslint": "^8.54.0",
|
|
80
|
+
"vitest": "^4.0.18"
|
|
81
81
|
}
|
|
82
82
|
}
|
package/src/color/hsl.test.ts
CHANGED
|
@@ -38,10 +38,9 @@ describe('HSL Class', () => {
|
|
|
38
38
|
expect(color2.asString()).toBe('hsla(120,50%,50%,0.5)');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it('asHSL
|
|
41
|
+
it('asHSL should return a clone', () => {
|
|
42
42
|
const color = new HSL(120, 50, 50);
|
|
43
43
|
expect(color.asHSL()).toStrictEqual(color);
|
|
44
|
-
expect(color.toHSL()).toStrictEqual(color);
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
it('asHSV should correctly convert HSL to HSV', () => {
|
package/src/color/hsl.ts
CHANGED
|
@@ -87,14 +87,6 @@ export class HSL extends Color {
|
|
|
87
87
|
return this.clone();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
* Returns the current HSL color.
|
|
92
|
-
* @returns The current HSL color.
|
|
93
|
-
*/
|
|
94
|
-
toHSL(): HSL {
|
|
95
|
-
return this;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
90
|
/**
|
|
99
91
|
* Converts the HSL color to an HSV color.
|
|
100
92
|
* @returns A new HSV color representing the same color.
|
package/src/color/hsv.ts
CHANGED
|
@@ -85,7 +85,7 @@ export class HSV extends Color {
|
|
|
85
85
|
const v = this.v / 100;
|
|
86
86
|
const k = (2 - s) * v;
|
|
87
87
|
const q = k < 1 ? k : 2 - k;
|
|
88
|
-
return new HSL(this.h, q
|
|
88
|
+
return new HSL(this.h, q === 0 ? 0 : (100 * s * v) / q, (100 * k) / 2, this.a);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
package/src/color/index.test.ts
CHANGED
|
@@ -73,7 +73,9 @@ describe('Color.parse', () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
it('throws an error for unsupported formats', () => {
|
|
76
|
-
expect(() => Color.parse('invalid color string')).toThrow(
|
|
76
|
+
expect(() => Color.parse('invalid color string')).toThrow(
|
|
77
|
+
'Color.parse: Unknown color format "invalid color string"'
|
|
78
|
+
);
|
|
77
79
|
});
|
|
78
80
|
});
|
|
79
81
|
|
|
@@ -97,3 +99,89 @@ describe('Exported Module', () => {
|
|
|
97
99
|
expect(module.Color).toBe(Color);
|
|
98
100
|
});
|
|
99
101
|
});
|
|
102
|
+
|
|
103
|
+
describe('Color Transformation Methods', () => {
|
|
104
|
+
it('gamma() applies gamma correction', () => {
|
|
105
|
+
const color = Color.parse('#808080');
|
|
106
|
+
expect(color.gamma(2.2).asString()).toBe('rgb(56,56,56)');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('gamma() works from HSL', () => {
|
|
110
|
+
const hsl = new HSL(120, 50, 50, 1);
|
|
111
|
+
expect(hsl.gamma(2.2).asString()).toBe('rgb(12,135,12)');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('contrast() adjusts contrast', () => {
|
|
115
|
+
const color = Color.parse('#FF8040');
|
|
116
|
+
expect(color.contrast(1.5).asString()).toBe('rgb(255,128,32)');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('contrast() works from HSV', () => {
|
|
120
|
+
const hsv = new HSV(180, 50, 50, 1);
|
|
121
|
+
expect(hsv.contrast(1.5).asString()).toBe('rgb(32,128,128)');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('brightness() works from HSL', () => {
|
|
125
|
+
const hsl = new HSL(240, 100, 50, 1);
|
|
126
|
+
expect(hsl.brightness(0.3).asString()).toBe('rgb(77,77,255)');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('lighten() works from HSL', () => {
|
|
130
|
+
const hsl = new HSL(0, 100, 30, 1);
|
|
131
|
+
expect(hsl.lighten(0.2).asString()).toBe('rgb(173,51,51)');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('darken() works from HSV', () => {
|
|
135
|
+
const hsv = new HSV(60, 100, 80, 1);
|
|
136
|
+
expect(hsv.darken(0.3).asString()).toBe('rgb(143,143,0)');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('tint() blends with a tint color', () => {
|
|
140
|
+
const color = Color.parse('#FF0000');
|
|
141
|
+
const tintColor = Color.parse('#0000FF');
|
|
142
|
+
expect(color.tint(0.5, tintColor).asString()).toBe('rgb(128,0,128)');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('tint() works from HSL', () => {
|
|
146
|
+
const hsl = new HSL(0, 100, 50, 1);
|
|
147
|
+
const tintColor = new HSL(120, 100, 50, 1);
|
|
148
|
+
expect(hsl.tint(0.5, tintColor).asString()).toBe('rgb(128,128,0)');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('blend() blends with another color', () => {
|
|
152
|
+
const color1 = Color.parse('#FF0000');
|
|
153
|
+
const color2 = Color.parse('#0000FF');
|
|
154
|
+
expect(color1.blend(0.5, color2).asString()).toBe('rgb(128,0,128)');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('blend() works from HSV', () => {
|
|
158
|
+
const hsv1 = new HSV(0, 100, 100, 1);
|
|
159
|
+
const hsv2 = new HSV(240, 100, 100, 1);
|
|
160
|
+
expect(hsv1.blend(0.3, hsv2).asString()).toBe('rgb(179,0,77)');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('setHue() changes the hue', () => {
|
|
164
|
+
const color = Color.parse('#FF0000');
|
|
165
|
+
expect(color.setHue(180).asString()).toBe('hsl(180,100%,50%)');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('invertLuminosity() inverts from HSV', () => {
|
|
169
|
+
const hsv = new HSV(200, 50, 60, 1);
|
|
170
|
+
expect(hsv.invertLuminosity().asString()).toBe('hsl(200,33%,55%)');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('rotateHue() rotates from RGB', () => {
|
|
174
|
+
const rgb = new RGB(255, 0, 0, 1);
|
|
175
|
+
expect(rgb.rotateHue(120).asString()).toBe('hsl(120,100%,50%)');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('saturate() increases saturation from HSV', () => {
|
|
179
|
+
const hsv = new HSV(100, 30, 70, 1);
|
|
180
|
+
expect(hsv.saturate(1.5).asString()).toBe('hsl(100,65%,60%)');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('invert() inverts from HSL', () => {
|
|
184
|
+
const hsl = new HSL(180, 100, 50, 1);
|
|
185
|
+
expect(hsl.invert().asString()).toBe('rgb(255,0,0)');
|
|
186
|
+
});
|
|
187
|
+
});
|
package/src/color/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ Color.parse = function (input: string | Color): Color {
|
|
|
20
20
|
case 'hsla(':
|
|
21
21
|
return HSL.parse(input);
|
|
22
22
|
default:
|
|
23
|
-
throw Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Color.parse: Unknown color format "${input}". Expected formats: "#RRGGBB", "#RGB", "rgb(...)", "rgba(...)", "hsl(...)", or "hsla(...)".`
|
|
25
|
+
);
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
|
package/src/color/random.test.ts
CHANGED
|
@@ -42,6 +42,60 @@ describe('RandomColor', () => {
|
|
|
42
42
|
expect(t({ seed: 'testSeed' })).toBe('hsl(185,90%,23%)');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
it('generates light colors with luminosity: "light"', () => {
|
|
46
|
+
const color = randomColor({ seed: 'lightSeed', luminosity: 'light' });
|
|
47
|
+
const hsv = color.asArray();
|
|
48
|
+
// Light colors should have higher brightness values
|
|
49
|
+
expect(hsv[2]).toBeGreaterThan(50);
|
|
50
|
+
expect(color).toBeInstanceOf(HSV);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('generates random luminosity colors with luminosity: "random"', () => {
|
|
54
|
+
const color = randomColor({ seed: 'randomSeed', luminosity: 'random' });
|
|
55
|
+
const hsv = color.asArray();
|
|
56
|
+
// Random luminosity can be anywhere from 0-100
|
|
57
|
+
expect(hsv[2]).toBeGreaterThanOrEqual(0);
|
|
58
|
+
expect(hsv[2]).toBeLessThanOrEqual(100);
|
|
59
|
+
expect(color).toBeInstanceOf(HSV);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('generates light saturation with luminosity: "light"', () => {
|
|
63
|
+
// Test light luminosity affects saturation picking
|
|
64
|
+
const color1 = randomColor({ seed: 'lightTest1', luminosity: 'light', hue: 'blue' });
|
|
65
|
+
const color2 = randomColor({ seed: 'lightTest2', luminosity: 'light', hue: 'green' });
|
|
66
|
+
expect(color1).toBeInstanceOf(HSV);
|
|
67
|
+
expect(color2).toBeInstanceOf(HSV);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('generates colors with various saturation options', () => {
|
|
71
|
+
const weak = randomColor({ seed: 'satTest', saturation: 'weak' });
|
|
72
|
+
const strong = randomColor({ seed: 'satTest', saturation: 'strong' });
|
|
73
|
+
|
|
74
|
+
expect(weak).toBeInstanceOf(HSV);
|
|
75
|
+
expect(strong).toBeInstanceOf(HSV);
|
|
76
|
+
// Strong saturation should have higher saturation values
|
|
77
|
+
expect(strong.s).toBeGreaterThan(80);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('generates colors with all hue name options', () => {
|
|
81
|
+
const hues: Array<string | number> = [
|
|
82
|
+
'red',
|
|
83
|
+
'orange',
|
|
84
|
+
'yellow',
|
|
85
|
+
'green',
|
|
86
|
+
'blue',
|
|
87
|
+
'purple',
|
|
88
|
+
'pink',
|
|
89
|
+
'monochrome',
|
|
90
|
+
180,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
hues.forEach((hue) => {
|
|
94
|
+
const color = randomColor({ seed: `hue-${hue}`, hue });
|
|
95
|
+
expect(color).toBeInstanceOf(HSV);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
45
99
|
it('consistent color generation with a seed', () => {
|
|
46
100
|
const color1 = randomColor({ seed: 'consistentSeed' });
|
|
47
101
|
const color2 = randomColor({ seed: 'consistentSeed' });
|
package/src/color/random.ts
CHANGED
|
@@ -263,5 +263,7 @@ function getColorInfo(hue: number): ColorInfo {
|
|
|
263
263
|
return color;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
-
throw Error(
|
|
266
|
+
throw new Error(
|
|
267
|
+
`getColorInfo: No color info found for hue value ${hue}. This indicates a gap in the color dictionary hue ranges.`
|
|
268
|
+
);
|
|
267
269
|
}
|
package/src/color/rgb.test.ts
CHANGED
|
@@ -84,10 +84,9 @@ describe('RGB Class', () => {
|
|
|
84
84
|
expect(RGB.parse('#FF0080').asHSV().round().asArray()).toStrictEqual([330, 100, 100, 1]);
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
it('asRGB
|
|
87
|
+
it('asRGB returns a clone', () => {
|
|
88
88
|
const color = new RGB(255, 128, 64, 0.5);
|
|
89
89
|
expect(color.asRGB()).toStrictEqual(color);
|
|
90
|
-
expect(color.toRGB()).toStrictEqual(color);
|
|
91
90
|
});
|
|
92
91
|
|
|
93
92
|
it('handles black correctly', () => {
|
|
@@ -182,6 +181,8 @@ describe('RGB Class', () => {
|
|
|
182
181
|
expect(pc((c) => c.tint(0.5, tintColor))).toStrictEqual([125, 100, 125, 0.8]);
|
|
183
182
|
expect(pc((c) => c.tint(1, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
|
|
184
183
|
expect(pc((c) => c.tint(0, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
|
|
184
|
+
expect(pc((c) => c.tint(-1, tintColor))).toStrictEqual([50, 150, 200, 0.8]);
|
|
185
|
+
expect(pc((c) => c.tint(2, tintColor))).toStrictEqual([200, 50, 50, 0.8]);
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
it('blends color correctly', () => {
|
|
@@ -191,6 +192,8 @@ describe('RGB Class', () => {
|
|
|
191
192
|
expect(pc((c) => c.blend(0.8, blendColor))).toStrictEqual([214, 30, 40, 0.8]);
|
|
192
193
|
expect(pc((c) => c.blend(1, blendColor))).toStrictEqual([255, 0, 0, 0.8]);
|
|
193
194
|
expect(pc((c) => c.blend(0, blendColor))).toStrictEqual([50, 150, 200, 0.8]);
|
|
195
|
+
expect(pc((c) => c.blend(-1, blendColor))).toStrictEqual([50, 150, 200, 0.8]);
|
|
196
|
+
expect(pc((c) => c.blend(2, blendColor))).toStrictEqual([255, 0, 0, 0.8]);
|
|
194
197
|
});
|
|
195
198
|
|
|
196
199
|
it('lightens the color correctly', () => {
|
package/src/color/rgb.ts
CHANGED
|
@@ -181,15 +181,6 @@ export class RGB extends Color {
|
|
|
181
181
|
return this.clone();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/**
|
|
185
|
-
* Returns the RGB color.
|
|
186
|
-
*
|
|
187
|
-
* @returns The current RGB instance.
|
|
188
|
-
*/
|
|
189
|
-
toRGB(): RGB {
|
|
190
|
-
return this;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
184
|
/**
|
|
194
185
|
* Parses a string or Color instance into an RGB color.
|
|
195
186
|
*
|
package/src/color/utils.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function clamp(value: number, min: number, max: number): number {
|
|
|
8
8
|
export function mod(value: number, max: number): number {
|
|
9
9
|
value = value % max;
|
|
10
10
|
if (value < 0) value += max;
|
|
11
|
-
if (value
|
|
11
|
+
if (value === 0) return 0;
|
|
12
12
|
return value;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -58,7 +58,9 @@ export function guessStyle(tileJSON: TileJSONSpecification, options?: GuessStyle
|
|
|
58
58
|
tileJSON.tiles = tileJSON.tiles.map((url) => resolveUrl(baseUrl, url));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
if (!isTileJSONSpecification(tileJSON))
|
|
61
|
+
if (!isTileJSONSpecification(tileJSON)) {
|
|
62
|
+
throw new Error('guessStyle: Invalid TileJSON specification (this error should never be reached)');
|
|
63
|
+
}
|
|
62
64
|
|
|
63
65
|
let style: StyleSpecification;
|
|
64
66
|
if (isRasterTileJSONSpecification(tileJSON)) {
|
package/src/index.test.ts
CHANGED
|
@@ -110,7 +110,7 @@ describe('exports', () => {
|
|
|
110
110
|
it('should export styles', () => {
|
|
111
111
|
type something = Record<string, unknown>;
|
|
112
112
|
expect(typeof lib.styles).toBe('object');
|
|
113
|
-
const styleNames = ['colorful', 'eclipse', 'graybeard', 'neutrino', 'shadow'];
|
|
113
|
+
const styleNames = ['colorful', 'eclipse', 'graybeard', 'neutrino', 'shadow', 'satellite'];
|
|
114
114
|
for (const name of styleNames) {
|
|
115
115
|
expect(typeof (lib as something)[name]).toBe('function');
|
|
116
116
|
expect(typeof (lib.styles as something)[name]).toBe('function');
|
package/src/index.ts
CHANGED
|
@@ -80,14 +80,24 @@
|
|
|
80
80
|
* @module
|
|
81
81
|
*/
|
|
82
82
|
|
|
83
|
-
export {
|
|
84
|
-
|
|
83
|
+
export {
|
|
84
|
+
colorful,
|
|
85
|
+
eclipse,
|
|
86
|
+
graybeard,
|
|
87
|
+
neutrino,
|
|
88
|
+
shadow,
|
|
89
|
+
satellite,
|
|
90
|
+
type StyleBuilderFunction,
|
|
91
|
+
type SatelliteStyleOptions,
|
|
92
|
+
} from './styles/index.js';
|
|
93
|
+
import { colorful, eclipse, graybeard, neutrino, shadow, satellite } from './styles/index.js';
|
|
85
94
|
export const styles = {
|
|
86
95
|
colorful,
|
|
87
96
|
eclipse,
|
|
88
97
|
graybeard,
|
|
89
98
|
shadow,
|
|
90
99
|
neutrino,
|
|
100
|
+
satellite,
|
|
91
101
|
};
|
|
92
102
|
|
|
93
103
|
export type { GuessStyleOptions } from './guess_style/index.js';
|
package/src/lib/utils.test.ts
CHANGED
|
@@ -85,7 +85,7 @@ describe('isBasicType', () => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
it('throws an error for unsupported types like functions', () => {
|
|
88
|
-
expect(() => isBasicType(() => true)).toThrow('
|
|
88
|
+
expect(() => isBasicType(() => true)).toThrow('isBasicType: Unknown type "function"');
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
@@ -123,6 +123,89 @@ describe('deepMerge', () => {
|
|
|
123
123
|
const source = { a: { b: 1 } };
|
|
124
124
|
expect(() => deepMerge(target, source)).toThrow('Not implemented yet: "function" case');
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
it('merges multiple source objects', () => {
|
|
128
|
+
const target = { a: 1, b: 2, c: 3 };
|
|
129
|
+
const source1 = { b: 10, d: 4 };
|
|
130
|
+
const source2 = { c: 20, e: 5 };
|
|
131
|
+
const result = deepMerge(target, source1, source2);
|
|
132
|
+
expect(result).toEqual({ a: 1, b: 10, c: 20, d: 4, e: 5 });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('handles undefined values in source', () => {
|
|
136
|
+
const target = { a: 1, b: 2 };
|
|
137
|
+
const source = { a: undefined, c: 3 };
|
|
138
|
+
const result = deepMerge(target, source);
|
|
139
|
+
expect(result).toEqual({ a: undefined, b: 2, c: 3 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('overwrites with null when target is a basic type', () => {
|
|
143
|
+
const target = { a: 1, b: 'string' };
|
|
144
|
+
const source = { a: null, b: null };
|
|
145
|
+
const result = deepMerge(target, source);
|
|
146
|
+
expect(result).toEqual({ a: null, b: null });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('throws error when merging null with object', () => {
|
|
150
|
+
const target = { a: { x: 10 } } as { a: object | null };
|
|
151
|
+
const source = { a: null };
|
|
152
|
+
expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('merges with empty source object', () => {
|
|
156
|
+
const target = { a: 1, b: 2 };
|
|
157
|
+
const source = {};
|
|
158
|
+
const result = deepMerge(target, source);
|
|
159
|
+
expect(result).toEqual({ a: 1, b: 2 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('skips non-object sources', () => {
|
|
163
|
+
const target = { a: 1, b: 2 };
|
|
164
|
+
// @ts-expect-error Testing runtime behavior with invalid input
|
|
165
|
+
const result = deepMerge(target, null, undefined, 'string', 123, { c: 3 });
|
|
166
|
+
expect(result).toEqual({ a: 1, b: 2, c: 3 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('throws error when merging incompatible types (array with object)', () => {
|
|
170
|
+
const target = { a: [1, 2, 3] } as { a: object };
|
|
171
|
+
const source = { a: { b: 1 } };
|
|
172
|
+
expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('throws error when merging incompatible types (object with array)', () => {
|
|
176
|
+
const target = { a: { b: 1 } } as { a: object };
|
|
177
|
+
const source = { a: [1, 2, 3] };
|
|
178
|
+
expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('overwrites basic types with arrays', () => {
|
|
182
|
+
const target = { a: 1, b: 'string' };
|
|
183
|
+
const source = { a: [1, 2, 3], b: [4, 5] };
|
|
184
|
+
const result = deepMerge(target, source);
|
|
185
|
+
expect(result).toEqual({ a: [1, 2, 3], b: [4, 5] });
|
|
186
|
+
expect(result.a).not.toBe(source.a); // Should be a deep clone
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('throws error when merging array with array', () => {
|
|
190
|
+
const target = { a: [1, 2, 3] };
|
|
191
|
+
const source = { a: [4, 5] };
|
|
192
|
+
expect(() => deepMerge(target, source)).toThrow('deepMerge: Cannot merge incompatible types for key "a"');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('deeply merges nested objects across multiple levels', () => {
|
|
196
|
+
const target = { a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 };
|
|
197
|
+
const source = { a: { b: { c: 10 }, g: 5 }, h: 6 };
|
|
198
|
+
const result = deepMerge(target, source);
|
|
199
|
+
expect(result).toEqual({ a: { b: { c: 10, d: 2 }, e: 3, g: 5 }, f: 4, h: 6 });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('does not mutate the original target object', () => {
|
|
203
|
+
const target = { a: { b: 1 }, c: 2 };
|
|
204
|
+
const source = { a: { b: 10 }, d: 3 };
|
|
205
|
+
const result = deepMerge(target, source);
|
|
206
|
+
expect(target).toEqual({ a: { b: 1 }, c: 2 }); // Original unchanged
|
|
207
|
+
expect(result).toEqual({ a: { b: 10 }, c: 2, d: 3 });
|
|
208
|
+
});
|
|
126
209
|
});
|
|
127
210
|
|
|
128
211
|
describe('resolveUrl', () => {
|
package/src/lib/utils.ts
CHANGED
|
@@ -29,9 +29,7 @@ export function deepClone<T>(obj: T): T {
|
|
|
29
29
|
|
|
30
30
|
if (obj == null) return obj;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
console.log('obj.prototype', Object.getPrototypeOf(obj));
|
|
34
|
-
throw Error();
|
|
32
|
+
throw new Error(`deepClone: Unsupported object type "${Object.getPrototypeOf(obj)?.constructor?.name ?? 'unknown'}"`);
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export function isSimpleObject(item: unknown): item is object {
|
|
@@ -54,7 +52,7 @@ export function isBasicType(item: unknown): item is boolean | number | string |
|
|
|
54
52
|
case 'object':
|
|
55
53
|
return false;
|
|
56
54
|
default:
|
|
57
|
-
throw Error(
|
|
55
|
+
throw new Error(`isBasicType: Unknown type "${typeof item}"`);
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
@@ -68,9 +66,7 @@ export function deepMerge<T extends object>(source0: T, ...sources: Partial<T>[]
|
|
|
68
66
|
|
|
69
67
|
const sourceValue = source[key] as T[typeof key];
|
|
70
68
|
|
|
71
|
-
//
|
|
72
|
-
// overwrite
|
|
73
|
-
// *********
|
|
69
|
+
// Handle basic types (number, string, boolean) - always overwrite
|
|
74
70
|
switch (typeof sourceValue) {
|
|
75
71
|
case 'number':
|
|
76
72
|
case 'string':
|
|
@@ -80,33 +76,28 @@ export function deepMerge<T extends object>(source0: T, ...sources: Partial<T>[]
|
|
|
80
76
|
default:
|
|
81
77
|
}
|
|
82
78
|
|
|
79
|
+
// If target is a basic type, overwrite with deep clone of source
|
|
83
80
|
if (isBasicType(target[key])) {
|
|
84
81
|
target[key] = deepClone(sourceValue);
|
|
85
82
|
continue;
|
|
86
83
|
}
|
|
87
84
|
|
|
85
|
+
// Handle Color instances - clone the source color
|
|
88
86
|
if (sourceValue instanceof Color) {
|
|
89
87
|
target[key] = sourceValue.clone() as T[typeof key];
|
|
90
88
|
continue;
|
|
91
89
|
}
|
|
92
90
|
|
|
91
|
+
// If both are simple objects, merge them recursively
|
|
93
92
|
if (isSimpleObject(target[key]) && isSimpleObject(sourceValue)) {
|
|
94
93
|
target[key] = deepMerge(target[key], sourceValue);
|
|
95
94
|
continue;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (isSimpleObject(target[key]) && isSimpleObject(sourceValue)) {
|
|
103
|
-
target[key] = deepMerge(target[key], sourceValue);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log('target[key]:', target[key]);
|
|
108
|
-
console.log('source[key]:', source[key]);
|
|
109
|
-
throw Error('unpredicted case');
|
|
97
|
+
// Incompatible types - throw error
|
|
98
|
+
throw new Error(
|
|
99
|
+
`deepMerge: Cannot merge incompatible types for key "${String(key)}" (target: ${typeof target[key]}, source: ${typeof sourceValue})`
|
|
100
|
+
);
|
|
110
101
|
}
|
|
111
102
|
}
|
|
112
103
|
return target;
|
|
@@ -22,7 +22,9 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
|
|
|
22
22
|
if (!id.includes('*')) return id;
|
|
23
23
|
const regExpString = id.replace(/[^a-z_:-]/g, (c) => {
|
|
24
24
|
if (c === '*') return '[a-z_-]*';
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error(
|
|
26
|
+
`decorator: Invalid character ${JSON.stringify(c)} in layer ID pattern "${id}". Only alphanumeric, underscore, colon, hyphen, and asterisk are allowed.`
|
|
27
|
+
);
|
|
26
28
|
});
|
|
27
29
|
const regExp = new RegExp(`^${regExpString}$`, 'i');
|
|
28
30
|
return layerIds.filter((layerId) => regExp.test(layerId));
|
|
@@ -78,26 +80,27 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
|
|
|
78
80
|
value = processExpression(value);
|
|
79
81
|
break;
|
|
80
82
|
default:
|
|
81
|
-
throw new Error(
|
|
83
|
+
throw new Error(
|
|
84
|
+
`decorator: Unknown property value type "${propertyDef.valueType}" for key "${key}" on layer type "${layer.type}". This may indicate a MapLibre property definition mismatch.`
|
|
85
|
+
);
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
switch (propertyDef.parent) {
|
|
85
89
|
case 'layer':
|
|
86
|
-
|
|
87
|
-
layer[key] = value;
|
|
90
|
+
(layer as Record<string, unknown>)[key] = value;
|
|
88
91
|
break;
|
|
89
92
|
case 'layout':
|
|
90
93
|
if (!layer.layout) layer.layout = {};
|
|
91
|
-
|
|
92
|
-
layer.layout[key] = value;
|
|
94
|
+
(layer.layout as Record<string, unknown>)[key] = value;
|
|
93
95
|
break;
|
|
94
96
|
case 'paint':
|
|
95
97
|
if (!layer.paint) layer.paint = {};
|
|
96
|
-
|
|
97
|
-
layer.paint[key] = value;
|
|
98
|
+
(layer.paint as Record<string, unknown>)[key] = value;
|
|
98
99
|
break;
|
|
99
100
|
default:
|
|
100
|
-
throw new Error(
|
|
101
|
+
throw new Error(
|
|
102
|
+
`decorator: Unknown property parent "${propertyDef.parent}" for key "${key}" on layer type "${layer.type}". Expected "layer", "layout", or "paint".`
|
|
103
|
+
);
|
|
101
104
|
}
|
|
102
105
|
});
|
|
103
106
|
}
|
|
@@ -108,12 +111,16 @@ export function decorate(layers: MaplibreLayer[], rules: StyleRules, recolor: Ca
|
|
|
108
111
|
const color = recolor.do(value as Color);
|
|
109
112
|
return color.asString();
|
|
110
113
|
}
|
|
111
|
-
throw new Error(
|
|
114
|
+
throw new Error(
|
|
115
|
+
`decorator.processColor: Expected a color string or Color instance, but got ${typeof value}. Value: ${JSON.stringify(value)}`
|
|
116
|
+
);
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
function processFont(value: StyleRuleValue): string[] {
|
|
115
120
|
if (typeof value === 'string') return [value];
|
|
116
|
-
throw new Error(
|
|
121
|
+
throw new Error(
|
|
122
|
+
`decorator.processFont: Expected a font name string, but got ${typeof value}. Value: ${JSON.stringify(value)}`
|
|
123
|
+
);
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
function processExpression(
|