mirror of
				https://github.com/9001/copyparty.git
				synced 2025-11-04 05:43:17 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					09557fbe83 | ||
| 
						 | 
					1c0f44fa4e | ||
| 
						 | 
					fc4d59d2d7 | ||
| 
						 | 
					12345fbacc | ||
| 
						 | 
					2e33c8d222 | ||
| 
						 | 
					db5f07f164 | ||
| 
						 | 
					e050e69a43 | ||
| 
						 | 
					27cb1d4fc7 | ||
| 
						 | 
					5d6a740947 | ||
| 
						 | 
					da3f68c363 | ||
| 
						 | 
					d7d1c3685c | 
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							@@ -59,13 +59,16 @@ launch either of them and it'll unpack and run copyparty, assuming you have pyth
 | 
			
		||||
 | 
			
		||||
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
 | 
			
		||||
 | 
			
		||||
if you don't need all the features you can repack the sfx and save a bunch of space, tho currently the only removable feature is the opus/vorbis javascript decoder which is needed by apple devices to play foss audio files
 | 
			
		||||
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
 | 
			
		||||
* `724K` original size as of v0.4.0
 | 
			
		||||
* `256K` after `./scripts/make-sfx.sh re no-ogv`
 | 
			
		||||
* `164K` after `./scripts/make-sfx.sh re no-ogv no-cm`
 | 
			
		||||
 | 
			
		||||
steps to reduce the sfx size from `720 kB` to `250 kB` roughly:
 | 
			
		||||
* run one of the sfx'es once to unpack it
 | 
			
		||||
* `./scripts/make-sfx.sh re no-ogv` creates a new pair of sfx
 | 
			
		||||
the features you can opt to drop are
 | 
			
		||||
* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
 | 
			
		||||
* `cm`/easymde, the "fancy" markdown editor
 | 
			
		||||
 | 
			
		||||
no internet connection needed, just download an sfx and the repo zip (also if you're on windows use msys2)
 | 
			
		||||
for the `re`pack to work, first run one of the sfx'es once to unpack it
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# install on android
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								bin/copyparty-fuse.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										48
									
								
								bin/copyparty-fuse.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							@@ -22,7 +22,9 @@ from urllib.parse import quote_from_bytes as quote
 | 
			
		||||
try:
 | 
			
		||||
    from fuse import FUSE, FuseOSError, Operations
 | 
			
		||||
except:
 | 
			
		||||
    print("\n    could not import fuse;\n    pip install fusepy\n")
 | 
			
		||||
    print(
 | 
			
		||||
        "\n  could not import fuse; these may help:\n    python3 -m pip install --user fusepy\n    apt install libfuse\n    modprobe fuse"
 | 
			
		||||
    )
 | 
			
		||||
    raise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -34,9 +36,7 @@ usage:
 | 
			
		||||
 | 
			
		||||
dependencies:
 | 
			
		||||
  sudo apk add fuse-dev
 | 
			
		||||
  python3 -m venv ~/pe/ve.fusepy
 | 
			
		||||
  . ~/pe/ve.fusepy/bin/activate
 | 
			
		||||
  pip install fusepy
 | 
			
		||||
  python3 -m pip install --user fusepy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MB/s
 | 
			
		||||
@@ -60,20 +60,21 @@ def boring_log(msg):
 | 
			
		||||
def rice_tid():
 | 
			
		||||
    tid = threading.current_thread().ident
 | 
			
		||||
    c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
 | 
			
		||||
    return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c)
 | 
			
		||||
    return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fancy_log(msg):
 | 
			
		||||
    print("{}\033[0m {}\n".format(rice_tid(), msg), end="")
 | 
			
		||||
    print("{} {}\n".format(rice_tid(), msg), end="")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def null_log(msg):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = boring_log
 | 
			
		||||
info = fancy_log
 | 
			
		||||
log = fancy_log
 | 
			
		||||
log = threadless_log
 | 
			
		||||
dbg = fancy_log
 | 
			
		||||
log = null_log
 | 
			
		||||
dbg = null_log
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -118,7 +119,7 @@ class Gateway(object):
 | 
			
		||||
        try:
 | 
			
		||||
            return self.conns[tid]
 | 
			
		||||
        except:
 | 
			
		||||
            log("new conn [{}] [{}]".format(self.web_host, self.web_port))
 | 
			
		||||
            info("new conn [{}] [{}]".format(self.web_host, self.web_port))
 | 
			
		||||
 | 
			
		||||
            conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
 | 
			
		||||
 | 
			
		||||
