Compare commits

..

17 Commits

Author SHA1 Message Date
ed
dab3407beb v0.4.0 2020-05-13 00:44:23 +02:00
ed
592987a54a support smol screens 2020-05-13 00:39:29 +02:00
ed
8dca8326f7 osx fixes + shrinking 2020-05-12 22:36:21 +02:00
ed
633481fae3 fix preview 2020-05-12 21:11:38 +02:00
ed
e7b99e6fb7 (ノ ゚ヮ゚)ノ 彡┻━┻ 2020-05-12 20:56:42 +02:00
ed
2a6a3aedd0 shrink sfx some more 2020-05-12 00:26:40 +02:00
ed
866c74c841 autoindent 2020-05-12 00:00:54 +02:00
ed
dad92bde26 smart-home 2020-05-11 22:04:02 +02:00
ed
a994e034f7 lol wow 2020-05-11 02:07:21 +02:00
ed
2801c04f2e bit too aggressive 2020-05-11 01:56:26 +02:00
ed
316e3abfab NIH! NIH! NIH! 2020-05-11 01:38:30 +02:00
ed
c15ecb6c8e ver 0.3.1 2020-05-07 00:20:22 +02:00
ed
ee96005026 sortable file list 2020-05-07 00:08:06 +02:00
ed
5b55d05a20 ux tweaks 2020-05-06 23:40:36 +02:00
ed
2f09c62c4e indicate version history in the browser 2020-05-06 23:10:30 +02:00
ed
1cc8b873d4 add missing urldecodes in js 2020-05-06 23:07:18 +02:00
ed
15d5859750 deal with illegal filenames on windows 2020-05-06 23:06:26 +02:00
28 changed files with 1298 additions and 171 deletions

10
.gitignore vendored
View File

@@ -11,14 +11,12 @@ dist/
sfx/
.venv/
# sublime
# ide
*.sublime-workspace
# winmerge
*.bak
# other licenses
contrib/
# deps
copyparty/web/deps
# derived
copyparty/web/deps/
srv/

View File

@@ -104,10 +104,8 @@ in the `scripts` folder:
roughly sorted by priority
* sortable browser columns
* up2k handle filename too long
* up2k fails on empty files? alert then stuck
* unexpected filepath on dupe up2k
* drop onto folders
* look into android thumbnail cache file format
* support pillow-simd

View File

@@ -16,6 +16,8 @@ if platform.system() == "Windows":
VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393]
# introduced in anniversary update
MACOS = platform.system() == "Darwin"
class EnvParams(object):
def __init__(self):

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (0, 3, 0)
CODENAME = "docuparty"
BUILD_DT = (2020, 5, 6)
VERSION = (0, 4, 0)
CODENAME = "NIH"
BUILD_DT = (2020, 5, 13)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -4,7 +4,6 @@ from __future__ import print_function, unicode_literals
import traceback
from .__init__ import PY2
from .util import Pebkac, Queue

View File

