@birdapi/velinstyle 0.9.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.de.md +15 -11
  2. package/README.md +15 -11
  3. package/cli/cli-manifest.json +1 -1
  4. package/cli/docgen/extract-a11y.js +4 -2
  5. package/cli/index.js +4 -2
  6. package/components/index.js +2 -0
  7. package/components/runtime/component-loaders.js +2 -0
  8. package/components/velin-data-table.js +361 -0
  9. package/components/velin-form-summary.js +348 -0
  10. package/core/a11y/component-contracts.json +18 -1
  11. package/core/highlight/languages/go.js +28 -0
  12. package/core/highlight/languages/python.js +29 -0
  13. package/core/highlight/languages/rust.js +32 -0
  14. package/core/highlight/languages/yaml.js +23 -0
  15. package/core/highlight/registry.js +13 -2
  16. package/core/meta/build.js +1 -1
  17. package/core/search/worker-client.js +2 -3
  18. package/dist/chunks/attributes-353XDOAX.js +391 -0
  19. package/dist/chunks/attributes-ADOKLKA7.js +391 -0
  20. package/dist/chunks/chunk-ERF5YVP4.js +253 -0
  21. package/dist/chunks/chunk-IEHKRARD.js +109 -0
  22. package/dist/chunks/chunk-QVHYL3R4.js +111 -0
  23. package/dist/chunks/go-HZ6XTFCK.js +31 -0
  24. package/dist/chunks/highlight-IBDX4XWS.js +37 -0
  25. package/dist/chunks/python-BMPKDKNO.js +32 -0
  26. package/dist/chunks/runtime-entry.js +1 -1
  27. package/dist/chunks/rust-2BWKI2MN.js +35 -0
  28. package/dist/chunks/velin-code-block-ITCLIC6T.js +132 -0
  29. package/dist/chunks/velin-data-table-KK7IDGTP.js +302 -0
  30. package/dist/chunks/velin-form-summary-XRQ7COQH.js +273 -0
  31. package/dist/chunks/velin-lightbox-NXA3U2FM.js +149 -0
  32. package/dist/chunks/velin-search-FDHEMCSO.js +557 -0
  33. package/dist/chunks/worker-client-MPT7PNQ4.js +47 -0
  34. package/dist/chunks/yaml-O67LEQYT.js +26 -0
  35. package/dist/llms.txt +2 -2
  36. package/dist/search-index.json +28 -2
  37. package/dist/velin-agent.json +31 -5
  38. package/dist/velinstyle-components.iife.js +745 -7
  39. package/dist/velinstyle-components.js +746 -5
  40. package/dist/velinstyle-components.min.js +85 -85
  41. package/dist/velinstyle.css +141 -19
  42. package/dist/velinstyle.d.ts +2 -0
  43. package/dist/velinstyle.min.css +1 -1
  44. package/package.json +8 -2
  45. package/src/base/wc-placeholder.css +7 -0
  46. package/src/components/data-table.css +93 -0
  47. package/src/components/form-validation.css +40 -0
  48. package/src/velinstyle.css +1 -0
  49. package/dist/llms.test.txt +0 -40
  50. package/dist/search-index.test.json +0 -1773
  51. package/dist/velin-agent.test.json +0 -684