@@ -152,7 +153,7 @@ class Gateway(object):
 | 
			
		||||
        if r.status != 200:
 | 
			
		||||
            self.closeconn()
 | 
			
		||||
            raise Exception(
 | 
			
		||||
                "http error {} reading dir {} in {:x}".format(
 | 
			
		||||
                "http error {} reading dir {} in {}".format(
 | 
			
		||||
                    r.status, web_path, rice_tid()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@@ -161,14 +162,14 @@ class Gateway(object):
 | 
			
		||||
 | 
			
		||||
    def download_file_range(self, path, ofs1, ofs2):
 | 
			
		||||
        web_path = "/" + "/".join([self.web_root, path])
 | 
			
		||||
        hdr_range = "bytes={}-{}".format(ofs1, ofs2)
 | 
			
		||||
        hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
 | 
			
		||||
        log("downloading {}".format(hdr_range))
 | 
			
		||||
 | 
			
		||||
        r = self.sendreq("GET", self.quotep(web_path), headers={"Range": hdr_range})
 | 
			
		||||
        if r.status != http.client.PARTIAL_CONTENT:
 | 
			
		||||
            self.closeconn()
 | 
			
		||||
            raise Exception(
 | 
			
		||||
                "http error {} reading file {} range {} in {:x}".format(
 | 
			
		||||
                "http error {} reading file {} range {} in {}".format(
 | 
			
		||||
                    r.status, web_path, hdr_range, rice_tid()
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
@@ -246,14 +247,14 @@ class CPPF(Operations):
 | 
			
		||||
        self.filecache = []
 | 
			
		||||
        self.filecache_mtx = threading.Lock()
 | 
			
		||||
 | 
			
		||||
        log("up")
 | 
			
		||||
        info("up")
 | 
			
		||||
 | 
			
		||||
    def clean_dircache(self):
 | 
			
		||||
        """not threadsafe"""
 | 
			
		||||
        now = time.time()
 | 
			
		||||
        cutoff = 0
 | 
			
		||||
        for cn in self.dircache:
 | 
			
		||||
            if cn.ts - now > 1:
 | 
			
		||||
            if now - cn.ts > 1:
 | 
			
		||||
                cutoff += 1
 | 
			
		||||
            else:
 | 
			
		||||
                break
 | 
			
		||||
@@ -398,7 +399,7 @@ class CPPF(Operations):
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
			
		||||
            ret = buf[-buf_ofs:] + cdr
 | 
			
		||||
 | 
			
		||||
        elif car:
 | 
			
		||||
@@ -416,7 +417,7 @@ class CPPF(Operations):
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
			
		||||
            ret = car + buf[:buf_ofs]
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
@@ -438,7 +439,7 @@ class CPPF(Operations):
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
			
		||||
            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
			
		||||
            ret = buf[buf_ofs:buf_end]
 | 
			
		||||
 | 
			
		||||
        cn = CacheNode([path, h_ofs], buf)
 | 
			
		||||
@@ -472,13 +473,16 @@ class CPPF(Operations):
 | 
			
		||||
        log("read {} @ {} len {} end {}".format(path, offset, length, ofs2))
 | 
			
		||||
 | 
			
		||||
        file_sz = self.getattr(path)["st_size"]
 | 
			
		||||
        if ofs2 >= file_sz:
 | 
			
		||||
            ofs2 = file_sz - 1
 | 
			
		||||
            log("truncate to len {} end {}".format((ofs2 - offset) + 1, ofs2))
 | 
			
		||||
        if ofs2 > file_sz:
 | 
			
		||||
            ofs2 = file_sz
 | 
			
		||||
            log("truncate to len {} end {}".format(ofs2 - offset, ofs2))
 | 
			
		||||
 | 
			
		||||
        if file_sz == 0 or offset >= ofs2:
 | 
			
		||||
            return b""
 | 
			
		||||
 | 
			
		||||
        # toggle cache here i suppose
 | 
			
		||||
        # return self.get_cached_file(path, offset, ofs2, file_sz)
 | 
			
		||||
        return self.gw.download_file_range(path, offset, ofs2 - 1)
 | 
			
		||||
        return self.gw.download_file_range(path, offset, ofs2)
 | 
			
		||||
 | 
			
		||||
    def getattr(self, path, fh=None):
 | 
			
		||||
        path = path.strip("/")
 | 
			
		||||
@@ -495,7 +499,7 @@ class CPPF(Operations):
 | 
			
		||||
 | 
			
		||||
        cn = self.get_cached_dir(dirpath)
 | 
			
		||||
        if cn:
 | 
			
		||||
            # log('cache ok')
 | 
			
		||||
            log("cache ok")
 | 
			
		||||
            dents = cn.data
 | 
			
		||||
        else:
 | 
			
		||||
            log("cache miss")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
# coding: utf-8
 | 
			
		||||
 | 
			
		||||
VERSION = (0, 4, 0)
 | 
			
		||||
VERSION = (0, 4, 2)
 | 
			
		||||
CODENAME = "NIH"
 | 
			
		||||
BUILD_DT = (2020, 5, 13)
 | 
			
		||||
BUILD_DT = (2020, 5, 15)
 | 
			
		||||
 | 
			
		||||
S_VERSION = ".".join(map(str, VERSION))
 | 
			
		||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
 | 
			
		||||
 
 | 
			
		||||
@@ -769,11 +769,18 @@ class HttpCli(object):
 | 
			
		||||
                else:
 | 
			
		||||
                    upper = file_sz
 | 
			
		||||
 | 
			
		||||
                if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz:
 | 
			
		||||
                if upper > file_sz:
 | 
			
		||||
                    upper = file_sz
 | 
			
		||||
 | 
			
		||||
                if lower < 0 or lower >= upper:
 | 
			
		||||
                    raise Exception()
 | 
			
		||||
 | 
			
		||||
            except:
 | 
			
		||||
                raise Pebkac(400, "invalid range requested: " + hrange)
 | 
			
		||||
                err = "invalid range ({}), size={}".format(hrange, file_sz)
 | 
			
		||||
                self.loud_reply(err, status=416, headers={
 | 
			
		||||
                    "Content-Range": "bytes */{}".format(file_sz)
 | 
			
		||||
                })
 | 
			
		||||
                return True
 | 
			
		||||
 | 
			
		||||
            status = 206
 | 
			
		||||
            self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
 | 
			
		||||
 
 | 
			
		||||
@@ -80,8 +80,9 @@ class HttpSrv(object):
 | 
			
		||||
                        "%s %s" % addr,
 | 
			
		||||
                        "shut_rdwr err:\n  {}\n  {}".format(repr(sck), ex),
 | 
			
		||||
                    )
 | 
			
		||||
                if ex.errno not in [10038, 107, 57, 9]:
 | 
			
		||||
                if ex.errno not in [10038, 10054, 107, 57, 9]:
 | 
			
		||||
                    # 10038 No longer considered a socket
 | 
			
		||||
                    # 10054 Foribly closed by remote
 | 
			
		||||
                    #   107 Transport endpoint not connected
 | 
			
		||||
                    #    57 Socket is not connected
 | 
			
		||||
                    #     9 Bad file descriptor
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@ HTTPCODE = {
 | 
			
		||||
    404: "Not Found",
 | 
			
		||||
    405: "Method Not Allowed",
 | 
			
		||||
    413: "Payload Too Large",
 | 
			
		||||
    416: "Requested Range Not Satisfiable",
 | 
			
		||||
    422: "Unprocessable Entity",
 | 
			
		||||
    500: "Internal Server Error",
 | 
			
		||||
    501: "Not Implemented",
 | 
			
		||||
@@ -309,18 +310,7 @@ def get_boundary(headers):
 | 
			
		||||
def read_header(sr):
 | 
			
		||||
    ret = b""
 | 
			
		||||
    while True:
 | 
			
		||||
        if ret.endswith(b"\r\n\r\n"):
 | 
			
		||||
            break
 | 
			
		||||
        elif ret.endswith(b"\r\n\r"):
 | 
			
		||||
            n = 1
 | 
			
		||||
        elif ret.endswith(b"\r\n"):
 | 
			
		||||
            n = 2
 | 
			
		||||
        elif ret.endswith(b"\r"):
 | 
			
		||||
            n = 3
 | 
			
		||||
        else:
 | 
			
		||||
            n = 4
 | 
			
		||||
 | 
			
		||||
        buf = sr.recv(n)
 | 
			
		||||
        buf = sr.recv(1024)
 | 
			
		||||
        if not buf:
 | 
			
		||||
            if not ret:
 | 
			
		||||
                return None
 | 
			
		||||
@@ -332,11 +322,15 @@ def read_header(sr):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        ret += buf
 | 
			
		||||
 | 
			
		||||
        ofs = ret.find(b"\r\n\r\n")
 | 
			
		||||
        if ofs < 0:
 | 
			
		||||
            if len(ret) > 1024 * 64:
 | 
			
		||||
                raise Pebkac(400, "header 2big")
 | 
			
		||||
            else:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
    return ret[:-4].decode("utf-8", "surrogateescape").split("\r\n")
 | 
			
		||||
        sr.unrecv(ret[ofs + 4 :])
 | 
			
		||||
        return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def undot(path):
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
	}
 | 
			
		||||
@@ -424,6 +425,16 @@ blink {
 | 
			
		||||
	html.dark #mw {
 | 
			
		||||
		scrollbar-color: #b80 #282828;
 | 
			
		||||
	}
 | 
			
		||||
	html.dark #toc::-webkit-scrollbar-track {
 | 
			
		||||
		background: #282828;
 | 
			
		||||
	}
 | 
			
		||||
	html.dark #toc::-webkit-scrollbar {
 | 
			
		||||
		background: #282828;
 | 
			
		||||
		width: .8em;
 | 
			
		||||
	}
 | 
			
		||||
	html.dark #toc::-webkit-scrollbar-thumb {
 | 
			
		||||
		background: #eb0;
 | 
			
		||||
	}
 | 
			
		||||
	html.dark #mn.undocked {
 | 
			
		||||
		box-shadow: 0 0 .5em #555;
 | 
			
		||||
		border: none;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,35 @@ 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, "<").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 : ''));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function static(obj) {
 | 
			
		||||
    return JSON.parse(JSON.stringify(obj));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// add navbar
 | 
			
		||||
(function () {
 | 
			
		||||
    var n = document.location + '';
 | 
			
		||||
@@ -28,17 +53,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 +171,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 <code> for each line in <pre>
 | 
			
		||||
    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,7 +191,36 @@ 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 = '<a href="#' + id + '">' + el.innerHTML + '</a>';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    copydom(md_dom, dom_pre, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function init_toc() {
 | 
			
		||||
    var loader = document.getElementById('ml');
 | 
			
		||||
@@ -85,10 +228,8 @@ function init_toc() {
 | 
			
		||||
 | 
			
		||||
    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 +238,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 +251,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 = '<a href="#' + id + '">' +
 | 
			
		||||
                elm.innerHTML + '</a>';
 | 
			
		||||
 | 
			
		||||
            html.push('<li>' + ahref + '</li>');
 | 
			
		||||
            elm.innerHTML = ahref;
 | 
			
		||||
            html.push('<li>' + elm.innerHTML + '</li>');
 | 
			
		||||
 | 
			
		||||
            if (anchor != null)
 | 
			
		||||
                anchors.push(anchor);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,9 @@
 | 
			
		||||
}
 | 
			
		||||
#mw {
 | 
			
		||||
    left: calc(100% - 57em);
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -21,15 +24,17 @@
 | 
			
		||||
}
 | 
			
		||||
#mw.preview,
 | 
			
		||||
#mtw.editor {
 | 
			
		||||
    z-index: 3;
 | 
			
		||||
    z-index: 5;
 | 
			
		||||
}
 | 
			
		||||
#mtw.single,
 | 
			
		||||
#mw.single {
 | 
			
		||||
    left: calc((100% - 58em) / 2);
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    left: 1em;
 | 
			
		||||
    left: max(1em, calc((100% - 58em) / 2));
 | 
			
		||||
}
 | 
			
		||||
#mtw.single {
 | 
			
		||||
    width: 57em;
 | 
			
		||||
    width: min(57em, calc(100% - 2em));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -42,25 +47,30 @@
 | 
			
		||||
    color: #444;
 | 
			
		||||
    background: #f7f7f7;
 | 
			
		||||
    border: 1px solid #999;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    font-family: 'consolas', monospace, monospace;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    word-break: break-word;
 | 
			
		||||
    overflow-wrap: break-word;
 | 
			
		||||
    word-wrap: break-word; /*ie*/
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    line-height: 1.3em;
 | 
			
		||||
    font-size: .9em;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    scrollbar-color: #eb0 #f7f7f7;
 | 
			
		||||
}
 | 
			
		||||
html.dark #mt {
 | 
			
		||||
    color: #eee;
 | 
			
		||||
    background: #222;
 | 
			
		||||
    border: 1px solid #777;
 | 
			
		||||
    scrollbar-color: #b80 #282828;
 | 
			
		||||
}
 | 
			
		||||
