var defaultsettings = { bgcolor: "white", fontfamily: "'Inconsolata', 'Consolas', monospace", fontsize: "90%", fontcolor: "black", lineheight: "130%", accentcolor: "#5AA7CE", savedelay: 2000, foldmarkstart: 22232, defaultpreviewinsplit: false, enablefolding: false, tagautocomplete: false, titleinaccentcolor: false }; //builtin var markerslist = ["* ", "- ", " * ", " - ", ">> ", "> ", "=> ", "— ", "[ ] ", " "]; var sectionmarks = ["---", "### ", "## ", "# ", "```"]; var codelanguages = ["xml", "js", "sql"]; // globals var currentnote = null; var fileindex = 0; var workerid = null; var folds = []; var backup = ""; var localdata = null; var saved = true; var pending = false; var settings = null; var tags = null; var currentvault = ""; var currenttag = ""; var stat = { ses: { q: 0, t: timestamp() }, cur: { q: 0, t: timestamp() } } var themes = { Default: { bgcolor: "white", fontfamily: "'Inconsolata', 'Consolas', monospace", fontsize: "90%", fontcolor: "black", lineheight: "130%", accentcolor: "#5AA7CE" }, Notion: { bgcolor: "white", fontfamily: "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, 'Apple Color Emoji', Arial, sans-serif, 'Segoe UI Emoji', 'Segoe UI Symbol'", fontsize: "16px", fontcolor: "rgb(55,53,47)", lineheight: "24px", accentcolor: "rgb(55,53,47)" }, Monkey: { bgcolor: "rgb(227,227,227)", fontfamily: "'Hack', 'Consolas', monospace", fontsize: "14px", fontcolor: "rgb(55,55,55)", lineheight: "24px", accentcolor: "#5AA7CE" }, Mariana: { bgcolor: "rgb(48,56,65)", fontfamily: "'Consolas', monospace", fontsize: "15px", fontcolor: "rgb(216,222,233)", lineheight: "110%", accentcolor: "rgb(249,174,88)" }, "Plus plus": { bgcolor: "white", fontfamily: "'Consolas', 'Courier New', monospace", fontsize: "15px", fontcolor: "black", lineheight: "110%", accentcolor: "rgb(128,0,255)" }, Calmly: { bgcolor: "rgb(250,250,250)", fontfamily: "'Droid Serif', serif", fontsize: "19px", fontcolor: "rgb(60,60,60)", lineheight: "28.5px", accentcolor: "rgb(60,60,60)" }, Breakers: { bgcolor: "rgb(252,253,253)", fontfamily: "'Consolas', monospace", fontsize: "15px", fontcolor: "rgb(50,50,50)", lineheight: "110%", accentcolor: "rgb(95,180,180)" }, Cryptee: { bgcolor: "white", fontfamily: "'Josefin Sans', sans-serif", fontsize: "16px", fontcolor: "rgb(78,78,78)", lineheight: "24px", accentcolor: "rgb(54,54,54)" } }; var commands = [ { hint: "Close menu" }, { shortcut: "ctrl+p", hint: "Show notes list", action: searchandloadnote }, { hint: "Share note", action: share, allowunsaved: true }, { hint: "Share note (html)", action: sharehtml }, { shortcut: "ctrl+n", hint: "New note", action: startnewnote }, { shortcut: "ctrl+shift+P", hint: "Command palette", allowunsaved: true, action: commandpalette, excludepalette: true }, { shortcut: "ctrl+t", hint: "Open todo", action: loadtodo }, { shortcut: "ctrl+q", hint: "Open quick note", action: loadquicknote }, { shortcut: "ctrl+g", hint: "Find in notes", action: showgrep }, { shortcut: "ctrl+i", hint: "Toggle title", action: toggletitle, allowunsaved: true }, { shortcut: "ctrl+m", hint: "Toggle preview", action: togglepreview, allowunsaved: true }, { shortcut: "ctrl+d", hint: "Delete note", action: deletenote }, { hint: "Restore note", action: restore }, { shortcut: "ctrl+h", hint: "Insert markdown header", action: insertheader, allowunsaved: true }, { shortcut: "F1", hint: "Show help", action: showhelp }, { shortcut: "ctrl+shift+C", hint: "Fold", action: fold, allowunsaved: true }, { shortcut: "ctrl+shift+O", hint: "Unfold", action: unfold, allowunsaved: true }, { hint: "Unfold all", action: unfoldall, allowunsaved: true }, { hint: "Download note", action: downloadnote }, { hint: "Download note with subnotes", action: downloadnotewithsubs }, { hint: "Download local data", action: downloadlocal, shortcut: "ctrl+shift+S" }, { hint: "Search tags", action: searchtags, shortcut: "ctrl+shift+T" }, { hint: "Log out", action: logout, }, { hint: "Toggle split view", action: togglesplit }, { hint: "Load previous note", action: loadprevious, shortcut: "ctrl+b" }, { hint: "Sort text", action: sortselection, allowunsaved: true }, { hint: "Settings", action: editsettings }, { hint: "Restore default settings", action: restoresettings }, { hint: "Note outline", action: showoutline, shortcut: "ctrl+o", allowunsaved: true }, { hint: "Show connected notes", action: shownotelinks, shortcut: "ctrl+l" }, { hint: "Switch vault", action: switchvault, shortcut: "ctrl+shift+V" }, { hint: "Add tag filter", action: addtagfilter, shortcut: "ctrl+shift+F", }, { hint: "Select theme", action: selecttheme, allowunsaved: true }, { hint: "Show info", action: showinfo, shortcut: "ctrl+w", allowunsaved: true }, { hint: "Force save", action: save, shortcut: "ctrl+s", allowunsaved: true }, { hint: "Toggle spell check", action: togglespellcheck, allowunsaved: true, shortcut: "F7" }, { hint: "Create subnote from selection", action: createsubnote }, { hint: "Include subnote", action: includesub }, { hint: "Comment selection", action: comment }]; 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 }, { command: "/-", hint: "Dialog mark", insert: "— ", cursor: 0 }]; function getnote(title) { return localdata.find(note => note.title == title); } function createsubnote() { var range = getlinesrange(); var content = md.value.substring(range.start, range.end); searchinlist([], null, "Title...") .then(title => { if (getnote(title)) { showtemporaryinfo("'" + title + "' already exists"); } else { var newnote = { title: title, content: content } localdata.unshift(newnote); md.value = md.value.substring(0, range.start) + "[[" + title + "]]" + md.value.substring(range.end); datachanged(); } }); } function comment() { md.value = md.value.substring(0, md.selectionStart) + "" + md.value.substring(md.selectionEnd); } function includesub() { var range = linkrangeatpos(); if (range) { var title = linkatpos(); if (confirm("Replace [[" + title + "]] by its content?")) { md.value = md.value.substring(0, range.start) + getnote(title).content + md.value.substring(range.end); datachanged(); } } } function togglespellcheck() { md.spellcheck = !md.spellcheck; } function showinfo() { var tags = gettags(currentnote); showtemporaryinfo( [ "title: " + currentnote.title, "vault: " + currentvault, (tags ? "tags: " + tags : ""), "saved: " + saved, "word count: " + getwords(), "current note start: " + stat.cur.t, "current note queries: " + stat.cur.q, "session start: " + stat.ses.t, "session queries: " + stat.ses.q ], "Note info"); } function loadtheme(theme) { for (var i in themes[theme]) { settings[i] = themes[theme][i]; } applystyle(); resize(); } function savesettings() { window.localStorage.setItem("settings", JSON.stringify(settings)); } function selecttheme() { searchinlist(Object.keys(themes), loadtheme) .then(t => { loadtheme(t); savesettings(); }); } function addtagfilter() { var command = commands.find(c => c.action == addtagfilter); if (!currenttag) { tagslist() .then(t => { currenttag = t; command.hint = "Remove tag filter '" + currenttag + "'"; setwindowtitle(); }); } else { currenttag = ""; command.hint = "Add tag filter"; setwindowtitle(); } } function switchvault() { window.localStorage.setItem("vault", othervault()); init(); } function ancestors(note) { var list = [... new Set(parents(note))]; list.forEach(title => { var parent = localdata.find(n => n.title == title); if (parent && !list.find(p => p.title == parent)) { list = list.concat(ancestors(parent)); } }) return list; } function descendants(note) { var list = [... new Set(children(note))]; list.forEach(title => { var child = localdata.find(n => n.title == title); if (child && !list.find(p => p.title == child)) { list = list.concat(descendants(localdata.find(n => n.title == title))); } }) return list; } function children(note) { return (note.content .match(/\[\[([^\]]*)\]\]/g) || []) .map(l => l.replace("[[", "").replace("]]", "")); } function parents(note) { return localdata .filter(n => n.content.indexOf("[[" + note.title + "]]") != -1) .map(n => n.title); } function shownotelinks() { try { var list = ancestors(currentnote).reverse(); var index = list.length; list.push(currentnote.title); list = list.concat(descendants(currentnote)); searchinlist(list, null, index) .then(loadnote); } catch(err) { showtemporaryinfo("Loop detected"); } } function showoutline() { var outline = {}; var pos = 0; getnotecontent().split("\n").forEach((line, index, lines) => { pos += line.length + 1; if (line.startsWith("#") || line == "---") { line = line .replace("# ", "") .replace(/#/g, "\xa0".repeat(4)); outline[line] = pos; } }); var keys = Object .keys(outline) .sort((a,b) => { return outline[a] - outline[b]; }); searchinlist(keys) .then(line => { md.setSelectionRange(outline[line], outline[line]); md.focus(); }); } function linkrangeatpos() { var start = md.value.lastIndexOf("[[", md.selectionStart); if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return null var end = md.value.indexOf("]]", md.selectionStart); if (end == -1 || md.value.substring(md.selectionStart, end).indexOf("\n") != -1) return null; return { start: start, end: end + 2 }; } function linkatpos() { var range = linkrangeatpos(); if (range) { return md.value.substring(range.start + 2, range.end - 2); } return null; } function tagatpos() { var start = md.value.lastIndexOf(" ", md.selectionStart); if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return ""; var nextcomma = md.value.indexOf(",", md.selectionStart); var nexteol = md.value.indexOf("\n", md.selectionStart); var end = Math.min(nexteol, nextcomma); if (end == -1 || md.value.substring(md.selectionStart, end).indexOf("\n") != -1) return ""; return md.value.substring(start + 1, end); } function clickeditor() { if (event.ctrlKey) { var link = linkatpos(); var tag = tagatpos(); if (link) { loadnote(link); } else if (tag) { tagslist(); searchinlist(tags[tag.toLowerCase()]) .then(loadnote); } else { checkfolding(); } } } function restoresettings() { if (confirm("Restore default settings?")) { settings = defaultsettings; savesettings(); loadsettings(); } } function editsettings() { bindfile( { title: "settings.json", content: JSON.stringify(settings, null, " ") }); } function showtemporaryinfo(data, title) { if (typeof data == "string") { data = new Array(data); } filter.placeholder = title || "Info"; searchinlist(data) .then(() => { filter.placeholder = "Search..."; }); md.focus(); } function getwords() { return 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 isremote() { return currentvault == "remote"; } function logout() { if (isremote()) { window.localStorage.removeItem("password"); togglepassword(); } } function tagslist() { tags = {}; localdata .forEach(n => { var ts = gettags(n); ts.forEach(t => { tags[t] = tags[t] || []; tags[t].push(n.title); }); }); return searchinlist(Object.keys(tags).sort()); } function searchtags() { tagslist() .then(tag => searchinlist(tags[tag])) .then(loadnote); } 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 share() { if (navigator.share) { navigator.share( { text: getnotecontent(), title: currentnote.title }); } } function sharehtml() { if (navigator.share) { var file = new File(['
' + md2html(getnotecontent()) + ''], currentnote.title + ".html", { type: "text/html", }); navigator.share( { title: currentnote.title, files: [file] }); } } 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 .forEach(note => { download(note.title + ".md", note.content); }); } function downloadlocal() { var data = { local : JSON.parse(window.localStorage.getItem("local")), remote : JSON.parse(window.localStorage.getItem("remote")) }; download(timestamp() + " notes.json", JSON.stringify(data)); } function downloadnotewithsubs() { try { descendants(currentnote); } catch (err) { showtemporaryinfo("Loop detected"); return; } var tempnote = { title: currentnote.title + " (with sub notes)", content: getnotecontent() }; var kids = children(tempnote); while (kids.length) { kids.forEach(t => { tempnote.content = tempnote.content.replaceAll("[[" + t + "]]", getnote(t).content); }); kids = children(tempnote); } download(tempnote.title + ".md", tempnote.content); } function downloadnote() { download(currentnote.title + ".md", getnotecontent()); } function remotecallfailed(error) { if (error) { console.warn(error); showtemporaryinfo(error, "Error"); } } function loadstorage() { var item = window.localStorage.getItem(currentvault); localdata = item ? JSON.parse(item) : []; // only refresh content? if (currentnote) { currentnote = localdata.find(n => n.title == currentnote.title); } if (currentnote) { bindfile(currentnote); } else { loadlast(); } } function applystyle() { document.body.style.background = settings.bgcolor; document.body.style.fontFamily = settings.fontfamily; document.body.style.fontSize = settings.fontsize; document.body.style.lineHeight = settings.lineheight; document.body.style.color = settings.fontcolor; document.body.style.caretColor = settings.accentcolor; if (settings.titleinaccentcolor) { title.style.color = settings.accentcolor; } } function loadsettings() { settings = {...defaultsettings}; var item = window.localStorage.getItem("settings"); if (item) { item = JSON.parse(item); for (var key in settings) { if (typeof item[key] !== "undefined") { settings[key] = item[key]; } } } applystyle(); if (!settings.enablefolding) { commands = commands.filter(c => !c.hint.toLowerCase().includes("fold")); } } function checksaved() { if (!saved) { return "not saved"; } } function initsnippets() { // code languages codelanguages.forEach(lang => { if (!snippets.find(s => s.command == "/" + lang)) { snippets.push( { command: "/" + lang, hint: lang + " code block", insert: "```" + lang + "\n\n```", cursor: -4 }); } }); // md headings for (var i = 1; i <= 3; i++) { if (!snippets.find(s => s.command == "/" + i)) { snippets.push( { command: "/" + i, hint: "Heading " + i, insert: "#".repeat(i) + " ", cursor: 0 }); } } } function othervault() { return isremote() ? "local" : "remote"; } function initvault() { currentvault = window.localStorage.getItem("vault") || "local"; } function init() { loadsettings(); initvault(); commands.find(c => c.action == switchvault).hint = "Switch to " + othervault() + " vault"; window.onbeforeunload = checksaved; window.onclick = focuseditor; initsnippets(); currenttag = ""; if (isremote()) { queryremote({action: "fetch"}) .then(data => { localdata = data; window.localStorage.setItem("remote", JSON.stringify(data)); loadlast(); }) .catch(remotecallfailed); } else { loadstorage(); } if (issplit()) { window.onstorage = loadstorage; 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 queryremote(params) { return new Promise( (apply, failed) => { stat.cur.q++; stat.ses.q++; 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(); 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); datachanged(); } 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)) { event.preventDefault(); 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, "