@softwarity/geojson-editor 1.0.16 → 1.0.18
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 +8 -5
- package/dist/geojson-editor.js +2 -2
- package/package.json +2 -2
- package/src/constants.ts +74 -0
- package/src/geojson-editor.css +15 -3
- package/src/geojson-editor.d.ts +155 -0
- package/src/geojson-editor.ts +712 -767
- package/src/internal-types.ts +111 -0
- package/src/syntax-highlighter.ts +197 -0
- package/src/types.ts +48 -0
- package/src/utils.ts +59 -0
- package/src/validation.ts +90 -0
- package/types/geojson-editor.d.ts +146 -488
- package/types/types.d.ts +44 -0
- package/types/geojson-editor.template.d.ts +0 -4
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection } from 'geojson';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal types - not exported publicly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Position in the editor (line and column) */
|
|
8
|
+
export interface CursorPosition {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Input types accepted by API methods */
|
|
14
|
+
export type FeatureInput = Feature | Feature[] | FeatureCollection;
|
|
15
|
+
|
|
16
|
+
/** Color metadata for a line */
|
|
17
|
+
export interface ColorMeta {
|
|
18
|
+
attributeName: string;
|
|
19
|
+
color: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Boolean metadata for a line */
|
|
23
|
+
export interface BooleanMeta {
|
|
24
|
+
attributeName: string;
|
|
25
|
+
value: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Collapse button metadata */
|
|
29
|
+
export interface CollapseButtonMeta {
|
|
30
|
+
nodeKey: string;
|
|
31
|
+
nodeId: string;
|
|
32
|
+
isCollapsed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Visibility button metadata */
|
|
36
|
+
export interface VisibilityButtonMeta {
|
|
37
|
+
featureKey: string;
|
|
38
|
+
isHidden: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Line metadata */
|
|
42
|
+
export interface LineMeta {
|
|
43
|
+
colors: ColorMeta[];
|
|
44
|
+
booleans: BooleanMeta[];
|
|
45
|
+
collapseButton: CollapseButtonMeta | null;
|
|
46
|
+
visibilityButton: VisibilityButtonMeta | null;
|
|
47
|
+
isHidden: boolean;
|
|
48
|
+
isCollapsed: boolean;
|
|
49
|
+
featureKey: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Visible line data */
|
|
53
|
+
export interface VisibleLine {
|
|
54
|
+
index: number;
|
|
55
|
+
content: string;
|
|
56
|
+
meta: LineMeta | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Feature range in the editor */
|
|
60
|
+
export interface FeatureRange {
|
|
61
|
+
startLine: number;
|
|
62
|
+
endLine: number;
|
|
63
|
+
featureIndex: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Node range info */
|
|
67
|
+
export interface NodeRangeInfo {
|
|
68
|
+
startLine: number;
|
|
69
|
+
endLine: number;
|
|
70
|
+
nodeKey?: string;
|
|
71
|
+
isRootFeature?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Collapsible range info */
|
|
75
|
+
export interface CollapsibleRange extends NodeRangeInfo {
|
|
76
|
+
nodeId: string;
|
|
77
|
+
openBracket: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Editor state snapshot for undo/redo */
|
|
81
|
+
export interface EditorSnapshot {
|
|
82
|
+
lines: string[];
|
|
83
|
+
cursorLine: number;
|
|
84
|
+
cursorColumn: number;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Bracket count result */
|
|
89
|
+
export interface BracketCount {
|
|
90
|
+
open: number;
|
|
91
|
+
close: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Context stack item */
|
|
95
|
+
export interface ContextStackItem {
|
|
96
|
+
context: string;
|
|
97
|
+
isArray: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Collapsed zone context for keydown handlers */
|
|
101
|
+
export interface CollapsedZoneContext {
|
|
102
|
+
inCollapsedZone: CollapsedNodeInfo | null;
|
|
103
|
+
onCollapsedNode: CollapsedNodeInfo | null;
|
|
104
|
+
onClosingLine: CollapsedNodeInfo | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Collapsed node info with nodeId */
|
|
108
|
+
export interface CollapsedNodeInfo extends NodeRangeInfo {
|
|
109
|
+
nodeId: string;
|
|
110
|
+
isCollapsed?: boolean;
|
|
111
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { LineMeta } from './internal-types.js';
|
|
2
|
+
import {
|
|
3
|
+
GEOJSON_KEYS,
|
|
4
|
+
GEOMETRY_TYPES,
|
|
5
|
+
RE_COLLAPSED_BRACKET,
|
|
6
|
+
RE_COLLAPSED_ROOT,
|
|
7
|
+
RE_ESCAPE_AMP,
|
|
8
|
+
RE_ESCAPE_LT,
|
|
9
|
+
RE_ESCAPE_GT,
|
|
10
|
+
RE_PUNCTUATION,
|
|
11
|
+
RE_JSON_KEYS,
|
|
12
|
+
RE_TYPE_VALUES,
|
|
13
|
+
RE_STRING_VALUES,
|
|
14
|
+
RE_COLOR_HEX,
|
|
15
|
+
RE_NUMBERS_COLON,
|
|
16
|
+
RE_NUMBERS_ARRAY,
|
|
17
|
+
RE_NUMBERS_START,
|
|
18
|
+
RE_BOOLEANS,
|
|
19
|
+
RE_NULL,
|
|
20
|
+
RE_UNRECOGNIZED,
|
|
21
|
+
RE_WHITESPACE_ONLY,
|
|
22
|
+
RE_WHITESPACE_SPLIT
|
|
23
|
+
} from './constants.js';
|
|
24
|
+
|
|
25
|
+
// CSS named colors (147 colors, ~1.2KB) - format: ,color1,color2,...,
|
|
26
|
+
// Using string with indexOf for O(n) lookup, simpler than Set
|
|
27
|
+
const CSS_COLORS = ',aliceblue,antiquewhite,aqua,aquamarine,azure,beige,bisque,black,blanchedalmond,blue,blueviolet,brown,burlywood,cadetblue,chartreuse,chocolate,coral,cornflowerblue,cornsilk,crimson,cyan,darkblue,darkcyan,darkgoldenrod,darkgray,darkgreen,darkgrey,darkkhaki,darkmagenta,darkolivegreen,darkorange,darkorchid,darkred,darksalmon,darkseagreen,darkslateblue,darkslategray,darkslategrey,darkturquoise,darkviolet,deeppink,deepskyblue,dimgray,dimgrey,dodgerblue,firebrick,floralwhite,forestgreen,fuchsia,gainsboro,ghostwhite,gold,goldenrod,gray,green,greenyellow,grey,honeydew,hotpink,indianred,indigo,ivory,khaki,lavender,lavenderblush,lawngreen,lemonchiffon,lightblue,lightcoral,lightcyan,lightgoldenrodyellow,lightgray,lightgreen,lightgrey,lightpink,lightsalmon,lightseagreen,lightskyblue,lightslategray,lightslategrey,lightsteelblue,lightyellow,lime,limegreen,linen,magenta,maroon,mediumaquamarine,mediumblue,mediumorchid,mediumpurple,mediumseagreen,mediumslateblue,mediumspringgreen,mediumturquoise,mediumvioletred,midnightblue,mintcream,mistyrose,moccasin,navajowhite,navy,oldlace,olive,olivedrab,orange,orangered,orchid,palegoldenrod,palegreen,paleturquoise,palevioletred,papayawhip,peachpuff,peru,pink,plum,powderblue,purple,rebeccapurple,red,rosybrown,royalblue,saddlebrown,salmon,sandybrown,seagreen,seashell,sienna,silver,skyblue,slateblue,slategray,slategrey,snow,springgreen,steelblue,tan,teal,thistle,tomato,turquoise,violet,wheat,white,whitesmoke,yellow,yellowgreen,';
|
|
28
|
+
|
|
29
|
+
// Reusable DOM element for color conversion (getComputedStyle)
|
|
30
|
+
let _colorTestEl: HTMLElement | null = null;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get or create the color test element (lazy initialization)
|
|
34
|
+
*/
|
|
35
|
+
function getColorTestEl(): HTMLElement {
|
|
36
|
+
if (!_colorTestEl) {
|
|
37
|
+
_colorTestEl = document.createElement('div');
|
|
38
|
+
_colorTestEl.style.display = 'none';
|
|
39
|
+
document.body.appendChild(_colorTestEl);
|
|
40
|
+
}
|
|
41
|
+
return _colorTestEl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a string is a valid CSS named color
|
|
46
|
+
*/
|
|
47
|
+
export function isNamedColor(value: string): boolean {
|
|
48
|
+
return CSS_COLORS.includes(',' + value.toLowerCase() + ',');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert a named CSS color to hex using browser's getComputedStyle
|
|
53
|
+
*/
|
|
54
|
+
export function namedColorToHex(colorName: string): string | null {
|
|
55
|
+
const el = getColorTestEl();
|
|
56
|
+
el.style.color = colorName;
|
|
57
|
+
|
|
58
|
+
// Get computed color (browser returns rgb(r, g, b) or rgba(r, g, b, a))
|
|
59
|
+
const computed = getComputedStyle(el).color;
|
|
60
|
+
if (!computed) return null;
|
|
61
|
+
|
|
62
|
+
// Parse rgb(r, g, b) or rgba(r, g, b, a)
|
|
63
|
+
const match = computed.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
const [ ,rd, gd, bd ] = match;
|
|
66
|
+
|
|
67
|
+
// Convert to hex
|
|
68
|
+
const r = parseInt(rd, 10).toString(16).padStart(2, '0');
|
|
69
|
+
const g = parseInt(gd, 10).toString(16).padStart(2, '0');
|
|
70
|
+
const b = parseInt(bd, 10).toString(16).padStart(2, '0');
|
|
71
|
+
|
|
72
|
+
return '#' + r + g + b;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Apply syntax highlighting to a line of JSON text
|
|
77
|
+
* Returns HTML with syntax highlighting spans
|
|
78
|
+
*/
|
|
79
|
+
export function highlightSyntax(text: string, context: string, meta: LineMeta | undefined): string {
|
|
80
|
+
if (!text) return '';
|
|
81
|
+
|
|
82
|
+
// For collapsed nodes, truncate the text at the opening bracket
|
|
83
|
+
let displayText = text;
|
|
84
|
+
let collapsedBracket: string | null = null;
|
|
85
|
+
|
|
86
|
+
if (meta?.collapseButton?.isCollapsed) {
|
|
87
|
+
// Match "key": { or "key": [
|
|
88
|
+
const bracketMatch = text.match(RE_COLLAPSED_BRACKET);
|
|
89
|
+
// Also match standalone { or [ (root Feature objects)
|
|
90
|
+
const rootMatch = !bracketMatch && text.match(RE_COLLAPSED_ROOT);
|
|
91
|
+
|
|
92
|
+
if (bracketMatch) {
|
|
93
|
+
displayText = bracketMatch[1] + bracketMatch[2];
|
|
94
|
+
collapsedBracket = bracketMatch[2];
|
|
95
|
+
} else if (rootMatch) {
|
|
96
|
+
displayText = rootMatch[1] + rootMatch[2];
|
|
97
|
+
collapsedBracket = rootMatch[2];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Escape HTML first
|
|
102
|
+
let result = displayText
|
|
103
|
+
.replace(RE_ESCAPE_AMP, '&')
|
|
104
|
+
.replace(RE_ESCAPE_LT, '<')
|
|
105
|
+
.replace(RE_ESCAPE_GT, '>');
|
|
106
|
+
|
|
107
|
+
// Punctuation FIRST (before other replacements can interfere)
|
|
108
|
+
result = result.replace(RE_PUNCTUATION, '<span class="json-punctuation">$1</span>');
|
|
109
|
+
|
|
110
|
+
// JSON keys - match "key" followed by :
|
|
111
|
+
// In properties context, all keys are treated as regular JSON keys
|
|
112
|
+
RE_JSON_KEYS.lastIndex = 0;
|
|
113
|
+
result = result.replace(RE_JSON_KEYS, (_match, key, colon) => {
|
|
114
|
+
if (context !== 'properties' && GEOJSON_KEYS.includes(key)) {
|
|
115
|
+
return `<span class="geojson-key">"${key}"</span>${colon}`;
|
|
116
|
+
}
|
|
117
|
+
return `<span class="json-key">"${key}"</span>${colon}`;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Type values - "type": "Value" - but NOT inside properties context
|
|
121
|
+
if (context !== 'properties') {
|
|
122
|
+
RE_TYPE_VALUES.lastIndex = 0;
|
|
123
|
+
result = result.replace(RE_TYPE_VALUES, (_match, space, type) => {
|
|
124
|
+
const isValid = type === 'Feature' || type === 'FeatureCollection' || GEOMETRY_TYPES.includes(type);
|
|
125
|
+
const cls = isValid ? 'geojson-type' : 'geojson-type-invalid';
|
|
126
|
+
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${space}<span class="${cls}">"${type}"</span>`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// String values (not already wrapped in spans)
|
|
131
|
+
RE_STRING_VALUES.lastIndex = 0;
|
|
132
|
+
result = result.replace(RE_STRING_VALUES, (match, colon, space, val) => {
|
|
133
|
+
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
134
|
+
// Check for hex color (#fff or #ffffff)
|
|
135
|
+
if (RE_COLOR_HEX.test(val)) {
|
|
136
|
+
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
137
|
+
}
|
|
138
|
+
// Check for named CSS color (red, blue, etc.) - uses cached browser validation
|
|
139
|
+
if (isNamedColor(val)) {
|
|
140
|
+
// Use the named color directly for swatch, browser handles it
|
|
141
|
+
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
142
|
+
}
|
|
143
|
+
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Numbers after colon
|
|
147
|
+
RE_NUMBERS_COLON.lastIndex = 0;
|
|
148
|
+
result = result.replace(RE_NUMBERS_COLON, '$1$2<span class="json-number">$3</span>');
|
|
149
|
+
|
|
150
|
+
// Numbers in arrays (after [ or ,)
|
|
151
|
+
RE_NUMBERS_ARRAY.lastIndex = 0;
|
|
152
|
+
result = result.replace(RE_NUMBERS_ARRAY, '$1$2<span class="json-number">$3</span>');
|
|
153
|
+
|
|
154
|
+
// Standalone numbers at start of line (coordinates arrays)
|
|
155
|
+
RE_NUMBERS_START.lastIndex = 0;
|
|
156
|
+
result = result.replace(RE_NUMBERS_START, '$1<span class="json-number">$2</span>');
|
|
157
|
+
|
|
158
|
+
// Booleans - use ::before for checkbox via CSS class
|
|
159
|
+
RE_BOOLEANS.lastIndex = 0;
|
|
160
|
+
result = result.replace(RE_BOOLEANS, (_match, colon, space, val) => {
|
|
161
|
+
const checkedClass = val === 'true' ? ' json-bool-true' : ' json-bool-false';
|
|
162
|
+
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Null
|
|
166
|
+
RE_NULL.lastIndex = 0;
|
|
167
|
+
result = result.replace(RE_NULL, '$1$2<span class="json-null">$3</span>');
|
|
168
|
+
|
|
169
|
+
// Collapsed bracket indicator
|
|
170
|
+
if (collapsedBracket) {
|
|
171
|
+
const bracketClass = collapsedBracket === '[' ? 'collapsed-bracket-array' : 'collapsed-bracket-object';
|
|
172
|
+
result = result.replace(
|
|
173
|
+
new RegExp(`<span class="json-punctuation">\\${collapsedBracket}<\\/span>$`),
|
|
174
|
+
`<span class="${bracketClass}">${collapsedBracket}</span>`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Mark unrecognized text as error
|
|
179
|
+
RE_UNRECOGNIZED.lastIndex = 0;
|
|
180
|
+
result = result.replace(RE_UNRECOGNIZED, (match, before, text, after) => {
|
|
181
|
+
if (!text || RE_WHITESPACE_ONLY.test(text)) return match;
|
|
182
|
+
// Check for unrecognized words/tokens (not whitespace, not just spaces/commas)
|
|
183
|
+
// Keep whitespace as-is, wrap any non-whitespace unrecognized token
|
|
184
|
+
const parts: string[] = text.split(RE_WHITESPACE_SPLIT);
|
|
185
|
+
let hasError = false;
|
|
186
|
+
const processed = parts.map(part => {
|
|
187
|
+
// If it's whitespace, keep it
|
|
188
|
+
if (RE_WHITESPACE_ONLY.test(part)) return part;
|
|
189
|
+
// Mark as error
|
|
190
|
+
hasError = true;
|
|
191
|
+
return `<span class="json-error">${part}</span>`;
|
|
192
|
+
}).join('');
|
|
193
|
+
return hasError ? before + processed + after : match;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Feature } from 'geojson';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public types - exported from the package
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Options for set/add/insertAt/open methods */
|
|
8
|
+
export interface SetOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Attributes to collapse after loading.
|
|
11
|
+
* - string[]: List of attribute names (e.g., ['coordinates', 'geometry'])
|
|
12
|
+
* - function: Dynamic function (feature, index) => string[]
|
|
13
|
+
* - '$root': Special keyword to collapse entire features
|
|
14
|
+
* - Empty array: No auto-collapse
|
|
15
|
+
* @default ['coordinates']
|
|
16
|
+
*/
|
|
17
|
+
collapsed?: string[] | ((feature: Feature | null, index: number) => string[]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Theme configuration */
|
|
21
|
+
export interface ThemeConfig {
|
|
22
|
+
bgColor?: string;
|
|
23
|
+
textColor?: string;
|
|
24
|
+
caretColor?: string;
|
|
25
|
+
gutterBg?: string;
|
|
26
|
+
gutterBorder?: string;
|
|
27
|
+
gutterText?: string;
|
|
28
|
+
jsonKey?: string;
|
|
29
|
+
jsonString?: string;
|
|
30
|
+
jsonNumber?: string;
|
|
31
|
+
jsonBoolean?: string;
|
|
32
|
+
jsonNull?: string;
|
|
33
|
+
jsonPunct?: string;
|
|
34
|
+
jsonError?: string;
|
|
35
|
+
controlColor?: string;
|
|
36
|
+
controlBg?: string;
|
|
37
|
+
controlBorder?: string;
|
|
38
|
+
geojsonKey?: string;
|
|
39
|
+
geojsonType?: string;
|
|
40
|
+
geojsonTypeInvalid?: string;
|
|
41
|
+
jsonKeyInvalid?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Theme settings for dark and light modes */
|
|
45
|
+
export interface ThemeSettings {
|
|
46
|
+
dark?: ThemeConfig;
|
|
47
|
+
light?: ThemeConfig;
|
|
48
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Feature } from 'geojson';
|
|
2
|
+
import type { BracketCount } from './internal-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Alias for document.createElement - optimized for minification
|
|
6
|
+
*/
|
|
7
|
+
export const createElement = (tag: string): HTMLElement => document.createElement(tag);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate a unique feature key from a feature object
|
|
11
|
+
* Uses id, properties.id, or a hash of geometry coordinates
|
|
12
|
+
*/
|
|
13
|
+
export function getFeatureKey(feature: Feature | null): string | null {
|
|
14
|
+
if (!feature) return null;
|
|
15
|
+
if (feature.id !== undefined) return `id:${feature.id}`;
|
|
16
|
+
if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
|
|
17
|
+
|
|
18
|
+
const geomType = feature.geometry?.type || 'null';
|
|
19
|
+
const geom = feature.geometry as { coordinates?: unknown } | null;
|
|
20
|
+
const coords = JSON.stringify(geom?.coordinates || []);
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < coords.length; i++) {
|
|
23
|
+
hash = ((hash << 5) - hash) + coords.charCodeAt(i);
|
|
24
|
+
hash = hash & hash;
|
|
25
|
+
}
|
|
26
|
+
return `hash:${geomType}:${hash.toString(36)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Count open and close brackets in a line
|
|
31
|
+
* Handles string escaping properly
|
|
32
|
+
*/
|
|
33
|
+
export function countBrackets(line: string, openBracket: string): BracketCount {
|
|
34
|
+
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
35
|
+
let open = 0, close = 0, inString = false, escape = false;
|
|
36
|
+
|
|
37
|
+
for (const char of line) {
|
|
38
|
+
if (escape) { escape = false; continue; }
|
|
39
|
+
if (char === '\\' && inString) { escape = true; continue; }
|
|
40
|
+
if (char === '"') { inString = !inString; continue; }
|
|
41
|
+
if (!inString) {
|
|
42
|
+
if (char === openBracket) open++;
|
|
43
|
+
if (char === closeBracket) close++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { open, close };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a CSS selector to a :host rule for shadow DOM
|
|
52
|
+
*/
|
|
53
|
+
export function parseSelectorToHostRule(selector: string | null): string {
|
|
54
|
+
if (!selector) return ':host([data-color-scheme="dark"])';
|
|
55
|
+
if (selector.startsWith('.') && !selector.includes(' ')) {
|
|
56
|
+
return `:host(${selector})`;
|
|
57
|
+
}
|
|
58
|
+
return `:host-context(${selector})`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection } from 'geojson';
|
|
2
|
+
import { GEOMETRY_TYPES, type GeometryType } from './constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validate a parsed FeatureCollection and return any errors
|
|
6
|
+
*/
|
|
7
|
+
export function validateGeoJSON(parsed: FeatureCollection): string[] {
|
|
8
|
+
const errors: string[] = [];
|
|
9
|
+
|
|
10
|
+
if (!parsed.features) return errors;
|
|
11
|
+
|
|
12
|
+
parsed.features.forEach((feature, i) => {
|
|
13
|
+
if (feature.type !== 'Feature') {
|
|
14
|
+
errors.push(`features[${i}]: type must be "Feature"`);
|
|
15
|
+
}
|
|
16
|
+
if (feature.geometry && feature.geometry.type) {
|
|
17
|
+
if (!GEOMETRY_TYPES.includes(feature.geometry.type as GeometryType)) {
|
|
18
|
+
errors.push(`features[${i}].geometry: invalid type "${feature.geometry.type}"`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate a single feature object
|
|
28
|
+
* @throws Error if the feature is invalid
|
|
29
|
+
*/
|
|
30
|
+
export function validateFeature(feature: Feature): void {
|
|
31
|
+
if (!feature || typeof feature !== 'object') {
|
|
32
|
+
throw new Error('Feature must be an object');
|
|
33
|
+
}
|
|
34
|
+
if (feature.type !== 'Feature') {
|
|
35
|
+
throw new Error('Feature type must be "Feature"');
|
|
36
|
+
}
|
|
37
|
+
if (!('geometry' in feature)) {
|
|
38
|
+
throw new Error('Feature must have a geometry property');
|
|
39
|
+
}
|
|
40
|
+
if (!('properties' in feature)) {
|
|
41
|
+
throw new Error('Feature must have a properties property');
|
|
42
|
+
}
|
|
43
|
+
if (feature.geometry !== null) {
|
|
44
|
+
if (typeof feature.geometry !== 'object') {
|
|
45
|
+
throw new Error('Feature geometry must be an object or null');
|
|
46
|
+
}
|
|
47
|
+
if (!feature.geometry.type) {
|
|
48
|
+
throw new Error('Feature geometry must have a type');
|
|
49
|
+
}
|
|
50
|
+
if (!GEOMETRY_TYPES.includes(feature.geometry.type as GeometryType)) {
|
|
51
|
+
throw new Error(`Invalid geometry type: "${feature.geometry.type}"`);
|
|
52
|
+
}
|
|
53
|
+
if (!('coordinates' in feature.geometry)) {
|
|
54
|
+
throw new Error('Feature geometry must have coordinates');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (feature.properties !== null && typeof feature.properties !== 'object') {
|
|
58
|
+
throw new Error('Feature properties must be an object or null');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize input to an array of features
|
|
64
|
+
* Accepts: FeatureCollection, Feature[], or single Feature
|
|
65
|
+
* @throws Error if input is invalid
|
|
66
|
+
*/
|
|
67
|
+
export function normalizeToFeatures(input: Feature | Feature[] | FeatureCollection): Feature[] {
|
|
68
|
+
let features: Feature[] = [];
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(input)) {
|
|
71
|
+
features = input;
|
|
72
|
+
} else if (input && typeof input === 'object') {
|
|
73
|
+
if (input.type === 'FeatureCollection' && 'features' in input && Array.isArray(input.features)) {
|
|
74
|
+
features = input.features;
|
|
75
|
+
} else if (input.type === 'Feature') {
|
|
76
|
+
features = [input as Feature];
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate each feature
|
|
85
|
+
for (const feature of features) {
|
|
86
|
+
validateFeature(feature);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return features;
|
|
90
|
+
}
|