diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..9f60b52
Binary files /dev/null and b/favicon.ico differ
diff --git a/handler.php b/handler.php
new file mode 100644
index 0000000..a0e4285
--- /dev/null
+++ b/handler.php
@@ -0,0 +1,50 @@
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..118d248
--- /dev/null
+++ b/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+ notes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..390b6a8
--- /dev/null
+++ b/main.js
@@ -0,0 +1,1478 @@
+var defaultsettings =
+{
+ savedelay: 1000,
+ bgcolor: "white",
+ fontfamily: "'Inconsolata', 'Consolas', monospace",
+ foldmarkstart: 22232,
+ defaultpreviewinsplit: false,
+ remote: false,
+ enablefolding: false
+};
+
+//builtin
+var markerslist = ["* ", "- ", " * ", " - ", ">> ", "> ", "=> "];
+var sectionmarks = ["---", "### ", "## ", "# ", "```"];
+var codelanguages = ["xml", "js", "sql"];
+
+// globals
+var currentnote = null;
+var fileindex = 0;
+var timeoutid = null;
+var folds = [];
+var backup = "";
+var localdata = null;
+var saved = true;
+var settings = null;
+
+var commands = [
+{
+ hint: "Close menu"
+},
+{
+ shortcut: "ctrl+p",
+ hint: "Show notes list",
+ savedonly: true,
+ action: searchandloadnote
+},
+{
+ hint: "Share note",
+ savedonly: true,
+ action: share
+}/*,
+{
+ hint: "Share note (html)",
+ savedonly: true,
+ action: sharehtml
+}*/,
+{
+ shortcut: "ctrl+n",
+ hint: "New note",
+ savedonly: true,
+ action: startnewnote
+},
+{
+ shortcut: "ctrl+shift+P",
+ hint: "Command palette",
+ savedonly: false,
+ action: commandpalette,
+ excludepalette: true
+},
+{
+ shortcut: "ctrl+t",
+ hint: "Open todo",
+ savedonly: true,
+ action: loadtodo
+},
+{
+ shortcut: "ctrl+q",
+ hint: "Open quick note",
+ savedonly: true,
+ action: loadquicknote
+},
+{
+ shortcut: "ctrl+shift+F",
+ hint: "Find in notes",
+ savedonly: true,
+ action: showgrep
+},
+{
+ shortcut: "ctrl+i",
+ hint: "Toggle title",
+ savedonly: true,
+ action: toggletitle
+},
+{
+ shortcut: "ctrl+m",
+ hint: "Toggle preview",
+ savedonly: true,
+ action: togglepreview
+},
+{
+ shortcut: "ctrl+d",
+ hint: "Delete note",
+ savedonly: true,
+ action: deletenote
+},
+{
+ hint: "Restore note",
+ savedonly: true,
+ action: restore
+},
+{
+ shortcut: "ctrl+h",
+ hint: "Insert markdown header",
+ action: insertheader
+},
+{
+ savedonly: true,
+ shortcut: "F1",
+ hint: "Show help",
+ action: showhelp
+},
+{
+ savedonly: true,
+ shortcut: "ctrl+shift+C",
+ hint: "Fold",
+ action: fold
+},
+{
+ savedonly: true,
+ shortcut: "ctrl+shift+O",
+ hint: "Unfold",
+ action: unfold
+},
+{
+ savedonly: true,
+ hint: "Unfold all",
+ action: unfoldall
+},
+{
+ savedonly: true,
+ hint: "Download note",
+ action: downloadnote
+},
+{
+ savedonly: true,
+ hint: "Download local data",
+ action: downloadlocal,
+ shortcut: "ctrl+shift+S"
+}/*,
+{
+ savedonly: true,
+ hint: "Download all notes",
+ action: downloadnotes
+}*/,
+{
+ hint: "Force push",
+ action: push,
+ shortcut: "ctrl+s"
+},
+{
+ hint: "Search tags",
+ action: searchtags,
+ savedonly: true,
+ shortcut: "ctrl+shift+T"
+},
+{
+ hint: "Log out",
+ action: logout,
+ savedonly: true
+},
+{
+ hint: "Toggle split view",
+ savedonly: true,
+ action: togglesplit
+},
+{
+ hint: "Open last modified note",
+ savedonly: true,
+ action: loadlast,
+ shortcut: "ctrl+l"
+},
+{
+ hint: "Sort text",
+ savedonly: true,
+ action: sortselection
+},
+{
+ hint: "Word count",
+ action: wordcount,
+ shortcut: "ctrl+w"
+},
+{
+ hint: "Settings",
+ savedonly: true,
+ action: editsettings
+},
+{
+ hint: "Restore default settings",
+ savedonly: true,
+ action: restoresettings
+},
+{
+ hint: "Document outline",
+ savedonly: true,
+ action: showoutline
+},
+{
+ hint: "Internal links",
+ savedonly: true,
+ action: showinternallinks
+}];
+
+var snippets = [
+{
+ command: "/code",
+ hint: "Code block",
+ insert: "```\n\n```",
+ cursor: -4
+},
+{
+ command: "/date",
+ hint: "Current date",
+ insert: (new Date).toISOString().substring(0, 10) + " ",
+ cursor: 0
+},
+{
+ command: "/bonjour",
+ hint: "Standard answer (fr)",
+ insert: "Bonjour ,\n\n\n\nBien cordialement,\nSimon",
+ cursor: -29
+},
+{
+ command: "/hello",
+ hint: "Standard answer (en)",
+ insert: "Hello ,\n\n\n\nKind regards,\nSimon",
+ cursor: -24
+}];
+
+function showinternallinks()
+{
+ searchinlist(
+ getnotecontent()
+ .match(/\[\[([^\]]*)\]\]/g || [])
+ .map(l => l.replace("[[", "").replace("]]", "")))
+ .then(loadfile);
+}
+
+function showoutline()
+{
+ var outline = {};
+ var pos = 0;
+ getnotecontent().split("\n").forEach(line =>
+ {
+ pos += line.length + 1;
+ if (line.startsWith("#"))
+ {
+ line = line
+ .replace("# ", "")
+ .replace(/#/g, "\xa0\xa0\xa0\xa0");
+ outline[line] = pos;
+ }
+ });
+ searchinlist(Object.keys(outline))
+ .then(line =>
+ {
+ md.setSelectionRange(outline[line], outline[line]);
+ md.focus();
+ });
+}
+
+function restoresettings()
+{
+ if (confirm("Restore default settings?"))
+ {
+ window.localStorage.setItem("settings", JSON.stringify(defaultsettings));
+ loadsettings();
+ }
+}
+
+function editsettings()
+{
+ bindfile(
+ {
+ title: "settings.json",
+ content: JSON.stringify(settings, null, " ")
+ });
+}
+
+function showtemporaryinfo(str)
+{
+ /*var prev = mark.textContent;
+ mark.textContent = str;
+ setTimeout(function()
+ {
+ mark.textContent = prev;
+ }, 5000);*/
+ alert(str);
+}
+
+function wordcount()
+{
+ showtemporaryinfo(getnotecontent().split(/\s+\b/).length);
+}
+
+function issplit()
+{
+ return window.location !== window.parent.location;
+}
+
+function togglesplit()
+{
+ if (issplit())
+ {
+ window.parent.location = "index.html";
+ }
+ else
+ {
+ window.location = "split.html";
+ }
+}
+
+function logout()
+{
+ window.localStorage.removeItem("password");
+ togglepassword();
+}
+
+function searchtags()
+{
+ var tags = {};
+
+ localdata
+ .filter(n => !n.title.startsWith("."))
+ .forEach(n =>
+ {
+ var ts = gettags(n);
+ ts.forEach(t =>
+ {
+ tags[t] = tags[t] || [];
+ tags[t].push(n.title);
+ });
+ });
+
+ searchinlist(Object.keys(tags).sort())
+ .then(tag => searchinlist(tags[tag]))
+ .then(loadfile);
+}
+
+function gettags(note)
+{
+ var i = note.content.indexOf("tags: ");
+ if (i > -1)
+ {
+ var j = note.content.indexOf("\n", i);
+ return note.content.substring(i + 6, j).split(",").map(t => t.toLowerCase().trim());
+ }
+ return [];
+}
+
+function marksaved()
+{
+ saved = true;
+ mark.textContent = "\xa0";
+}
+
+function markunsaved(text)
+{
+ saved = false;
+ if (text)
+ {
+ mark.textContent = text;
+ }
+}
+
+function share(html)
+{
+ if (navigator.share)
+ {
+ navigator.share(
+ {
+ text: html ? "" + md2html(getnotecontent()) + "": getnotecontent(),
+ title: currentnote.title
+ });
+ }
+}
+
+function sharehtml()
+{
+ share(true);
+}
+
+function download(filename, content)
+{
+ // trick: https://www.bitdegree.org/learn/javascript-download
+ // to improve...
+ var element = document.createElement('a');
+ element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
+ element.setAttribute('download', filename);
+ element.style.display = 'none';
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+}
+
+function downloadnotes()
+{
+ localdata
+ .filter(note => !note.title.startsWith("."))
+ .forEach(note =>
+ {
+ download(note.title + ".md", note.content);
+ });
+}
+
+function downloadlocal()
+{
+ download(timestamp() + " " + (window.location.hostname || "serverless") + " notes.json", JSON.stringify(localdata));
+}
+
+function downloadnote()
+{
+ download(currentnote.title + ".md", getnotecontent());
+}
+
+function serialize()
+{
+ putontop();
+
+ window.localStorage.setItem("data", JSON.stringify(localdata));
+
+ if (currentnote.title == "settings.json")
+ {
+ window.localStorage.setItem("settings", getnotecontent());
+ }
+
+ if (settings.remote)
+ {
+ clearTimeout(timeoutid);
+ timeoutid = setTimeout(push, settings.savedelay);
+ }
+ else
+ {
+ marksaved();
+ }
+}
+
+function remotecallfailed(error)
+{
+ markunsaved("!");
+ if (error)
+ {
+ console.warn(error);
+ showtemporaryinfo(error);
+ }
+}
+
+function loadstorage()
+{
+ var item = window.localStorage.getItem("data");
+ localdata = item ? JSON.parse(item) : [];
+
+ if (currentnote)
+ {
+ currentnote = localdata.find(n => n.title == currentnote.title);
+ }
+
+ if (currentnote)
+ {
+ bindfile(currentnote);
+ }
+ else
+ {
+ loadlast();
+ }
+}
+
+function loadsettings()
+{
+ settings = {...defaultsettings};
+ var item = window.localStorage.getItem("settings");
+ if (item)
+ {
+ item = JSON.parse(item);
+ for (var key in item)
+ {
+ settings[key] = item[key];
+ }
+ }
+
+ document.body.style.background = settings.bgcolor;
+ document.body.style.fontFamily = settings.fontfamily;
+
+ if (!settings.enablefolding)
+ {
+ commands = commands.filter(c => !c.hint.toLowerCase().includes("fold"));
+ }
+}
+
+function checksaved()
+{
+ if (!saved)
+ {
+ return "not saved";
+ }
+}
+
+function init()
+{
+ loadsettings();
+
+ window.onbeforeunload = checksaved;
+ window.onclick = focuseditor;
+ window.onstorage = loadstorage;
+
+ codelanguages.forEach(lang =>
+ {
+ if (!snippets.includes(s => s.command == "/" + lang))
+ {
+ snippets.push(
+ {
+ command: "/" + lang,
+ hint: lang + " code block",
+ insert: "```" + lang + "\n\n```",
+ cursor: -4
+ });
+ }
+ });
+
+ if (settings.remote)
+ {
+ markunsaved();
+ queryremote({action: "fetch"})
+ .then(data =>
+ {
+ marksaved();
+ localdata = data;
+ loadlast();
+ })
+ .catch(remotecallfailed);
+ }
+ else
+ {
+ delete commands.find(c => c.action == push).action;
+ loadstorage();
+ }
+
+ if (issplit())
+ {
+ if (settings.defaultpreviewinsplit && name == "right")
+ {
+ togglepreview();
+ }
+ else if (name == "left")
+ {
+ md.focus();
+ }
+ }
+}
+
+function togglepassword()
+{
+ password.value = "";
+ authentpage.hidden = false;
+ notepage.style.display = "none";
+ document.title = "notes";
+ password.focus();
+}
+
+function push()
+{
+ if (localdata)
+ {
+ clearTimeout(timeoutid);
+ var content = getnotecontent();
+ queryremote({action: "push", data: JSON.stringify(localdata)})
+ .then(() => {
+ console.log("Data sent to server");
+ if (getnotecontent() == content)
+ {
+ marksaved();
+ }
+ else
+ {
+ console.warn("Content changed");
+ timeoutid = setTimeout(push, settings.savedelay);
+ }
+ })
+ .catch(remotecallfailed);
+ }
+ else
+ {
+ console.warn("Cannot push empty data");
+ }
+}
+
+function queryremote(params)
+{
+ return new Promise( (apply, failed) => {
+
+ params.password = window.localStorage.getItem("password");
+
+ var paramlist = [];
+ for (var i in params)
+ {
+ paramlist.push(i + "=" + encodeURIComponent(params[i]));
+ }
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("POST", "handler.php");
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+
+ xhr.onerror = function()
+ {
+ failed("XMLHttpRequest error");
+ }
+
+ xhr.onload = function()
+ {
+ if (xhr.status !== 200)
+ {
+ failed("Http status " + xhr.status);
+ }
+ else
+ {
+ var data = {};
+ try
+ {
+ data = JSON.parse(xhr.responseText);
+
+ if (data.error)
+ {
+ if (data.error == "authent")
+ {
+ failed(null);
+ togglepassword();
+ }
+ else
+ {
+ failed("Remote handler returned an error: " + data.error);
+ }
+ }
+ else
+ {
+ authentpage.hidden = true;
+ notepage.style.display = "table";
+ apply(data);
+ }
+ }
+ catch(error)
+ {
+ failed("Handler result is not valid. JS error: " + error);
+ }
+ }
+ }
+ xhr.send(paramlist.join("&"));
+ });
+}
+
+function applyfolds(content)
+{
+ for (var i = folds.length - 1; i >= 0; i--)
+ {
+ content = content.replace(String.fromCodePoint(settings.foldmarkstart + i), folds[i]);
+ }
+ return content;
+}
+
+function getlinesrange()
+{
+ var start = md.selectionStart;
+ var end = md.selectionEnd;
+
+ while (start > 0 && md.value[start - 1] != "\n") start--;
+ while (end < md.value.length && md.value[end] != "\n") end++;
+
+ return {
+ start: start,
+ end: end
+ };
+}
+
+function sortselection()
+{
+ var content = getnotecontent();
+ var range = {start: 0, end: content.length};
+ if (md.selectionStart != md.selectionEnd)
+ {
+ range = getlinesrange();
+ }
+
+ var selection = content.substring(range.start, range.end);
+ var sorted = selection.split("\n").sort().join("\n");
+ md.value = content.substring(0, range.start) + sorted + content.substring(range.end);
+ notecontentchanged();
+}
+
+function selectlines()
+{
+ var range = getlinesrange();
+ md.selectionStart = range.start;
+ md.selectionEnd = range.end;
+}
+
+function isfold(linecontent)
+{
+ var res = linecontent.length == 1 && linecontent.codePointAt(0) >= settings.foldmarkstart;
+ if (res)
+ {
+ //security: if > 100, probably not a fold. Maybe an emoji. To improve.
+ res &= foldindex(linecontent) < 100;
+ }
+ return res;
+}
+
+function foldindex(foldmark)
+{
+ return foldmark.codePointAt(0) - settings.foldmarkstart;
+}
+
+function fold()
+{
+ // todo: forbid if > 100?
+ var start = md.selectionStart;
+ selectlines();
+
+ var content = md.value;
+ var char = String.fromCodePoint(settings.foldmarkstart + folds.length);
+ var value = content.substring(md.selectionStart, md.selectionEnd)
+
+ folds.push(value);
+
+ setnotecontent(content.substring(0, md.selectionStart)
+ + char
+ + content.substring(md.selectionEnd));
+
+ md.focus();
+ setpos(start);
+
+ resize();
+}
+
+function unfold()
+{
+ var range = getlinesrange();
+ var linecontent = md.value.substring(range.start, range.end);
+ if (isfold(linecontent))
+ {
+ var i = foldindex(linecontent);
+ md.value = md.value.replace(linecontent, folds[i]);
+ md.focus();
+ setpos(range.start + folds[i].length);
+ }
+
+ resize();
+}
+
+function unfoldall()
+{
+ md.value = getnotecontent();
+ resetfolds();
+ setpos(0);
+ md.focus();
+
+ resize();
+}
+
+function checkfolding()
+{
+ if (!settings.enablefolding)
+ {
+ console.log("folding is disabled.");
+ return;
+ }
+
+ var range = getlinesrange();
+ var line = md.value.substring(range.start, range.end);
+ var sectionmark = sectionmarks.find(m => line.startsWith(m));
+ if (sectionmark)
+ {
+ event.preventDefault();
+
+ // move to next line
+ setpos(range.end + 1);
+ range = getlinesrange();
+ var nextline = md.value.substring(range.start, range.end);
+
+ if (isfold(nextline))
+ {
+ unfold();
+ }
+ else
+ {
+ // find next occurence. If not found take all the remaining file.
+ if (md.value.includes("\n" + sectionmark, range.end))
+ {
+ sectionend = md.value.indexOf("\n" + sectionmark, range.end);
+ }
+ else
+ {
+ sectionend = md.value.length;
+ }
+
+ // keep last empty line if any
+ if (md.value[sectionend] == "\n")
+ {
+ sectionend--;
+ }
+
+ md.setSelectionRange(range.start, sectionend);
+ fold();
+ }
+ }
+ else if (isfold(line))
+ {
+ unfold();
+ }
+}
+
+function setnotecontent(content)
+{
+ md.value = content;
+}
+
+function getnotecontent()
+{
+ return applyfolds(md.value);
+}
+
+function ontopbarclick()
+{
+ if (title.hidden)
+ {
+ commandpalette();
+ }
+}
+
+/*function checkfoldmismatch()
+{
+ start = settings.foldmarkstart.toString(16);
+ end = (settings.foldmarkstart + 100).toString(16);
+ var match = md.value.match(new RegExp("[\\u" + start + "-\\u" + end + "]", "g"));
+ var markcount = 0;
+ if (match)
+ {
+ markcount = match.length;
+ }
+ var diff = folds.length - markcount;
+ if (diff)
+ {
+ console.warn(diff + " fold(s) missing.");
+ }
+}*/
+
+function md2html(content)
+{
+ // dumb fallback for offline mode
+ if (typeof showdown == "undefined") return content
+ .replace(/\*\*([^\*]*)\*\*/g, "$1")
+ .replace(/\*([^\*]*)\*/g, "$1")
+ .replace(/\## (.*)/g, "$1
")
+ .replace(/\# (.*)/g, "$1
")
+ .replace(/\n/g, "
");
+ var converter = new showdown.Converter();
+ converter.setOption("simplifiedAutoLink", true);
+ converter.setOption("simpleLineBreaks", true);
+ converter.setOption("metadata", true);
+
+ var html = converter.makeHtml(content);
+
+ // internal links
+ html = html.replace(/\[\[([^\]]*)\]\]/g, "$1");
+
+ return html;
+}
+
+function list()
+{
+ return localdata.map(n => n.title).filter(t => !t.startsWith("."));
+}
+
+function loadlast()
+{
+ loadfile(list().shift() || timestamp());
+}
+
+function grep(needle)
+{
+ var result = {};
+
+ localdata
+ .filter(n => !n.title.startsWith("."))
+ .forEach(note =>
+ {
+ if (note.title.toLowerCase().includes(needle.toLowerCase()))
+ {
+ result[note.title] = {};
+ }
+ note.content.split("\n")
+ .forEach((line, nb) => {
+ if (line.toLowerCase().includes(needle.toLowerCase()))
+ {
+ result[note.title] = result[note.title] || {};
+ result[note.title][nb] = line;
+ }
+ });
+ });
+
+ return result;
+}
+
+function showgrepresult(grepresult)
+{
+ var grepcontent = ["# Search results: \"" + filter.value + "\""];
+ for (var file in grepresult)
+ {
+ grepcontent.push("[[" + file + "]]");
+ for (var l in grepresult[file])
+ {
+ grepcontent.push("[" + l + "] " + grepresult[file][l].replace(new RegExp("(" + filter.value + ")", "gi"), "**$1**"));
+ }
+ grepcontent.push("");
+ }
+
+ if (grepcontent.length == 0)
+ {
+ grepcontent.push("No result.");
+ }
+
+ bindfile(
+ {
+ title: "Search result",
+ content: grepcontent.join("\n")
+ });
+
+ if (preview.hidden)
+ {
+ togglepreview();
+ }
+}
+
+function showgrep()
+{
+ filteredlist.hidden = true;
+ searchdialog.hidden = false;
+ filter.focus();
+ filter.select();
+
+ filter.onkeydown = function()
+ {
+ if (event.key === "Enter")
+ {
+ event.preventDefault();
+ searchdialog.hidden = true;
+ showgrepresult(grep(filter.value));
+ }
+ }
+
+ // live search
+ /*filter.oninput = function()
+ {
+ if (filter.value.length > 1)
+ {
+ showgrepresult(grep(filter.value));
+ }
+ }*/
+}
+
+function commandpalette()
+{
+ searchinlist(commands
+ .filter(c => !c.excludepalette)
+ .map(c => c.hint)
+ .concat(snippets.map(s => "Insert snippet: " + s.hint)))
+ .then(hint =>
+ {
+ var command = commands.find(c => c.hint == hint);
+ if (command && command.action)
+ {
+ command.action();
+ }
+ else
+ {
+ var snippet = snippets.find(s => "Insert snippet: " + s.hint == hint);
+ if (snippet)
+ {
+ insert(snippet.insert, snippet.cursor);
+ notecontentchanged();
+ md.focus();
+ }
+ }
+ });
+}
+
+function insert(text, cursoroffset = 0, nbtodelete = 0)
+{
+ var pos = md.selectionStart;
+ var content = md.value;
+ md.value =
+ content.substring(0, pos - nbtodelete)
+ + text
+ + content.substring(pos);
+ setpos(pos - nbtodelete + text.length + cursoroffset);
+ resize();
+}
+
+function searchinlist(list)
+{
+ return new Promise(selectitem =>
+ {
+ fileindex = 0;
+ searchdialog.hidden = false;
+ filteredlist.hidden = false;
+
+ filteredlist.innerHTML = "";
+ filter.value = "";
+ filter.focus();
+
+ list.forEach(item =>
+ {
+ var elt = document.createElement("div");
+ elt.textContent = item;
+ elt.onclick = function()
+ {
+ searchdialog.hidden = true;
+ selectitem(item);
+ }
+ filteredlist.appendChild(elt);
+ });
+
+ applyfilter();
+
+ filter.onkeydown = function()
+ {
+ // doesn't work if focus is lost.
+ if (event.key === "Enter")
+ {
+ event.preventDefault();
+ searchdialog.hidden = true;
+ var selected = document.getElementsByClassName("selected")[0];
+ selectitem(selected ? selected.textContent : filter.value);
+ }
+ }
+ });
+}
+
+function applyfileindex()
+{
+ var i = 0;
+ [...filteredlist.children].forEach(child =>
+ {
+ if (child.nodeName == "DIV")
+ {
+ child.className = "";
+ if(!child.hidden)
+ {
+ if (i++ == fileindex)
+ {
+ child.className = "selected";
+ }
+ }
+ }
+ });
+}
+
+function getpos()
+{
+ return md.selectionStart;
+}
+
+function setpos(pos)
+{
+ md.setSelectionRange(pos, pos);
+}
+
+function before(nb)
+{
+ return md.value.substring(getpos() - nb, getpos());
+}
+
+function resize()
+{
+ if (md.clientHeight > md.scrollHeight) return;
+
+ md.rows = (md.value.match(/\n/g) || []).length + 1;
+ while (md.scrollHeight > md.clientHeight)
+ {
+ md.rows *= 1.5;
+ }
+}
+
+function putontop()
+{
+ if (localdata.find(n => n == currentnote))
+ {
+ localdata.splice(localdata.indexOf(currentnote), 1);
+ localdata.unshift(currentnote);
+ }
+}
+
+function notecontentchanged()
+{
+ markunsaved("*");
+
+ // check snippets and autocomplete
+ if (before(2) == "[[")
+ {
+ searchautocomplete();
+ }
+ else
+ {
+ var snippet = snippets.find(s => before(s.command.length) == s.command);
+ if (snippet)
+ {
+ insert(snippet.insert, snippet.cursor, snippet.command.length);
+ }
+ }
+ resize();
+
+ // save
+ var content = getnotecontent();
+ if (content == "" || content == "null" || content == "undefined")
+ {
+ console.warn("Invalid content '" + content + "', file '" + currentnote.title + "' not saved");
+ }
+ else
+ {
+ currentnote.content = content;
+ serialize();
+ }
+}
+
+function loadtodo()
+{
+ loadfile("todo");
+}
+
+function loadquicknote()
+{
+ loadfile("Quick note");
+}
+
+function timestamp()
+{
+ return (new Date).toISOString().replace("T", " ").replace(/\..*/, "").replace(/:/g, ".");
+}
+
+function startnewnote()
+{
+ loadfile(timestamp());
+}
+
+function showhelp()
+{
+ var help = ["# Notes"];
+ help.push("## Shortcuts");
+
+ commands
+ .filter(command => Boolean(command.shortcut))
+ .forEach(command => help.push(command.hint + ": " + command.shortcut));
+
+ help.push("## Snippets");
+ snippets.forEach(snippet =>
+ {
+ help.push(snippet.hint + ": " + snippet.command);
+ });
+
+ help.push("## Libs");
+ help.push("[Showdown](https://showdownjs.com/)");
+
+ help.push("## Inspiration");
+ help.push("[rwtxt](https://rwtxt.com)");
+ help.push("[Offline Notepad](https://offlinenotepad.com/)");
+ help.push("[Writemonkey3](http://writemonkey.com/wm3/)");
+ help.push("[Sublime Text](https://www.sublimetext.com/)");
+
+ bindfile(
+ {
+ title: "Help",
+ content: help.join("\n")
+ });
+
+ if (preview.hidden)
+ {
+ togglepreview();
+ }
+}
+
+function toggletitle()
+{
+ if (title.hidden)
+ {
+ title.hidden = false;
+ title.focus();
+ }
+ else
+ {
+ title.hidden = true;
+ md.focus();
+ }
+}
+
+function selectnote()
+{
+ return searchinlist(list());
+}
+
+function searchautocomplete()
+{
+ selectnote().then(insertautocomplete);
+}
+
+function searchandloadnote()
+{
+ selectnote().then(loadfile);
+}
+
+function rename(newname)
+{
+ if (localdata.find(n => n.title == newname))
+ {
+ var error = newname + " alreday exists";
+ console.warn(error);
+ return error;
+ }
+
+ // rename internal references
+ localdata
+ .filter(note => note != currentnote)
+ .forEach(note =>
+ {
+ note.content = note.content.replaceAll("[[" + currentnote.title + "]]", "[[" + newname + "]]");
+ });
+
+ currentnote.title = newname;
+
+ serialize();
+ return "";
+}
+
+function deletenote()
+{
+ if (confirm('delete "' + currentnote.title + '"?'))
+ {
+ var error = rename(".deleted_" + currentnote.title)
+ if (!error)
+ {
+ loadlast();
+ }
+ else
+ {
+ console.warn("Failed to delete '" + currentnote.title + "'");
+ }
+ }
+}
+
+function restore()
+{
+ if (confirm('restore "' + currentnote.title + '"?'))
+ {
+ setnotecontent(backup);
+ notecontentchanged();
+ }
+}
+
+function insertheader()
+{
+ if (!getnotecontent().startsWith("---"))
+ {
+ var headers = "---\ndate: " + (new Date).toISOString().substring(0, 10) + "\ntags: \n---\n\n";
+ md.value = headers + md.value;
+ setpos(27);
+ }
+ resize();
+}
+
+function splitshortcut(s)
+{
+ var r = {};
+ s = s.split("+");
+ r.key = s.pop();
+ s.forEach(e => {
+ r[e] = true;
+ })
+ return r;
+}
+
+function mainkeydownhandler()
+{
+ if (event.key == "Escape")
+ {
+ if (!searchdialog.hidden)
+ {
+ event.preventDefault();
+ searchdialog.hidden = true;
+ md.focus();
+ }
+ else if (preview.hidden == false)
+ {
+ togglepreview();
+ }
+ }
+
+ if (!searchdialog.hidden && (event.key == "Tab" || event.keyCode == "40" || event.keyCode == "38"))
+ {
+ event.preventDefault();
+ fileindex += (event.shiftKey || event.keyCode == "38") ? -1 : 1;
+ fileindex = Math.min(fileindex, filteredlist.children.length - 1);
+ fileindex = Math.max(fileindex, 0);
+ applyfileindex();
+ }
+
+ commands.filter(c => c.shortcut)
+ .forEach(command =>
+ {
+ var s = splitshortcut(command.shortcut);
+ if (event.key == s.key && !(s.ctrl && !event.ctrlKey && !event.altKey) && !(s.shift && !event.shiftKey))
+ {
+ event.preventDefault();
+ if (command.savedonly && !saved)
+ {
+ console.log("Data note saved, try again")
+ }
+ else if (command.action)
+ {
+ command.action();
+ }
+ }
+ });
+}
+
+function ontitlechange()
+{
+ var oldname = currentnote.title;
+ var error = rename(title.value);
+
+ if (!error)
+ {
+ console.log("'" + oldname + "' renamed to '" + currentnote.title + "'");
+ document.title = title.value;
+ }
+ else
+ {
+ title.value = currentnote.title;
+ }
+}
+
+function applyfilter()
+{
+ [...filteredlist.children].forEach(div =>
+ {
+ div.hidden = div.textContent.toLowerCase().indexOf(filter.value.toLowerCase()) < 0;
+ });
+
+ fileindex = 0;
+ applyfileindex();
+}
+
+function backspace(nb)
+{
+ var pos = getpos();
+ var c = md.value;
+ md.value = c.substring(0, pos - nb) + c.substring(pos);
+ setpos(pos - nb);
+}
+
+function editorkeydown()
+{
+ if (event.key == "Enter")
+ {
+ var currentline = md.value.substring(0, getpos()).split("\n").pop();
+ markerslist.filter(marker => currentline.startsWith(marker))
+ .forEach(marker =>
+ {
+ event.preventDefault();
+ if (currentline != marker)
+ {
+ insert("\n" + marker);
+ }
+ else
+ {
+ backspace(marker.length);
+ }
+ notecontentchanged();
+ });
+ }
+ else if (event.key === "Tab")
+ {
+ event.preventDefault();
+ // todo: reverse with shift
+ if (before(2) == "* " || before(2) == "- ")
+ {
+ setpos(getpos() - 2);
+ insert(" ", 2);
+ }
+ // disable tab
+ /*else
+ {
+ insert(" ");
+ }*/
+ }
+}
+
+function insertautocomplete(selectednote)
+{
+ md.focus();
+ insert(selectednote + "]] ");
+ notecontentchanged();
+}
+
+function togglepreview()
+{
+ preview.innerHTML = md2html(getnotecontent());
+ md.hidden = !md.hidden;
+ preview.hidden = !preview.hidden;
+
+ if (preview.hidden)
+ {
+ resize();
+ md.focus();
+ }
+}
+
+function resetfolds()
+{
+ folds = [];
+}
+
+function bindfile(note)
+{
+ if (currentnote && currentnote.title == "settings.json")
+ {
+ loadsettings();
+ }
+
+ backup = note.content;
+ currentnote = note;
+ title.value = note.title;
+ document.title = note.title;
+
+ setnotecontent(note.content || "");
+ preview.innerHTML = md2html(getnotecontent());
+
+ resetfolds();
+ resize();
+ setpos(0);
+
+ // to improve...
+ if (!issplit())
+ {
+ md.focus();
+ }
+}
+
+function loadfile(name)
+{
+ var note = localdata.find(n => n.title == name);
+ if (!note)
+ {
+ note = {title: name, content: ""};
+ localdata.unshift(note);
+ }
+
+ if (!preview.hidden)
+ {
+ togglepreview();
+ }
+
+ bindfile(note);
+}
+
+function sendpassword()
+{
+ if (event.type == "blur" || event.key == "Enter")
+ {
+ event.preventDefault();
+ window.localStorage.setItem("password", password.value);
+ init();
+ }
+}
+
+function focuseditor()
+{
+ if (document.documentElement == event.srcElement)
+ {
+ md.focus();
+ console.log("Forced focus");
+ }
+}
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..b581382
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,38 @@
+# notes
+
+## getting started
+
+### local
+
+* download files
+* launch index.html
+
+for a more app-feel on windows, try:
+
+```
+chrome_proxy.exe --app=index.html
+```
+
+### remote
+
+* put files on your php web server
+* navigate to index.html
+* edit settings (ctrl+shift+p, settings)
+* change: remote=true
+* reload page
+
+to protect your data by a password, edit handler.php and change `$password` variable.
+
+your password will be sent from browser to server through a POST http query with no more encryption than ssl, if enabled. it is stored unencrypted in your browser local storage.
+
+## usage
+
+* help: f1
+* notes list: ctrl+p
+* command palette: ctrl+shift+p
+
+## reclaim you data
+
+your notes are stored in your browser local storage. download them in a single json file with ctrl+shift+s.
+
+write a moulinette to flatten them as md files.
\ No newline at end of file
diff --git a/split.css b/split.css
new file mode 100644
index 0000000..83c7805
--- /dev/null
+++ b/split.css
@@ -0,0 +1,21 @@
+body {
+ margin: 0;
+}
+
+div {
+ width: 100%;
+}
+
+iframe {
+ width: 50%;
+ height: 100%;
+ border: none;
+}
+
+#left {
+ float: left;
+}
+
+#right {
+ float: right;
+}
\ No newline at end of file
diff --git a/split.html b/split.html
new file mode 100644
index 0000000..4b2e3be
--- /dev/null
+++ b/split.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ notes
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..ea6ab1a
--- /dev/null
+++ b/style.css
@@ -0,0 +1,106 @@
+/* globals */
+
+body {
+ margin-left: 5%;
+ margin-right: 5%;
+ font-size: 90%;
+ line-height: 130%;
+ caret-color: #5AA7CE;
+}
+
+input {
+ outline: none;
+ border: none;
+
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+/* scrollbars */
+:root
+{
+ scrollbar-width: thin;
+ scrollbar-color: RGBA(90, 167, 206, 0.5) lightgray;
+}
+
+body::-webkit-scrollbar {
+ height: 5px;
+ width: 5px;
+ background-color: lightgray;
+}
+
+body::-webkit-scrollbar-thumb {
+ background-color: RGBA(90, 167, 206, 0.5);
+ border-radius: 10px;
+}
+
+/* note page and editor */
+
+#notepage {
+ width: 100%;
+}
+
+#md {
+ outline: none;
+ border: none;
+ width: 100%;
+
+ resize: none;
+
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+ background-color: inherit;
+}
+
+/* top bar content */
+
+#topbar {
+ height: 50px;
+ width: 100%;
+}
+
+#topbar > span {
+ display: table-cell;
+}
+
+#titlecontainer {
+ width: 100%;
+}
+
+#title {
+ width: 100%;
+ padding-top: 10px;
+ font-size: 20px;
+ background-color: inherit;
+}
+
+/* search file dialog */
+
+.selected {
+ background-color: darkgray;
+}
+
+#filter {
+ width: 100%;
+
+ /* wm style
+ background-color: #f0f0f0;
+ */
+}
+
+#searchdialog {
+ position: absolute;
+ top: 0;
+ background-color: lightgray;
+ opacity: 1;
+ width: 90%;
+}
+
+/* authent */
+
+#bigtitle {
+ font-size: 50px;
+ line-height: 100%;
+}