utopia-project 0.37.6 → 0.38.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 (104) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/documentation-guidelines.md +12 -7
  4. data/lib/utopia/project/import_map.rb +1 -0
  5. data/lib/utopia/project/version.rb +1 -1
  6. data/pages/_page.xnode +8 -13
  7. data/public/_components/@socketry/syntax/Syntax/CodeElement.js +293 -0
  8. data/public/_components/@socketry/syntax/Syntax/Errors.js +52 -0
  9. data/public/_components/@socketry/syntax/Syntax/Language/apache.js +49 -0
  10. data/public/_components/@socketry/syntax/Syntax/Language/applescript.js +157 -0
  11. data/public/_components/@socketry/syntax/Syntax/Language/assembly.js +42 -0
  12. data/public/_components/@socketry/syntax/Syntax/Language/bash-script.js +108 -0
  13. data/public/_components/@socketry/syntax/Syntax/Language/bash.js +32 -0
  14. data/public/_components/@socketry/syntax/Syntax/Language/basic.js +232 -0
  15. data/public/_components/@socketry/syntax/Syntax/Language/c++.js +1 -0
  16. data/public/_components/@socketry/syntax/Syntax/Language/c.js +1 -0
  17. data/public/_components/@socketry/syntax/Syntax/Language/clang.js +201 -0
  18. data/public/_components/@socketry/syntax/Syntax/Language/cpp.js +1 -0
  19. data/public/_components/@socketry/syntax/Syntax/Language/csharp.js +166 -0
  20. data/public/_components/@socketry/syntax/Syntax/Language/css.js +244 -0
  21. data/public/_components/@socketry/syntax/Syntax/Language/diff.js +24 -0
  22. data/public/_components/@socketry/syntax/Syntax/Language/go.js +135 -0
  23. data/public/_components/@socketry/syntax/Syntax/Language/haskell.js +110 -0
  24. data/public/_components/@socketry/syntax/Syntax/Language/html.js +69 -0
  25. data/public/_components/@socketry/syntax/Syntax/Language/io.js +68 -0
  26. data/public/_components/@socketry/syntax/Syntax/Language/java.js +134 -0
  27. data/public/_components/@socketry/syntax/Syntax/Language/javascript.js +89 -0
  28. data/public/_components/@socketry/syntax/Syntax/Language/json.js +36 -0
  29. data/public/_components/@socketry/syntax/Syntax/Language/lisp.js +38 -0
  30. data/public/_components/@socketry/syntax/Syntax/Language/lua.js +87 -0
  31. data/public/_components/@socketry/syntax/Syntax/Language/markdown.js +112 -0
  32. data/public/_components/@socketry/syntax/Syntax/Language/nginx.js +37 -0
  33. data/public/_components/@socketry/syntax/Syntax/Language/objective-c.js +1 -0
  34. data/public/_components/@socketry/syntax/Syntax/Language/ocaml.js +225 -0
  35. data/public/_components/@socketry/syntax/Syntax/Language/pascal.js +166 -0
  36. data/public/_components/@socketry/syntax/Syntax/Language/patch.js +2 -0
  37. data/public/_components/@socketry/syntax/Syntax/Language/perl5.js +317 -0
  38. data/public/_components/@socketry/syntax/Syntax/Language/php-script.js +112 -0
  39. data/public/_components/@socketry/syntax/Syntax/Language/php.js +18 -0
  40. data/public/_components/@socketry/syntax/Syntax/Language/plain.js +20 -0
  41. data/public/_components/@socketry/syntax/Syntax/Language/protobuf.js +77 -0
  42. data/public/_components/@socketry/syntax/Syntax/Language/python.js +208 -0
  43. data/public/_components/@socketry/syntax/Syntax/Language/ruby.js +124 -0
  44. data/public/_components/@socketry/syntax/Syntax/Language/scala.js +81 -0
  45. data/public/_components/@socketry/syntax/Syntax/Language/smalltalk.js +30 -0
  46. data/public/_components/@socketry/syntax/Syntax/Language/sql.js +865 -0
  47. data/public/_components/@socketry/syntax/Syntax/Language/super-collider.js +70 -0
  48. data/public/_components/@socketry/syntax/Syntax/Language/swift.js +176 -0
  49. data/public/_components/@socketry/syntax/Syntax/Language/xml.js +76 -0
  50. data/public/_components/@socketry/syntax/Syntax/Language/xrb.js +33 -0
  51. data/public/_components/@socketry/syntax/Syntax/Language/yaml.js +29 -0
  52. data/public/_components/@socketry/syntax/Syntax/Language.js +276 -0
  53. data/public/_components/@socketry/syntax/Syntax/Loader.js +78 -0
  54. data/public/_components/@socketry/syntax/Syntax/Match.js +546 -0
  55. data/public/_components/@socketry/syntax/Syntax/Rule.js +306 -0
  56. data/public/_components/@socketry/syntax/Syntax.js +356 -0
  57. data/public/_components/@socketry/syntax/license.md +21 -0
  58. data/public/_components/@socketry/syntax/package.json +43 -0
  59. data/public/_components/@socketry/syntax/readme.md +162 -0
  60. data/public/_components/@socketry/syntax/themes/base/apache.css +1 -0
  61. data/public/_components/@socketry/syntax/themes/base/applescript.css +1 -0
  62. data/public/_components/@socketry/syntax/themes/base/assembly.css +1 -0
  63. data/public/_components/@socketry/syntax/themes/base/bash.css +1 -0
  64. data/public/_components/@socketry/syntax/themes/base/basic.css +1 -0
  65. data/public/_components/@socketry/syntax/themes/base/c.css +1 -0
  66. data/public/_components/@socketry/syntax/themes/base/clang.css +0 -0
  67. data/public/_components/@socketry/syntax/themes/base/csharp.css +1 -0
  68. data/public/_components/@socketry/syntax/themes/base/css.css +22 -0
  69. data/public/_components/@socketry/syntax/themes/base/diff.css +48 -0
  70. data/public/_components/@socketry/syntax/themes/base/go.css +1 -0
  71. data/public/_components/@socketry/syntax/themes/base/haskell.css +1 -0
  72. data/public/_components/@socketry/syntax/themes/base/html.css +1 -0
  73. data/public/_components/@socketry/syntax/themes/base/io.css +1 -0
  74. data/public/_components/@socketry/syntax/themes/base/java.css +1 -0
  75. data/public/_components/@socketry/syntax/themes/base/javascript.css +1 -0
  76. data/public/_components/@socketry/syntax/themes/base/json.css +41 -0
  77. data/public/_components/@socketry/syntax/themes/base/lisp.css +1 -0
  78. data/public/_components/@socketry/syntax/themes/base/lua.css +1 -0
  79. data/public/_components/@socketry/syntax/themes/base/markdown.css +16 -0
  80. data/public/_components/@socketry/syntax/themes/base/nginx.css +1 -0
  81. data/public/_components/@socketry/syntax/themes/base/ocaml.css +1 -0
  82. data/public/_components/@socketry/syntax/themes/base/pascal.css +1 -0
  83. data/public/_components/@socketry/syntax/themes/base/perl5.css +1 -0
  84. data/public/_components/@socketry/syntax/themes/base/php-script.css +1 -0
  85. data/public/_components/@socketry/syntax/themes/base/php.css +1 -0
  86. data/public/_components/@socketry/syntax/themes/base/plain.css +1 -0
  87. data/public/_components/@socketry/syntax/themes/base/protobuf.css +1 -0
  88. data/public/_components/@socketry/syntax/themes/base/python.css +1 -0
  89. data/public/_components/@socketry/syntax/themes/base/ruby.css +23 -0
  90. data/public/_components/@socketry/syntax/themes/base/scala.css +3 -0
  91. data/public/_components/@socketry/syntax/themes/base/smalltalk.css +1 -0
  92. data/public/_components/@socketry/syntax/themes/base/sql.css +1 -0
  93. data/public/_components/@socketry/syntax/themes/base/super-collider.css +33 -0
  94. data/public/_components/@socketry/syntax/themes/base/swift.css +1 -0
  95. data/public/_components/@socketry/syntax/themes/base/syntax.css +63 -0
  96. data/public/_components/@socketry/syntax/themes/base/xml.css +1 -0
  97. data/public/_components/@socketry/syntax/themes/base/xrb.css +29 -0
  98. data/public/_components/@socketry/syntax/themes/base/yaml.css +1 -0
  99. data/public/_components/@socketry/syntax/themes/theming.md +233 -0
  100. data/public/_static/sidebar.js +55 -22
  101. data/public/_static/site.css +0 -4
  102. data.tar.gz.sig +0 -0
  103. metadata +94 -1
  104. metadata.gz.sig +0 -0