@@ -0,0 +1,348 @@
1
+ import { announce } from './a11y-utils.js';
2
+
3
+ /**
4
+ * Accessible error summary for a light-DOM `<form>`.
5
+ *
6
+ * Native constraint validation shows a transient browser bubble on one field at
7
+ * a time, which is unusable with a screen reader or on a long form. This element
8
+ * takes over submit handling to build a persistent, focusable summary, wire
9
+ * `aria-invalid` and `aria-describedby` per field, and announce the error count.
10
+ *
11
+ * Covers WCAG 3.3.1 Error Identification, 3.3.3 Error Suggestion and the
12
+ * `aria-describedby` half of 4.1.2 Name, Role, Value.
13
+ */
14
+
15
+ const FIELD_SELECTOR = 'input, select, textarea';
16
+ const IGNORED_TYPES = new Set(['submit', 'reset', 'button', 'image', 'hidden']);
17
+
18
+ let fieldIdCounter = 0;
19
+
20
+ /** `CSS.escape` is missing in some non-browser DOM implementations. */
21
+ function escapeSelector(value) {
22
+ const text = String(value ?? '');
23
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(text);
24
+ return text.replace(/[^\w-]/g, (char) => `\\${char}`);
25
+ }
26
+
27
+ /** @param {HTMLElement} field */
28
+ function fieldLabel(field) {
29
+ const explicit = field.getAttribute('data-error-label');
30
+ if (explicit) return explicit;
31
+
32
+ const ariaLabel = field.getAttribute('aria-label');
33
+ if (ariaLabel?.trim()) return ariaLabel.trim();
34
+
35
+ const labelledBy = field.getAttribute('aria-labelledby');
36
+ if (labelledBy) {
37
+ const text = labelledBy
38
+ .split(/\s+/)
39
+ .map((id) => field.ownerDocument.getElementById(id)?.textContent?.trim() || '')
40
+ .filter(Boolean)
41
+ .join(' ');
42
+ if (text) return text;
43
+ }
44
+
45
+ if (field.id) {
46
+ const label = field.ownerDocument.querySelector(`label[for="${escapeSelector(field.id)}"]`);
47
+ if (label?.textContent.trim()) return label.textContent.trim();
48
+ }
49
+
50
+ const wrapping = field.closest('label');
51
+ if (wrapping?.textContent.trim()) return wrapping.textContent.trim();
52
+
53
+ return field.name || 'This field';
54
+ }
55
+
56
+ /** @param {HTMLElement} field */
57
+ function fieldMessage(field) {
58
+ return field.getAttribute('data-error-message')?.trim() || field.validationMessage || 'Invalid value';
59
+ }
60
+
61
+ /**
62
+ * @param {HTMLElement} field
63
+ * @param {string} id
64
+ */
65
+ function addDescribedBy(field, id) {
66
+ const ids = (field.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
67
+ if (!ids.includes(id)) {
68
+ ids.push(id);
69
+ field.setAttribute('aria-describedby', ids.join(' '));
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {HTMLElement} field
75
+ * @param {string} id
76
+ */
77
+ function removeDescribedBy(field, id) {
78
+ const ids = (field.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
79
+ const next = ids.filter((value) => value !== id);
80
+ if (next.length) field.setAttribute('aria-describedby', next.join(' '));
81
+ else field.removeAttribute('aria-describedby');
82
+ }
83
+
84
+ class VelinFormSummary extends HTMLElement {
85
+ static get observedAttributes() {
86
+ return ['for', 'heading'];
87
+ }
88
+
89
+ constructor() {
90
+ super();
91
+ this._form = null;
92
+ this._panel = null;
93
+ this._errors = [];
94
+ this._onSubmit = this._onSubmit.bind(this);
95
+ this._onFieldChange = this._onFieldChange.bind(this);
96
+ this._onReset = this._onReset.bind(this);
97
+ }
98
+
99
+ connectedCallback() {
100
+ this.classList.add('velin-form-summary');
101
+ requestAnimationFrame(() => this._bindForm());
102
+ }
103
+
104
+ disconnectedCallback() {
105
+ this._unbindForm();
106
+ }
107
+
108
+ attributeChangedCallback(name, previous, next) {
109
+ if (previous === next) return;
110
+ if (name === 'for' && this.isConnected) {
111
+ this._unbindForm();
112
+ this._bindForm();
113
+ } else if (name === 'heading' && this._panel) {
114
+ const heading = this._panel.querySelector('.velin-form-summary__heading');
115
+ if (heading) heading.textContent = this.headingText;
116
+ }
117
+ }
118
+
119
+ // ── Public API ─────────────────────────────────────────────────────────────
120
+
121
+ get form() {
122
+ return this._form;
123
+ }
124
+
125
+ /** @returns {{ field: HTMLElement, label: string, message: string }[]} */
126
+ get errors() {
127
+ return this._errors.slice();
128
+ }
129
+
130
+ get headingText() {
131
+ return this.getAttribute('heading') || 'There is a problem';
132
+ }
133
+
134
+ /** Validate the form and render the summary. @returns {boolean} valid */
135
+ validate() {
136
+ if (!this._form) return true;
137
+ const errors = [];
138
+ for (const field of this._fields()) {
139
+ if (field.checkValidity()) {
140
+ this._clearFieldError(field);
141
+ continue;
142
+ }
143
+ const error = { field, label: fieldLabel(field), message: fieldMessage(field) };
144
+ this._markFieldError(field, error.message);
145
+ errors.push(error);
146
+ }
147
+ this._errors = errors;
148
+ this._render();
149
+ return errors.length === 0;
150
+ }
151
+
152
+ /** Remove the summary and all field error state. */
153
+ clear() {
154
+ for (const field of this._fields()) this._clearFieldError(field);
155
+ this._errors = [];
156
+ this._render();
157
+ }
158
+
159
+ /** Move focus to the first field with an error. */
160
+ focusFirstError() {
161
+ const first = this._errors[0];
162
+ if (first) this._focusField(first.field);
163
+ }
164
+
165
+ // ── Form wiring ────────────────────────────────────────────────────────────
166
+
167
+ _bindForm() {
168
+ const id = this.getAttribute('for');
169
+ this._form = id ? this.ownerDocument.getElementById(id) : this.closest('form');
170
+ if (!this._form) return;
171
+
172
+ // Native bubbles show one error at a time and vanish; the summary replaces them.
173
+ if (!this.hasAttribute('native-validation')) this._form.noValidate = true;
174
+
175
+ this._form.addEventListener('submit', this._onSubmit);
176
+ this._form.addEventListener('reset', this._onReset);
177
+ this._form.addEventListener('input', this._onFieldChange);
178
+ this._form.addEventListener('change', this._onFieldChange);
179
+ }
180
+
181
+ _unbindForm() {
182
+ if (!this._form) return;
183
+ this._form.removeEventListener('submit', this._onSubmit);
184
+ this._form.removeEventListener('reset', this._onReset);
185
+ this._form.removeEventListener('input', this._onFieldChange);
186
+ this._form.removeEventListener('change', this._onFieldChange);
187
+ this._form = null;
188
+ }
189
+
190
+ /** @returns {HTMLElement[]} */
191
+ _fields() {
192
+ if (!this._form) return [];
193
+ const seenRadioNames = new Set();
194
+ return [...this._form.querySelectorAll(FIELD_SELECTOR)].filter((field) => {
195
+ if (IGNORED_TYPES.has(field.type)) return false;
196
+ if (field.disabled || field.hasAttribute('data-error-ignore')) return false;
197
+ if (typeof field.checkValidity !== 'function') return false;
198
+ // One entry per radio group so the summary lists the group once.
199
+ if (field.type === 'radio' && field.name) {
200
+ if (seenRadioNames.has(field.name)) return false;
201
+ seenRadioNames.add(field.name);
202
+ }
203
+ return true;
204
+ });
205
+ }
206
+
207
+ _onSubmit(event) {
208
+ if (this.validate()) return;
209
+ event.preventDefault();
210
+ this._announceErrors();
211
+ this._focusPanel();
212
+ this.dispatchEvent(new CustomEvent('velin-form-invalid', {
213
+ bubbles: true,
214
+ detail: { errors: this.errors.map(({ label, message }) => ({ label, message })) },
215
+ }));
216
+ }
217
+
218
+ _onReset() {
219
+ requestAnimationFrame(() => this.clear());
220
+ }
221
+
222
+ /** Re-validate a single field once it already had an error, never before. */
223
+ _onFieldChange(event) {
224
+ const field = event.target;
225
+ if (!field || !this._errors.some((error) => error.field === field)) return;
226
+ if (!field.checkValidity()) return;
227
+
228
+ this._clearFieldError(field);
229
+ this._errors = this._errors.filter((error) => error.field !== field);
230
+ this._render();
231
+ if (this._errors.length === 0) {
232
+ this.dispatchEvent(new CustomEvent('velin-form-valid', { bubbles: true }));
233
+ }
234
+ }
235
+
236
+ // ── Field state ────────────────────────────────────────────────────────────
237
+
238
+ /** @param {HTMLElement} field */
239
+ _errorId(field) {
240
+ if (!field.id) field.id = `velin-field-${++fieldIdCounter}`;
241
+ return `${field.id}-error`;
242
+ }
243
+
244
+ /**
245
+ * @param {HTMLElement} field
246
+ * @param {string} message
247
+ */
248
+ _markFieldError(field, message) {
249
+ const errorId = this._errorId(field);
250
+ field.setAttribute('aria-invalid', 'true');
251
+
252
+ let holder = this.ownerDocument.getElementById(errorId);
253
+ if (!holder) {
254
+ holder = this._form.querySelector(`[data-velin-error-for="${escapeSelector(field.name || field.id)}"]`);
255
+ }
256
+ if (!holder) {
257
+ holder = this.ownerDocument.createElement('p');
258
+ holder.dataset.velinErrorGenerated = 'true';
259
+ field.insertAdjacentElement('afterend', holder);
260
+ }
261
+ holder.id = errorId;
262
+ holder.classList.add('velin-field-error');
263
+ holder.textContent = message;
264
+ addDescribedBy(field, errorId);
265
+ }
266
+
267
+ /** @param {HTMLElement} field */
268
+ _clearFieldError(field) {
269
+ if (!field.id) return;
270
+ const errorId = `${field.id}-error`;
271
+ field.removeAttribute('aria-invalid');
272
+ removeDescribedBy(field, errorId);
273
+
274
+ const holder = this.ownerDocument.getElementById(errorId);
275
+ if (!holder) return;
276
+ if (holder.dataset.velinErrorGenerated) holder.remove();
277
+ else holder.textContent = '';
278
+ }
279
+
280
+ /** @param {HTMLElement} field */
281
+ _focusField(field) {
282
+ const target = field.type === 'radio' && field.name
283
+ ? this._form.querySelector(`input[type="radio"][name="${escapeSelector(field.name)}"]`) || field
284
+ : field;
285
+ target.focus();
286
+ this.dispatchEvent(new CustomEvent('velin-form-error-focus', {
287
+ bubbles: true,
288
+ detail: { name: target.name || target.id },
289
+ }));
290
+ }
291
+
292
+ // ── Summary panel ──────────────────────────────────────────────────────────
293
+
294
+ _render() {
295
+ if (!this._errors.length) {
296
+ this._panel?.remove();
297
+ this._panel = null;
298
+ this.hidden = true;
299
+ return;
300
+ }
301
+
302
+ this.hidden = false;
303
+ if (!this._panel?.isConnected) {
304
+ const panel = this.ownerDocument.createElement('div');
305
+ panel.className = 'velin-form-summary__panel velin-alert velin-alert--danger';
306
+ panel.setAttribute('role', 'alert');
307
+ panel.tabIndex = -1;
308
+
309
+ const heading = this.ownerDocument.createElement('p');
310
+ heading.className = 'velin-form-summary__heading';
311
+ heading.textContent = this.headingText;
312
+
313
+ const list = this.ownerDocument.createElement('ul');
314
+ list.className = 'velin-form-summary__list';
315
+
316
+ panel.append(heading, list);
317
+ this.appendChild(panel);
318
+ this._panel = panel;
319
+ }
320
+
321
+ const list = this._panel.querySelector('.velin-form-summary__list');
322
+ list.textContent = '';
323
+ for (const error of this._errors) {
324
+ const item = this.ownerDocument.createElement('li');
325
+ const link = this.ownerDocument.createElement('a');
326
+ link.href = `#${this._errorId(error.field).replace(/-error$/, '')}`;
327
+ link.textContent = `${error.label}: ${error.message}`;
328
+ link.addEventListener('click', (event) => {
329
+ event.preventDefault();
330
+ this._focusField(error.field);
331
+ });
332
+ item.appendChild(link);
333
+ list.appendChild(item);
334
+ }
335
+ }
336
+
337
+ _focusPanel() {
338
+ this._panel?.focus();
339
+ }
340
+
341
+ _announceErrors() {
342
+ const count = this._errors.length;
343
+ announce(count === 1 ? '1 field needs attention' : `${count} fields need attention`, 'assertive');
344
+ }
345
+ }
346
+
347
+ customElements.define('velin-form-summary', VelinFormSummary);
348
+ export default VelinFormSummary;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wcagLevel": "AAA",
3
- "version": "0.9.0",
3
+ "version": "1.1.0",
4
4
  "components": {
5
5
  "velin-accordion": {
6
6
  "status": "pass",
@@ -74,6 +74,15 @@
74
74
  "keyboard": "N/A",
75
75
  "reducedMotion": true
76
76
  },
77
+ "velin-data-table": {
78
+ "status": "pass",
79
+ "roles": ["button"],
80
+ "keyboard": "Native buttons for column sort and pagination",
81
+ "liveRegion": "polite",
82
+ "reducedMotion": true,
83
+ "requiredAttributes": [],
84
+ "notes": "Enhances a light-DOM table. Sort state exposed via aria-sort; sort triggers are real buttons. Needs a caption, aria-label, or label attribute (warns otherwise). Filtered and off-page rows use hidden so they leave the a11y tree; sort, filter and page changes are announced."
85
+ },
77
86
  "velin-dialog": {
78
87
  "status": "pass",
79
88
  "roles": ["dialog"],
@@ -97,6 +106,14 @@
97
106
  "roles": ["button"],
98
107
  "requiredAttributes": ["aria-label"]
99
108
  },
109
+ "velin-form-summary": {
110
+ "status": "pass",
111
+ "roles": ["alert", "link"],
112
+ "keyboard": "Summary links move focus to the offending field",
113
+ "liveRegion": "assertive",
114
+ "requiredAttributes": [],
115
+ "notes": "Disables native validation bubbles and renders a focusable role=alert summary. Sets aria-invalid and wires aria-describedby to per-field messages (WCAG 3.3.1, 3.3.3, 4.1.2). Field errors clear as soon as the field becomes valid."
116
+ },
100
117
  "velin-icon": {
101
118
  "status": "pass",
102
119
  "roles": ["img", "presentation"],
@@ -0,0 +1,28 @@
1
+ /** @import { LexerFn } from '../types.js' */
2
+ import { tokenize } from './_utils.js';
3
+
4
+ const RULES = [
5
+ { type: 'comment', re: /\/\/[^\n]*/y },
6
+ { type: 'comment', re: /\/\*[\s\S]*?\*\//y },
7
+ // Raw strings may span lines.
8
+ { type: 'string', re: /`[^`]*`/y },
9
+ { type: 'string', re: /"(?:\\.|[^"\\\n])*"/y },
10
+ { type: 'string', re: /'(?:\\.|[^'\\\n])*'/y },
11
+ { type: 'number', re: /\b0[xX][\da-fA-F_]+\b|\b\d[\d_]*(?:\.[\d_]*)?(?:[eE][+-]?\d+)?i?\b/y },
12
+ {
13
+ type: 'keyword',
14
+ re: /\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/y,
15
+ },
16
+ {
17
+ type: 'builtin',
18
+ re: /\b(?:append|bool|byte|cap|clear|close|complex|complex64|complex128|copy|delete|error|float32|float64|imag|int|int8|int16|int32|int64|len|make|max|min|new|nil|panic|print|println|real|recover|rune|string|true|false|iota|uint|uint8|uint16|uint32|uint64|uintptr|any)\b/y,
19
+ },
20
+ { type: 'operator', re: /:=|\.\.\.|&&|\|\||<-|\+\+|--|<<=?|>>=?|&\^=?|[-+*/%&|^<>!=]=?|~/y },
21
+ { type: 'punctuation', re: /[[\]{}(),;:.]/y },
22
+ { type: 'identifier', re: /\b[A-Za-z_]\w*\b/y },
23
+ ];
24
+
25
+ /** @type {LexerFn} */
26
+ export default function lexGo(code) {
27
+ return tokenize(code, RULES);
28
+ }
@@ -0,0 +1,29 @@
1
+ /** @import { LexerFn } from '../types.js' */
2
+ import { tokenize } from './_utils.js';
3
+
4
+ const RULES = [
5
+ { type: 'comment', re: /#[^\n]*/y },
6
+ // Triple-quoted strings first so the single-quote rules cannot split them.
7
+ { type: 'string', re: /[rbfu]{0,2}"""[\s\S]*?"""/y },
8
+ { type: 'string', re: /[rbfu]{0,2}'''[\s\S]*?'''/y },
9
+ { type: 'string', re: /[rbfu]{0,2}"(?:\\.|[^"\\\n])*"/y },
10
+ { type: 'string', re: /[rbfu]{0,2}'(?:\\.|[^'\\\n])*'/y },
11
+ { type: 'number', re: /\b0[xX][\da-fA-F_]+\b|\b\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?j?\b/y },
12
+ {
13
+ type: 'keyword',
14
+ re: /\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b/y,
15
+ },
16
+ {
17
+ type: 'builtin',
18
+ re: /\b(?:None|True|False|self|cls|abs|all|any|bool|bytes|dict|dir|enumerate|filter|float|format|frozenset|getattr|hasattr|int|isinstance|issubclass|iter|len|list|map|max|min|next|object|open|print|range|repr|reversed|round|set|setattr|sorted|str|sum|super|tuple|type|zip)\b/y,
19
+ },
20
+ // Multi-character operators first so `->` is not split into `-` and `>`.
21
+ { type: 'operator', re: /->|:=|\*\*=?|\/\/=?|<<=?|>>=?|[-+*/%&|^~<>!=]=?/y },
22
+ { type: 'punctuation', re: /[[\]{}(),:;.@]/y },
23
+ { type: 'identifier', re: /\b[A-Za-z_]\w*\b/y },
24
+ ];
25
+
26
+ /** @type {LexerFn} */
27
+ export default function lexPython(code) {
28
+ return tokenize(code, RULES);
29
+ }
@@ -0,0 +1,32 @@
1
+ /** @import { LexerFn } from '../types.js' */
2
+ import { tokenize } from './_utils.js';
3
+
4
+ const RULES = [
5
+ { type: 'comment', re: /\/\/\/?[^\n]*/y },
6
+ { type: 'comment', re: /\/\*[\s\S]*?\*\//y },
7
+ // Raw strings close on the same number of hashes they opened with.
8
+ { type: 'string', re: /b?r(#*)"[\s\S]*?"\1/y },
9
+ { type: 'string', re: /b?"(?:\\.|[^"\\])*"/y },
10
+ { type: 'string', re: /b?'(?:\\.|[^'\\])'/y },
11
+ // Attributes such as #[derive(Debug)] read as annotations.
12
+ { type: 'builtin', re: /#!?\[[^\]]*\]/y },
13
+ { type: 'number', re: /\b0[xXbo][\da-fA-F_]+\b|\b\d[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?(?:[iuf](?:8|16|32|64|128|size))?\b/y },
14
+ {
15
+ type: 'keyword',
16
+ re: /\b(?:as|async|await|break|const|continue|crate|dyn|else|enum|extern|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|type|unsafe|use|where|while)\b/y,
17
+ },
18
+ {
19
+ type: 'builtin',
20
+ re: /\b(?:bool|char|f32|f64|i8|i16|i32|i64|i128|isize|str|u8|u16|u32|u64|u128|usize|String|Vec|Option|Result|Some|None|Ok|Err|Box|Rc|Arc|true|false)\b/y,
21
+ },
22
+ // Lifetimes such as 'a must not be read as an unterminated char literal.
23
+ { type: 'operator', re: /'[a-z_]\w*\b/y },
24
+ { type: 'operator', re: /=>|->|::|\.\.=?|&&|\|\||<<=?|>>=?|[-+*/%&|^<>!=]=?|[?@]/y },
25
+ { type: 'punctuation', re: /[[\]{}(),;:.#]/y },
26
+ { type: 'identifier', re: /\b[A-Za-z_]\w*!?\b/y },
27
+ ];
28
+
29
+ /** @type {LexerFn} */
30
+ export default function lexRust(code) {
31
+ return tokenize(code, RULES);
32
+ }
@@ -0,0 +1,23 @@
1
+ /** @import { LexerFn } from '../types.js' */
2
+ import { tokenize } from './_utils.js';
3
+
4
+ const RULES = [
5
+ { type: 'comment', re: /#[^\n]*/y },
6
+ // Document markers and block scalar indicators.
7
+ { type: 'punctuation', re: /^(?:---|\.\.\.)$/my },
8
+ { type: 'string', re: /"(?:\\.|[^"\\])*"/y },
9
+ { type: 'string', re: /'(?:''|[^'])*'/y },
10
+ // Keys are the primary structure in YAML, so they get the attr-name colour.
11
+ { type: 'attr-name', re: /^[ \t]*-?[ \t]*[\w.$-]+(?=[ \t]*:(?:[ \t]|$))/my },
12
+ { type: 'punctuation', re: /^[ \t]*-(?=[ \t]|$)/my },
13
+ { type: 'keyword', re: /\b(?:true|false|null|yes|no|on|off|~)\b/yi },
14
+ { type: 'number', re: /\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/y },
15
+ // Anchors, aliases, tags and block scalar headers.
16
+ { type: 'operator', re: /[&*]\w+|![\w/!-]*|[|>][+-]?\d*(?=\s*$)/my },
17
+ { type: 'punctuation', re: /[:,[\]{}]/y },
18
+ ];
19
+
20
+ /** @type {LexerFn} */
21
+ export default function lexYaml(code) {
22
+ return tokenize(code, RULES);
23
+ }
@@ -28,6 +28,14 @@ const LAZY_LOADERS = {
28
28
  console: () => import('./languages/plain.js'),
29
29
  php: () => import('./languages/php.js'),
30
30
  blade: () => import('./languages/blade.js'),
31
+ python: () => import('./languages/python.js'),
32
+ py: () => import('./languages/python.js'),
33
+ yaml: () => import('./languages/yaml.js'),
34
+ yml: () => import('./languages/yaml.js'),
35
+ go: () => import('./languages/go.js'),
36
+ golang: () => import('./languages/go.js'),
37
+ rust: () => import('./languages/rust.js'),
38
+ rs: () => import('./languages/rust.js'),
31
39
  };
32
40
 
33
41
  /** @param {string} name */
@@ -49,11 +57,14 @@ export function normalizeLanguage(name) {
49
57
  md: 'markdown',
50
58
  plaintext: 'text',
51
59
  'plain-text': 'text',
52
- yml: 'text',
53
- yaml: 'text',
60
+ yml: 'yaml',
54
61
  toml: 'text',
55
62
  ini: 'text',
56
63
  php8: 'php',
64
+ py: 'python',
65
+ python3: 'python',
66
+ golang: 'go',
67
+ rs: 'rust',
57
68
  };
58
69
  return map[n] || n;
59
70
  }
@@ -85,7 +85,7 @@ export function buildPageMeta(html, sourcePath = '', pkgRoot = '.') {
85
85
  : 'page';
86
86
 
87
87
  return {
88
- version: readPackageMeta(pkgRoot).version || '0.9.0',
88
+ version: readPackageMeta(pkgRoot).version || '0.0.0',
89
89
  mime: VELIN_META_MIME,
90
90
  page: {
91
91
  intent,
@@ -5,10 +5,9 @@ let _nextId = 0;
5
5
  */
6
6
  export function createSearchWorker(workerUrl) {
7
7
  if (typeof Worker === 'undefined') return null;
8
+ if (!workerUrl) return null;
8
9
 
9
- const url =
10
- workerUrl ||
11
- new URL('./worker.js', import.meta.url).href;
10
+ const url = workerUrl;
12
11
 
13
12
  let worker;
14
13
  try {