diff --git a/copyparty/web/md.css b/copyparty/web/md.css index 392deb99..c4924526 100644 --- a/copyparty/web/md.css +++ b/copyparty/web/md.css @@ -83,6 +83,7 @@ h3 { h1 a, h3 a, h5 a, h2 a, h4 a, h6 a { color: inherit; + display: block; background: none; border: none; padding: 0; @@ -239,7 +240,7 @@ blink { } #mn.undocked { position: fixed; - padding: 1.2em 0 1em 1em; + padding: 1.7em 0 1.5em 1em; box-shadow: 0 0 .5em rgba(0, 0, 0, 0.3); background: #f7f7f7; } diff --git a/copyparty/web/md.js b/copyparty/web/md.js index 5bc9b81f..4e85f4a0 100644 --- a/copyparty/web/md.js +++ b/copyparty/web/md.js @@ -6,10 +6,30 @@ var dom_pre = document.getElementById('mp'); var dom_src = document.getElementById('mt'); var dom_navtgl = document.getElementById('navtoggle'); + +// chrome 49 needs this +var chromedbg = function () { console.log(arguments); } + +// null-logger +var dbg = function () { }; + +// replace dbg with the real deal here or in the console: +// dbg = chromedbg +// dbg = console.log + + function hesc(txt) { return txt.replace(/&/g, "&").replace(//g, ">"); } + +function cls(dom, name, add) { + var re = new RegExp('(^| )' + name + '( |$)'); + var lst = (dom.getAttribute('class') + '').replace(re, "$1$2").replace(/ /, ""); + dom.setAttribute('class', lst + (add ? ' ' + name : '')); +} + + // add navbar (function () { var n = document.location + ''; @@ -28,17 +48,105 @@ function hesc(txt) { dom_nav.innerHTML = nav.join(''); })(); + +// faster than replacing the entire html (chrome 1.8x, firefox 1.6x) +function copydom(src, dst, lv) { + var sc = src.childNodes, + dc = dst.childNodes; + + if (sc.length !== dc.length) { + dbg("replace L%d (%d/%d) |%d|", + lv, sc.length, dc.length, src.innerHTML.length); + + dst.innerHTML = src.innerHTML; + return; + } + + var rpl = []; + for (var a = sc.length - 1; a >= 0; a--) { + var st = sc[a].tagName, + dt = dc[a].tagName; + + if (st !== dt) { + dbg("replace L%d (%d/%d) type %s/%s", lv, a, sc.length, st, dt); + rpl.push(a); + continue; + } + + var sa = sc[a].attributes || [], + da = dc[a].attributes || []; + + if (sa.length !== da.length) { + dbg("replace L%d (%d/%d) attr# %d/%d", + lv, a, sc.length, sa.length, da.length); + + rpl.push(a); + continue; + } + + var dirty = false; + for (var b = sa.length - 1; b >= 0; b--) { + var name = sa[b].name, + sv = sa[b].value, + dv = dc[a].getAttribute(name); + + if (name == "data-ln" && sv !== dv) { + dc[a].setAttribute(name, sv); + continue; + } + + if (sv !== dv) { + dbg("replace L%d (%d/%d) attr %s [%s] [%s]", + lv, a, sc.length, name, sv, dv); + + dirty = true; + break; + } + } + if (dirty) + rpl.push(a); + } + + // TODO pure guessing + if (rpl.length > sc.length / 3) { + dbg("replace L%d fully, %s (%d/%d) |%d|", + lv, rpl.length, sc.length, src.innerHTML.length); + + dst.innerHTML = src.innerHTML; + return; + } + + // repl is reversed; build top-down + var nbytes = 0; + for (var a = rpl.length - 1; a >= 0; a--) { + var html = sc[rpl[a]].outerHTML; + dc[rpl[a]].outerHTML = html; + nbytes += html.length; + } + if (nbytes > 0) + dbg("replaced %d bytes L%d", nbytes, lv); + + for (var a = 0; a < sc.length; a++) + copydom(sc[a], dc[a], lv + 1); + + if (src.innerHTML !== dst.innerHTML) { + dbg("setting %d bytes L%d", src.innerHTML.length, lv); + dst.innerHTML = src.innerHTML; + } +} + + function convert_markdown(md_text) { marked.setOptions({ //headerPrefix: 'h-', breaks: true, gfm: true }); - var html = marked(md_text); - dom_pre.innerHTML = html; + var md_html = marked(md_text); + var md_dom = new DOMParser().parseFromString(md_html, "text/html").body; // todo-lists (should probably be a marked extension) - var nodes = dom_pre.getElementsByTagName('input'); + var nodes = md_dom.getElementsByTagName('input'); for (var a = nodes.length - 1; a >= 0; a--) { var dom_box = nodes[a]; if (dom_box.getAttribute('type') !== 'checkbox') @@ -58,9 +166,10 @@ function convert_markdown(md_text) { html.substr(html.indexOf('>') + 1); } - var manip_nodes = dom_pre.getElementsByTagName('*'); - for (var a = manip_nodes.length - 1; a >= 0; a--) { - var el = manip_nodes[a]; + // separate for each line in
+    var nodes = md_dom.getElementsByTagName('pre');
+    for (var a = nodes.length - 1; a >= 0; a--) {
+        var el = nodes[a];
 
         var is_precode =
             el.tagName == 'PRE' &&
@@ -77,18 +186,45 @@ function convert_markdown(md_text) {
 
         el.innerHTML = lines.join('');
     }
+
+    // self-link headers
+    var id_seen = {},
+        dyn = md_dom.getElementsByTagName('*');
+
+    nodes = [];
+    for (var a = 0, aa = dyn.length; a < aa; a++)
+        if (/^[Hh]([1-6])/.exec(dyn[a].tagName) !== null)
+            nodes.push(dyn[a]);
+
+    for (var a = 0; a < nodes.length; a++) {
+        el = nodes[a];
+        var id = el.getAttribute('id'),
+            orig_id = id;
+
+        if (id_seen[id]) {
+            for (var n = 1; n < 4096; n++) {
+                id = orig_id + '-' + n;
+                if (!id_seen[id])
+                    break;
+            }
+            el.setAttribute('id', id);
+        }
+        id_seen[id] = 1;
+        el.innerHTML = '' + el.innerHTML + '';
+    }
+
+    copydom(md_dom, dom_pre, 0);
 }
 
+
 function init_toc() {
     var loader = document.getElementById('ml');
     loader.parentNode.removeChild(loader);
 
     var anchors = [];  // list of toc entries, complex objects
     var anchor = null; // current toc node
-    var id_seen = {};  // taken IDs
     var html = [];     // generated toc html
     var lv = 0;        // current indentation level in the toc html
-    var re = new RegExp('^[Hh]([1-3])');
 
     var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
     var manip_nodes = [];
@@ -97,7 +233,7 @@ function init_toc() {
 
     for (var a = 0, aa = manip_nodes.length; a < aa; a++) {
         var elm = manip_nodes[a];
-        var m = re.exec(elm.tagName);
+        var m = /^[Hh]([1-6])/.exec(elm.tagName);
         var is_header = m !== null;
         if (is_header) {
             var nlv = m[1];
@@ -110,23 +246,7 @@ function init_toc() {
                 lv--;
             }
 
-            var orig_id = elm.getAttribute('id');
-            var id = orig_id;
-            if (id_seen[id]) {
-                for (var n = 1; n < 4096; n++) {
-                    id = orig_id + '-' + n;
-                    if (!id_seen[id])
-                        break;
-                }
-                elm.setAttribute('id', id);
-            }
-            id_seen[id] = 1;
-
-            var ahref = '' +
-                elm.innerHTML + '';
-
-            html.push('
  • ' + ahref + '
  • '); - elm.innerHTML = ahref; + html.push('
  • ' + elm.innerHTML + '
  • '); if (anchor != null) anchors.push(anchor); diff --git a/copyparty/web/md2.css b/copyparty/web/md2.css index 0cd77226..80f02123 100644 --- a/copyparty/web/md2.css +++ b/copyparty/web/md2.css @@ -10,6 +10,9 @@ } #mw { left: calc(100% - 57em); + overflow-y: auto; + position: fixed; + bottom: 0; } @@ -21,7 +24,7 @@ } #mw.preview, #mtw.editor { - z-index: 3; + z-index: 5; } #mtw.single, #mw.single { diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index ec9ad888..259850be 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -18,11 +18,6 @@ var dom_ref = (function () { })(); -// replace it with the real deal in the console -var dbg = function () { }; -// dbg = console.log - - // line->scrollpos maps var map_src = []; var map_pre = []; @@ -62,8 +57,10 @@ function genmap(dom) { // input handler var action_stack = null; var nlines = 0; -(function () { - dom_src.oninput = function (e) { +var draw_md = (function () { + var delay = 1; + function draw_md() { + var t0 = new Date().getTime(); var src = dom_src.value; convert_markdown(src); @@ -77,16 +74,22 @@ var nlines = 0; map_src = genmap(dom_ref); map_pre = genmap(dom_pre); - var sb = document.getElementById('save'); - var cl = (sb.getAttribute('class') + '').replace(/ disabled/, ""); - if (src == server_md) - cl += ' disabled'; + cls(document.getElementById('save'), 'disabled', src == server_md); - sb.setAttribute('class', cl); + var t1 = new Date().getTime(); + delay = t1 - t0; + } + + var timeout = null; + dom_src.oninput = function (e) { + clearTimeout(timeout); + timeout = setTimeout(draw_md, delay); if (action_stack) action_stack.push(); - } - dom_src.oninput(); + }; + + draw_md(); + return draw_md; })(); @@ -296,7 +299,7 @@ function save_chk() { last_modified = this.lastmod; server_md = this.txt; - dom_src.oninput(); + draw_md(); var ok = document.createElement('div'); ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1'); @@ -366,7 +369,7 @@ function setsel(s) { dom_src.value = [s.pre, s.sel, s.post].join(''); dom_src.setSelectionRange(s.car, s.cdr); try { - dom_src.oninput(); + draw_md(); } catch (ex) { } } @@ -426,7 +429,7 @@ function md_home(shift) { function md_newline() { var s = linebounds(true), ln = s.md.substring(s.n1, s.n2), - m = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/.exec(ln); + m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(ln); s.pre = s.md.substring(0, s.car) + '\n' + m[0]; s.sel = ''; @@ -561,7 +564,7 @@ action_stack = (function () { dom_src.value = ref; dom_src.setSelectionRange(state.cursor, state.cursor); ignore = true; // all browsers - dom_src.oninput(); + draw_md(); return true; } @@ -577,6 +580,10 @@ action_stack = (function () { } function undo() { + if (redos.length == 0) { + clearTimeout(sched_timer); + push(); + } return apply(undos, redos); }