k-l-lambda commited on
Commit
5f1356b
·
1 Parent(s): bd633c5

added grammar highlight for lilylet editor.

Browse files
app.py CHANGED
@@ -340,10 +340,15 @@ def build_head ():
340
  os.path.join(vendor, 'verovio.bundle.js'),
341
  os.path.join(vendor, 'musicWidgetsBrowser.umd.min.js'),
342
  os.path.join(WEB_DIR, 'score-player.js'),
 
 
 
 
343
  ]
344
  tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;</script>'
345
  % (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/')]
346
  tags.append('<link rel="stylesheet" href="%s">' % _file_url(os.path.join(WEB_DIR, 'score-player.css')))
 
347
  for s in scripts:
348
  tags.append('<script src="%s"></script>' % _file_url(s))
349
  return '\n'.join(tags)
@@ -369,10 +374,9 @@ function () {
369
  '''
370
 
371
  # Render the editor text to SVG. The text is passed in by Gradio as the event's
372
- # input value we must NOT scrape it from the DOM: gr.Code is a CodeMirror editor
373
- # that virtualises long documents (only the rows near the viewport exist in the
374
- # DOM), so `.cm-content`.innerText is truncated for long scores and the render
375
- # comes out incomplete. Taking the value as an argument gives the full text.
376
  _JS_RENDER = '''
377
  function (text) {
378
  if (window.LilyScore) window.LilyScore.render(text || '');
@@ -507,8 +511,18 @@ def build_ui ():
507
  with gr.Column(scale=5):
508
  with gr.Group():
509
  gr.Markdown('## Lilylet editor')
510
- editor = gr.Code(show_label=False, language=None, lines=18,
511
- max_lines=18, interactive=True, elem_id='ls-editor')
 
 
 
 
 
 
 
 
 
 
512
 
513
  # ---------------- RIGHT ----------------
514
  with gr.Column(scale=6):
 
340
  os.path.join(vendor, 'verovio.bundle.js'),
341
  os.path.join(vendor, 'musicWidgetsBrowser.umd.min.js'),
342
  os.path.join(WEB_DIR, 'score-player.js'),
343
+ # our own CodeMirror 6 editor (CM + grammar-derived lilylet() highlighter) and
344
+ # the mount/bridge script that wires it to the hidden #ls-editor-state textbox.
345
+ os.path.join(vendor, 'lyl-editor.bundle.js'),
346
+ os.path.join(WEB_DIR, 'lyl-editor-mount.js'),
347
  ]
348
  tags = ['<script>window.__LILYSCRIPT_SOUNDFONT_URL=%r;</script>'
349
  % (_file_url(os.path.join(WEB_DIR, 'soundfont')) + '/')]
350
  tags.append('<link rel="stylesheet" href="%s">' % _file_url(os.path.join(WEB_DIR, 'score-player.css')))
351
+ tags.append('<link rel="stylesheet" href="%s">' % _file_url(os.path.join(WEB_DIR, 'lyl-editor.css')))
352
  for s in scripts:
353
  tags.append('<script src="%s"></script>' % _file_url(s))
354
  return '\n'.join(tags)
 
374
  '''
375
 
376
  # Render the editor text to SVG. The text is passed in by Gradio as the event's
377
+ # input value (the hidden #ls-editor-state textbox, kept in sync with the embedded
378
+ # CodeMirror editor) taking the value as an argument gives the full text directly,
379
+ # no DOM scraping needed.
 
380
  _JS_RENDER = '''
381
  function (text) {
382
  if (window.LilyScore) window.LilyScore.render(text || '');
 
511
  with gr.Column(scale=5):
512
  with gr.Group():
513
  gr.Markdown('## Lilylet editor')
514
+ # Our own CodeMirror 6 editor (lyl-editor.bundle.js) mounts into
515
+ # this div and bridges to the hidden textbox below — Gradio's
516
+ # gr.Code can't be syntax-highlighted (its CM is sealed), so we
517
+ # embed our own with the grammar-derived lilylet() highlighter.
518
+ gr.HTML('<div id="ls-editor-mount" class="ls-editor-mount"></div>')
519
+ # Canonical editor text as Gradio state: generation/file-load write
520
+ # it, the SVG render reads it, and the embedded CM mirrors it both
521
+ # ways (see web/lyl-editor-mount.js). Hidden via CSS (NOT visible=False,
522
+ # which removes the element from the DOM so the bridge can't find its
523
+ # <textarea>); the .ls-editor-state-hidden rule sets display:none.
524
+ editor = gr.Textbox(elem_id='ls-editor-state',
525
+ elem_classes=['ls-editor-state-hidden'], show_label=False, container=False)
526
 
527
  # ---------------- RIGHT ----------------
528
  with gr.Column(scale=6):
web/lyl-editor-mount.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* LilyScript editor mount + state bridge.
2
+ *
3
+ * Mounts our own CodeMirror 6 editor (window.LilyEditor, from lyl-editor.bundle.js)
4
+ * into #ls-editor-mount and bridges it to a hidden Gradio Textbox (#ls-editor-state),
5
+ * which is the canonical text Gradio reads/writes:
6
+ *
7
+ * CM edit -> write hidden <textarea>.value + dispatch native 'input' -> Gradio
8
+ * updates the Python value and fires its .change -> SVG render.
9
+ * Gradio write -> generation stream / file load set the <textarea>.value; we poll
10
+ * for external changes and push them into CM via setValue().
11
+ *
12
+ * An `applyingExternal` flag prevents the echo loop (CM.setValue must not re-emit a
13
+ * change back to the textarea), mirroring live-editor's `isEditorUpdate` guard.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ var MOUNT_ID = 'ls-editor-mount';
19
+ var STATE_ID = 'ls-editor-state';
20
+ var DEBOUNCE_MS = 300;
21
+
22
+ var handle = null; // { getValue, setValue, destroy }
23
+ var textarea = null;
24
+ var lastTextareaValue = null; // last value we've seen on the textarea
25
+ var applyingExternal = false; // true while pushing a Gradio value into CM
26
+ var debounceTimer = null;
27
+
28
+ function log (msg) { console.log('[lyl-editor]', msg); }
29
+
30
+ function ready () {
31
+ return window.LilyEditor && typeof window.LilyEditor.mount === 'function';
32
+ }
33
+
34
+ // Write text into the hidden textarea using the native setter (so Svelte/Gradio's
35
+ // controlled <textarea> actually registers it) and dispatch a bubbling 'input'.
36
+ function pushToGradio (text) {
37
+ if (!textarea) return;
38
+ var proto = window.HTMLTextAreaElement && window.HTMLTextAreaElement.prototype;
39
+ var setter = proto && Object.getOwnPropertyDescriptor(proto, 'value');
40
+ if (setter && setter.set) setter.set.call(textarea, text);
41
+ else textarea.value = text;
42
+ lastTextareaValue = text;
43
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
44
+ textarea.dispatchEvent(new Event('change', { bubbles: true }));
45
+ }
46
+
47
+ // CM editor -> Gradio (debounced). Skipped while we're applying an external value.
48
+ function onEditorChange (text) {
49
+ if (applyingExternal) return;
50
+ if (debounceTimer) clearTimeout(debounceTimer);
51
+ debounceTimer = setTimeout(function () { pushToGradio(text); }, DEBOUNCE_MS);
52
+ }
53
+
54
+ // Gradio -> CM: when the textarea value changes from OUTSIDE (generation stream,
55
+ // file load), push it into the editor. We diff against lastTextareaValue so our
56
+ // own pushToGradio writes don't bounce back.
57
+ function pollExternal () {
58
+ if (!textarea || !handle) return;
59
+ var v = textarea.value;
60
+ if (v === lastTextareaValue) return; // unchanged, or our own write
61
+ lastTextareaValue = v;
62
+ applyingExternal = true;
63
+ handle.setValue(v); // no-op guarded inside if equal
64
+ applyingExternal = false;
65
+ }
66
+
67
+ function attach () {
68
+ var mountEl = document.getElementById(MOUNT_ID);
69
+ var stateWrap = document.getElementById(STATE_ID);
70
+ if (!mountEl || !stateWrap || !ready()) return false;
71
+ // gr.Textbox renders a <textarea> inside its elem_id wrapper.
72
+ var ta = stateWrap.querySelector('textarea');
73
+ if (!ta) return false;
74
+ if (handle && textarea === ta && mountEl.firstChild) return true; // already mounted
75
+
76
+ textarea = ta;
77
+ lastTextareaValue = textarea.value;
78
+ // Gradio wraps gr.HTML content in a `.prose` block whose global rule
79
+ // `.gradio-container .prose * { color: var(--body-text-color) }` overrides
80
+ // CodeMirror's per-token syntax colours (it out-specifies CM's atomic class
81
+ // rules). Strip `prose` from the editor's wrapper so CM's oneDark highlight
82
+ // colours win. (Scoped to the editor; the score panel keeps its own.)
83
+ var proseWrap = mountEl.closest('.prose');
84
+ if (proseWrap) proseWrap.classList.remove('prose');
85
+ mountEl.innerHTML = '';
86
+ handle = window.LilyEditor.mount(mountEl, {
87
+ value: textarea.value || '',
88
+ onChange: onEditorChange,
89
+ });
90
+ log('mounted CM editor; initial ' + (textarea.value || '').length + ' chars');
91
+ return true;
92
+ }
93
+
94
+ function boot () {
95
+ var mounted = attach();
96
+ if (!mounted) {
97
+ var iv = setInterval(function () { if (attach()) clearInterval(iv); }, 300);
98
+ setTimeout(function () { clearInterval(iv); }, 30000);
99
+ }
100
+ // poll the hidden textarea for external (Gradio-driven) value changes, and
101
+ // re-attach if Gradio re-renders the editor DOM (mount/textarea swapped).
102
+ setInterval(function () {
103
+ var mountEl = document.getElementById(MOUNT_ID);
104
+ var stateWrap = document.getElementById(STATE_ID);
105
+ var ta = stateWrap && stateWrap.querySelector('textarea');
106
+ if (mountEl && ta && (ta !== textarea || !mountEl.firstChild)) { attach(); return; }
107
+ pollExternal();
108
+ }, 200);
109
+ }
110
+
111
+ if (document.readyState === 'loading') {
112
+ document.addEventListener('DOMContentLoaded', boot);
113
+ } else {
114
+ boot();
115
+ }
116
+ log('lyl-editor-mount.js loaded');
117
+ })();
web/lyl-editor.css ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* LilyScript embedded CodeMirror 6 editor (mounted by web/lyl-editor-mount.js into
2
+ * #ls-editor-mount). The editor uses the oneDark theme (dark panel) like
3
+ * lilylet-live-editor; these rules only size the container and font so it matches the
4
+ * old gr.Code 18-line viewport and the surrounding Gradio Soft layout. */
5
+
6
+ #ls-editor-mount {
7
+ height: 470px; /* ~18 lines at 14px/1.6 — matches the old gr.Code lines=18 */
8
+ border-radius: 8px;
9
+ overflow: hidden;
10
+ background: #282c34; /* oneDark bg, so there's no flash before CM mounts */
11
+ }
12
+
13
+ /* The state-carrier Textbox is kept in the DOM (so the bridge can read/write its
14
+ * <textarea> + receive Gradio's writes) but hidden from view. We can't use
15
+ * visible=False — that removes the element entirely. */
16
+ .ls-editor-state-hidden { display: none !important; }
17
+
18
+ /* CM fills the mount; scroll inside it. */
19
+ #ls-editor-mount .cm-editor {
20
+ height: 100%;
21
+ }
22
+
23
+ #ls-editor-mount .cm-scroller {
24
+ font-family: 'Fira Code', 'Consolas', 'Menlo', monospace;
25
+ font-size: 14px;
26
+ line-height: 1.6;
27
+ }
28
+
29
+ /* The cm-editor focus ring is fine; keep CM's own selection/caret from oneDark. */
30
+ #ls-editor-mount .cm-editor.cm-focused {
31
+ outline: none;
32
+ }
web/vendor/lilylet.bundle.js CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:869c6da801fee553a8c8d6c1ca24b6fae3fbe65993167f9a3c0a01540cd413a8
3
- size 86557
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b45c667b547fdfc09c871c347fa5596781f13d03499a5b1f5d543d7c927089ca
3
+ size 90045
web/vendor/lyl-editor.bundle.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:98b611d7dbc3cf0da8c1afd45c2f03db7894da3400ba8bd7c3ddb2b1d46a9884
3
+ size 392848