#mtr {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 1px;
 | 
			
		||||
    left: 1px;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: 0;
 | 
			
		||||
}
 | 
			
		||||
#save.force-save {
 | 
			
		||||
    color: #400;
 | 
			
		||||
@@ -95,8 +105,4 @@ html.dark #helpbox {
 | 
			
		||||
    border-width: 1px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* dbg:
 | 
			
		||||
#mt {
 | 
			
		||||
    opacity: .5;
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
# mt {opacity: .5;top:1px}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,16 +18,12 @@ 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 = [];
 | 
			
		||||
function genmap(dom) {
 | 
			
		||||
    var ret = [];
 | 
			
		||||
    var last_y = -1;
 | 
			
		||||
    var parent_y = 0;
 | 
			
		||||
    var parent_n = null;
 | 
			
		||||
    var nodes = dom.querySelectorAll('*[data-ln]');
 | 
			
		||||
@@ -53,7 +49,14 @@ function genmap(dom) {
 | 
			
		||||
        while (ln > ret.length)
 | 
			
		||||
            ret.push(null);
 | 
			
		||||
 | 
			
		||||
        ret.push(parent_y + n.offsetTop);
 | 
			
		||||
        var y = parent_y + n.offsetTop;
 | 
			
		||||
        if (y <= last_y)
 | 
			
		||||
            //console.log('awawa');
 | 
			
		||||
            continue;
 | 
			
		||||
 | 
			
		||||
        //console.log('%d  %d  (%d+%d)', a, y, parent_y, n.offsetTop);
 | 
			
		||||
        ret.push(y);
 | 
			
		||||
        last_y = y;
 | 
			
		||||
    }
 | 
			
		||||
    return ret;
 | 
			
		||||
}
 | 
			
		||||
