solarite 0.2.1 → 0.2.3

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 (50) hide show
  1. package/package.json +7 -5
  2. package/benchmarks/naive/Solarite.min.js +0 -4
  3. package/benchmarks/naive/index.html +0 -14
  4. package/benchmarks/naive/main.js +0 -339
  5. package/benchmarks/naive/package-lock.json +0 -13
  6. package/benchmarks/naive/package.json +0 -23
  7. package/benchmarks/readme.md +0 -48
  8. package/build/build.bat +0 -4
  9. package/build/build.js +0 -139
  10. package/build/lib/rollup.min.js +0 -11
  11. package/build/lib/source-map.min.js +0 -1
  12. package/build/lib/terser.min.js +0 -1
  13. package/docs/index.md +0 -883
  14. package/docs/js/Playground.js +0 -184
  15. package/docs/js/codemirror/codemirror6.js +0 -32513
  16. package/docs/js/codemirror/themeSolarIce.js +0 -312
  17. package/docs/js/documentation.js +0 -32
  18. package/docs/js/ui/CodeEditor.js +0 -978
  19. package/docs/js/ui/DarkToggle.js +0 -52
  20. package/docs/js/ui/FlexResizer.js +0 -152
  21. package/docs/js/util/Draggable2.js +0 -151
  22. package/docs/js/util/Errors.js +0 -20
  23. package/docs/js/util/Html.js +0 -147
  24. package/docs/js/util/Icons.js +0 -623
  25. package/docs/js/util/Input.js +0 -253
  26. package/docs/js/util/Util.js +0 -88
  27. package/docs/js/util/delve.js +0 -43
  28. package/docs/media/FiraCode400.woff2 +0 -0
  29. package/docs/media/cabin-latin-700.woff2 +0 -0
  30. package/docs/media/documentation.css +0 -96
  31. package/docs/media/eternium.css +0 -1123
  32. package/docs/media/solarite-machine.webp +0 -0
  33. package/index.html +0 -761
  34. package/src/unused/FastLookupArray.js +0 -54
  35. package/src/unused/Hashes.js +0 -339
  36. package/src/unused/InUse.test.js +0 -92
  37. package/src/unused/InUseMap.js +0 -98
  38. package/src/unused/LinkedList.js +0 -117
  39. package/src/unused/LinkedList.test.js +0 -115
  40. package/src/unused/NodeGroupManager.js +0 -444
  41. package/src/unused/Perf.js +0 -47
  42. package/src/unused/Template.js +0 -108
  43. package/src/unused/onConnect.js +0 -79
  44. package/src/unused/watch.js +0 -302
  45. package/src/unused/watch2.js +0 -439
  46. package/tests/Benchmark.test.js +0 -319
  47. package/tests/Solarite.test.js +0 -3838
  48. package/tests/Testimony.js +0 -809
  49. package/tests/index.html +0 -46
  50. package/tests/run.bat +0 -2