@@ -420,9 +420,11 @@ class HttpCli(object):
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
self._assert_safe_rem(rem)
sanitized = sanitize_fn(new_dir)
if not nullwrite:
fdir = os.path.join(vfs.realpath, rem)
fn = os.path.join(fdir, sanitize_fn(new_dir))
fn = os.path.join(fdir, sanitized)
if not os.path.isdir(fsenc(fdir)):
raise Pebkac(500, "parent folder does not exist")
@@ -435,7 +437,7 @@ class HttpCli(object):
except:
raise Pebkac(500, "mkdir failed, check the logs")
vpath = "{}/{}".format(self.vpath, new_dir).lstrip("/")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.conn.tpl_msg.render(
h2='<a href="/{}">go to /{}</a>'.format(
quotep(vpath), html_escape(vpath, quote=False)
@@ -457,9 +459,11 @@ class HttpCli(object):
if not new_file.endswith(".md"):
new_file += ".md"
sanitized = sanitize_fn(new_file)
if not nullwrite:
fdir = os.path.join(vfs.realpath, rem)
fn = os.path.join(fdir, sanitize_fn(new_file))
fn = os.path.join(fdir, sanitized)
if os.path.exists(fsenc(fn)):
raise Pebkac(500, "that file exists already")
@@ -467,7 +471,7 @@ class HttpCli(object):
with open(fsenc(fn), "wb") as f:
f.write(b"`GRUNNUR`\n")
vpath = "{}/{}".format(self.vpath, new_file).lstrip("/")
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
html = self.conn.tpl_msg.render(
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
quotep(vpath), html_escape(vpath, quote=False)
@@ -600,10 +604,10 @@ class HttpCli(object):
self.reply(response.encode("utf-8"))
return True
fn = os.path.join(vfs.realpath, rem)
fp = os.path.join(vfs.realpath, rem)
srv_lastmod = -1
try:
st = os.stat(fsenc(fn))
st = os.stat(fsenc(fp))
srv_lastmod = st.st_mtime
srv_lastmod3 = int(srv_lastmod * 1000)
except OSError as ex:
@@ -631,16 +635,22 @@ class HttpCli(object):
return True
# TODO another hack re: pending permissions rework
os.rename(fn, "{}.bak-{:.3f}.md".format(fn[:-3], srv_lastmod))
mdir, mfile = os.path.split(fp)
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
try:
os.mkdir(os.path.join(mdir, ".hist"))
except:
pass
os.rename(fp, os.path.join(mdir, ".hist", mfile2))
p_field, _, p_data = next(self.parser.gen)
if p_field != "body":
raise Pebkac(400, "expected body, got {}".format(p_field))
with open(fn, "wb") as f:
with open(fp, "wb") as f:
sz, sha512, _ = hashcopy(self.conn, p_data, f)
new_lastmod = os.stat(fsenc(fn)).st_mtime
new_lastmod = os.stat(fsenc(fp)).st_mtime
new_lastmod3 = int(new_lastmod * 1000)
sha512 = sha512[:56]
@@ -824,7 +834,7 @@ class HttpCli(object):
def tx_md(self, fs_path):
logmsg = "{:4} {} ".format("", self.req)
if "edit" in self.uparam:
if "edit2" in self.uparam:
html_path = "web/mde.html"
template = self.conn.tpl_mde
else:
@@ -834,18 +844,26 @@ class HttpCli(object):
html_path = os.path.join(E.mod, html_path)
st = os.stat(fsenc(fs_path))
sz_md = st.st_size
# sz_md = st.st_size
ts_md = st.st_mtime
st = os.stat(fsenc(html_path))
ts_html = st.st_mtime
# TODO dont load into memory ;_;
# (trivial fix, count the &'s)
with open(fsenc(fs_path), "rb") as f:
md = f.read().replace(b"&", b"&amp;")
sz_md = len(md)
file_ts = max(ts_md, ts_html)
file_lastmod, do_send = self._chk_lastmod(file_ts)
self.out_headers["Last-Modified"] = file_lastmod
self.out_headers["Cache-Control"] = "no-cache"
status = 200 if do_send else 304
targs = {
"edit": "edit" in self.uparam,
"title": html_escape(self.vpath, quote=False),
"lastmod": int(ts_md * 1000),
"md": "",
@@ -858,9 +876,7 @@ class HttpCli(object):
self.log(logmsg)
return True
with open(fsenc(fs_path), "rb") as f:
md = f.read()
# TODO jinja2 can stream this right?
targs["md"] = md.decode("utf-8", "replace")
html = template.render(**targs).encode("utf-8")
try:
@@ -909,12 +925,30 @@ class HttpCli(object):
fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname)
vfs_ls.extend(vfs_virt.keys())
# check for old versions of files,
hist = {} # [num-backups, most-recent, hist-path]
histdir = os.path.join(fsroot, ".hist")
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
try:
for hfn in os.listdir(histdir):
m = ptn.match(hfn)
if not m:
continue
fn = m.group(1) + m.group(3)
n, ts, _ = hist.get(fn, [0, 0, ""])
hist[fn] = [n + 1, max(ts, float(m.group(2))), hfn]
except:
pass
dirs = []
files = []
for fn in exclude_dotfiles(vfs_ls):
base = ""
href = fn
if self.absolute_urls and vpath:
href = "/" + vpath + "/" + fn
base = "/" + vpath + "/"
href = base + fn
if fn in vfs_virt:
fspath = vfs_virt[fn].realpath
@@ -931,6 +965,10 @@ class HttpCli(object):
if is_dir:
margin = "DIR"
href += "/"
elif fn in hist:
margin = '<a href="{}.hist/{}">#{}</a>'.format(
base, html_escape(hist[fn][2], quote=True), hist[fn][0]
)
else:
margin = "-"

View File

@@ -6,7 +6,7 @@ import time
import socket
import threading
from .__init__ import E
from .__init__ import E, MACOS
from .httpconn import HttpConn
from .authsrv import AuthSrv
@@ -75,9 +75,11 @@ class HttpSrv(object):
sck.shutdown(socket.SHUT_RDWR)
sck.close()
except (OSError, socket.error) as ex:
self.log(
"%s %s" % addr, "shut_rdwr err:\n {}\n {}".format(repr(sck), ex),
)
if not MACOS:
self.log(
"%s %s" % addr,
"shut_rdwr err:\n {}\n {}".format(repr(sck), ex),
)
if ex.errno not in [10038, 107, 57, 9]:
# 10038 No longer considered a socket
# 107 Transport endpoint not connected

View File

@@ -8,7 +8,7 @@ import threading
from datetime import datetime, timedelta
import calendar
from .__init__ import PY2, WINDOWS, VT100
from .__init__ import PY2, WINDOWS, MACOS, VT100
from .tcpsrv import TcpSrv
from .up2k import Up2k
from .util import mp
@@ -111,6 +111,8 @@ class SvcHub(object):
return msg
elif vmin < 3:
return msg
elif MACOS:
return "multiprocessing is wonky on mac osx;"
else:
msg = "need python 2.7 or 3.3+ for multiprocessing;"
if not PY2 and vmin < 3:
@@ -133,7 +135,7 @@ class SvcHub(object):
if mp.cpu_count() <= 1:
return False
try:
# support vscode debugger (bonus: same behavior as on windows)
mp.set_start_method("spawn", True)

View File

@@ -24,7 +24,7 @@ class TcpSrv(object):
ip = "127.0.0.1"
eps = {ip: "local only"}
if self.args.i != ip:
eps = self.detect_interfaces(self.args.i) or eps
eps = self.detect_interfaces(self.args.i) or {self.args.i: "external"}
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
self.log(

View File

@@ -13,7 +13,7 @@ import threading
from copy import deepcopy
from .__init__ import WINDOWS
from .util import Pebkac, Queue, fsenc
from .util import Pebkac, Queue, fsenc, sanitize_fn
class Up2k(object):
@@ -48,6 +48,7 @@ class Up2k(object):
self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
def handle_json(self, cj):
cj["name"] = sanitize_fn(cj["name"])
wark = self._get_wark(cj)
now = time.time()
with self.mutex:

View File

@@ -356,7 +356,30 @@ def undot(path):
def sanitize_fn(fn):
return fn.replace("\\", "/").split("/")[-1].strip()
fn = fn.replace("\\", "/").split("/")[-1]
if WINDOWS:
for bad, good in [
["<", ""],
[">", ""],
[":", ""],
['"', ""],
["/", ""],
["\\", ""],
["|", ""],
["?", ""],
["*", ""],
]:
fn = fn.replace(bad, good)
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += "com{0} lpt{0}".format(n).split(" ")
if fn.lower() in bad:
fn = "_" + fn
return fn.strip()
def exclude_dotfiles(filepaths):

View File

@@ -68,7 +68,7 @@ a {
}
#files thead th:last-child {
background: #444;
border-radius: .7em 0 0 0;
border-radius: .7em .7em 0 0;
}
#files thead th:first-child {
background: #222;

View File

@@ -34,6 +34,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
}
document.body.style.fontSize = '0.8em';
document.body.style.padding = '0 1em 1em 1em';
hcroak(html.join('\n'));
};
@@ -78,6 +79,39 @@ function ev(e) {
}
function sortTable(table, col) {
var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
th = table.tHead.rows[0].cells,
tr = Array.prototype.slice.call(tb.rows, 0),
i, reverse = th[col].className == 'sort1' ? -1 : 1;
for (var a = 0, thl = th.length; a < thl; a++)
th[a].className = '';
th[col].className = 'sort' + reverse;
var stype = th[col].getAttribute('sort');
tr = tr.sort(function (a, b) {
var v1 = a.cells[col].textContent.trim();
var v2 = b.cells[col].textContent.trim();
if (stype == 'int') {
v1 = parseInt(v1.replace(/,/g, ''));
v2 = parseInt(v2.replace(/,/g, ''));
return reverse * (v1 - v2);
}
return reverse * (v1.localeCompare(v2));
});
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[i]);
}
function makeSortable(table) {
var th = table.tHead, i;
th && (th = th.rows[0]) && (th = th.cells);
if (th) i = th.length;
else return; // if no `<thead>` then do nothing
while (--i >= 0) (function (i) {
th[i].addEventListener('click', function () { sortTable(table, i) });
}(i));
}
makeSortable(o('files'));
// extract songs + add play column
var mp = (function () {
var tracks = [];

View File

@@ -4,10 +4,11 @@ html, body {
font-family: sans-serif;
line-height: 1.5em;
}
#mtw {
display: none;
}
#mw {
width: 48.5em;
margin: 0 auto;
margin-bottom: 6em;
}
pre, code, a {
color: #480;
@@ -76,6 +77,9 @@ h2 {
padding-left: .4em;
margin-top: 3em;
}
h3 {
border-bottom: .1em solid #999;
}
h1 a, h3 a, h5 a,
h2 a, h4 a, h6 a {
color: inherit;
@@ -84,19 +88,19 @@ h2 a, h4 a, h6 a {
padding: 0;
margin: 0;
}
#m ul,
#m ol {
#mp ul,
#mp ol {
border-left: .3em solid #ddd;
}
#m>ul,
#m>ol {
border-color: #bbb;
}
#m ul>li {
#mp ul>li {
list-style-type: disc;
}
#m ul>li,
#m ol>li {
#mp ul>li,
#mp ol>li {
margin: .7em 0;
}
p>em,
@@ -116,8 +120,9 @@ small {
opacity: .8;
}
#toc {
width: 48.5em;
margin: 0 auto;
margin: 0 1em;
-ms-scroll-chaining: none;
overscroll-behavior-y: none;
}
#toc ul {
padding-left: 1em;
@@ -181,10 +186,24 @@ blink {
opacity: 1;
}
}
@media screen {
html, body {
margin: 0;
padding: 0;
outline: 0;
border: none;
width: 100%;
height: 100%;
}
#mw {
padding: 0 1em;
margin: 0 auto;
right: 0;
}
#mp {
max-width: 54em;
margin-bottom: 6em;
}
a {
color: #fff;
@@ -212,15 +231,23 @@ blink {
padding: .5em 0;
}
#mn {
font-weight: normal;
padding: 1.3em 0 .7em 1em;
font-size: 1.4em;
border-bottom: 1px solid #ccc;
background: #eee;
z-index: 10;
width: calc(100% - 1em);
}
#mn.undocked {
position: fixed;
padding: 1.2em 0 1em 1em;
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.3);
background: #f7f7f7;
}
#mn a {
color: #444;
background: none;
margin: 0 0 0 -.2em;
padding: 0 0 0 .4em;
padding: .3em 0 .3em .4em;
text-decoration: none;
border: none;
/* ie: */
@@ -239,8 +266,8 @@ blink {
height: 1.05em;
margin: -.2em .3em -.2em -.4em;
display: inline-block;
border: 1px solid rgba(0,0,0,0.3);
border-width: .05em .05em 0 0;
border: 1px solid rgba(0,0,0,0.2);
border-width: .2em .2em 0 0;
transform: rotate(45deg);
}
#mn a:hover {
@@ -248,7 +275,19 @@ blink {
text-decoration: underline;
}
#mh {
margin: 0 0 1.5em 0;
padding: .4em 1em;
position: relative;
width: 100%;
width: calc(100% - 3em);
background: #eee;
z-index: 9;
top: 0;
}
#mh a {
color: #444;
background: none;
text-decoration: underline;
border: none;
}
@@ -270,13 +309,12 @@ blink {
html.dark #toc li {
border-width: 0;
}
html.dark #m a,
html.dark #mh a {
html.dark #mp a {
background: #057;
}
html.dark #m h1 a, html.dark #m h4 a,
html.dark #m h2 a, html.dark #m h5 a,
html.dark #m h3 a, html.dark #m h6 a {
html.dark #mp h1 a, html.dark #mp h4 a,
html.dark #mp h2 a, html.dark #mp h5 a,
html.dark #mp h3 a, html.dark #mp h6 a {
color: inherit;
background: none;
}
@@ -286,8 +324,8 @@ blink {
background: #1a1a1a;
border: .07em solid #333;
}
html.dark #m ul,
html.dark #m ol {
html.dark #mp ul,
html.dark #mp ol {
border-color: #444;
}
html.dark #m>ul,
@@ -322,26 +360,44 @@ blink {
html.dark #mn a {
color: #ccc;
}
html.dark #mn {
border-bottom: 1px solid #333;
}
html.dark #mn,
html.dark #mh {
background: #222;
}
html.dark #mh a {
color: #ccc;
background: none;
}
}
@media screen and (min-width: 64em) {
@media screen and (min-width: 70em) {
#mw {
margin-left: 14em;
margin-left: calc(100% - 50em);
position: fixed;
overflow-y: auto;
left: 14em;
left: calc(100% - 57em);
max-width: none;
bottom: 0;
scrollbar-color: #eb0 #f7f7f7;
}
#toc {
width: 13em;
width: calc(100% - 52.3em);
width: calc(100% - 57.3em);
max-width: 30em;
background: #eee;
position: fixed;
overflow-y: auto;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
bottom: 0;
padding: 0;
margin: 0;
box-shadow: 0 0 1em #ccc;
scrollbar-color: #eb0 #f7f7f7;
xscrollbar-width: thin;
box-shadow: 0 0 1em rgba(0,0,0,0.1);
border-top: 1px solid #d7d7d7;
}
#toc li {
border-left: .3em solid #ccc;
@@ -361,13 +417,22 @@ blink {
html.dark #toc {
background: #282828;
border-top: 1px solid #2c2c2c;
box-shadow: 0 0 1em #181818;
}
html.dark #toc,
html.dark #mw {
scrollbar-color: #b80 #282828;
}
html.dark #mn.undocked {
box-shadow: 0 0 .5em #555;
border: none;
background: #0a0a0a;
}
}
@media screen and (min-width: 84em) {
@media screen and (min-width: 87.5em) {
#toc { width: 30em }
#mw { margin-left: 32em }
#mw { left: 30.5em }
}
@media print {
a {

View File

@@ -4,32 +4,73 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.7">
<link href="/.cpr/md.css" rel="stylesheet">
{%- if edit %}
<link href="/.cpr/md2.css" rel="stylesheet">
{%- endif %}
</head>
<body>
<div id="mn"></div>
<div id="mn">navbar</div>
<div id="mh">
<a id="lightswitch" href="#">go dark</a>
<a id="navtoggle" href="#">hide nav</a>
{%- if edit %}
<a id="save" href="?edit">save</a>
<a id="sbs" href="#">sbs</a>
<a id="nsbs" href="#">editor</a>
<a id="help" href="#">help</a>
{%- else %}
<a href="?edit">edit (basic)</a>
<a href="?edit2">edit (fancy)</a>
{%- endif %}
</div>
<div id="toc"></div>
<div id="mtw">
<textarea id="mt">{{ md }}</textarea>
</div>
<div id="mw">
<div id="mh">
<a id="lightswitch" href="#">go dark</a> //
<a id="edit" href="?edit">edit this</a>
</div>
<div id="ml">
<div style="text-align:center;margin:5em 0">
<div style="font-size:2em;margin:1em 0">Loading</div>
if you're still reading this, check that javascript is allowed
</div>
</div>
<div id="m">
<textarea id="mt" style="display:none">{{ md }}</textarea>
</div>
<div id="mp"></div>
</div>
{%- if edit %}
<div id="helpbox">
<textarea>
write markdown (html is permitted)
### hotkey list
* `Ctrl-S` to save
* `Ctrl-H` / `Ctrl-Shift-H` to create a header
* `TAB` / `Shift-TAB` to indent/dedent a selection
### toolbar
1. toggle dark mode
2. show/hide navigation bar
3. save changes on server
4. side-by-side editing
5. toggle editor/preview
6. this thing :^)
.
</textarea>
</div>
{%- endif %}
<script>
var link_md_as_html = false; // TODO (does nothing)
var last_modified = {{ lastmod }};
(function () {
var btn = document.getElementById("lightswitch");
var toggle = function () {
var toggle = function (e) {
if (e) e.preventDefault();
var dark = !document.documentElement.getAttribute("class");
document.documentElement.setAttribute("class", dark ? "dark" : "");
btn.innerHTML = "go " + (dark ? "light" : "dark");
@@ -41,7 +82,17 @@ var link_md_as_html = false; // TODO (does nothing)
toggle();
})();
if (!String.startsWith) {
String.prototype.startsWith = function(s, i) {
i = i>0 ? i|0 : 0;
return this.substring(i, i + s.length) === s;
};
}
</script>
<script src="/.cpr/deps/marked.full.js"></script>
<script src="/.cpr/md.js"></script>
{%- if edit %}
<script src="/.cpr/md2.js"></script>
{%- endif %}
</body></html>

View File

@@ -1,17 +1,16 @@
/*var conv = new showdown.Converter();
conv.setFlavor('github');
conv.setOption('tasklists', 0);
var mhtml = conv.makeHtml(dom_md.value);
*/
var dom_toc = document.getElementById('toc');
var dom_wrap = document.getElementById('mw');
var dom_head = document.getElementById('mh');
var dom_hbar = document.getElementById('mh');
var dom_nav = document.getElementById('mn');
var dom_doc = document.getElementById('m');
var dom_md = document.getElementById('mt');
var dom_pre = document.getElementById('mp');
var dom_src = document.getElementById('mt');
var dom_navtgl = document.getElementById('navtoggle');
// add toolbar buttons
function hesc(txt) {
return txt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// add navbar
(function () {
var n = document.location + '';
n = n.substr(n.indexOf('//') + 2).split('?')[0].split('/');
@@ -22,7 +21,9 @@ var dom_md = document.getElementById('mt');
if (a > 0)
loc.push(n[a]);
nav.push('<a href="/' + loc.join('/') + '">' + n[a] + '</a>');
var dec = hesc(decodeURIComponent(n[a]));
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
}
dom_nav.innerHTML = nav.join('');
})();
@@ -34,13 +35,10 @@ function convert_markdown(md_text) {
gfm: true
});
var html = marked(md_text);
dom_doc.innerHTML = html;
var loader = document.getElementById('ml');
loader.parentNode.removeChild(loader);
dom_pre.innerHTML = html;
// todo-lists (should probably be a marked extension)
var nodes = dom_doc.getElementsByTagName('input');
var nodes = dom_pre.getElementsByTagName('input');
for (var a = nodes.length - 1; a >= 0; a--) {
var dom_box = nodes[a];
if (dom_box.getAttribute('type') !== 'checkbox')
@@ -59,9 +57,32 @@ function convert_markdown(md_text) {
'<span class="todo_' + clas + '">' + char + '</span>' +
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];
var is_precode =
el.tagName == 'PRE' &&
el.childNodes.length === 1 &&
el.childNodes[0].tagName == 'CODE';
if (!is_precode)
continue;
var nline = parseInt(el.getAttribute('data-ln')) + 1;
var lines = el.innerHTML.replace(/\r?\n<\/code>$/i, '</code>').split(/\r?\n/g);
for (var b = 0; b < lines.length - 1; b++)
lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">';
el.innerHTML = lines.join('');
}
}
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
@@ -69,7 +90,7 @@ function init_toc() {
var lv = 0; // current indentation level in the toc html
var re = new RegExp('^[Hh]([1-3])');
var manip_nodes_dyn = dom_doc.getElementsByTagName('*');
var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
var manip_nodes = [];
for (var a = 0, aa = manip_nodes_dyn.length; a < aa; a++)
manip_nodes.push(manip_nodes_dyn[a]);
@@ -77,16 +98,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 is_header =
m !== null;
var is_precode =
!is_header &&
elm.tagName == 'PRE' &&
elm.childNodes.length === 1 &&
elm.childNodes[0].tagName == 'CODE';
var is_header = m !== null;
if (is_header) {
var nlv = m[1];
while (lv < nlv) {
@@ -125,17 +137,6 @@ function init_toc() {
y: null
};
}
else if (is_precode) {
// not actually toc-related (sorry),
// split <pre><code /></pre> into one <code> per line
var nline = parseInt(elm.getAttribute('data-ln')) + 1;
var lines = elm.innerHTML.replace(/\r?\n<\/code>$/i, '</code>').split(/\r?\n/g);
for (var b = 0; b < lines.length - 1; b++)
lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">';
elm.innerHTML = lines.join('');
}
if (!is_header && anchor)
anchor.kids.push(elm);
}
@@ -207,41 +208,77 @@ function init_toc() {
// "main" :p
convert_markdown(dom_md.value);
convert_markdown(dom_src.value);
var toc = init_toc();
// scroll handler
(function () {
var timer_active = false;
var final = null;
var redraw = (function () {
var sbs = false;
function onresize() {
sbs = window.matchMedia('(min-width: 64em)').matches;
var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
if (sbs) {
dom_toc.style.top = y;
dom_wrap.style.top = y;
dom_toc.style.marginTop = '0';
}
onscroll();
}
function onscroll() {
clearTimeout(final);
timer_active = false;
toc.refresh();
var y = 0;
if (window.matchMedia('(min-width: 64em)').matches)
y = parseInt(dom_nav.offsetHeight) - window.scrollY;
dom_toc.style.marginTop = y < 0 ? 0 : y + "px";
}
onscroll();
function ev_onscroll() {
// long timeout: scroll ended
clearTimeout(final);
final = setTimeout(onscroll, 100);
window.onresize = onresize;
window.onscroll = onscroll;
dom_wrap.onscroll = onscroll;
// short timeout: continuous updates
if (timer_active)
onresize();
return onresize;
})();
dom_navtgl.onclick = function () {
var timeout = null;
function show_nav(e) {
if (e && e.target == dom_hbar && e.pageX && e.pageX < dom_hbar.offsetWidth / 2)
return;
timer_active = true;
setTimeout(onscroll, 10);
};
clearTimeout(timeout);
dom_nav.style.display = 'block';
}
function hide_nav() {
clearTimeout(timeout);
timeout = setTimeout(function () {
dom_nav.style.display = 'none';
}, 30);
}
var hidden = dom_navtgl.innerHTML == 'hide nav';
dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav';
if (hidden) {
dom_nav.setAttribute('class', 'undocked');
dom_nav.style.display = 'none';
dom_nav.style.top = dom_hbar.offsetHeight + 'px';
dom_nav.onmouseenter = show_nav;
dom_nav.onmouseleave = hide_nav;
dom_hbar.onmouseenter = show_nav;
dom_hbar.onmouseleave = hide_nav;
}
else {
dom_nav.setAttribute('class', '');
dom_nav.style.display = 'block';
dom_nav.style.top = '0';
dom_nav.onmouseenter = null;
dom_nav.onmouseleave = null;
dom_hbar.onmouseenter = null;
dom_hbar.onmouseleave = null;
}
if (window.localStorage)
localStorage.setItem('hidenav', hidden ? 1 : 0);
window.onscroll = ev_onscroll;
window.onresize = ev_onscroll;
})();
redraw();
};
if (window.localStorage && localStorage.getItem('hidenav') == 1)
dom_navtgl.onclick();

102
copyparty/web/md2.css Normal file
View File

@@ -0,0 +1,102 @@
#toc {
display: none;
}
#mtw {
display: block;
position: fixed;
left: 0;
bottom: 0;
width: calc(100% - 58em);
}
#mw {
left: calc(100% - 57em);
}
/* single-screen */
#mtw.preview,
#mw.editor {
opacity: 0;
z-index: 1;
}
#mw.preview,
#mtw.editor {
z-index: 3;
}
#mtw.single,
#mw.single {
left: calc((100% - 58em) / 2);
margin: 0;
}
#mtw.single {
width: 57em;
}
#mp {
position: relative;
}
#mt, #mtr {
width: 100%;
height: calc(100% - 5px);
color: #444;
background: #f7f7f7;
border: 1px solid #999;
font-family: 'consolas', monospace, monospace;
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: break-word;
word-wrap: break-word; /*ie*/
overflow-y: scroll;
line-height: 1.3em;
font-size: .9em;
position: relative;
}
html.dark #mt {
color: #eee;
background: #222;
border: 1px solid #777;
}
#mtr {
position: absolute;
top: 1px;
left: 1px;
}
#save.force-save {
color: #400;
background: #f97;
border-radius: .15em;
}
#save.disabled {
opacity: .4;
}
#helpbox {
display: none;
position: fixed;
background: #f7f7f7;
box-shadow: 0 .5em 2em #777;
border-radius: .4em;
padding: 2em;
top: 4em;
overflow-y: auto;
height: calc(100% - 12em);
left: calc(50% - 15em);
right: 0;
width: 30em;
z-index: 9001;
}
#helpclose {
display: block;
}
html.dark #helpbox {
background: #222;
box-shadow: 0 .5em 2em #444;
border: 1px solid #079;
border-width: 1px 0;
}
/* dbg:
#mt {
opacity: .5;
}
*/

