@santi020k/lumen-core 0.3.0 → 1.0.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
@@ -7,6 +7,28 @@ Most consumers should install a framework package such as `@santi020k/lumen-astr
7
7
  The package exports `lumenColors` and `lumenGlass` for consumers that need to mirror Lumen theme
8
8
  tokens outside the shared stylesheet.
9
9
 
10
+ ## Chart Helpers
11
+
12
+ `@santi020k/lumen-core/charts` exports the shared `LumenChartSeries` contract plus deterministic
13
+ domain, tick, scaling, line/area, grouped/stacked bar, and pie/donut geometry helpers. They render
14
+ no DOM and perform no statistical analysis; framework packages use them to keep chart output
15
+ aligned.
16
+
17
+ ```ts
18
+ import {
19
+ createLumenLineGeometry,
20
+ type LumenChartSeries
21
+ } from '@santi020k/lumen-core/charts'
22
+
23
+ const series: LumenChartSeries = {
24
+ id: 'views',
25
+ label: 'Views',
26
+ data: [{ x: 'Mon', y: 42 }, { x: 'Tue', y: 68 }]
27
+ }
28
+
29
+ createLumenLineGeometry(series.data)
30
+ ```
31
+
10
32
  It also exports the Lucide-backed icon map (`lumenIcons`, `lumenIconNames`) and helpers such as
11
33
  `renderLumenIconSvg` so framework adapters can render icons by name.
12
34
 
@@ -0,0 +1,106 @@
1
+ export declare const lumenChartTones: readonly ["series-1", "series-2", "series-3", "series-4", "series-5", "series-6", "series-7", "series-8", "brand", "accent", "success", "warning", "danger", "neutral"];
2
+ export type LumenChartTone = (typeof lumenChartTones)[number];
3
+ export interface LumenChartDatum {
4
+ label?: string;
5
+ tone?: LumenChartTone;
6
+ x: number | string;
7
+ xLabel?: string;
8
+ y: number | null;
9
+ }
10
+ export interface LumenChartSeries {
11
+ data: readonly LumenChartDatum[];
12
+ id: string;
13
+ label: string;
14
+ tone?: LumenChartTone;
15
+ }
16
+ export interface LumenChartDomain {
17
+ max: number;
18
+ min: number;
19
+ }
20
+ export interface LumenChartGeometryPoint extends LumenChartDatum {
21
+ xCoordinate: number;
22
+ yCoordinate: number;
23
+ }
24
+ export interface LumenLineGeometry {
25
+ areaPaths: readonly string[];
26
+ domain: LumenChartDomain;
27
+ path: string;
28
+ points: readonly LumenChartGeometryPoint[];
29
+ }
30
+ export interface LumenLineGeometryOptions {
31
+ domain?: Partial<LumenChartDomain>;
32
+ height?: number;
33
+ includeZero?: boolean;
34
+ padding?: number;
35
+ width?: number;
36
+ }
37
+ export type LumenBarChartLayout = 'grouped' | 'stacked';
38
+ export type LumenChartOrientation = 'horizontal' | 'vertical';
39
+ export interface LumenBarGeometryMark {
40
+ category: number | string;
41
+ height: number;
42
+ seriesId: string;
43
+ seriesLabel: string;
44
+ tone: LumenChartTone;
45
+ value: number;
46
+ width: number;
47
+ x: number;
48
+ y: number;
49
+ }
50
+ export interface LumenBarGeometryCategory {
51
+ category: number | string;
52
+ label: number | string;
53
+ x: number;
54
+ y: number;
55
+ }
56
+ export interface LumenBarGeometry {
57
+ categories: readonly LumenBarGeometryCategory[];
58
+ domain: LumenChartDomain;
59
+ height: number;
60
+ marks: readonly LumenBarGeometryMark[];
61
+ width: number;
62
+ }
63
+ export interface LumenBarGeometryOptions {
64
+ categoryWidth?: number;
65
+ height?: number;
66
+ layout?: LumenBarChartLayout;
67
+ orientation?: LumenChartOrientation;
68
+ width?: number;
69
+ }
70
+ export type LumenPieChartVariant = 'donut' | 'pie';
71
+ export interface LumenPieGeometrySlice {
72
+ label: string;
73
+ path: string;
74
+ percentage: number;
75
+ tone: LumenChartTone;
76
+ value: number;
77
+ x: number | string;
78
+ }
79
+ export interface LumenPieGeometry {
80
+ center: number;
81
+ innerRadius: number;
82
+ outerRadius: number;
83
+ size: number;
84
+ slices: readonly LumenPieGeometrySlice[];
85
+ total: number;
86
+ }
87
+ export interface LumenPieGeometryOptions {
88
+ padding?: number;
89
+ size?: number;
90
+ variant?: LumenPieChartVariant;
91
+ }
92
+ export declare const hasLumenChartData: (series: readonly LumenChartSeries[]) => boolean;
93
+ export declare const hasLumenPieData: (data: readonly LumenChartDatum[]) => boolean;
94
+ export declare const getLumenChartDomain: (values: readonly (number | null)[], includeZero?: boolean) => LumenChartDomain;
95
+ export declare const scaleLumenChartValue: (value: number, domain: LumenChartDomain, rangeStart: number, rangeEnd: number) => number;
96
+ export declare const getLumenChartTicks: (domain: LumenChartDomain, count?: number) => number[];
97
+ export declare const resolveLumenChartTone: (tone: LumenChartTone | undefined, index?: number) => LumenChartTone;
98
+ export declare const getLumenChartToneClassName: (tone: LumenChartTone | undefined, index?: number) => string;
99
+ export declare const getLumenPieChartVariantClassName: (variant: LumenPieChartVariant) => string;
100
+ export declare const createLumenPieGeometry: (data: readonly LumenChartDatum[], options?: LumenPieGeometryOptions) => LumenPieGeometry;
101
+ export declare const createLumenLineGeometry: (data: readonly LumenChartDatum[], options?: LumenLineGeometryOptions) => LumenLineGeometry;
102
+ export declare const getLumenChartValueRatio: (value: number, domain: LumenChartDomain) => number;
103
+ export declare const getLumenChartCategories: (series: readonly LumenChartSeries[]) => (number | string)[];
104
+ export declare const alignLumenChartSeries: (series: LumenChartSeries, categories: readonly (number | string)[]) => LumenChartSeries;
105
+ export declare const createLumenBarGeometry: (series: readonly LumenChartSeries[], options?: LumenBarGeometryOptions) => LumenBarGeometry;
106
+ //# sourceMappingURL=charts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"charts.d.ts","sourceRoot":"","sources":["../src/charts.ts"],"names":[],"mappings":"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"}
package/dist/charts.js ADDED
@@ -0,0 +1,380 @@
1
+ export const lumenChartTones = [
2
+ 'series-1',
3
+ 'series-2',
4
+ 'series-3',
5
+ 'series-4',
6
+ 'series-5',
7
+ 'series-6',
8
+ 'series-7',
9
+ 'series-8',
10
+ 'brand',
11
+ 'accent',
12
+ 'success',
13
+ 'warning',
14
+ 'danger',
15
+ 'neutral'
16
+ ];
17
+ const lumenChartToneClassNames = {
18
+ accent: 'ui-chart-tone--accent',
19
+ brand: 'ui-chart-tone--brand',
20
+ danger: 'ui-chart-tone--danger',
21
+ neutral: 'ui-chart-tone--neutral',
22
+ 'series-1': 'ui-chart-tone--series-1',
23
+ 'series-2': 'ui-chart-tone--series-2',
24
+ 'series-3': 'ui-chart-tone--series-3',
25
+ 'series-4': 'ui-chart-tone--series-4',
26
+ 'series-5': 'ui-chart-tone--series-5',
27
+ 'series-6': 'ui-chart-tone--series-6',
28
+ 'series-7': 'ui-chart-tone--series-7',
29
+ 'series-8': 'ui-chart-tone--series-8',
30
+ success: 'ui-chart-tone--success',
31
+ warning: 'ui-chart-tone--warning'
32
+ };
33
+ const lumenPieChartVariantClassNames = {
34
+ donut: 'ui-pie-chart--donut',
35
+ pie: 'ui-pie-chart--pie'
36
+ };
37
+ const defaultChartSize = 100;
38
+ const defaultChartPadding = 4;
39
+ const finiteValues = (values) => values.filter((value) => value !== null && Number.isFinite(value));
40
+ export const hasLumenChartData = (series) => series.some(item => item.data.some(datum => datum.y !== null && Number.isFinite(datum.y)));
41
+ export const hasLumenPieData = (data) => data.some(datum => datum.y !== null && Number.isFinite(datum.y) && datum.y > 0);
42
+ export const getLumenChartDomain = (values, includeZero = true) => {
43
+ const available = finiteValues(values);
44
+ if (available.length === 0)
45
+ return { max: 1, min: 0 };
46
+ let min = Math.min(...available);
47
+ let max = Math.max(...available);
48
+ if (includeZero) {
49
+ min = Math.min(0, min);
50
+ max = Math.max(0, max);
51
+ }
52
+ if (min === max) {
53
+ const offset = Math.abs(min || 1) * 0.1;
54
+ min -= offset;
55
+ max += offset;
56
+ }
57
+ return { max, min };
58
+ };
59
+ export const scaleLumenChartValue = (value, domain, rangeStart, rangeEnd) => {
60
+ const domainSize = domain.max - domain.min;
61
+ if (!Number.isFinite(value) || domainSize === 0)
62
+ return rangeStart;
63
+ const ratio = (value - domain.min) / domainSize;
64
+ return rangeStart + ratio * (rangeEnd - rangeStart);
65
+ };
66
+ export const getLumenChartTicks = (domain, count = 5) => {
67
+ const safeCount = Math.max(2, Math.round(count));
68
+ const step = (domain.max - domain.min) / (safeCount - 1);
69
+ return Array.from({ length: safeCount }, (_, index) => domain.min + step * index);
70
+ };
71
+ export const resolveLumenChartTone = (tone, index = 0) => tone ?? lumenChartTones[index % 8] ?? 'series-1';
72
+ export const getLumenChartToneClassName = (tone, index = 0) => lumenChartToneClassNames[resolveLumenChartTone(tone, index)];
73
+ export const getLumenPieChartVariantClassName = (variant) => lumenPieChartVariantClassNames[variant];
74
+ const polarPoint = (center, radius, angle) => {
75
+ const radians = (angle * Math.PI) / 180;
76
+ return {
77
+ x: center + Math.cos(radians) * radius,
78
+ y: center + Math.sin(radians) * radius
79
+ };
80
+ };
81
+ const formatPieCoordinate = (value) => value.toFixed(3);
82
+ const pieArc = (radius, largeArc, sweep, point) => [
83
+ 'A',
84
+ formatPieCoordinate(radius),
85
+ formatPieCoordinate(radius),
86
+ 0,
87
+ largeArc ? 1 : 0,
88
+ sweep,
89
+ formatPieCoordinate(point.x),
90
+ formatPieCoordinate(point.y)
91
+ ].join(' ');
92
+ const fullPiePath = (center, outerRadius, innerRadius) => {
93
+ const topOuter = polarPoint(center, outerRadius, -90);
94
+ const bottomOuter = polarPoint(center, outerRadius, 90);
95
+ const outer = [
96
+ `M ${formatPieCoordinate(topOuter.x)} ${formatPieCoordinate(topOuter.y)}`,
97
+ pieArc(outerRadius, true, 1, bottomOuter),
98
+ pieArc(outerRadius, true, 1, topOuter),
99
+ 'Z'
100
+ ];
101
+ if (innerRadius === 0)
102
+ return outer.join(' ');
103
+ const topInner = polarPoint(center, innerRadius, -90);
104
+ const bottomInner = polarPoint(center, innerRadius, 90);
105
+ return [
106
+ ...outer,
107
+ `M ${formatPieCoordinate(topInner.x)} ${formatPieCoordinate(topInner.y)}`,
108
+ pieArc(innerRadius, true, 0, bottomInner),
109
+ pieArc(innerRadius, true, 0, topInner),
110
+ 'Z'
111
+ ].join(' ');
112
+ };
113
+ const pieSlicePath = (center, outerRadius, innerRadius, startAngle, endAngle) => {
114
+ if (endAngle - startAngle >= 359.999) {
115
+ return fullPiePath(center, outerRadius, innerRadius);
116
+ }
117
+ const outerStart = polarPoint(center, outerRadius, startAngle);
118
+ const outerEnd = polarPoint(center, outerRadius, endAngle);
119
+ const largeArc = endAngle - startAngle > 180;
120
+ if (innerRadius === 0) {
121
+ return [
122
+ `M ${formatPieCoordinate(center)} ${formatPieCoordinate(center)}`,
123
+ `L ${formatPieCoordinate(outerStart.x)} ${formatPieCoordinate(outerStart.y)}`,
124
+ pieArc(outerRadius, largeArc, 1, outerEnd),
125
+ 'Z'
126
+ ].join(' ');
127
+ }
128
+ const innerStart = polarPoint(center, innerRadius, startAngle);
129
+ const innerEnd = polarPoint(center, innerRadius, endAngle);
130
+ return [
131
+ `M ${formatPieCoordinate(outerStart.x)} ${formatPieCoordinate(outerStart.y)}`,
132
+ pieArc(outerRadius, largeArc, 1, outerEnd),
133
+ `L ${formatPieCoordinate(innerEnd.x)} ${formatPieCoordinate(innerEnd.y)}`,
134
+ pieArc(innerRadius, largeArc, 0, innerStart),
135
+ 'Z'
136
+ ].join(' ');
137
+ };
138
+ export const createLumenPieGeometry = (data, options = {}) => {
139
+ const size = Math.max(1, options.size ?? 320);
140
+ const padding = Math.max(0, options.padding ?? 12);
141
+ const center = size / 2;
142
+ const outerRadius = Math.max(0, center - padding);
143
+ const innerRadius = options.variant === 'pie' ? 0 : outerRadius * 0.58;
144
+ const available = data.filter((datum) => datum.y !== null && Number.isFinite(datum.y) && datum.y > 0);
145
+ const total = available.reduce((sum, datum) => sum + datum.y, 0);
146
+ let angle = -90;
147
+ const slices = available.map((datum, index) => {
148
+ const percentage = total === 0 ? 0 : datum.y / total;
149
+ const startAngle = angle;
150
+ const endAngle = index === available.length - 1 ? 270 : startAngle + percentage * 360;
151
+ angle = endAngle;
152
+ return {
153
+ label: datum.label ?? datum.xLabel ?? String(datum.x),
154
+ path: pieSlicePath(center, outerRadius, innerRadius, startAngle, endAngle),
155
+ percentage,
156
+ tone: resolveLumenChartTone(datum.tone, index),
157
+ value: datum.y,
158
+ x: datum.x
159
+ };
160
+ });
161
+ return {
162
+ center,
163
+ innerRadius,
164
+ outerRadius,
165
+ size,
166
+ slices,
167
+ total
168
+ };
169
+ };
170
+ const splitGeometrySegments = (data, domain, width, height, padding) => {
171
+ const drawableWidth = Math.max(0, width - padding * 2);
172
+ const drawableHeight = Math.max(0, height - padding * 2);
173
+ const denominator = Math.max(1, data.length - 1);
174
+ const segments = [];
175
+ let current = [];
176
+ for (const [index, datum] of data.entries()) {
177
+ if (datum.y === null || !Number.isFinite(datum.y)) {
178
+ if (current.length > 0)
179
+ segments.push(current);
180
+ current = [];
181
+ continue;
182
+ }
183
+ current.push({
184
+ ...datum,
185
+ xCoordinate: padding +
186
+ (data.length === 1 ? 0.5 : index / denominator) * drawableWidth,
187
+ yCoordinate: scaleLumenChartValue(datum.y, domain, padding + drawableHeight, padding)
188
+ });
189
+ }
190
+ if (current.length > 0)
191
+ segments.push(current);
192
+ return segments;
193
+ };
194
+ const segmentPath = (points) => points
195
+ .map((point, index) => `${index === 0 ? 'M' : 'L'} ${point.xCoordinate.toFixed(3)} ${point.yCoordinate.toFixed(3)}`)
196
+ .join(' ');
197
+ export const createLumenLineGeometry = (data, options = {}) => {
198
+ const width = options.width ?? defaultChartSize;
199
+ const height = options.height ?? defaultChartSize;
200
+ const padding = options.padding ?? defaultChartPadding;
201
+ const calculatedDomain = getLumenChartDomain(data.map(datum => datum.y), options.includeZero ?? false);
202
+ const domain = {
203
+ max: options.domain?.max ?? calculatedDomain.max,
204
+ min: options.domain?.min ?? calculatedDomain.min
205
+ };
206
+ const segments = splitGeometrySegments(data, domain, width, height, padding);
207
+ const baseline = scaleLumenChartValue(Math.max(domain.min, Math.min(domain.max, 0)), domain, height - padding, padding);
208
+ return {
209
+ areaPaths: segments
210
+ .map(points => {
211
+ const first = points[0];
212
+ const last = points[points.length - 1];
213
+ if (!first || !last)
214
+ return '';
215
+ return [
216
+ segmentPath(points),
217
+ 'L',
218
+ last.xCoordinate.toFixed(3),
219
+ baseline.toFixed(3),
220
+ 'L',
221
+ first.xCoordinate.toFixed(3),
222
+ baseline.toFixed(3),
223
+ 'Z'
224
+ ].join(' ');
225
+ })
226
+ .filter(Boolean),
227
+ domain,
228
+ path: segments.map(segmentPath).join(' '),
229
+ points: segments.flat()
230
+ };
231
+ };
232
+ export const getLumenChartValueRatio = (value, domain) => {
233
+ const ratio = scaleLumenChartValue(value, domain, 0, 1);
234
+ return Math.max(0, Math.min(1, ratio));
235
+ };
236
+ export const getLumenChartCategories = (series) => {
237
+ const categories = new Map();
238
+ for (const item of series) {
239
+ for (const datum of item.data) {
240
+ const key = `${typeof datum.x}:${String(datum.x)}`;
241
+ if (!categories.has(key))
242
+ categories.set(key, datum.x);
243
+ }
244
+ }
245
+ return [...categories.values()];
246
+ };
247
+ export const alignLumenChartSeries = (series, categories) => ({
248
+ ...series,
249
+ data: categories.map(category => series.data.find(datum => datum.x === category) ?? {
250
+ x: category,
251
+ y: null
252
+ })
253
+ });
254
+ const getDatumValue = (series, category) => series.data.find(datum => datum.x === category)?.y ?? 0;
255
+ const getStackedDomain = (series, categories) => {
256
+ const totals = categories.flatMap(category => {
257
+ let positive = 0;
258
+ let negative = 0;
259
+ for (const item of series) {
260
+ const value = getDatumValue(item, category);
261
+ if (value >= 0)
262
+ positive += value;
263
+ else
264
+ negative += value;
265
+ }
266
+ return [negative, positive];
267
+ });
268
+ return getLumenChartDomain(totals);
269
+ };
270
+ const getBarStartValue = (layout, value, positiveOffset, negativeOffset) => {
271
+ if (layout !== 'stacked')
272
+ return 0;
273
+ return value >= 0 ? positiveOffset : negativeOffset;
274
+ };
275
+ /* eslint-disable complexity -- Bar geometry deliberately keeps both orientations and layouts in one deterministic pass. */
276
+ export const createLumenBarGeometry = (series, options = {}) => {
277
+ const width = options.width ?? 640;
278
+ const height = options.height ?? 320;
279
+ const orientation = options.orientation ?? 'vertical';
280
+ const layout = options.layout ?? 'grouped';
281
+ const categories = getLumenChartCategories(series);
282
+ const values = series.flatMap(item => item.data.map(datum => datum.y));
283
+ const domain = layout === 'stacked' ?
284
+ getStackedDomain(series, categories) :
285
+ getLumenChartDomain(values);
286
+ const margin = orientation === 'horizontal' ?
287
+ {
288
+ bottom: 24,
289
+ left: Math.max(64, Math.min(240, options.categoryWidth ?? 112)),
290
+ right: 20,
291
+ top: 16
292
+ } :
293
+ { bottom: 52, left: 52, right: 16, top: 16 };
294
+ const plotWidth = Math.max(1, width - margin.left - margin.right);
295
+ const plotHeight = Math.max(1, height - margin.top - margin.bottom);
296
+ const categorySize = (orientation === 'horizontal' ? plotHeight : plotWidth) /
297
+ Math.max(1, categories.length);
298
+ const categoryGap = Math.min(16, categorySize * 0.24);
299
+ const usableCategorySize = Math.max(1, categorySize - categoryGap);
300
+ const seriesSize = usableCategorySize / Math.max(1, series.length);
301
+ const marks = [];
302
+ const categoryPositions = [];
303
+ for (const [categoryIndex, category] of categories.entries()) {
304
+ const categoryStart = (orientation === 'horizontal' ? margin.top : margin.left) +
305
+ categoryIndex * categorySize +
306
+ categoryGap / 2;
307
+ let positiveOffset = 0;
308
+ let negativeOffset = 0;
309
+ categoryPositions.push({
310
+ category,
311
+ label: series
312
+ .flatMap(item => item.data)
313
+ .find(datum => datum.x === category)?.xLabel ?? category,
314
+ x: orientation === 'horizontal' ?
315
+ margin.left - 8 :
316
+ categoryStart + usableCategorySize / 2,
317
+ y: orientation === 'horizontal' ?
318
+ categoryStart + usableCategorySize / 2 :
319
+ height - 20
320
+ });
321
+ for (const [seriesIndex, item] of series.entries()) {
322
+ const value = getDatumValue(item, category);
323
+ const tone = resolveLumenChartTone(item.tone, seriesIndex);
324
+ if (orientation === 'horizontal') {
325
+ const startValue = getBarStartValue(layout, value, positiveOffset, negativeOffset);
326
+ const endValue = startValue + value;
327
+ const start = scaleLumenChartValue(startValue, domain, margin.left, margin.left + plotWidth);
328
+ const end = scaleLumenChartValue(endValue, domain, margin.left, margin.left + plotWidth);
329
+ marks.push({
330
+ category,
331
+ height: layout === 'stacked' ?
332
+ usableCategorySize :
333
+ Math.max(1, seriesSize - 2),
334
+ seriesId: item.id,
335
+ seriesLabel: item.label,
336
+ tone,
337
+ value,
338
+ width: Math.abs(end - start),
339
+ x: Math.min(start, end),
340
+ y: categoryStart +
341
+ (layout === 'stacked' ? 0 : seriesIndex * seriesSize + 1)
342
+ });
343
+ }
344
+ else {
345
+ const startValue = getBarStartValue(layout, value, positiveOffset, negativeOffset);
346
+ const endValue = startValue + value;
347
+ const start = scaleLumenChartValue(startValue, domain, margin.top + plotHeight, margin.top);
348
+ const end = scaleLumenChartValue(endValue, domain, margin.top + plotHeight, margin.top);
349
+ marks.push({
350
+ category,
351
+ height: Math.abs(end - start),
352
+ seriesId: item.id,
353
+ seriesLabel: item.label,
354
+ tone,
355
+ value,
356
+ width: layout === 'stacked' ?
357
+ usableCategorySize :
358
+ Math.max(1, seriesSize - 2),
359
+ x: categoryStart +
360
+ (layout === 'stacked' ? 0 : seriesIndex * seriesSize + 1),
361
+ y: Math.min(start, end)
362
+ });
363
+ }
364
+ if (layout === 'stacked') {
365
+ if (value >= 0)
366
+ positiveOffset += value;
367
+ else
368
+ negativeOffset += value;
369
+ }
370
+ }
371
+ }
372
+ return {
373
+ categories: categoryPositions,
374
+ domain,
375
+ height,
376
+ marks,
377
+ width
378
+ };
379
+ };
380
+ /* eslint-enable complexity */
@@ -1 +1 @@
1
- {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../src/code.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEhG,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,wBAAwB;;;;;;;CAOkB,CAAA;AA+NvD,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,WAc9C,CAAA;AAuKD,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,iBAAa,KAAG,cAAc,EA0B7E,CAAA;AAgBD,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,EAAE,iBAAa,WACW,CAAA"}
1
+ {"version":3,"file":"code.d.ts","sourceRoot":"","sources":["../src/code.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC5B,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEjE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,kBAAkB,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,eAAO,MAAM,wBAAwB;;;;;;;CAOkB,CAAA;AAiPvD,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,WAc9C,CAAA;AA0LD,eAAO,MAAM,iBAAiB,GAC5B,MAAM,MAAM,EACZ,iBAAa,KACZ,cAAc,EA0BhB,CAAA;AAeD,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,EAAE,iBAAa,WAA6E,CAAA"}
package/dist/code.js CHANGED
@@ -14,11 +14,7 @@ const bashLikeLanguages = new Set([
14
14
  'terminal',
15
15
  'zsh'
16
16
  ]);
17
- const jsonLikeLanguages = new Set([
18
- 'json',
19
- 'json5',
20
- 'jsonc'
21
- ]);
17
+ const jsonLikeLanguages = new Set(['json', 'json5', 'jsonc']);
22
18
  const javascriptLikeLanguages = new Set([
23
19
  'js',
24
20
  'jsx',
@@ -28,14 +24,8 @@ const javascriptLikeLanguages = new Set([
28
24
  'tsx',
29
25
  'typescript'
30
26
  ]);
31
- const luaLikeLanguages = new Set([
32
- 'lua'
33
- ]);
34
- const markdownLikeLanguages = new Set([
35
- 'md',
36
- 'markdown',
37
- 'mdx'
38
- ]);
27
+ const luaLikeLanguages = new Set(['lua']);
28
+ const markdownLikeLanguages = new Set(['md', 'markdown', 'mdx']);
39
29
  const markupLikeLanguages = new Set([
40
30
  'astro',
41
31
  'html',
@@ -44,13 +34,8 @@ const markupLikeLanguages = new Set([
44
34
  'vue',
45
35
  'xml'
46
36
  ]);
47
- const sqlLikeLanguages = new Set([
48
- 'sql'
49
- ]);
50
- const yamlLikeLanguages = new Set([
51
- 'yaml',
52
- 'yml'
53
- ]);
37
+ const sqlLikeLanguages = new Set(['sql']);
38
+ const yamlLikeLanguages = new Set(['yaml', 'yml']);
54
39
  const javascriptKeywords = new Set([
55
40
  'as',
56
41
  'async',
@@ -123,11 +108,7 @@ const bashKeywords = new Set([
123
108
  'unset',
124
109
  'while'
125
110
  ]);
126
- const jsonKeywords = new Set([
127
- 'false',
128
- 'null',
129
- 'true'
130
- ]);
111
+ const jsonKeywords = new Set(['false', 'null', 'true']);
131
112
  const luaKeywords = new Set([
132
113
  'and',
133
114
  'break',
@@ -192,14 +173,41 @@ const yamlKeywords = new Set([
192
173
  'true',
193
174
  'yes'
194
175
  ]);
195
- const bashTokenPattern = /#[^\n]*|"(?:\\.|[^"\\])*"|'[^']*'|\$\{?[A-Za-z_][\w]*\}?|--?[A-Za-z][\w-]*|\b[A-Za-z_][\w]*\b|\b\d+(?:\.\d+)?\b|[|&;<>()[\]{}=]+/g;
196
- const javascriptTokenPattern = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d[\d._]*\b|\b[A-Za-z_$][\w$]*\b|[=+\-*%<>!?:.,;()[\]{}|&/]+/g;
197
- const jsonTokenPattern = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|\b(?:false|null|true)\b|-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|[{},:]|\[|\]/gi;
198
- const luaTokenPattern = /--\[\[[\s\S]*?\]\]|--[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b[A-Za-z_][\w]*\b|\b\d+(?:\.\d+)?\b|[=+\-*%<>~#.,:;()[\]{}]+/g;
199
- const markdownTokenPattern = /```[\s\S]*?```|`[^`\n]+`|<!--[\s\S]*?-->|^#{1,6}(?=\s)|^>\s?|^\s*(?:[-+*]|\d+\.)\s+|\*\*|__|\*|_|\[[^\]]+\]\([^)]+\)/gm;
200
- const markupTokenPattern = /<!--[\s\S]*?-->|---|<\/?|\/?>|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b[A-Za-z_][\w:.-]*\b|[{}=]/g;
201
- const sqlTokenPattern = /--[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|\b[A-Za-z_][\w$]*\b|\b\d+(?:\.\d+)?\b|[=+\-*%<>!.,;()[\]]+/g;
202
- const yamlTokenPattern = /#[^\n]*|"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|\b[A-Za-z_][\w.-]*(?=\s*:)|\b(?:false|no|null|off|on|true|yes)\b|\b\d+(?:\.\d+)?\b|[{},:&*!?|>@`~-]+|\[|\]/gi;
176
+ const bashTokenPattern = new RegExp([
177
+ String.raw `#[^\n]*|"(?:\\.|[^"\\])*"|'[^']*'|\$\{?[A-Za-z_][\w]*\}?|`,
178
+ String.raw `--?[A-Za-z][\w-]*|\b[A-Za-z_][\w]*\b|\b\d+(?:\.\d+)?\b|[|&;<>()[\]{}=]+`
179
+ ].join(''), 'g');
180
+ const javascriptTokenPattern = new RegExp([
181
+ String.raw `\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`,
182
+ '`(?:\\\\.|[^`\\\\])*`|',
183
+ String.raw `\b\d[\d._]*\b|\b[A-Za-z_$][\w$]*\b|[=+\-*%<>!?:.,;()[\]{}|&/]+`
184
+ ].join(''), 'g');
185
+ const jsonTokenPattern = new RegExp([
186
+ String.raw `\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|`,
187
+ String.raw `\b(?:false|null|true)\b|-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b|[{},:]|\[|\]`
188
+ ].join(''), 'gi');
189
+ const luaTokenPattern = new RegExp([
190
+ String.raw `--\[\[[\s\S]*?\]\]|--[^\n]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`,
191
+ String.raw `\b[A-Za-z_][\w]*\b|\b\d+(?:\.\d+)?\b|[=+\-*%<>~#.,:;()[\]{}]+`
192
+ ].join(''), 'g');
193
+ const markdownTokenPattern = new RegExp([
194
+ '```[\\s\\S]*?```|`[^`\\n]+`|',
195
+ String.raw `<!--[\s\S]*?-->|^#{1,6}(?=\s)|^>\s?|^\s*(?:[-+*]|\d+\.)\s+|`,
196
+ String.raw `\*\*|__|\*|_|\[[^\]]+\]\([^)]+\)`
197
+ ].join(''), 'gm');
198
+ const markupTokenPattern = new RegExp([
199
+ String.raw `<!--[\s\S]*?-->|---|<\/?|\/?>|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`,
200
+ String.raw `\b[A-Za-z_][\w:.-]*\b|[{}=]`
201
+ ].join(''), 'g');
202
+ const sqlTokenPattern = new RegExp([
203
+ String.raw `--[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|`,
204
+ String.raw `\b[A-Za-z_][\w$]*\b|\b\d+(?:\.\d+)?\b|[=+\-*%<>!.,;()[\]]+`
205
+ ].join(''), 'g');
206
+ const yamlTokenPattern = new RegExp([
207
+ String.raw `#[^\n]*|"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|\b[A-Za-z_][\w.-]*(?=\s*:)|`,
208
+ String.raw `\b(?:false|no|null|off|on|true|yes)\b|\b\d+(?:\.\d+)?\b|`,
209
+ '[{},:&*!?|>@`~-]+|\\[|\\]'
210
+ ].join(''), 'gi');
203
211
  const leadingWhitespacePattern = /^[\t ]*/;
204
212
  const normalizeLanguage = (language = '') => language.trim().toLowerCase();
205
213
  const startsWithIdentifierPattern = /^[A-Za-z_$]/;
@@ -301,7 +309,10 @@ const getMarkupTokenKind = (value, source, start) => {
301
309
  const closingTagIndex = source.lastIndexOf('>', start);
302
310
  if (openingTagIndex < closingTagIndex)
303
311
  return undefined;
304
- const beforeValue = source.slice(openingTagIndex + 1, start).replace('/', '').trim();
312
+ const beforeValue = source
313
+ .slice(openingTagIndex + 1, start)
314
+ .replace('/', '')
315
+ .trim();
305
316
  return beforeValue === '' ? 'keyword' : 'type';
306
317
  };
307
318
  const getSqlTokenKind = (value) => {