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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/documentation-guidelines.md +12 -7
- data/lib/utopia/project/import_map.rb +1 -0
- data/lib/utopia/project/version.rb +1 -1
- data/pages/_page.xnode +8 -13
- data/public/_components/@socketry/syntax/Syntax/CodeElement.js +293 -0
- data/public/_components/@socketry/syntax/Syntax/Errors.js +52 -0
- data/public/_components/@socketry/syntax/Syntax/Language/apache.js +49 -0
- data/public/_components/@socketry/syntax/Syntax/Language/applescript.js +157 -0
- data/public/_components/@socketry/syntax/Syntax/Language/assembly.js +42 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash-script.js +108 -0
- data/public/_components/@socketry/syntax/Syntax/Language/bash.js +32 -0
- data/public/_components/@socketry/syntax/Syntax/Language/basic.js +232 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c++.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/clang.js +201 -0
- data/public/_components/@socketry/syntax/Syntax/Language/cpp.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/csharp.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/css.js +244 -0
- data/public/_components/@socketry/syntax/Syntax/Language/diff.js +24 -0
- data/public/_components/@socketry/syntax/Syntax/Language/go.js +135 -0
- data/public/_components/@socketry/syntax/Syntax/Language/haskell.js +110 -0
- data/public/_components/@socketry/syntax/Syntax/Language/html.js +69 -0
- data/public/_components/@socketry/syntax/Syntax/Language/io.js +68 -0
- data/public/_components/@socketry/syntax/Syntax/Language/java.js +134 -0
- data/public/_components/@socketry/syntax/Syntax/Language/javascript.js +89 -0
- data/public/_components/@socketry/syntax/Syntax/Language/json.js +36 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lisp.js +38 -0
- data/public/_components/@socketry/syntax/Syntax/Language/lua.js +87 -0
- data/public/_components/@socketry/syntax/Syntax/Language/markdown.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/nginx.js +37 -0
- data/public/_components/@socketry/syntax/Syntax/Language/objective-c.js +1 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ocaml.js +225 -0
- data/public/_components/@socketry/syntax/Syntax/Language/pascal.js +166 -0
- data/public/_components/@socketry/syntax/Syntax/Language/patch.js +2 -0
- data/public/_components/@socketry/syntax/Syntax/Language/perl5.js +317 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php-script.js +112 -0
- data/public/_components/@socketry/syntax/Syntax/Language/php.js +18 -0
- data/public/_components/@socketry/syntax/Syntax/Language/plain.js +20 -0
- data/public/_components/@socketry/syntax/Syntax/Language/protobuf.js +77 -0
- data/public/_components/@socketry/syntax/Syntax/Language/python.js +208 -0
- data/public/_components/@socketry/syntax/Syntax/Language/ruby.js +124 -0
- data/public/_components/@socketry/syntax/Syntax/Language/scala.js +81 -0
- data/public/_components/@socketry/syntax/Syntax/Language/smalltalk.js +30 -0
- data/public/_components/@socketry/syntax/Syntax/Language/sql.js +865 -0
- data/public/_components/@socketry/syntax/Syntax/Language/super-collider.js +70 -0
- data/public/_components/@socketry/syntax/Syntax/Language/swift.js +176 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xml.js +76 -0
- data/public/_components/@socketry/syntax/Syntax/Language/xrb.js +33 -0
- data/public/_components/@socketry/syntax/Syntax/Language/yaml.js +29 -0
- data/public/_components/@socketry/syntax/Syntax/Language.js +276 -0
- data/public/_components/@socketry/syntax/Syntax/Loader.js +78 -0
- data/public/_components/@socketry/syntax/Syntax/Match.js +546 -0
- data/public/_components/@socketry/syntax/Syntax/Rule.js +306 -0
- data/public/_components/@socketry/syntax/Syntax.js +356 -0
- data/public/_components/@socketry/syntax/license.md +21 -0
- data/public/_components/@socketry/syntax/package.json +43 -0
- data/public/_components/@socketry/syntax/readme.md +162 -0
- data/public/_components/@socketry/syntax/themes/base/apache.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/applescript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/assembly.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/bash.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/basic.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/c.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/clang.css +0 -0
- data/public/_components/@socketry/syntax/themes/base/csharp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/css.css +22 -0
- data/public/_components/@socketry/syntax/themes/base/diff.css +48 -0
- data/public/_components/@socketry/syntax/themes/base/go.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/haskell.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/html.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/io.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/java.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/javascript.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/json.css +41 -0
- data/public/_components/@socketry/syntax/themes/base/lisp.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/lua.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/markdown.css +16 -0
- data/public/_components/@socketry/syntax/themes/base/nginx.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ocaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/pascal.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/perl5.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php-script.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/php.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/plain.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/protobuf.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/python.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/ruby.css +23 -0
- data/public/_components/@socketry/syntax/themes/base/scala.css +3 -0
- data/public/_components/@socketry/syntax/themes/base/smalltalk.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/sql.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/super-collider.css +33 -0
- data/public/_components/@socketry/syntax/themes/base/swift.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/syntax.css +63 -0
- data/public/_components/@socketry/syntax/themes/base/xml.css +1 -0
- data/public/_components/@socketry/syntax/themes/base/xrb.css +29 -0
- data/public/_components/@socketry/syntax/themes/base/yaml.css +1 -0
- data/public/_components/@socketry/syntax/themes/theming.md +233 -0
- data/public/_static/sidebar.js +55 -22
- data/public/_static/site.css +0 -4
- data.tar.gz.sig +0 -0
- metadata +94 -1
- 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: /(<|<)!--[\s\S]*?--(>|>)/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.
|