609
copyparty/web/md2.js Normal file
View File

@@ -0,0 +1,609 @@
// server state
var server_md = dom_src.value;
// dom nodes
var dom_swrap = document.getElementById('mtw');
var dom_sbs = document.getElementById('sbs');
var dom_nsbs = document.getElementById('nsbs');
var dom_ref = (function () {
var d = document.createElement('div');
d.setAttribute('id', 'mtr');
dom_swrap.appendChild(d);
d = document.getElementById('mtr');
// hide behind the textarea (offsetTop is not computed if display:none)
dom_src.style.zIndex = '4';
d.style.zIndex = '3';
return d;
})();
// 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 parent_y = 0;
var parent_n = null;
var nodes = dom.querySelectorAll('*[data-ln]');
for (var a = 0; a < nodes.length; a++) {
var n = nodes[a];
var ln = parseInt(n.getAttribute('data-ln'));
if (ln in ret)
continue;
var y = 0;
var par = n.offsetParent;
if (par != parent_n) {
while (par && par != dom) {
y += par.offsetTop;
par = par.offsetParent;
}
if (par != dom)
continue;
parent_y = y;
parent_n = n.offsetParent;
}
while (ln > ret.length)
ret.push(null);
ret.push(parent_y + n.offsetTop);
}
return ret;
}
// input handler
var action_stack = null;
var nlines = 0;
(function () {
dom_src.oninput = function (e) {
var src = dom_src.value;
convert_markdown(src);
var lines = hesc(src).replace(/\r/g, "").split('\n');
nlines = lines.length;
var html = [];
for (var a = 0; a < lines.length; a++)
html.push('<span data-ln="' + (a + 1) + '">' + lines[a] + "</span>");
dom_ref.innerHTML = html.join('\n');
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';
sb.setAttribute('class', cl);
if (action_stack)
action_stack.push();
}
dom_src.oninput();
})();
// resize handler
redraw = (function () {
function onresize() {
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';
map_src = genmap(dom_ref);
map_pre = genmap(dom_pre);
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
}
function setsbs() {
dom_wrap.setAttribute('class', '');
dom_swrap.setAttribute('class', '');
onresize();
}
function modetoggle() {
mode = dom_nsbs.innerHTML;
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
mode += ' single';
dom_wrap.setAttribute('class', mode);
dom_swrap.setAttribute('class', mode);
onresize();
}
window.onresize = onresize;
window.onscroll = null;
dom_wrap.onscroll = null;
dom_sbs.onclick = setsbs;
dom_nsbs.onclick = modetoggle;
onresize();
return onresize;
})();
// scroll handlers
(function () {
var skip_src = false, skip_pre = false;
function scroll(src, srcmap, dst, dstmap) {
var y = src.scrollTop;
if (y < 8) {
dst.scrollTop = 0;
return;
}
if (y + 8 + src.clientHeight > src.scrollHeight) {
dst.scrollTop = dst.scrollHeight - dst.clientHeight;
return;
}
y += src.clientHeight / 2;
var sy1 = -1, sy2 = -1, dy1 = -1, dy2 = -1;
for (var a = 1; a < nlines + 1; a++) {
if (srcmap[a] === null || dstmap[a] === null)
continue;
if (srcmap[a] > y) {
sy2 = srcmap[a];
dy2 = dstmap[a];
break;
}
sy1 = srcmap[a];
dy1 = dstmap[a];
}
if (sy1 == -1)
return;
var dy = dy1;
if (sy2 != -1 && dy2 != -1) {
var mul = (y - sy1) / (sy2 - sy1);
dy = dy1 + (dy2 - dy1) * mul;
}
dst.scrollTop = dy - dst.clientHeight / 2;
}
dom_src.onscroll = function () {
//dbg: dom_ref.scrollTop = dom_src.scrollTop;
if (skip_src) {
skip_src = false;
return;
}
skip_pre = true;
scroll(dom_src, map_src, dom_wrap, map_pre);
};
dom_wrap.onscroll = function () {
if (skip_pre) {
skip_pre = false;
return;
}
skip_src = true;
scroll(dom_wrap, map_pre, dom_src, map_src);
};
})();
// save handler
function save(e) {
if (e) e.preventDefault();
var save_btn = document.getElementById("save"),
save_cls = save_btn.getAttribute('class') + '';
if (save_cls.indexOf('disabled') >= 0) {
alert('there is nothing to save');
return;
}
var force = (save_cls.indexOf('force-save') >= 0);
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
alert('ok, aborted');
return;
}
var txt = dom_src.value;
var fd = new FormData();
fd.append("act", "tput");
fd.append("lastmod", (force ? -1 : last_modified));
fd.append("body", txt);
var url = (document.location + '').split('?')[0] + '?raw';
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = save_cb;
xhr.btn = save_btn;
xhr.txt = txt;
xhr.send(fd);
}
function save_cb() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) {
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
return;
}
var r;
try {
r = JSON.parse(this.responseText);
}
catch (ex) {
alert('Failed to parse reply from server:\n\n' + this.responseText);
return;
}
if (!r.ok) {
if (!this.btn.classList.contains('force-save')) {
this.btn.classList.add('force-save');
var msg = [
'This file has been modified since you started editing it!\n',
'if you really want to overwrite, press save again.\n',
'modified ' + ((r.now - r.lastmod) / 1000) + ' seconds ago,',
((r.lastmod - last_modified) / 1000) + ' sec after you opened it\n',
last_modified + ' lastmod when you opened it,',
r.lastmod + ' lastmod on the server now,',
r.now + ' server time now,\n',
];
alert(msg.join('\n'));
}
else {
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
}
return;
}
this.btn.classList.remove('force-save');
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?raw';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = save_chk;
xhr.btn = this.save_btn;
xhr.txt = this.txt;
xhr.lastmod = r.lastmod;
xhr.send();
}
function save_chk() {
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) {
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
return;
}
var doc1 = this.txt.replace(/\r\n/g, "\n");
var doc2 = this.responseText.replace(/\r\n/g, "\n");
if (doc1 != doc2) {
alert(
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
'Length: yours=' + doc1.length + ', server=' + doc2.length
);
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
return;
}
last_modified = this.lastmod;
server_md = this.txt;
dom_src.oninput();
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');
ok.innerHTML = 'OK✔';
var parent = document.getElementById('m');
document.documentElement.appendChild(ok);
setTimeout(function () {
ok.style.opacity = 0;
}, 500);
setTimeout(function () {
ok.parentNode.removeChild(ok);
}, 750);
}
// returns car/cdr (selection bounds) and n1/n2 (grown to full lines)
function linebounds(just_car) {
var car = dom_src.selectionStart,
cdr = dom_src.selectionEnd;
dbg(car, cdr);
if (just_car)
cdr = car;
var md = dom_src.value,
n1 = Math.max(car, 0),
n2 = Math.min(cdr, md.length - 1);
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);
if (n2 < n1)
n2 = md.length;
return {
"car": car,
"cdr": cdr,
"n1": n1,
"n2": n2,
"md": md
}
}
// linebounds + the three textranges
function getsel() {
var s = linebounds(false);
s.pre = s.md.substring(0, s.n1);
s.sel = s.md.substring(s.n1, s.n2);
s.post = s.md.substring(s.n2);
return s;
}
// place modified getsel into markdown
function setsel(s) {
if (s.car != s.cdr) {
s.car = s.pre.length;
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.oninput();
}
catch (ex) { }
}
// indent/dedent
function md_indent(dedent) {
var s = getsel(),
sel0 = s.sel;
if (dedent)
s.sel = s.sel.replace(/^ /, "").replace(/\n /g, "\n");
else
s.sel = ' ' + s.sel.replace(/\n/g, '\n ');
if (s.car == s.cdr)
s.car = s.cdr += s.sel.length - sel0.length;
setsel(s);
}
// header
function md_header(dedent) {
var s = getsel(),
sel0 = s.sel;
if (dedent)
s.sel = s.sel.replace(/^#/, "").replace(/^ +/, "");
else
s.sel = s.sel.replace(/^(#*) ?/, "#$1 ");
if (s.car == s.cdr)
s.car = s.cdr += s.sel.length - sel0.length;
setsel(s);
}
// smart-home
function md_home(shift) {
var s = linebounds(!shift),
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;
if (car > cdr)
car = [cdr, cdr = car][0];
dom_src.setSelectionRange(car, cdr);
}
// autoindent
function md_newline() {
var s = linebounds(true),
ln = s.md.substring(s.n1, s.n2),
m = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/.exec(ln);
s.pre = s.md.substring(0, s.car) + '\n' + m[0];
s.sel = '';
s.post = s.md.substring(s.car);
s.car = s.cdr = s.pre.length;
setsel(s);
}
// hotkeys / toolbar
(function () {
function keydown(ev) {
ev = ev || window.event;
var kc = ev.keyCode || ev.which;
var ctrl = ev.ctrlKey || ev.metaKey;
//console.log(ev.code, kc);
if (ctrl && (ev.code == "KeyS" || kc == 83)) {
save();
return false;
}
if (document.activeElement == dom_src) {
if (ev.code == "Tab" || kc == 9) {
md_indent(ev.shiftKey);
return false;
}
if (ctrl && (ev.code == "KeyH" || kc == 72)) {
md_header(ev.shiftKey);
return false;
}
if (!ctrl && (ev.code == "Home" || kc == 36)) {
md_home(ev.shiftKey);
return false;
}
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
md_newline();
return false;
}
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
if (ev.shiftKey)
action_stack.redo();
else
action_stack.undo();
return false;
}
if (ctrl && (ev.code == "KeyY" || kc == 89)) {
action_stack.redo();
return false;
}
}
}
document.onkeydown = keydown;
document.getElementById('save').onclick = save;
})();
document.getElementById('help').onclick = function (e) {
if (e) e.preventDefault();
var dom = document.getElementById('helpbox');
var dtxt = dom.getElementsByTagName('textarea');
if (dtxt.length > 0)
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + marked(dtxt[0].value);
dom.style.display = 'block';
document.getElementById('helpclose').onclick = function () {
dom.style.display = 'none';
};
};
// blame steen
action_stack = (function () {
var undos = [];
var redos = [];
var sched_txt = '';
var sched_timer = null;
var ignore = false;
var ref = dom_src.value;
function diff(from, to) {
if (from === to)
return null;
var car = 0,
max = Math.max(from.length, to.length);
for (; car < max; car++)
if (from[car] != to[car])
break;
var p1 = from.length,
p2 = to.length;
while (p1 --> 0 && p2 --> 0)
if (from[p1] != to[p2])
break;
if (car > ++p1) {
car = p1;
}
var txt = from.substring(car, p1)
return {
car: car,
cdr: ++p2,
txt: txt
};
}
function undiff(from, change) {
return {
txt: from.substring(0, change.car) + change.txt + from.substring(change.cdr),
cursor: change.car + change.txt.length
};
}
function apply(src, dst) {
dbg('undos(%d) redos(%d)', undos.length, redos.length);
if (src.length === 0)
return false;
var state = undiff(ref, src.pop()),
change = diff(ref, state.txt);
if (change === null)
return false;
dst.push(change);
ref = state.txt;
ignore = true; // just some browsers
dom_src.value = ref;
dom_src.setSelectionRange(state.cursor, state.cursor);
ignore = true; // all browsers
dom_src.oninput();
return true;
}
function schedule_push() {
if (ignore) {
ignore = false;
return;
}
redos = [];
sched_txt = dom_src.value;
clearTimeout(sched_timer);
sched_timer = setTimeout(push, 500);
}
function undo() {
return apply(undos, redos);
}
function redo() {
return apply(redos, undos);
}
function push() {
var change = diff(ref, sched_txt, dom_src.selectionStart);
if (change !== null)
undos.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]);
}
return {
push: push,
undo: undo,
redo: redo,
push: schedule_push,
_undos: undos,
_redos: redos,
_ref: ref
}
})();