@@ -1,978 +0,0 @@
1
- import {
2
- autocompletion,
3
- bracketMatching,
4
- closeBrackets,
5
- closeBracketsKeymap,
6
- closeSearchPanel,
7
- Compartment,
8
- completionKeymap,
9
- crosshairCursor,
10
- css,
11
- cssParser,
12
- defaultHighlightStyle,
13
- defaultKeymap,
14
- drawSelection,
15
- EditorState,
16
- EditorView,
17
- foldKeymap,
18
- highlightActiveLine,
19
- highlightActiveLineGutter,
20
- highlightSelectionMatches,
21
- highlightSpecialChars,
22
- history,
23
- historyKeymap,
24
- html,
25
- htmlParser,
26
- indentOnInput,
27
- indentUnit,
28
- indentWithTab,
29
- javascript,
30
- json,
31
- jsParser,
32
- keymap,
33
- lineNumbers,
34
- LRLanguage,
35
- markdown,
36
- openSearchPanel,
37
- parseMixed,
38
- php,
39
- phpParser,
40
- python,
41
- rectangularSelection,
42
- redo,
43
- redoDepth,
44
- searchKeymap,
45
- searchPanelOpen,
46
- sql,
47
- StateEffect,
48
- StateField,
49
- syntaxHighlighting,
50
- undo,
51
- undoDepth
52
- } from '../codemirror/codemirror6.js';
53
- import themeSolarIce from "../codemirror/themeSolarIce.js";
54
- import {r, Solarite, getArg, ArgType} from "../../../src/solarite/Solarite.js";
55
- import Icons from "../util/Icons.js";
56
- import Util from "../util/Util.js";
57
-
58
-
59
-
60
- /**
61
- * Wraps CodeMirror into a web component and provides some useful functions and default settings.
62
- * TODO: Emmet support for html: https://discuss.codemirror.net/t/is-it-able-to-use-tab-to-generate-code-snippets-for-html-tag/5502
63
- *
64
- * @example
65
- * new CodeEditor('sql', 'SELECT * FROM users', {}, 'undo redo | run'); */
66
- export default class CodeEditor extends Solarite {
67
-
68
- /** @type {EditorView} */
69
- view;
70
-
71
- extensions = [];
72
-
73
- /**
74
- * @type {Callbacks|function}
75
- *
76
- * @example
77
- * codeEditor.onChange.push(update => {
78
- * console.log(update.state.doc.toString();
79
- * });
80
- * */
81
- onChange = Util.callback();
82
-
83
- /**
84
- * Called every time CodeMirror selection or content changes.
85
- * @type {Callbacks|function} */
86
- onInternalChange = Util.callback();
87
-
88
- /** @type {Callbacks|function(range:{start: {line:int, column:int}, end: {line:int, column:int}})} */
89
- onSelectionChange = Util.callback();
90
-
91
- /** @type {Callbacks|function} */
92
- onOptionChange = Util.callback();
93
-
94
- allowChanges = true;
95
-
96
- /**
97
- * Serialized selection range. Line numbers start at 0.
98
- * @type {string} */
99
- lastSelection = '0-0';
100
-
101
- /** @type {HTMLElement} */
102
- editor;
103
-
104
- /** @type {?CodeEditorToolbar} */
105
- toolbar;
106
-
107
- /** @type {string} */
108
- language;
109
-
110
- /**
111
- * Compartments wrap extensions and allow changing thier settings after init. */
112
- compartments = {
113
- lineWrapping: new Compartment(),
114
- language: new Compartment(),
115
- tabSize: new Compartment(),
116
- theme: new Compartment()
117
- };
118
-
119
- wordWrap = false;
120
-
121
- /**
122
- * Example languageConfig for SQL:
123
- * https://github.com/codemirror/lang-sql#----interface----sqlconfig
124
- * {
125
- * upperCaseKeywords: true,
126
- * schema: { // Define tables and their columns for auto-complete.
127
- * users: ['email', 'passwordHash'],
128
- * stats: ['id', 'created']
129
- * }
130
- * }
131
- *
132
- * @param value {string}
133
- * @param language {?string} Can be the name of the language or one of the file extensions.
134
- * Supported languages: php, php-plain, javascript, html, markdown, sql, css
135
- * @param languageconfig {?Object} Options for CodeMirror.
136
- * @param toolbar {string|Object<html:Template, update:function>[]} Names of buttons from CodeEditiorToolbar.buttonTemplates or an array of HTMLElements to use as buttons.
137
- * @param options {object}
138
- * @param options.wordWrap {boolean}
139
- * @param options.tabSize {int}
140
- * @param options.fontSize {int} */
141
- constructor({value='', language=null, languageconfig=null, toolbar='', options={}}={}) {
142
- super();
143
-
144
- value = getArg(this, 'value', value, ArgType.String);
145
- this.language = (language||'php').toLowerCase();
146
- this.languageConfig = languageconfig || {};
147
- this.wordWrap = options.wordWrap || false;
148
- this.tabSize = options.tabSize || 4;
149
-
150
-
151
-
152
- // https://github.com/codemirror/lang-php
153
- if (this.language === 'php-plain')
154
- this.languageConfig.plain = true;
155
-
156
- // TODO: Use this to specify markdown code block languages:
157
- // And add keybindings for ctrl+1 headers, etc.
158
- // https://github.com/codemirror/lang-markdown
159
-
160
- this.render();
161
-
162
- let state = EditorState.create({
163
- doc: value,
164
- extensions: this.getExtensions(options.wordWrap)
165
- })
166
- this.view = new EditorView({
167
- state,
168
- parent: this.editor
169
- });
170
-
171
- // Create the toolbar
172
- if (toolbar) {
173
- this.toolbar = new CodeEditorToolbar({ed: this, buttons: toolbar});
174
- this.insertBefore(this.toolbar, this.editor);
175
- }
176
-
177
-
178
- // Watch for dark attribute change on <html> element.
179
- // So we can update our own theme in response.
180
- this.onConnect.push(() => {
181
- if (this.ownerDocument.documentElement) {
182
- const observer = new MutationObserver((mutations) => {
183
- mutations.forEach((mutation) => {
184
- if (mutation.type === 'attributes' && mutation.attributeName === 'dark') {
185
- let state = mutation.target.hasAttribute('dark');
186
- this.setTheme(themeSolarIce(this.language, state));
187
- }
188
- });
189
- });
190
-
191
- observer.observe(this.ownerDocument.documentElement, {attributes: true});
192
- }
193
- });
194
-
195
- this.onInternalChange.push((update, codeEditor) => {
196
- let sel = update.state.selection.main;
197
- let lineObj = update.state.doc.lineAt(sel.head);
198
-
199
- let newSelection = lineObj.number + '-' + (sel.head - lineObj.from);
200
-
201
- if (newSelection !== codeEditor.lastSelection) {
202
- if (codeEditor.allowChanges && codeEditor.onSelectionChange.length) {
203
- let range = {
204
- start: {
205
- line: lineObj.number - 1,
206
- column: sel.head - lineObj.from
207
- },
208
- end: {}
209
- };
210
-
211
- lineObj = sel.head === sel.anchor ? lineObj : update.state.doc.lineAt(sel.anchor);
212
- range.end.line = lineObj.number - 1;
213
- range.end.column = sel.anchor - lineObj.from
214
-
215
- codeEditor.onSelectionChange(range, update);
216
- }
217
- this.lastSelection = newSelection;
218
- if (this.toolbar)
219
- this.toolbar.update(update);
220
- }
221
- // console.log(startLine, startColumn, endLine, endColumn, update)
222
- if (this.allowChanges && update.changedRanges.length && this.onChange.length) {
223
- let codeChange = {
224
- beforeStart: update.changedRanges[0].fromA,
225
- beforeEnd: update.changedRanges[0].toA,
226
- afterStart: update.changedRanges[0].fromB,
227
- afterEnd: update.changedRanges[0].toB,
228
- text: update.state.doc.sliceString(update.changedRanges[0].fromB, update.changedRanges[0].toB)
229
- };
230
- this.onChange(codeChange, update);
231
- }
232
- });
233
-
234
-
235
- Object.defineProperty(this, 'value', {
236
- get() {
237
- return this.getValue();
238
- },
239
- set(val) {
240
- this.setValue(val, false);
241
- }
242
- });
243
- }
244
-
245
- getExtensions(wordWrap=false, isDark=undefined) {
246
- let doc = this.ownerDocument || window.top.document;
247
- if (isDark === undefined)
248
- isDark = doc.documentElement.hasAttribute('dark');
249
-
250
- let languageExtensions = this.getLanguageExtension(this.language, this.languageConfig)
251
- return [
252
- //basicSetup,
253
-
254
- lineNumbers(),
255
- highlightActiveLineGutter(),
256
- highlightSpecialChars(),
257
- history(),
258
- //foldGutter(),
259
- drawSelection(),
260
- indentUnit.of("\t"),
261
- EditorState.allowMultipleSelections.of(true),
262
- indentOnInput(),
263
- bracketMatching(),
264
- closeBrackets(),
265
- autocompletion(),
266
- rectangularSelection(),
267
- crosshairCursor(),
268
- highlightActiveLine(),
269
- highlightSelectionMatches(),
270
- keymap.of([
271
- indentWithTab,
272
- ...closeBracketsKeymap,
273
- {
274
- key: 'Tab', // TODO: What does this do?
275
- //run: target => acceptCompletion(target),
276
- },
277
- ...defaultKeymap,
278
- ...historyKeymap,
279
- ...foldKeymap,
280
- ...completionKeymap,
281
- ...searchKeymap
282
- ]),
283
-
284
- Extensions.changeDetection(this),
285
- Extensions.asyncField,
286
-
287
-
288
- syntaxHighlighting(defaultHighlightStyle, {fallback: true}), // fallback: true lets us use our own theme.
289
-
290
- // Compartments let options be changed after init.
291
- this.compartments.theme.of(themeSolarIce(this.language, isDark)),
292
- this.compartments.tabSize.of(EditorState.tabSize.of(this.tabSize)),
293
- this.compartments.lineWrapping.of(wordWrap ? EditorView.lineWrapping : []),
294
- this.compartments.language.of(languageExtensions)
295
- ];
296
- }
297
-
298
- focus() {
299
- this.view.focus();
300
- }
301
-
302
- /**
303
- * Set the colors, fonts, and sizes.
304
- * @param theme
305
- * @returns {Promise<void>} */
306
- async setTheme(theme) {
307
- this.view.dispatch({
308
- effects: this.compartments.theme.reconfigure(theme)
309
- })
310
- }
311
-
312
- getValue() {
313
- return this.view.viewState.state.doc.toString()
314
- }
315
-
316
- undo() {
317
- undo(this.view);
318
- }
319
-
320
- redo() {
321
- redo(this.view);
322
- }
323
-
324
-
325
- toggleWordWrap(status=undefined) {
326
- if (status===undefined)
327
- status = !this.isWordWrapped();
328
-
329
- this.view.dispatch({ // Update codemirror.
330
- effects: this.compartments.lineWrapping.reconfigure(status ? EditorView.lineWrapping : [])
331
- });
332
- this.wordWrap = status;
333
-
334
- if (this.toolbar)
335
- this.toolbar.update();
336
-
337
- this.onOptionChange();
338
- }
339
-
340
- isWordWrapped() {
341
- return this.wordWrap;
342
- }
343
-
344
- toggleSearchPanel(status=undefined) {
345
- if (status===undefined)
346
- status = !searchPanelOpen(this.view.state);
347
-
348
- if (status) {
349
- openSearchPanel(this.view)
350
-
351
- // Make search panel styles a little closer to SiteCrafter styles.
352
- for (let btn of this.querySelectorAll('.cm-search .cm-button'))
353
- btn.classList.replace('cm-button', 'button')
354
- }
355
- else
356
- closeSearchPanel(this.view)
357
- if (this.toolbar)
358
- this.toolbar.update();
359
- }
360
-
361
- isSearchPanelOpen() {
362
- return this.view && searchPanelOpen(this.view.state);
363
- }
364
-
365
- /**
366
- * Used internally. Use setValue() to change the language. */
367
- getLanguageExtension(language, languageConfig) {
368
- this.language = language;
369
-
370
- // TODO: It may be possible to provide additional style overrides, e.g. for css tag selectors:
371
- // https://discuss.codemirror.net/t/highlighting-markdown-mark-only/3964/3
372
-
373
- let langExt;
374
- if (['css', 'scss', 'less'].includes(language))
375
- langExt = css;
376
- else if (['htm', 'html'].includes(language))
377
- langExt = ParseUtil.htmlWithJavaScript;
378
- else if (['js', 'ts', 'jsx', 'tsx', 'jscript', 'javascript'].includes(language))
379
- langExt = ParseUtil.javascriptWithHtml; // TODO: Support it inside html and php
380
- else if (language === 'json')
381
- langExt = json;
382
- // else if (language === 'ts') // TODO: jsx, tsx, python
383
- // langExt = typescript();
384
- else if (['md', 'markdown'].includes(language))
385
- langExt = markdown;
386
- else if (language === 'php')
387
- langExt = ParseUtil.phpWithHtmlWithJavaScript;
388
- else if (language === 'php-plain')
389
- langExt = php; // Use this to default into php mode without an opening <? tag.
390
- else if (language === 'sql')
391
- langExt = sql;
392
- else if (['py', 'python'].includes(language))
393
- langExt = python;
394
- if (!langExt)
395
- return []; // plain text.
396
-
397
- return [
398
- langExt(languageConfig)
399
- ];
400
- }
401
-
402
- /**
403
- *
404
- * @param range {CodeRange}*/
405
- async selectRange(range) {
406
- range = range.clone();
407
- if (range.start.line === null) {
408
- await asyncDispatch(this.view, {
409
- selection: { // Deselect all.
410
- head: 0,
411
- anchor: 0
412
- }
413
- });
414
- return;
415
- }
416
-
417
-
418
- let doc = this.view.state.doc;
419
-
420
- // Keep within bounds.
421
- if (doc.lines < range.start.line)
422
- range.start.line = doc.lines-1;
423
- if (doc.lines < range.end.line)
424
- range.end.line = doc.lines-1;
425
- if (range.end.line === null)
426
- range.end.line = range.start.line;
427
-
428
- // Sometimes these can be out of bounds if the line is pair of comments for a php print.
429
- // In that case we just select the whole line for now.
430
- if (doc.line(range.start.line+1).length < range.start.column)
431
- range.start.column = 0; //doc.text[range.start.line].length
432
- if (doc.line(range.end.line+1).length < range.end.column)
433
- range.end.column = doc.line(range.end.line+1).length;
434
-
435
- const startLineObj = doc.line(range.start.line+1);
436
- const endLineObj = doc.line(range.end.line+1);
437
-
438
- this.allowChanges = false; // Don't call onSelectionChange()
439
-
440
- await asyncDispatch(this.view, {
441
- // Set selection to that entire line.
442
- selection: {
443
- head: startLineObj.from + range.start.column,
444
- anchor: range.end.column === null
445
- ? endLineObj.to // If no end column, select until end of line.
446
- : endLineObj.from + range.end.column
447
- },
448
- // Ensure the selection is shown in viewport
449
- scrollIntoView: true
450
- });
451
-
452
- this.allowChanges = true;
453
- }
454
-
455
- setLanguage(language=null, languageConfig=null) {
456
- languageConfig = languageConfig || {};
457
- this.view.dispatch({ // Update codemirror.
458
- effects: this.compartments.language.reconfigure(this.getLanguageExtension(language, languageConfig))
459
- });
460
- }
461
-
462
- setTabSize(tabSize) {
463
- this.tabSize = tabSize;
464
- this.view.dispatch({ // Update codemirror.
465
- effects: this.compartments.tabSize.reconfigure(EditorState.tabSize.of(tabSize))
466
- });
467
- }
468
-
469
- /**
470
- * TODO: Can't change language if createHistory=false */
471
- async setValue(value, createHistory=true) {
472
- let scroller = this.querySelector('.cm-editor > .cm-scroller');
473
- let scroll = {x: scroller?.scrollLeft||0, y: scroller?.scrollTop||0};
474
-
475
- // Don't trigger the onChange function
476
- this.allowChanges = false;
477
-
478
- // https://codemirror.net/docs/migration/
479
- if (createHistory)
480
- await asyncDispatch(this.view, { // Also triggers the change event.
481
- changes: {from: 0, to: this.view.state.doc.length, insert: value}
482
- })
483
-
484
- // https://discuss.codemirror.net/t/codemirror-6-cm-clearhistory-equivalent/2851/2
485
- else {
486
- this.view.setState(EditorState.create({
487
- doc: value,
488
- extensions: this.getExtensions(this.wordWrap)
489
- }));
490
- }
491
-
492
- this.allowChanges = true;
493
-
494
- // if (!triggerChange)
495
- // this.allowChanges = true;
496
-
497
- // Not sure why both of these are necessary, but in my testing they were.
498
- if (scroller) {
499
- scroller.scrollTop = scroll.y;
500
- scroller.scrollLeft = scroll.x;
501
- requestAnimationFrame(() => {
502
- scroller.scrollTop = scroll.y;
503
- scroller.scrollLeft = scroll.x;
504
- });
505
- }
506
- }
507
-
508
- getUndoDepth() {
509
- if (this.view)
510
- return undoDepth(this.view.state);
511
- }
512
-
513
- getRedoDepth() {
514
- if (this.view)
515
- return redoDepth(this.view.state);
516
- }
517
-
518
-
519
- render() {
520
- r(this)`
521
- <code-editor>
522
- <style>
523
- :host { position: relative; display: flex; flex-direction: column; min-width: 0; min-height: 0 }
524
- :host [data-id=editor] { height: 100%; width: 100%; min-height: 0 }
525
- :host [data-id=editor] .cm-editor { height: 100%; width: 100%; min-height: 0 }
526
- :host .cm-scroller { overflow: auto; min-height: 10px; height: 100%; width: 100%; font: 13px Hack, monospace !important }
527
-
528
- /* Remove outline added by CodeMirror on focus. */
529
- :host [data-id=editor] .cm-editor.cm-focused { outline: none }
530
- :host .cm-activeLineGutter { background: transparent} /* Only highlight on focus, set in the theme.js file */
531
-
532
- /* Search Panel styles */
533
- :host .cm-panels.cm-panels-bottom { border-top: var(--border, 1px solid #cbcfd7) }
534
- :host .cm-panels { background-color: var(--background, white) !important; color: var(--text, #333) !important }
535
- :host .cm-panels label { display: inline-flex; align-items: center; user-select: none }
536
- :host .cm-textfield { font-size: inherit }
537
- </style>
538
- <div data-id="editor" class="col"></div>
539
- </code-editor>`
540
- }
541
- }
542
- CodeEditor.define();
543
-
544
-
545
-
546
-
547
-
548
- // Define a unique effect to listen for
549
- const asyncEffect = StateEffect.define();
550
-
551
-
552
-
553
- /**
554
- * Use this function to allow awaiting a CodeMirror state.dispatch() call.
555
- * @param view
556
- * @param options
557
- * @returns {Promise<unknown>}
558
- *
559
- * @example
560
- * await asyncDispatch(this.view, dispatchArgs); */
561
- async function asyncDispatch(view, options) {
562
- //view.dispatch(options); // non-async version
563
- return new Promise(resolve => {
564
- options.effects = asyncEffect.of(resolve);
565
- view.dispatch(options);
566
- });
567
- }
568
-
569
-
570
- var Extensions = {
571
-
572
- // Create a state field that listens for your effect
573
- asyncField: StateField.define({
574
- create() {
575
- return {}; // Initial state (can be anything relevant)
576
- },
577
- update(value, tr, a) {
578
- if (tr.effects.some(e => e.is(asyncEffect))) {
579
- // Supposedly called after dispatched event is done, but doesn't seem to be?
580
- let resolve = tr.effects[0].value; // arg sent to asncEffect.of() in asyncDispatch()
581
- resolve();
582
- }
583
- return value; // Return the updated state
584
- }
585
- }),
586
-
587
- // Call onChange() when the document changes.
588
- // https://discuss.codemirror.net/t/how-to-listen-to-changes-for-react-controlled-component/4506/4
589
- changeDetection(codeEditor) {
590
- return EditorView.updateListener.of(update => {
591
- if (update.docChanged || update.selectionSet)
592
- codeEditor.onInternalChange(update, codeEditor);
593
- })
594
- }
595
- }
596
-
597
- var ParseUtil = {
598
-
599
- /**
600
- * A langauge extension for CodeMirror to support highlighting nested javascript/html inside javascript templates.
601
- * https://codemirror.net/examples/mixed-language/
602
- * @return {LRLanguage} */
603
- javascriptWithHtml: () => LRLanguage.define({parser: ParseUtil.jsParser}),
604
- htmlWithJavaScript: () => LRLanguage.define({parser: ParseUtil.htmlParser}),
605
- phpWithHtmlWithJavaScript: () => LRLanguage.define({parser: ParseUtil.phpParser}),
606
-
607
- /**
608
- * Used by javascriptWithHtml() */
609
- htmlParser: htmlParser.configure({
610
- wrap: parseMixed((node, docInput) => {
611
- if (node.name == "StyleText")
612
- return {parser: cssParser}
613
- if (node.name == "ScriptText"/* || node.name == "Interpolation"*/)
614
- return {parser: ParseUtil.jsParser}
615
-
616
- // Use javascript parser for template expressions
617
- if (node.type.name === 'Document' && docInput.doc) {
618
- let code = docInput.doc.sliceString(node.from, node.to);
619
- let overlay = [];
620
- //code.replace(/\${/g, (a, startIndex) => {
621
-
622
- let ranges = ParseUtil.findJavaScriptRanges(code, 0);
623
-
624
- if (ranges.length) {
625
-
626
- ranges = ranges.map(item => ({
627
- from: node.from + item[0],
628
- to: node.from + item[1]
629
- }));
630
-
631
- overlay.push(...ranges);
632
- }
633
- //});
634
-
635
- // Make sure overlays don't overlap.
636
- // TODO: Is this still necessary?
637
- overlay.sort((a, b) => a.from - b.from);
638
- for (let i=0; i<overlay.length-1; i++) {
639
- let cur = overlay[i];
640
- let next = overlay[i+1];
641
- if (cur.to >= next.from)
642
- cur.to = next.from;
643
- }
644
-
645
- return {
646
- parser: ParseUtil.jsParser,
647
- overlay
648
- }
649
- }
650
- })
651
- }),
652
-
653
- /**
654
- * Used by javascriptWithHtml() */
655
- jsParser: jsParser.configure({
656
- wrap: parseMixed((node, docInput) => {
657
- if (node.name == "TemplateString") {
658
- let code = docInput.doc.sliceString(node.from, node.to);
659
-
660
- // Only if node text starts and ends with tags, allowing for spaces.
661
- if (code.match(/^`\s*</)/* && code.match(/>\s*`$/)*/) {
662
- return {
663
- parser: ParseUtil.htmlParser,
664
- }
665
- }
666
- }
667
- return null;
668
- })
669
- }),
670
-
671
- /**
672
- * Replaces CodeMirror's php() function with one that uses the htmlParser above.
673
- * This lets us syntax highlight html in javascript template tags.
674
- * Search codemirror6.js for "PHP language support"
675
- * to find the original function this is copied from. */
676
- phpParser: phpParser.configure({
677
- wrap: parseMixed(node => {
678
- if (!node.type.isTop)
679
- return null;
680
- return {
681
- parser: ParseUtil.htmlParser,
682
- overlay: node => node.name == "Text"
683
- };
684
- }),
685
- top: "Template"
686
-
687
- }),
688
-
689
- /**
690
- * Used by javascriptWithHtml()
691
- * Handles single and multiline comments and all three string types.
692
- * TODO: This needs tests!
693
- * @param str {string}
694
- * @param startIndex {int}
695
- * @returns {int[][]} */
696
- findJavaScriptRanges(str, startIndex) {
697
- let depth = 0;
698
- let depthStack = [];
699
- let inString = null;
700
- let stringStack = [];
701
- let inComment = null;
702
-
703
- let result = [];
704
-
705
- for (let i = startIndex; i < str.length; i++) {
706
-
707
- let char = str[i];
708
- if (inString && char === '\\') { // Skip escaped characters in strings.
709
- i++;
710
- continue;
711
- }
712
- let nextChar = i + 1 < str.length ? str[i + 1] : '';
713
-
714
- // String start/end
715
- if (!inComment) {
716
- if (!inString && (char === '`' || char === '`' || char === '`')) {
717
- inString = char;
718
- if (char === '`') {
719
- if (i > startIndex) {
720
- result.push([startIndex, i]);
721
- }
722
- }
723
- }
724
- else if (inString === char) {
725
- if (inString === '`')
726
- startIndex = i + 1;
727
- inString = null;
728
- }
729
- }
730
-
731
-
732
- if (!inString) {
733
- // Comment start/end
734
- if (char === '/' && (nextChar === '*' || nextChar === '/')) {
735
- inComment = nextChar === '*' ? 'block' : 'line';
736
- i++; // Skip the next character as it's part of the comment syntax
737
- }
738
-
739
- // End block comment
740
- else if (inComment === 'block' && char === '*' && nextChar === '/') {
741
- inComment = null;
742
- i++; // Skip the '/' character
743
- }
744
-
745
- // End line comment
746
- else if (inComment === 'line' && char === '\n')
747
- inComment = null;
748
- }
749
-
750
- // Template literal boundaries
751
- if (!inComment) {
752
-
753
- if (inString) {
754
- if (char === '$' && nextChar === '{') {
755
- startIndex = i;
756
- i++; // Skip the '{' character
757
- stringStack.push(inString);
758
- inString = null;
759
- depthStack.push(depth);
760
- depth = 0;
761
- }
762
- }
763
-
764
- else {
765
- if (char === '{')
766
- depth++;
767
-
768
- else if (char === '}') {
769
- if (depth === 0) {
770
- inString = stringStack.pop();
771
- depth = depthStack.pop();
772
- if (i > startIndex)
773
- result.push([startIndex, i+1]);
774
- }
775
- else
776
- depth--;
777
- }
778
- }
779
- }
780
- }
781
-
782
- return result;
783
- },
784
-
785
- /**
786
- * @deprecated
787
- * Used by javascriptWithHtml()
788
- * TODO: This needs tests!
789
- * @param str {string}
790
- * @param startIndex {int}
791
- * @returns {int} -1 if no match. */
792
- findMatchingBrace(str, startIndex) {
793
- let stack = 0;
794
- let inString = null;
795
- let inComment = null;
796
-
797
- for (let i = startIndex; i < str.length; i++) {
798
- let char = str[i];
799
- if (inString && char === '\\') { // Skip escaped characters in strings.
800
- i++;
801
- continue;
802
- }
803
- let nextChar = i + 1 < str.length ? str[i + 1] : '';
804
-
805
- // String start/end
806
- if (!inComment) {
807
- if (!inString && (char === '`' || char === '`' || char === '`'))
808
- inString = char;
809
- else if (inString === char)
810
- inString = null;
811
- }
812
-
813
- // Comment start/end
814
- if (!inString) {
815
- if (char === '/' && (nextChar === '*' || nextChar === '/')) {
816
- inComment = nextChar === '*' ? 'block' : 'line';
817
- i++; // Skip the next character as it's part of the comment syntax
818
- }
819
-
820
- // End block comment
821
- else if (inComment === 'block' && char === '*' && nextChar === '/') {
822
- inComment = null;
823
- i++; // Skip the '/' character
824
- }
825
-
826
- // End line comment
827
- else if (inComment === 'line' && char === '\n')
828
- inComment = null;
829
- }
830
-
831
- // Template literal boundaries
832
- if (!inString && !inComment) {
833
- if (char === '{')
834
- stack++;
835
-
836
- else if (char === '$' && nextChar === '{') {
837
- stack++;
838
- i++; // Skip the '{' character
839
- }
840
-
841
- else if (char === '}') {
842
- if (stack === 0)
843
- return i; // Matching closing brace found
844
- stack--;
845
- }
846
- }
847
- }
848
-
849
- return -1;
850
- }
851
- };
852
-
853
-
854
- /**
855
- * @typedef {Object} ToolbarButton
856
- * @property {string} html
857
- * @property {function(el:Node|HTMLElement)=} update */
858
-
859
- /**
860
- *
861
- */
862
- export class CodeEditorToolbar extends HTMLElement {
863
-
864
- /** @type {object} Should have functions for everything the buttons array uses. */
865
- ed;
866
-
867
- /**
868
- * Internal representation of buttons.
869
- * @type {{html:string, update:function}[]} */
870
- buttons = [];
871
-
872
- buttonTemplates
873
-
874
- /** @type {HTMLElement} */
875
- buttonEls;
876
-
877
- /**
878
- * @param ed {CodeEditor}
879
- * @param buttons {(string|ToolbarButton)[]} Names of built-in buttons to use, or objects to define new buttons */
880
- constructor({ed, buttons=[]}={}) {
881
- super();
882
- this.ed = ed;
883
-
884
- this.buttonTemplates = {
885
- '|': {
886
- html: r`<span>|</span>`
887
- },
888
- undo: {
889
- html: r`<button onclick="${e => this.ed.undo()}" class="flat" title="Undo">${r(Icons.undo)}</button>`,
890
- update: (el, sel) => el.toggleAttribute('disabled', this.ed.getUndoDepth() === 0)
891
- },
892
- redo: {
893
- html: r`<button onclick="${e => this.ed.redo()}" class="flat" title="Redo">${r(Icons.redo)}</button>`,
894
- update: (el, sel) => el.toggleAttribute('disabled', this.ed.getRedoDepth() === 0)
895
- },
896
- wordWrap: {
897
- html: r`<button onclick="${e => this.ed.toggleWordWrap()}" class="flat ${ed.isWordWrapped() && 'selected'}" title="Word Wrap">${r(Icons.wordWrap)}</button>`,
898
- update: (el, sel) => el.classList.toggle('selected', this.ed.isWordWrapped())
899
- },
900
- run: {
901
- html: r`<button onclick="${e => this.ed.run()}" data-id="btnRun" title="Ctrl+Enter" class="flat">${r(Icons.triangleRight)}Run</button>`
902
- },
903
- findReplace: {
904
- html: r`<button onclick="${e => this.ed.toggleSearchPanel()}" class="flat" title="Find and replace (ctrl+f)">${r(Icons.findReplace)}</button>`,
905
- update: (el, sel) => el.classList.toggle('selected', this.ed.isSearchPanelOpen())
906
- }
907
- };
908
-
909
- if (typeof buttons === 'string')
910
- buttons = buttons.split(/[,\s]+/g);
911
-
912
- for (let button of buttons||[]) {
913
- let item = typeof button === 'string'
914
- ? this.buttonTemplates[button]
915
- : button;
916
- if (!item)
917
- throw new Error('Invalid button ' + button);
918
- this.buttons.push(item);
919
- }
920
-
921
- this.render();
922
-
923
- Util.on(this, 'mousedown', '[data-id=buttonEls] .icon', e => {
924
- let btn = e.target.closest('.icon');
925
-
926
- if (btn.hasAttribute('disabled')) {
927
- e.stopPropagation();
928
- e.stopImmediatePropagation();
929
- e.preventDefault();
930
- return false;
931
- }
932
- });
933
-
934
- this.update();
935
- }
936
-
937
- command(e, func) {
938
- e.preventDefault(); // Don't lose focus
939
- this.ed[func]();
940
- }
941
-
942
- /**
943
- * Update toolbar buttons depending on the editor state*/
944
- update() {
945
- for (let i in this.buttons) {
946
- let button = this.buttons[i];
947
- if (button.update)
948
- button.update(this.buttonEls.children[i], this);
949
- }
950
- }
951
-
952
- render() {
953
- r(this)`
954
- <code-editor-toolbar>
955
- <style>
956
- :host { display: block }
957
- :host [data-id='buttonEls'] span { user-select: none }
958
- :host .blockSelect [data-id='dropdown'] > div span { opacity: .5 } /* Hotkey text */
959
- </style>
960
-
961
- <div data-id="buttonEls" class="row wrap center-v toolbar">
962
- ${this.buttons.map(button => button.html || button)}
963
- </div>
964
-
965
- <!--
966
- Run
967
- Word wrap
968
- format
969
- find/replace
970
- undo/redo
971
- show line numbers
972
- -->
973
- </code-editor-toolbar>`
974
- }
975
-
976
- }
977
- customElements.define('code-editor-toolbar', CodeEditorToolbar);
978
-