@@ -62,8 +65,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 +82,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 > 150 ? 25 : 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +107,7 @@ redraw = (function () {
 | 
			
		||||
        var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
 | 
			
		||||
        dom_wrap.style.top = y;
 | 
			
		||||
        dom_swrap.style.top = y;
 | 
			
		||||
        dom_ref.style.width = (dom_src.offsetWidth - 4) + 'px';
 | 
			
		||||
        dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';
 | 
			
		||||
        map_src = genmap(dom_ref);
 | 
			
		||||
        map_pre = genmap(dom_pre);
 | 
			
		||||
        dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
 | 
			
		||||
@@ -296,7 +307,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');
 | 
			
		||||
@@ -312,13 +323,26 @@ function save_chk() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// firefox bug: initial selection offset isn't cleared properly through js
 | 
			
		||||
var ff_clearsel = (function () {
 | 
			
		||||
    if (navigator.userAgent.indexOf(') Gecko/') === -1)
 | 
			
		||||
        return function () { }
 | 
			
		||||
 | 
			
		||||
    return function () {
 | 
			
		||||
        var txt = dom_src.value;
 | 
			
		||||
        var y = dom_src.scrollTop;
 | 
			
		||||
        dom_src.value = '';
 | 
			
		||||
        dom_src.value = txt;
 | 
			
		||||
        dom_src.scrollTop = y;
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// returns car/cdr (selection bounds) and n1/n2 (grown to full lines)
 | 
			
		||||
function linebounds(just_car) {
 | 
			
		||||
function linebounds(just_car, greedy_growth) {
 | 
			
		||||
    var car = dom_src.selectionStart,
 | 
			
		||||
        cdr = dom_src.selectionEnd;
 | 
			
		||||
 | 
			
		||||
    dbg(car, cdr);
 | 
			
		||||
 | 
			
		||||
    if (just_car)
 | 
			
		||||
        cdr = car;
 | 
			
		||||
 | 
			
		||||
@@ -326,11 +350,13 @@ function linebounds(just_car) {
 | 
			
		||||
        n1 = Math.max(car, 0),
 | 
			
		||||
        n2 = Math.min(cdr, md.length - 1);
 | 
			
		||||
 | 
			
		||||
    if (greedy_growth !== true) {
 | 
			
		||||
        if (n1 < n2 && md[n1] == '\n')
 | 
			
		||||
            n1++;
 | 
			
		||||
 | 
			
		||||
        if (n1 < n2 && md[n2 - 1] == '\n')
 | 
			
		||||
            n2 -= 2;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    n1 = md.lastIndexOf('\n', n1 - 1) + 1;
 | 
			
		||||
    n2 = md.indexOf('\n', n2);
 | 
			
		||||
@@ -364,12 +390,9 @@ function setsel(s) {
 | 
			
		||||
        s.cdr = s.pre.length + s.sel.length;
 | 
			
		||||
    }
 | 
			
		||||
    dom_src.value = [s.pre, s.sel, s.post].join('');
 | 
			
		||||
    dom_src.setSelectionRange(s.car, s.cdr);
 | 
			
		||||
    try {
 | 
			
		||||
    dom_src.setSelectionRange(s.car, s.cdr, dom_src.selectionDirection);
 | 
			
		||||
    dom_src.oninput();
 | 
			
		||||
}
 | 
			
		||||
    catch (ex) { }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// indent/dedent
 | 
			
		||||
@@ -408,17 +431,32 @@ function md_header(dedent) {
 | 
			
		||||
 | 
			
		||||
// smart-home
 | 
			
		||||
function md_home(shift) {
 | 
			
		||||
    var s = linebounds(!shift),
 | 
			
		||||
    var s = linebounds(false, true),
 | 
			
		||||
        ln = s.md.substring(s.n1, s.n2),
 | 
			
		||||
        m = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/.exec(ln),
 | 
			
		||||
        home = s.n1 + m[0].length,
 | 
			
		||||
        car = (s.car == home) ? s.n1 : home,
 | 
			
		||||
        cdr = shift ? s.cdr : car;
 | 
			
		||||
        dir = dom_src.selectionDirection,
 | 
			
		||||
        rev = dir === 'backward',
 | 
			
		||||
        p1 = rev ? s.car : s.cdr,
 | 
			
		||||
        p2 = rev ? s.cdr : s.car,
 | 
			
		||||
        home = 0,
 | 
			
		||||
        lf = ln.lastIndexOf('\n') + 1,
 | 
			
		||||
        re = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/;
 | 
			
		||||
 | 
			
		||||
    if (car > cdr)
 | 
			
		||||
        car = [cdr, cdr = car][0];
 | 
			
		||||
    if (rev)
 | 
			
		||||
        home = s.n1 + re.exec(ln)[0].length;
 | 
			
		||||
    else
 | 
			
		||||
        home = s.n1 + lf + re.exec(ln.substring(lf))[0].length;
 | 
			
		||||
 | 
			
		||||
    dom_src.setSelectionRange(car, cdr);
 | 
			
		||||
    p1 = (p1 !== home) ? home : (rev ? s.n1 : s.n1 + lf);
 | 
			
		||||
    if (!shift)
 | 
			
		||||
        p2 = p1;
 | 
			
		||||
 | 
			
		||||
    if (rev !== p1 < p2)
 | 
			
		||||
        dir = rev ? 'forward' : 'backward';
 | 
			
		||||
 | 
			
		||||
    if (!shift)
 | 
			
		||||
        ff_clearsel();
 | 
			
		||||
 | 
			
		||||
    dom_src.setSelectionRange(Math.min(p1, p2), Math.max(p1, p2), dir);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -426,9 +464,14 @@ 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);
 | 
			
		||||
        m1 = /^( *)([0-9]+)(\. +)/.exec(ln),
 | 
			
		||||
        m2 = /^[ \t>+-]*(\* )?/.exec(ln);
 | 
			
		||||
 | 
			
		||||
    s.pre = s.md.substring(0, s.car) + '\n' + m[0];
 | 
			
		||||
    var pre = m2[0];
 | 
			
		||||
    if (m1 !== null)
 | 
			
		||||
        pre = m1[1] + (parseInt(m1[2]) + 1) + m1[3];
 | 
			
		||||
 | 
			
		||||
    s.pre = s.md.substring(0, s.car) + '\n' + pre;
 | 
			
		||||
    s.sel = '';
 | 
			
		||||
    s.post = s.md.substring(s.car);
 | 
			
		||||
    s.car = s.cdr = s.pre.length;
 | 
			
		||||
@@ -436,6 +479,25 @@ function md_newline() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// backspace
 | 
			
		||||
function md_backspace() {
 | 
			
		||||
    var s = linebounds(true),
 | 
			
		||||
        ln = s.md.substring(s.n1, s.n2),
 | 
			
		||||
        m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(ln);
 | 
			
		||||
 | 
			
		||||
    var v = m[0].replace(/[^ ]/g, " ");
 | 
			
		||||
    if (v === m[0] || v.length !== ln.length)
 | 
			
		||||
        return true;
 | 
			
		||||
 | 
			
		||||
    s.pre = s.md.substring(0, s.n1) + v;
 | 
			
		||||
    s.sel = '';
 | 
			
		||||
    s.post = s.md.substring(s.car);
 | 
			
		||||
    s.car = s.cdr = s.pre.length;
 | 
			
		||||
    setsel(s);
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// hotkeys / toolbar
 | 
			
		||||
(function () {
 | 
			
		||||
    function keydown(ev) {
 | 
			
		||||
@@ -476,6 +538,9 @@ function md_newline() {
 | 
			
		||||
                action_stack.redo();
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            if (!ctrl && !ev.shiftKey && kc == 8) {
 | 
			
		||||
                return md_backspace();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    document.onkeydown = keydown;
 | 
			
		||||
@@ -499,14 +564,16 @@ document.getElementById('help').onclick = function (e) {
 | 
			
		||||
 | 
			
		||||
// blame steen
 | 
			
		||||
action_stack = (function () {
 | 
			
		||||
    var undos = [];
 | 
			
		||||
    var redos = [];
 | 
			
		||||
    var sched_txt = '';
 | 
			
		||||
    var hist = {
 | 
			
		||||
        un: [],
 | 
			
		||||
        re: []
 | 
			
		||||
    };
 | 
			
		||||
    var sched_cpos = 0;
 | 
			
		||||
    var sched_timer = null;
 | 
			
		||||
    var ignore = false;
 | 
			
		||||
    var ref = dom_src.value;
 | 
			
		||||
 | 
			
		||||
    function diff(from, to) {
 | 
			
		||||
    function diff(from, to, cpos) {
 | 
			
		||||
        if (from === to)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
@@ -532,36 +599,39 @@ action_stack = (function () {
 | 
			
		||||
        return {
 | 
			
		||||
            car: car,
 | 
			
		||||
            cdr: ++p2,
 | 
			
		||||
            txt: txt
 | 
			
		||||
            txt: txt,
 | 
			
		||||
            cpos: cpos
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function undiff(from, change) {
 | 
			
		||||
        return {
 | 
			
		||||
            txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
 | 
			
		||||
            cursor: change.car + change.txt.length
 | 
			
		||||
            cpos: change.cpos
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function apply(src, dst) {
 | 
			
		||||
        dbg('undos(%d) redos(%d)', undos.length, redos.length);
 | 
			
		||||
        dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
 | 
			
		||||
 | 
			
		||||
        if (src.length === 0)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        var state = undiff(ref, src.pop()),
 | 
			
		||||
            change = diff(ref, state.txt);
 | 
			
		||||
        var patch = src.pop(),
 | 
			
		||||
            applied = undiff(ref, patch),
 | 
			
		||||
            cpos = patch.cpos - (patch.cdr - patch.car) + patch.txt.length,
 | 
			
		||||
            reverse = diff(ref, applied.txt, cpos);
 | 
			
		||||
 | 
			
		||||
        if (change === null)
 | 
			
		||||
        if (reverse === null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        dst.push(change);
 | 
			
		||||
        ref = state.txt;
 | 
			
		||||
        dst.push(reverse);
 | 
			
		||||
        ref = applied.txt;
 | 
			
		||||
        ignore = true; // just some browsers
 | 
			
		||||
        dom_src.value = ref;
 | 
			
		||||
        dom_src.setSelectionRange(state.cursor, state.cursor);
 | 
			
		||||
        dom_src.setSelectionRange(cpos, cpos);
 | 
			
		||||
        ignore = true; // all browsers
 | 
			
		||||
        dom_src.oninput();
 | 
			
		||||
        draw_md();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -570,31 +640,36 @@ action_stack = (function () {
 | 
			
		||||
            ignore = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        redos = [];
 | 
			
		||||
        sched_txt = dom_src.value;
 | 
			
		||||
        hist.re = [];
 | 
			
		||||
        clearTimeout(sched_timer);
 | 
			
		||||
        sched_cpos = dom_src.selectionEnd;
 | 
			
		||||
        sched_timer = setTimeout(push, 500);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function undo() {
 | 
			
		||||
        return apply(undos, redos);
 | 
			
		||||
        if (hist.re.length == 0) {
 | 
			
		||||
            clearTimeout(sched_timer);
 | 
			
		||||
            push();
 | 
			
		||||
        }
 | 
			
		||||
        return apply(hist.un, hist.re);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function redo() {
 | 
			
		||||
        return apply(redos, undos);
 | 
			
		||||
        return apply(hist.re, hist.un);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function push() {
 | 
			
		||||
        var change = diff(ref, sched_txt, dom_src.selectionStart);
 | 
			
		||||
        var newtxt = dom_src.value;
 | 
			
		||||
        var change = diff(ref, newtxt, sched_cpos);
 | 
			
		||||
        if (change !== null)
 | 
			
		||||
            undos.push(change);
 | 
			
		||||
            hist.un.push(change);
 | 
			
		||||
 | 
			
		||||
        ref = sched_txt;
 | 
			
		||||
        dbg('undos(%d) redos(%d)', undos.length, redos.length);
 | 
			
		||||
        if (undos.length > 0)
 | 
			
		||||
            dbg(undos.slice(-1)[0]);
 | 
			
		||||
        if (redos.length > 0)
 | 
			
		||||
            dbg(redos.slice(-1)[0]);
 | 
			
		||||
        ref = newtxt;
 | 
			
		||||
        dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
 | 
			
		||||
        if (hist.un.length > 0)
 | 
			
		||||
            dbg(static(hist.un.slice(-1)[0]));
 | 
			
		||||
        if (hist.re.length > 0)
 | 
			
		||||
            dbg(static(hist.re.slice(-1)[0]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
@@ -602,8 +677,18 @@ action_stack = (function () {
 | 
			
		||||
        undo: undo,
 | 
			
		||||
        redo: redo,
 | 
			
		||||
        push: schedule_push,
 | 
			
		||||
        _undos: undos,
 | 
			
		||||
        _redos: redos,
 | 
			
		||||
        _hist: hist,
 | 
			
		||||
        _ref: ref
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
document.getElementById('help').onclick = function () {
 | 
			
		||||
    var c1 = getComputedStyle(dom_src).cssText.split(';');
 | 
			
		||||
    var c2 = getComputedStyle(dom_ref).cssText.split(';');
 | 
			
		||||
    var max = Math.min(c1.length, c2.length);
 | 
			
		||||
    for (var a = 0; a < max; a++)
 | 
			
		||||
        if (c1[a] !== c2[a])
 | 
			
		||||
            console.log(c1[a] + '\n' + c2[a]);
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
@@ -80,3 +80,22 @@ for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS=
 | 
			
		||||
# py2 on osx
 | 
			
		||||
brew install python@2
 | 
			
		||||
pip install virtualenv
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##
 | 
			
		||||
## http 206
 | 
			
		||||
 | 
			
		||||
# az = abcdefghijklmnopqrstuvwxyz
 | 
			
		||||
 | 
			
		||||
printf '%s\r\n' 'GET /az HTTP/1.1' 'Host: ocv.me' 'Range: bytes=5-10' '' | ncat ocv.me 80 
 | 
			
		||||
# Content-Range: bytes 5-10/26
 | 
			
		||||
# Content-Length: 6
 | 
			
		||||
# fghijk
 | 
			
		||||
 | 
			
		||||
Range: bytes=0-1    "ab" Content-Range: bytes 0-1/26
 | 
			
		||||
Range: bytes=24-24  "y"  Content-Range: bytes 24-24/26
 | 
			
		||||
Range: bytes=24-25  "yz" Content-Range: bytes 24-25/26
 | 
			
		||||
Range: bytes=24-    "yz" Content-Range: bytes 24-25/26
 | 
			
		||||
Range: bytes=25-29  "z"  Content-Range: bytes 25-25/26
 | 
			
		||||
Range: bytes=26-         Content-Range: bytes */26
 | 
			
		||||
  HTTP/1.1 416 Requested Range Not Satisfiable
 | 
			
		||||
 
 | 
			
		||||
@@ -180,7 +180,7 @@ diff --git a/src/Parser.js b/src/Parser.js
 | 
			
		||||
+            // similar to tables, writing contents before the <ul> tag
 | 
			
		||||
+            // so update the tag attribute as we go
 | 
			
		||||
+            // (assuming all list entries got tagged with a source-line, probably safe w)
 | 
			
		||||
+            body += this.renderer.tag_ln(item.tokens[0].ln).listitem(itemBody, task, checked);
 | 
			
		||||
+            body += this.renderer.tag_ln((item.tokens[0] || token).ln).listitem(itemBody, task, checked);
 | 
			
		||||
           }
 | 
			
		||||
 
 | 
			
		||||
-          out += this.renderer.list(body, ordered, start);
 | 
			
		||||
 
 | 
			
		||||
@@ -166,3 +166,6 @@ chmod 755 $sfx_out.*
 | 
			
		||||
printf "done:\n"
 | 
			
		||||
printf "  %s\n" "$(realpath $sfx_out)."{sh,py}
 | 
			
		||||
# rm -rf *
 | 
			
		||||
 | 
			
		||||
# tar -tvf ../sfx/tar | sed -r 's/(.* ....-..-.. ..:.. )(.*)/\2 `` \1/' | sort | sed -r 's/(.*) `` (.*)/\2 \1/'| less
 | 
			
		||||
# for n in {1..9}; do tar -tf tar | grep -vE '/$' | sed -r 's/(.*)\.(.*)/\2.\1/' | sort | sed -r 's/([^\.]+)\.(.*)/\2.\1/' | tar -cT- | bzip2 -c$n | wc -c; done 
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user