Spaces:
Running
Running
Commit ·
5f1356b
1
Parent(s): bd633c5
added grammar highlight for lilylet editor.
Browse files- app.py +20 -6
- web/lyl-editor-mount.js +117 -0
- web/lyl-editor.css +32 -0
- web/vendor/lilylet.bundle.js +2 -2
- web/vendor/lyl-editor.bundle.js +3 -0
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
|
| 373 |
-
#
|
| 374 |
-
# DOM
|
| 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 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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
|