@@ -0,0 +1,306 @@
1
+ import {Match} from './Match.js';
2
+ import {Language} from './Language.js';
3
+
4
+ export class Rule {
5
+ /**
6
+ * Convert string to token pattern with word boundaries.
7
+ */
8
+ static convertStringToTokenPattern(pattern, escape) {
9
+ let prefix = '\\b',
10
+ postfix = '\\b';
11
+
12
+ if (!pattern.match(/^\w/)) {
13
+ if (!pattern.match(/\w$/)) {
14
+ prefix = postfix = '';
15
+ } else {
16
+ prefix = '\\B';
17
+ }
18
+ } else {
19
+ if (!pattern.match(/\w$/)) {
20
+ postfix = '\\B';
21
+ }
22
+ }
23
+
24
+ if (escape) {
25
+ pattern = pattern.replace(/[\-\[\]{}()*+?.\\\^$|,#\s]/g, '\\$&');
26
+ }
27
+
28
+ return prefix + pattern + postfix;
29
+ }
30
+
31
+ /**
32
+ * Normalize a rule by converting string patterns to RegExp and setting defaults.
33
+ */
34
+ static normalizeRule(rule, owner) {
35
+ const normalized = {...rule, owner};
36
+
37
+ if (typeof normalized.pattern === 'string') {
38
+ normalized.string = normalized.pattern;
39
+ normalized.pattern = new RegExp(
40
+ Rule.convertStringToTokenPattern(normalized.string, true),
41
+ normalized.options
42
+ );
43
+ }
44
+
45
+ // Default pattern extraction algorithm
46
+ normalized.apply = normalized.apply || Rule.matchPattern;
47
+
48
+ return normalized;
49
+ }
50
+
51
+ /**
52
+ * Match a pattern against text and return matches.
53
+ */
54
+ static async matchPattern(syntax, rule, text) {
55
+ if (!rule.pattern) {
56
+ return [];
57
+ }
58
+
59
+ const matches = [];
60
+
61
+ // Duplicate the pattern and ensure 'g' flag is set:
62
+ let pattern = rule.pattern;
63
+ if (!pattern.global) {
64
+ pattern = new RegExp(pattern.source, pattern.flags + 'g');
65
+ }
66
+
67
+ let match;
68
+ while ((match = pattern.exec(text)) !== null) {
69
+ if (rule.matches) {
70
+ // Try new signature first (syntax, match, rule), fall back to old signature (match, rule)
71
+ // This supports both extractMatches (new) and custom matches functions (old)
72
+ const result =
73
+ rule.matches.length >= 3
74
+ ? rule.matches(syntax, match, rule)
75
+ : rule.matches(match, rule);
76
+ // Handle both sync and async matches functions
77
+ matches.push(...(result instanceof Promise ? await result : result));
78
+ } else if (rule.language) {
79
+ // Use the owning language's syntax to build sub-tree for embedded language
80
+ matches.push(
81
+ await Language.buildTree(syntax, rule, match[0], match.index, undefined)
82
+ );
83
+ } else {
84
+ matches.push(new Match(match.index, match[0].length, rule, match[0]));
85
+ }
86
+ if (rule.incremental) {
87
+ // Don't start scanning from the end of the match
88
+ pattern.lastIndex = match.index + 1;
89
+ }
90
+ }
91
+ return matches;
92
+ }
93
+
94
+ /**
95
+ * Create a matches extractor based on capture groups.
96
+ * Each provided rule corresponds to a capture group (default i+1) in the RegExp match.
97
+ * If a rule has {language: 'name'}, a subtree will be built using that language.
98
+ * If a rule has other expression fields (e.g. type), a Match will be created.
99
+ */
100
+ static extractMatches(...rules) {
101
+ return async function (syntax, match, expression) {
102
+ const results = [];
103
+ for (let i = 0; i < rules.length; i += 1) {
104
+ const rule = rules[i];
105
+ // Skip null/undefined rules:
106
+ if (rule == null) continue;
107
+
108
+ // Determine the capture group index to use:
109
+ let index = typeof rule.index !== 'undefined' ? rule.index : i + 1;
110
+ const value = match[index];
111
+
112
+ // Skip empty captures:
113
+ if (!value || value.length === 0) continue;
114
+
115
+ // Compute absolute offset of this capture group within the full match:
116
+ const offset = match.index + match[0].indexOf(value);
117
+
118
+ // Create either a subtree or a Match based on the rule:
119
+ if (rule.language) {
120
+ results.push(
121
+ await Language.buildTree(syntax, rule, value, offset, undefined)
122
+ );
123
+ } else {
124
+ const nestedExpression = {owner: expression?.owner, ...rule};
125
+ results.push(new Match(offset, value.length, nestedExpression, value));
126
+ }
127
+ }
128
+
129
+ return results;
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Create a conditional matcher that selects a rule based on another capture group.
135
+ * Tests a condition capture group against patterns to determine which rule to apply
136
+ * to a content capture group.
137
+ *
138
+ * @param {number} conditionIndex - Capture group index to test against patterns
139
+ * @param {number} contentIndex - Capture group index containing content to match
140
+ * @param {Array<{pattern?: RegExp, ...rule}>} conditions - Array of condition objects. Each can have:
141
+ * - pattern: RegExp to test against the condition group (optional - if omitted, acts as fallback)
142
+ * - Any rule properties (language, type, etc.) to apply when pattern matches
143
+ * @returns {Function} A matches function for use in language rules
144
+ *
145
+ * @example
146
+ * // Script tags with type-based language selection
147
+ * language.push({
148
+ * pattern: /<script(\s+[^>]*?)?>((.|\n)*?)<\/script>/im,
149
+ * matches: Rule.extractConditionalMatch(1, 2, [
150
+ * {pattern: /type\s*=\s*["']importmap["']/i, language: 'json'},
151
+ * {pattern: /type\s*=\s*["'](?:text|application)\/javascript["']/i, language: 'javascript'},
152
+ * {language: 'javascript'} // Fallback for no type or unknown types
153
+ * ])
154
+ * });
155
+ *
156
+ * @example
157
+ * // Code fence with language specifier
158
+ * language.push({
159
+ * pattern: /```(\w+)?\n([\s\S]*?)```/,
160
+ * matches: Rule.extractConditionalMatch(1, 2, [
161
+ * {pattern: /^javascript$/i, language: 'javascript'},
162
+ * {pattern: /^python$/i, language: 'python'},
163
+ * {language: 'plaintext'} // Fallback
164
+ * ])
165
+ * });
166
+ *
167
+ * @example
168
+ * // Conditional type based on prefix
169
+ * language.push({
170
+ * pattern: /(TODO|FIXME|NOTE):\s*(.+)/,
171
+ * matches: Rule.extractConditionalMatch(1, 2, [
172
+ * {pattern: /^TODO$/i, type: 'todo'},
173
+ * {pattern: /^FIXME$/i, type: 'error'},
174
+ * {pattern: /^NOTE$/i, type: 'comment'}
175
+ * ])
176
+ * });
177
+ */
178
+ static extractConditionalMatch(conditionIndex, contentIndex, conditions) {
179
+ return async function (syntax, match, expression) {
180
+ const condition = match[conditionIndex] || '';
181
+ const content = match[contentIndex];
182
+
183
+ // Skip if no content
184
+ if (!content) return [];
185
+
186
+ // Find matching rule
187
+ let rule = null;
188
+ for (const candidate of conditions) {
189
+ // If no pattern specified, it's a fallback
190
+ if (!candidate.pattern || candidate.pattern.test(condition)) {
191
+ rule = candidate;
192
+ break;
193
+ }
194
+ }
195
+
196
+ // If no rule determined, return empty
197
+ if (!rule) return [];
198
+
199
+ // Extract rule properties (exclude pattern)
200
+ const {pattern, ...ruleProps} = rule;
201
+
202
+ // Build syntax tree or create match based on rule properties
203
+ const offset = match.index + match[0].indexOf(content);
204
+
205
+ if (ruleProps.language) {
206
+ return [
207
+ await Language.buildTree(
208
+ syntax,
209
+ {...ruleProps, owner: expression?.owner},
210
+ content,
211
+ offset,
212
+ undefined
213
+ )
214
+ ];
215
+ } else {
216
+ const nestedExpression = {owner: expression?.owner, ...ruleProps};
217
+ return [new Match(offset, content.length, nestedExpression, content)];
218
+ }
219
+ };
220
+ }
221
+
222
+ static cStyleComment = {
223
+ pattern: /\/\*[\s\S]*?\*\//m,
224
+ type: 'comment',
225
+ allow: ['href']
226
+ };
227
+ static cppStyleComment = {
228
+ pattern: /\/\/.*$/m,
229
+ type: 'comment',
230
+ allow: ['href']
231
+ };
232
+ static perlStyleComment = {
233
+ pattern: /#.*$/m,
234
+ type: 'comment',
235
+ allow: ['href']
236
+ };
237
+
238
+ static perlStyleRegularExpression = {
239
+ pattern: /\B\/([^\\\/]|\\.)*\/[a-z]*(?=\s*($|[^\w\s'"\(]))/m,
240
+ type: 'constant',
241
+ incremental: true
242
+ };
243
+ static rubyStyleRegularExpression = {
244
+ pattern: /\B\/([^\\\/]|\\.)*\/[a-z]*(?=\s*($|[^\w\s'"\(]|do))/m,
245
+ type: 'constant',
246
+ incremental: true
247
+ };
248
+
249
+ static cStyleFunction = {
250
+ pattern: /([a-z_][a-z0-9_]*)\s*\(/i,
251
+ matches: this.extractMatches({type: 'function'})
252
+ };
253
+ static camelCaseType = {pattern: /\b_*[A-Z][\w]*\b/, type: 'type'};
254
+ static cStyleType = {pattern: /\b[_a-z][_\w]*_t\b/i, type: 'type'};
255
+
256
+ static xmlComment = {
257
+ pattern: /(&lt;|<)!--[\s\S]*?--(&gt;|>)/m,
258
+ type: 'comment'
259
+ };
260
+ static webLink = {pattern: /\w+:\/\/[\w\-.\/?%&=@:;#]*/, type: 'href'};
261
+
262
+ static hexNumber = {pattern: /\b0x[0-9a-fA-F]+/, type: 'constant'};
263
+ static decimalNumber = {
264
+ pattern: /\b[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/,
265
+ type: 'constant'
266
+ };
267
+
268
+ static doubleQuotedString = {pattern: /"([^\\"\n]|\\.)*"/, type: 'string'};
269
+ static singleQuotedString = {pattern: /'([^\\'\n]|\\.)*'/, type: 'string'};
270
+ static multiLineDoubleQuotedString = {
271
+ pattern: /"([^\\"]|\\.)*"/,
272
+ type: 'string'
273
+ };
274
+ static multiLineSingleQuotedString = {
275
+ pattern: /'([^\\']|\\.)*'/,
276
+ type: 'string'
277
+ };
278
+ static stringEscape = {pattern: /\\./, type: 'escape', only: ['string']};
279
+
280
+ /**
281
+ * Create a process function that wraps matched tokens in documentation links.
282
+ *
283
+ * @param {string} baseUrl - The base URL for documentation lookups
284
+ * @returns {Function} A process function for language.processes
285
+ *
286
+ * @example
287
+ * language.processes['function'] = Rule.webLinkProcess('http://docs.python.org/search.html?q=');
288
+ */
289
+ static webLinkProcess(baseUrl) {
290
+ return function (container, match, options) {
291
+ // Replace the span with an anchor element
292
+ const anchor = document.createElement('a');
293
+
294
+ // Copy className and content from container
295
+ anchor.className = container.className;
296
+ anchor.innerHTML = container.innerHTML;
297
+
298
+ // Append the matched text to the base URL
299
+ anchor.href = `${baseUrl}${encodeURIComponent(match.value)}`;
300
+
301
+ return anchor;
302
+ };
303
+ }
304
+ }
305
+
306
+ export default Rule;
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Syntax - Core highlighting engine
3
+ * A modern, framework-agnostic syntax highlighter
4
+ *
5
+ * @package @socketry/syntax
6
+ * @author Samuel G. D. Williams
7
+ * @license MIT
8
+ */
9
+
10
+ import {Loader} from './Syntax/Loader.js';
11
+ import {
12
+ LanguageNotFoundError,
13
+ LanguageLoadError,
14
+ StyleSheetLoadError
15
+ } from './Syntax/Errors.js';
16
+
17
+ export class Syntax {
18
+ static #default = null;
19
+
20
+ #root = null;
21
+ #aliases = {};
22
+ #languages = new Loader((loader, name) => this.#loadLanguage(loader, name));
23
+ #styleSheets = new Loader((loader, url) => this.#loadStyleSheet(loader, url));
24
+ #styles = {};
25
+ #themes = {};
26
+ #themeRoot = null; // Base URL for theme assets (CSS)
27
+
28
+ #defaultOptions = {
29
+ theme: 'base',
30
+ linkify: true,
31
+ strict: false
32
+ };
33
+
34
+ /**
35
+ * Get or create the default Syntax instance
36
+ */
37
+ static get default() {
38
+ if (!this.#default) {
39
+ this.#default = new Syntax();
40
+ }
41
+ return this.#default;
42
+ }
43
+
44
+ /**
45
+ * Set a custom default Syntax instance
46
+ */
47
+ static set default(instance) {
48
+ this.#default = instance;
49
+ }
50
+
51
+ /**
52
+ * Detect the default root path for loading language modules
53
+ * Uses import.meta.url to reliably locate the Syntax module directory
54
+ */
55
+ static detectRoot() {
56
+ try {
57
+ const url = new URL('./', import.meta.url);
58
+ return url.href;
59
+ } catch (error) {
60
+ // Fallback: try document.currentScript (may not work reliably)
61
+ if (
62
+ typeof document !== 'undefined' &&
63
+ document.currentScript &&
64
+ document.currentScript.src
65
+ ) {
66
+ const url = new URL(document.currentScript.src);
67
+ return url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
68
+ }
69
+ // Last resort fallback
70
+ return '/';
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Initialize syntax highlighting on the page
76
+ * Registers the web component and sets up the default syntax instance
77
+ * Languages will be auto-loaded on demand when referenced by elements
78
+ *
79
+ * @param {Object} options - Configuration options
80
+ * @param {Syntax} options.syntax - Syntax instance to use (defaults to Syntax.default)
81
+ * @param {boolean} options.upgradeAll - Whether to automatically upgrade existing elements (default: true)
82
+ * @param {string} options.selector - CSS selector for upgrading (default: 'code[class*="language-"]')
83
+ * @param {string} options.root - Base URL for loading language modules (default: auto-detected)
84
+ * @returns {Promise<void>}
85
+ */
86
+ static async highlight(options = {}) {
87
+ const {
88
+ syntax = Syntax.default,
89
+ upgradeAll: shouldUpgradeAll = true,
90
+ selector = 'code[class*="language-"]',
91
+ root = null
92
+ } = options;
93
+
94
+ // Set the default syntax instance:
95
+ Syntax.default = syntax;
96
+
97
+ // Configure root for auto-loading if provided:
98
+ if (root && !syntax.root) {
99
+ syntax.root = root;
100
+ }
101
+
102
+ // Import and register the web component:
103
+ const {CodeElement, upgradeAll} = await import('./Syntax/CodeElement.js');
104
+
105
+ if (!customElements.get('syntax-code')) {
106
+ customElements.define('syntax-code', CodeElement);
107
+ }
108
+
109
+ // Upgrade existing code blocks if requested:
110
+ if (shouldUpgradeAll) {
111
+ if (upgradeAll) {
112
+ // Use the upgradeAll function with the selector:
113
+ upgradeAll(selector, syntax);
114
+ } else {
115
+ // Fallback to customElements.upgrade if the function isn't available:
116
+ customElements.upgrade(document.body);
117
+ }
118
+ }
119
+ }
120
+
121
+ constructor(options = {}) {
122
+ // Allow customization via constructor options
123
+ if (options.root !== undefined) this.#root = options.root;
124
+ if (options.theme !== undefined) this.#defaultOptions.theme = options.theme;
125
+ if (options.themeRoot !== undefined) this.#themeRoot = options.themeRoot;
126
+
127
+ // Set default root if not provided
128
+ if (this.#root === null) {
129
+ this.#root = Syntax.detectRoot();
130
+ }
131
+ }
132
+
133
+ // Public getters for commonly accessed properties
134
+ get defaultOptions() {
135
+ return this.#defaultOptions;
136
+ }
137
+
138
+ get languages() {
139
+ return this.#languages;
140
+ }
141
+
142
+ get aliases() {
143
+ return this.#aliases;
144
+ }
145
+
146
+ get styles() {
147
+ return this.#styles;
148
+ }
149
+
150
+ get themes() {
151
+ return this.#themes;
152
+ }
153
+
154
+ // Theme root for CSS assets
155
+ get themeRoot() {
156
+ return this.#resolveThemeRoot().toString();
157
+ }
158
+
159
+ set themeRoot(value) {
160
+ this.#themeRoot = value;
161
+ // Clear the stylesheet cache when theme changes
162
+ this.#styleSheets.clear();
163
+ }
164
+
165
+ get root() {
166
+ return this.#root;
167
+ }
168
+
169
+ set root(value) {
170
+ this.#root = value;
171
+ }
172
+
173
+ /**
174
+ * Resolve the base URL for the current theme's assets.
175
+ * Priority:
176
+ * 1) Explicit options.themeRoot or setter
177
+ * 2) Relative to this module location: ./themes/<theme>/
178
+ */
179
+ #resolveThemeRoot() {
180
+ try {
181
+ if (this.#themeRoot) {
182
+ return new URL(
183
+ this.#themeRoot,
184
+ typeof document !== 'undefined' ? document.baseURI : import.meta.url
185
+ );
186
+ }
187
+ // Default to a folder next to Syntax.js
188
+ return new URL(`./themes/${this.#defaultOptions.theme}/`, import.meta.url);
189
+ } catch (error) {
190
+ // As a last resort, fall back to root if provided or current location:
191
+ const base =
192
+ this.#root || (typeof location !== 'undefined' ? location.href : '');
193
+ return new URL(`themes/${this.#defaultOptions.theme}/`, base);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Fetch CSS for use in Shadow DOM
199
+ * Returns an object with {sheet, cssText} where sheet is a CSSStyleSheet (if supported)
200
+ * The result is cached and deduplicated across calls
201
+ */
202
+ async getStyleSheet(url) {
203
+ return this.#styleSheets.load(url);
204
+ }
205
+
206
+ /**
207
+ * Load a stylesheet from a URL
208
+ * Used internally by the stylesheet loader
209
+ */
210
+ async #loadStyleSheet(loader, url) {
211
+ const response = await fetch(url);
212
+ if (!response.ok) {
213
+ throw new StyleSheetLoadError(url.toString(), response.status);
214
+ }
215
+ const cssText = await response.text();
216
+
217
+ // If CSSStyleSheet constructor is available, create and return a stylesheet:
218
+ if (typeof CSSStyleSheet !== 'undefined') {
219
+ const sheet = new CSSStyleSheet();
220
+ await sheet.replace(cssText);
221
+ return {sheet, cssText};
222
+ }
223
+
224
+ // Otherwise just return the text:
225
+ return {cssText};
226
+ }
227
+
228
+ /**
229
+ * Load a language module from disk/network
230
+ * Used internally by the language loader
231
+ */
232
+ async #loadLanguage(loader, name) {
233
+ const path = `${this.#root}Syntax/Language/${name}.js`;
234
+ let module;
235
+ try {
236
+ module = await import(path);
237
+ } catch (error) {
238
+ throw new LanguageLoadError(name, path, {cause: error});
239
+ }
240
+
241
+ // If the module exports a register function, call it with this instance
242
+ if (typeof module.default === 'function') {
243
+ module.default(this);
244
+ }
245
+
246
+ // After calling register, aliases have been registered. Re-resolve the name:
247
+ let resolvedName = this.#aliases[name] || name;
248
+ return loader.get(resolvedName);
249
+ }
250
+
251
+ /**
252
+ * Load a language module dynamically
253
+ */
254
+ async getResource(name) {
255
+ // First check if the language is already loaded (including via alias)
256
+ const resolvedName = this.#aliases[name] || name;
257
+ if (this.#languages.has(resolvedName)) {
258
+ return this.#languages.get(resolvedName);
259
+ }
260
+
261
+ // Use the loader to deduplicate concurrent loads
262
+ return this.#languages.load(resolvedName);
263
+ }
264
+
265
+ /**
266
+ * Register language aliases
267
+ */
268
+ alias(name, aliases) {
269
+ this.#aliases[name] = name;
270
+
271
+ for (const alias of aliases) {
272
+ this.#aliases[alias] = name;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Register a language with this Syntax instance
278
+ */
279
+ register(name, language) {
280
+ // Store directly in the loader's cache using the new set() method
281
+ this.#languages.set(name, language);
282
+
283
+ // Also store in aliases if not already there
284
+ if (!this.#aliases[name]) {
285
+ this.#aliases[name] = name;
286
+ }
287
+
288
+ return language;
289
+ }
290
+
291
+ /**
292
+ * Get a language by name or alias
293
+ * Auto-loads the language if not already registered
294
+ */
295
+ async getLanguage(name) {
296
+ // Resolve alias
297
+ const resolvedName = (this.#aliases[name] || name).toLowerCase();
298
+
299
+ // If already loaded, return it
300
+ if (this.#languages.has(resolvedName)) {
301
+ return this.#languages.get(resolvedName);
302
+ }
303
+
304
+ // Otherwise, try to load it
305
+ try {
306
+ return await this.getResource(name);
307
+ } catch (error) {
308
+ if (this.#defaultOptions.strict && !(error instanceof LanguageLoadError)) {
309
+ // If strict and not a load error, ensure a consistent error type:
310
+ throw new LanguageNotFoundError(resolvedName, {cause: error});
311
+ }
312
+ throw error;
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Check if a language is already registered (synchronous)
318
+ */
319
+ hasLanguage(name) {
320
+ // Resolve alias
321
+ const resolvedName = this.#aliases[name] || name;
322
+ return this.#languages.has(resolvedName);
323
+ }
324
+
325
+ /**
326
+ * Get all aliases for a language
327
+ */
328
+ languageAliases(language) {
329
+ const aliases = [];
330
+
331
+ for (const [name, target] of Object.entries(this.#aliases)) {
332
+ if (target === language) {
333
+ aliases.push(name);
334
+ }
335
+ }
336
+
337
+ return aliases;
338
+ }
339
+
340
+ /**
341
+ * Get all language names (primary names, not aliases)
342
+ */
343
+ languageNames() {
344
+ const names = [];
345
+
346
+ for (const [name, target] of Object.entries(this.#aliases)) {
347
+ if (name === target) {
348
+ names.push(name);
349
+ }
350
+ }
351
+
352
+ return names;
353
+ }
354
+ }
355
+
356
+ export default Syntax;
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.