View File

@@ -21,7 +21,6 @@ html, body {
#mn {
font-weight: normal;
margin: 1.3em 0 .7em 1em;
font-size: 1.4em;
}
#mn a {
color: #444;
@@ -44,8 +43,8 @@ html, body {
height: 1.05em;
margin: -.2em .3em -.2em -.4em;
display: inline-block;
border: 1px solid rgba(0,0,0,0.3);
border-width: .05em .05em 0 0;
border: 1px solid rgba(0,0,0,0.2);
border-width: .2em .2em 0 0;
transform: rotate(45deg);
}
#mn a:hover {

View File

@@ -13,7 +13,9 @@ var dom_md = document.getElementById('mt');
if (a > 0)
loc.push(n[a]);
nav.push('<a href="/' + loc.join('/') + '">' + n[a] + '</a>');
var dec = decodeURIComponent(n[a]).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
}
dom_nav.innerHTML = nav.join('');
})();
@@ -51,7 +53,8 @@ var mde = (function () {
"save": "Ctrl-S"
},
insertTexts: ["[](", ")"],
tabSize: 4,
indentWithTabs: false,
tabSize: 2,
toolbar: tbar,
previewClass: 'mdo',
onToggleFullScreen: set_jumpto,

View File

@@ -13,6 +13,7 @@ h1 {
border-bottom: 1px solid #ccc;
margin: 2em 0 .4em 0;
padding: 0 0 .2em 0;
font-weight: normal;
}
li {
margin: 1em 0;
@@ -24,4 +25,29 @@ a {
border-bottom: 1px solid #aaa;
border-radius: .2em;
padding: .2em .8em;
}
html.dark,
html.dark body,
html.dark #wrap {
background: #222;
color: #ccc;
}
html.dark h1 {
border-color: #777;
}
html.dark a {
color: #fff;
background: #057;
border-color: #37a;
}
html.dark input {
color: #fff;
background: #624;
border: 1px solid #c27;
border-width: 1px 0 0 0;
border-radius: .5em;
padding: .5em .7em;
margin: 0 .5em 0 0;
}

View File

@@ -36,7 +36,11 @@
</form>
</ul>
</div>
<!-- script src="/.cpr/splash.js"></script -->
</body>
<script>
if (window.localStorage && localStorage.getItem('darkmode') == 1)
document.documentElement.setAttribute("class", "dark");
</script>
</body>
</html>

View File

@@ -34,6 +34,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
}
document.body.style.fontSize = '0.8em';
document.body.style.padding = '0 1em 1em 1em';
hcroak(html.join('\n'));
};
@@ -211,7 +212,8 @@ function up2k_init(have_crypto) {
// handle user intent to use the basic uploader instead
o('u2nope').onclick = function (e) {
e.preventDefault();
un2k();
setmsg('');
goto('bup');
};
if (!String.prototype.format) {

View File

@@ -66,5 +66,5 @@
</table>
<p id="u2foot"></p>
<p>( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope" onclick="javascript:goto('bup');">basic uploader</a>)</p>
<p>( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope">basic uploader</a>)</p>
</div>

View File

@@ -13,6 +13,9 @@ echo
#
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
# (only affects apple devices; everything else has native support)
#
# `no-cm` saves ~90k by removing easymde/codemirror
# (the fancy markdown editor)
command -v gtar >/dev/null &&
@@ -21,6 +24,7 @@ command -v gfind >/dev/null && {
sed() { gsed "$@"; }
find() { gfind "$@"; }
sort() { gsort "$@"; }
unexpand() { gunexpand "$@"; }
}
[ -e copyparty/__main__.py ] || cd ..
@@ -35,9 +39,15 @@ while [ ! -z "$1" ]; do
[ "$1" = clean ] && clean=1 && shift && continue
[ "$1" = re ] && repack=1 && shift && continue
[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
[ "$1" = no-cm ] && no_cm=1 && shift && continue
break
done
tmv() {
touch -r "$1" t
mv t "$1"
}
rm -rf sfx/*
mkdir -p sfx build
cd sfx
@@ -62,7 +72,15 @@ cd sfx
tar -zxf $f
mv Jinja2-*/jinja2 .
rm -rf Jinja2-* jinja2/testsuite
rm -rf Jinja2-* jinja2/testsuite jinja2/_markupsafe/tests.py jinja2/_stringdefs.py
f=jinja2/lexer.py
sed -r '/.*föö.*/ raise SyntaxError/' <$f >t
tmv $f
f=jinja2/_markupsafe/_constants.py
awk '!/: [0-9]+,?$/ || /(amp|gt|lt|quot|apos|nbsp).:/' <$f >t
tmv $f
# msys2 tar is bad, make the best of it
echo collecting source
@@ -98,10 +116,27 @@ rm -f copyparty/web/deps/*.full.*
# it's fine dw
grep -lE '\.full\.(js|css)' copyparty/web/* |
while IFS= read -r x; do sed -ri 's/\.full\.(js|css)/.\1/g' "$x"; done
while IFS= read -r x; do
sed -r 's/\.full\.(js|css)/.\1/g' <"$x" >t
tmv "$x"
done
[ $no_ogv ] &&
rm -rf copyparty/web/deps/{dynamicaudio,ogv}* copyparty/web/browser.js
rm -rf copyparty/web/deps/{dynamicaudio,ogv}*
[ $no_cm ] && {
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
echo h > copyparty/web/mde.html
f=copyparty/web/md.html
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
}
# up2k goes from 28k to 22k laff
echo entabbening
find | grep -E '\.(js|css|html|py)$' | while IFS= read -r f; do
unexpand -t 4 --first-only <"$f" >t
tmv "$f"
done
echo creating tar
args=(--owner=1000 --group=1000)
@@ -131,20 +166,3 @@ chmod 755 $sfx_out.*
printf "done:\n"
printf " %s\n" "$(realpath $sfx_out)."{sh,py}
# rm -rf *
# -rw-r--r-- 1 ed ed 811271 May 5 14:35 tar.bz2
# -rw-r--r-- 1 ed ed 732016 May 5 14:35 tar.xz
# -rwxr-xr-x 1 ed ed 830425 May 5 14:35 copyparty-sfx.py*
# -rwxr-xr-x 1 ed ed 734088 May 5 14:35 copyparty-sfx.sh*
# -rwxr-xr-x 1 ed ed 799690 May 5 14:45 copyparty-sfx.py*
# -rwxr-xr-x 1 ed ed 735004 May 5 14:45 copyparty-sfx.sh*
# time pigz -11 -J 34 -I 5730 < tar > tar.gz.5730
# real 8m50.622s
# user 33m9.821s
# -rw-r--r-- 1 ed ed 1136640 May 5 14:50 tar
# -rw-r--r-- 1 ed ed 296334 May 5 14:50 tar.bz2
# -rw-r--r-- 1 ed ed 324705 May 5 15:01 tar.gz.5730
# -rw-r--r-- 1 ed ed 257208 May 5 14:50 tar.xz

View File

@@ -260,7 +260,7 @@ def read_py(binp):
def get_pys():
ver, chk = read_py(sys.executable)
if chk:
if chk or PY2:
return [[chk, ver, sys.executable]]
hits = {sys.executable.lower(): sys.executable}

84
srv/ceditable.html Normal file
View File

@@ -0,0 +1,84 @@
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<style>
* {
margin: 0;
padding: 0;
outline: 0;
border: none;
font-size: 1em;
line-height: 1em;
font-family: monospace, monospace;
color: #333;
}
html, body {
width: 100%;
height: 100%;
background: #ddd;
}
html {
font-size: 1.3em;
}
li, #edit {
list-style-type: none;
white-space: pre-wrap;
word-break: break-all;
overflow-wrap: break-word;
word-wrap: break-word; /*ie*/
}
li:nth-child(even) {
background: #ddd;
}
#edit, #html, #txt1, #txt2 {
background: #eee;
position: fixed;
width: calc(50% - .8em);
height: calc(50% - .8em);
}
#txt1 { top: .5em; left: .5em }
#edit { top: .5em; right: .5em }
#html { bottom: .5em; left: .5em }
#txt2 { bottom: .5em; right: .5em }
</style></head><body>
<pre id="edit" contenteditable="true"></pre>
<textarea id="html"></textarea>
<ul id="txt1"></ul>
<ul id="txt2"></ul>
<script>
var edit = document.getElementById('edit'),
html = document.getElementById('html'),
txt1 = document.getElementById('txt1'),
txt2 = document.getElementById('txt2');
var oh = null;
function fun() {
var h = edit.innerHTML;
if (oh != h) {
oh = h;
html.value = h;
var t = edit.innerText;
if (h.indexOf('<div><br></div>') >= 0)
t = t.replace(/\n\n/g, "\n");
t = '<li>' + t.
replace(/&/g, "&amp;").
replace(/</g, "&lt;").
replace(/>/g, "&gt;").
split('\n').join('</li>\n<li>') + '</li>';
t = t.replace(/<li><\/li>/g, '<li> </li>');
txt1.innerHTML = t;
txt2.innerHTML = t;
}
setTimeout(fun, 100);
}
fun();
</script>
</body>
</html>

View File

@@ -1,3 +1,33 @@
### hello world
```
[72....................................................................]
[80............................................................................]
```
* foo
```
[72....................................................................]
[80............................................................................]
```
* bar
```
[72....................................................................]
[80............................................................................]
```
a123456789b123456789c123456789d123456789e123456789f123456789g123456789h123456789i123456789j123456789k123456789l123456789m123456789n123456789o123456789p123456789q123456789r123456789s123456789t123456789u123456789v123456789w123456789x123456789y123456789z123456789
<foo> &nbsp; bar &amp; <span>baz</span>
<a href="?foo=bar&baz=qwe&amp;rty">?foo=bar&baz=qwe&amp;rty</a>
<!-- hidden -->
```
<foo> &nbsp; bar &amp; <span>baz</span>
<a href="?foo=bar&baz=qwe&amp;rty">?foo=bar&baz=qwe&amp;rty</a>
<!-- visible -->
```
*fails marked/showdown/tui/simplemde (just italics), **OK: markdown-it/simplemde:***
testing just google.com and underscored _google.com_ also with _google.com,_ trailing comma and _google.com_, comma after