diff --git a/main.js b/main.js index f3300d3..e0c7929 100644 --- a/main.js +++ b/main.js @@ -1,2580 +1,2580 @@ -var defaultsettings = -{ - fontsize: "16px", - lineheight: "24px", - margins: "20%", - fontfamily: "helvetica,system-ui", - savedelay: 2000, - defaultpreviewinsplit: false, - tagautocomplete: false, - enablenetwork: true, - titlebydefault: false, - linksinnewtab: true, - colors: true, - tagsinlists: true, - uselinkpopup: true, - downloadextension: ".md", - firstname: "Simon" -}; - -//builtin -const markerslist = ["* ", "- ", " * ", " - ", ">> ", "> ", "=> ", "— ", "[ ] ", " ", "• ", "- [ ]", "[x] ", "- [x]"]; -const codelanguages = ["xml", "js", "sql"]; -const tagmark = "+"; - -// globals -var previoustitle = ""; -var metadata = null; -var fileindex = 0; -var workerid = null; -var backup = ""; -var settings = null; -var tags = null; - -var commands = [ -{ - hint: "Close menu" -}, -{ - shortcut: "ctrl+shift+P", - hint: "Command palette", - action: commandpalette, -}, -{ - shortcut: "ctrl+p", - hint: "Show notes list", - action: searchandloadnote -}, -{ - shortcut: "ctrl+n", - hint: "New note", - action: startnewnote -}, -{ - shortcut: "ctrl+shift+N", - hint: "Quick new note", - action: quicknewnote -}, -{ - hint: "Force save", - action: flush, - shortcut: "ctrl+s" -}, -{ - hint: "Share note", - action: share -}, -{ - hint: "Share note (html)", - action: sharehtml -}, -{ - shortcut: "ctrl+g", - hint: "Find in notes", - action: showgrep -}, -{ - shortcut: "ctrl+i", - hint: "Toggle title", - action: toggletitle -}, -{ - shortcut: "ctrl+m", - hint: "Toggle preview", - action: togglepreview -}, -{ - shortcut: "ctrl+shift+M", - hint: "Toggle preview with merged subnotes", - action: togglepreviewwithsubs -}, -{ - shortcut: "ctrl+d", - hint: "Delete note", - action: deletecurrentnote -}, -{ - hint: "Restore current note", - action: restore -}, -{ - hint: "Insert markdown header", - action: insertheader -}, -{ - hint: "Show help", - action: showhelp -}, -{ - hint: "Search tags", - action: searchtags, - shortcut: "ctrl+shift+T" -}, -{ - hint: "Toggle split view", - action: togglesplit -}, -{ - hint: "Load previous note", - action: loadprevious, - shortcut: "alt+ArrowLeft" -}, -{ - hint: "Load next note", - action: loadnext, - shortcut: "alt+ArrowRight" -}, -{ - hint: "Settings", - action: editsettings -}, -{ - hint: "Change a setting", - action: changesetting -}, -{ - hint: "Restore default settings", - action: restoresettings -}, -{ - hint: "Note outline", - action: showoutline, - shortcut: "ctrl+o" -}, -{ - hint: "Show connected notes", - action: shownotelinks, - shortcut: "ctrl+l" -}, -{ - hint: "Show stats", - action: showinfo, - shortcut: "ctrl+w" -}, -{ - hint: "Toggle spell check", - action: togglespellcheck, - shortcut: "F7" -}, -{ - hint: "Create subnote from selection", - action: createsubnote -}, -{ - hint: "Merge subnote", - action: includesub -}, -{ - hint: "Comment selection", - action: comment -}, -{ - hint: "Download current note", - action: downloadnote -}, -{ - hint: "Download current note with merged subnotes", - action: downloadnotewithsubs -}, -{ - hint: "Download all notes (md files in zip archive)", - action: downloadnotes, - shortcut: "ctrl+shift+S" -}, -{ - hint: "Download all notes of tag (md files in zip archive)", - action: downloadtag -}, -{ - hint: "Download all notes (html files in zip archive)", - action: downloadhtmlnotes -}, -{ - hint: "Insert text in todo", - action: promptinserttodo -}, -{ - hint: "Replace", - shortcut: "ctrl+h", - action: searchandreplace -}, -{ - hint: "Sort text", - action: sortselection -}, -{ - hint: "Show backlinks", - action: backlinks -}, -{ - hint: "Import notes", - action: importnotes -}]; - -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,\n", - cursor: -29 -}, -{ - command: "/hello", - hint: "Standard answer (en)", - insert: "Hello ,\n\n\n\nKind regards,\n", - cursor: -24 -}, -{ - command: "/-", - hint: "Dialog mark", - insert: "— ", - cursor: 0 -}, -{ - command: "/comment", - hint: "Comment", - insert: "", - cursor: -4 -}, -{ - command: "/x", - hint: "Mark todo entry done", - insert: "x " + (new Date).toISOString().substring(0, 10) + " " -}]; - -function importnotes() -{ - const input = document.createElement('input'); - input.type = "file"; - input.webkitdirectory = true; - - input.addEventListener("change", () => - { - let files = Array.from(input.files); - - files.forEach(file => - { - var title = file.name.replace(".md", ""); - if (getguid(title)) - { - console.warn(title + " already exists, skip import"); - } - else - { - file.text().then(content => - { - var guid = createnote(title, content); - }); - } - }); - }); - - if ('showPicker' in HTMLInputElement.prototype) - { - input.showPicker(); - } - else - { - input.click(); - } - } - -function genguid() -{ - return crypto.randomUUID(); -} - -function seteditorcontent(content) -{ - md.value = content; - applycolors(); - flush(); - resize(); -} - -function getrangecontent(range) -{ - return md.value.substring(range.start, range.end); -} - -function currentrange() -{ - return { - start: md.selectionStart, - end: md.selectionEnd - }; -} - -function serialize(key, value) -{ - localStorage.setItem(key, value); - - var name = metadata && metadata[key] ? metadata[key].title : key; - console.log("'" + name + "' serialized locally"); -} - -function createsubnote() -{ - var title = prompt("Subnote tite:"); - if (!title) - { - showtemporaryinfo("No title provided"); - setpos(md.selectionStart); - md.focus(); - } - else - { - var guid = getguid(title); - if (guid) - { - showtemporaryinfo("'" + title + "' already exists"); - setpos(md.selectionStart); - md.focus(); - } - else - { - var range = currentrange(); - var content = getrangecontent(range); - guid = createnote(title); - serialize(guid, content); - - seteditorcontent(md.value.substring(0, range.start) - + "[[" + title + "]]" - + md.value.substring(range.end)); - } - } -} - -function comment() -{ - seteditorcontent(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?")) - { - var subnote = getnote(title); - seteditorcontent(md.value.substring(0, range.start) - + subnote.content - + md.value.substring(range.end)); - - if (confirm("Delete '" + title + "'?")) - { - deletenote(title); - } - } - } -} - -function togglespellcheck() -{ - md.spellcheck = !md.spellcheck; -} - -function formatsize(size) -{ - var unit = "b"; - if (size > 1024) - { - size /= 1024; - unit = "kb"; - } - if (size > 1024) - { - size /= 1024; - unit = "mb"; - } - return size.toFixed(2) + " " + unit; -} - -function pospercent() -{ - return md.value.length > 0 ?(100 * md.selectionStart / md.value.length).toFixed(2) : 100; -} - -function showinfo() -{ - var tags = gettags(md.value); - showtemporaryinfo( - [ - "title: " + title.value, - "line count: " + md.value.split("\n").length, - "word count: " + getwords(), - "cursor position: " + md.selectionStart + " (" + pospercent() + "%)", - (tags ? "tags: " + tags : ""), - "notes count: " + sortedlist().length, - "spell check: " + (md.spellcheck ? "en" : "dis") + "abled" - ].join("\n")); -} - -function savesettings() -{ - localStorage.setItem("settings", JSON.stringify(settings)); -} - -function children(guid) -{ - var content = localStorage.getItem(guid); - return (content - .match(/\[\[([^\]]*)\]\]/g) || []) - .map(l => l.replace("[[", "").replace("]]", "")) - .filter(l => !l.includes("(deleted)")) - .map(l => getguid(l)); -} - -function parents(guid) -{ - return Object.keys(metadata) - .filter(g => localStorage.getItem(g).indexOf("[[" + metadata[guid].title + "]]") != -1); -} - -function connected(guid) -{ - var list = [guid]; - var result = []; - - while (list.length) - { - var current = list.shift(); - if (result.indexOf(current) == -1) - { - result.push(current); - list = list.concat(children(current)).concat(parents(current)); - } - } - return result; -} - -function toggleeditor(hidden) -{ - md.hidden = hidden; - - if (settings.colors) - { - colored.hidden = hidden; - } -} - -function shownotelinks() -{ - if (settings.enablenetwork) - { - networkpage.hidden = false; - toggleeditor(true); - - var nodes = []; - var edges = []; - - var list = [getguid(title.value)]; - - while (list.length) - { - var current = list.shift(); - if (!nodes.find(n => n.id == current)) - { - nodes.push( - { - id: current, - label: metadata[current].title - }); - - var buddies = children(current).concat(parents(current)); - - list = list.concat(buddies); - - buddies. - forEach(buddy => { - if (!edges.find(edge => (edge.to == current && edge.from == buddy) || (edge.to == buddy && edge.from == current))) - { - edges.push({ - from: current, - to: buddy - }); - } - }); - } - } - - var data = { - nodes: nodes, - edges: edges - }; - - var options = - { - nodes: - { - color: - { - background: "white", - border: "black", - }, - font: - { - color: "black", - size: 16 - } - } - }; - - var graph = new vis.Network(network, data, options); - graph.on("click", function(event) - { - networkpage.hidden = true; - toggleeditor(false); - loadnote(nodes.find(n => n.id == event.nodes[0]).label); - }); - graph.setSelection( - { - nodes : [getguid(title.value)] - }); - } - else - { - searchinlist(connected(getguid(title.value)).map(g => metadata[g].title)) - .then(loadnote); - } -} - - -function showoutline() -{ - var outline = {}; - var pos = 0; - md.value.split("\n").forEach((line, index, lines) => - { - pos += line.length + 1; - if (line.startsWith("#")) - { - line = line - .replace("# ", "") - .replace(/#/g, "\xa0".repeat(4)); - outline[line] = pos; - } - else if (line == "---" && index != 0 && index != 3) - { - var next = lines.find((current, i) => - { - return i > index && current != ""; - }); - if (next) - { - var nbcar = 80; - next = next.length < nbcar ? next : next.substring(0, nbcar) + "..."; - outline[next] = 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() -{ - if (md.value.lastIndexOf("tags: ", md.selectionStart) < md.value.lastIndexOf("\n", md.selectionStart) || md.selectionStart < 6) - { - return null; - } - - var start = md.value.lastIndexOf(" ", md.selectionStart); - if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return ""; - - var eol = md.value.indexOf("\n", md.selectionStart); - var end = md.value.indexOf(",", md.selectionStart); - - if (end == -1 || eol < end) - { - end = eol; - } - - return md.value.substring(start + 1, end); -} - -function removelinkdialog() -{ - if (typeof linkdialog != "undefined") - { - notepage.removeChild(linkdialog); - } -} - -function showlinkdialog(link) -{ - var div = document.createElement("div"); - div.setAttribute("style", "top:" + event.pageY + "px;left:" + event.pageX + "px"); - div.setAttribute("id", "linkdialog"); - - var a = document.createElement("a"); - a.setAttribute("id", "linkelt"); - - if (link.startsWith("http")) - { - a.setAttribute("href", link); - a.setAttribute("target", "_blank"); - div.onclick = removelinkdialog; - a.innerText = "Open on " + - link.replace("https://", "") - .replace("http://", "") - .replace("www.", "") - .replace(/\/.*/, ""); - } - else - { - a.setAttribute("href", "#"); - a.innerText = link; - div.onclick = function() - { - removelinkdialog(); - loadnote(link); - }; - } - - div.appendChild(a); - - notepage.appendChild(div); -} - -function checkatpos(pos) -{ - if (pos > 0 && md.value[pos - 1] == "[" && md.value[pos + 1] == "]") - { - return { - pos: pos, - val: md.value[pos] - }; - } - return null; -} - -function clickedcheck() -{ - return checkatpos(md.selectionStart) || checkatpos(md.selectionStart + 1) || checkatpos(md.selectionStart - 1); -} - -function clickeditor() -{ - var word, link; - if (event.ctrlKey) - { - link = linkatpos(); - var tag = tagatpos(); - word = wordatpos(); - if (link) - { - loadnote(link); - } - else if (tag) - { - tagslist(); - searchinlist(tags[tag.toLowerCase()]) - .then(loadnote); - } - else if (word.startsWith("http")) - { - window.open(word, '_blank'); - } - } - else if (clickedcheck()) - { - var res = clickedcheck(); - seteditorcontent(md.value.substring(0, res.pos) - + (res.val == " " ? "x" : " ") - + md.value.substring(res.pos + 1)); - setpos(res.pos); - } - else if (settings.uselinkpopup) - { - removelinkdialog(); - link = linkatpos(); - if (link) - { - showlinkdialog(link); - } - else - { - word = wordatpos(); - if (word.startsWith("http")) - { - showlinkdialog(word); - } - } - } -} - -function restoresettings() -{ - if (confirm("Restore default settings?")) - { - settings = defaultsettings; - savesettings(); - loadsettings(); - } -} - -function editsettings() -{ - bind("settings.json", JSON.stringify(settings, null, " ")); -} - -function editsetting(name) -{ - if (typeof settings[name] != "undefined") - { - var value = settings[name]; - var type = typeof value; - if (type != "undefined") - { - value = prompt(name, value); - if (value !== null) - { - if (type == "number") - { - value = parseInt(value); - } - else if (type == "boolean") - { - value = value === "true"; - } - settings[name] = value; - savesettings(); - loadsettings(); - } - } - } -} - -function changesetting() -{ - searchinlist(Object.keys(settings).map(name => name + ": " + settings[name])) - .then(setting => - { - editsetting(setting.split(":").shift()); - }); -} - -function showtemporaryinfo(info) -{ - alert(info); -} - -function getwords() -{ - return md.value.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 tagslist() -{ - tags = {}; - - Object.values(metadata) - .forEach(n => - { - var ts = n.header.tags; - 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(content) -{ - var i = content.indexOf("tags: "); - if (i > -1) - { - var j = content.indexOf("\n", i); - return content.substring(i + 6, j) - .split(",") - .map(t => t.toLowerCase().trim()) - .filter(t => t.length); - } - return []; -} - -function share() -{ - if (navigator.share) - { - navigator.share( - { - text: md.value, - title: title.value - }); - } -} - -function sharehtml() -{ - if (navigator.share) - { - var file = new File(['' + md2html(md.value) + ''], - title.value + ".html", - { - type: "text/html", - }); - - navigator.share( - { - title: title.value, - files: [file] - }); - } -} - -function getfilename(title) -{ - return title.replace(/[\?\"<>|\*:\/\\]/g, "_") + settings.downloadextension; -} - -function download(filename, content) -{ - var element = document.createElement('a'); - element.setAttribute('href', 'data:text/markdown;charset=utf-8,' + encodeURIComponent(content)); - element.setAttribute('download', filename); - element.style.display = 'none'; - document.body.appendChild(element); - element.click(); - document.body.removeChild(element); - element = null; -} - -function downloadtag() -{ - tagslist() - .then(tag => - { - var zip = new JSZip(); - Object.keys(metadata).forEach(guid => - { - if (metadata[guid].header.tags.includes(tag)) - { - zip.file(getfilename(metadata[guid].title), localStorage.getItem(guid)); - } - }); - zip.generateAsync({type:"blob"}) - .then(function(content) - { - saveAs(content, "notes-" + tag + "-" + timestamp() + ".zip"); - }); - }); -} - -function downloadnotes() -{ - var zip = new JSZip(); - Object.keys(metadata).forEach(guid => - { - zip.file(getfilename(metadata[guid].title), localStorage.getItem(guid)); - }); - zip.generateAsync({type:"blob"}) - .then(function(content) - { - saveAs(content, "notes-" + timestamp() + ".zip"); - }); -} - -function downloadhtmlnotes() -{ - var zip = new JSZip(); - Object.keys(metadata).forEach(guid => - { - zip.file(getfilename(metadata[guid].title) + ".html", md2html(localStorage.getItem(guid))); - }); - zip.generateAsync({type:"blob"}) - .then(function(content) - { - saveAs(content, "notes-html-" + timestamp() + ".zip"); - }); -} - -function headerandtext(content) -{ - var result = - { - header: "", - text: content - }; - if (content.startsWith("---\n")) - { - var end = content.indexOf("---\n", 4); - if (end > -1) - { - result.header = content.substring(0, end + 4); - result.text = content.substring(end + 4); - } - } - return result; -} - -function inserttodo(text) -{ - var guid = getguid("todo"); - if (!guid) - { - guid = createnote("todo"); - } - var content = localStorage.getItem(guid); - var split = headerandtext(content); - content = split.header + text + "\n" + split.text; - if (title.value == "todo") - { - seteditorcontent(content); - } - else - { - serialize(guid, content); - metadata[guid].lastchanged = Date.now(); - flush(); - } -} - -function promptinserttodo() -{ - var text = prompt("Text:"); - if (text) - { - inserttodo(text); - } -} - -function downloadnotewithsubs() -{ - var note = withsubs(); - if (note) - { - download(getfilename(note.title), note.content); - } -} - -function downloadnote() -{ - download(getfilename(title.value), md.value); -} - -function getguid(title) -{ - return Object.keys(metadata).find(guid => metadata[guid].title === title); -} - -function gotoline(line) -{ - var i = 0; - var pos = 0; - while (i < line && pos > -1) - { - pos = md.value.indexOf("\n", pos + 1); - i++; - } - if (pos > -1) - { - setpos(pos + 1); - } -} - -function createnote(title, content) -{ - var guid = genguid(); - content = content || defaultheaders(); - var item = { - lastchanged: Date.now(), - title: title, - pos: content.length, - header: indexheader(content) - }; - metadata[guid] = item; - serializeindex() - serialize(guid, content); - return guid; -} - -function loadstorage() -{ - metadata = JSON.parse(localStorage.getItem("index")); - if (!metadata) - { - metadata = {}; - createnote(timestamp()); - } - - var params = new URLSearchParams(window.location.search); - var clip = params.get("c"); - - if (clip) - { - settings.savedelay = 0; - colored.hidden = true; - md.hidden = true; - var msg = document.createElement("div"); - msg.innerText = "Clipping..."; - notepage.appendChild(msg); - - inserttodo("@clip " + clip) - window.close(); - } - - var title = params.get("n") || params.get("name"); - if (window.title.value) - { - // reload current - loadnote(window.title.value); - } - else if (title) - { - loadnote(title); - var line = params.get("l"); - if (line) - { - gotoline(parseInt(line)); - } - } - else - { - loadlast(); - } -} - -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]; - } - } - } - - document.body.style.fontSize = settings.fontsize; - document.body.style.lineHeight = settings.lineheight; - document.body.style.marginLeft = settings.margins; - document.body.style.marginRight = settings.margins; - document.body.style.fontFamily = settings.fontfamily; - - if (settings.titlebydefault && title.hidden) - { - toggletitle(); - } - - colored.hidden = !settings.colors; - md.style.color = settings.colors ? "transparent" : "inherit"; - md.style.background = settings.colors ? "transparent" : "inherit"; - - snippets.find(s => s.command == "/bonjour").insert = "Bonjour ,\n\n\n\nBien cordialement,\n" + settings.firstname; - snippets.find(s => s.command == "/hello").insert = "Hello ,\n\n\n\nKind regards,\n" + settings.firstname; -} - -function checksaved() -{ - if (!unsavedmark.hidden) - { - 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 - }); - } - }); -} - -function indexheader(content) -{ - var indexedheader = { - tags: [] - }; - var hat = headerandtext(content); - var header = hat.header; - if (header) - { - header.split("\n").forEach(line => - { - if (line && line != "---") - { - var t = line.split(":"); - var val = t.pop(); - var key = t.pop(); - if (key == "tags") - { - val = val.split(",").map(t => t.trim()); - if (val.length == 1 && !val[0]) - { - val.pop(); - } - } - indexedheader[key] = val; - } - }); - } - return indexedheader; -} - -function migratelegacystorage() -{ - var legacy = localStorage.getItem("data"); - if (legacy) - { - alert("Hey! I am about to migrate your notes to the brand new data model. No worries, I will keep a backup somewhere in case things go wrong. Take a deep breath, and click ok when you're ready."); - localStorage.setItem("legacy", legacy); - legacy = JSON.parse(legacy); - var index = {}; - legacy.reverse().forEach( (note, i) => - { - var guid = genguid(); - localStorage.setItem(guid, note.content); - note.header = indexheader(note.content); - note.lastchanged = i; - delete note.content; - index[guid] = note; - }); - localStorage.setItem("index", JSON.stringify(index)); - localStorage.removeItem("data"); - } -} - -function init() -{ - migratelegacystorage(); - loadsettings(); - - window.onbeforeunload = checksaved; - window.onclick = focuseditor; - title.value = ""; - - initsnippets(); - - loadstorage(); - - if (issplit()) - { - if (settings.defaultpreviewinsplit && name == "right") - { - togglepreview(); - } - else if (name == "left") - { - md.focus(); - } - } -} - -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 backlinks() -{ - searchinlist(Object.keys(metadata) - .filter(guid => localStorage.getItem(guid).includes("[[" + title.value + "]]")) - .map(guid => metadata[guid].title)) - .then(loadnote); -} - -function sortselection() -{ - var content = md.value; - 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"); - seteditorcontent(content.substring(0, range.start) + sorted + content.substring(range.end)); -} - -function wordatpos() -{ - var words = md.value.split(/\s/); - var i = 0; - var word = ""; - while (i < md.selectionStart) - { - word = words.shift(); - i += word.length + 1; - } - return word; -} - -function ontopbarclick() -{ - if (title.hidden) - { - commandpalette(); - } -} - -function md2html(content) -{ - var converter = new showdown.Converter(); - converter.setOption("simplifiedAutoLink", true); - converter.setOption("simpleLineBreaks", true); - converter.setOption("metadata", true); - converter.setOption("tasklists", true); - converter.setOption("literalMidWordUnderscores", true); - - if (settings.linksinnewtab) - { - converter.setOption("openLinksInNewWindow", true); - } - - var html = converter.makeHtml(content); - - // internal links - html = html.replace(/\[\[([^\]]*)\]\]/g, "$1"); - - return html; -} - -function loadlast() -{ - loadnote(sortedlist()[0].title); -} - -function loadprevious() -{ - var list = sortedlist(); - var index = list.findIndex(i => i.title == title.value); - if (index > -1 && index < list.length - 1) - { - loadnote(list[index + 1].title); - } -} - -function loadnext() -{ - var list = sortedlist(); - var index = list.findIndex(i => i.title == title.value); - if (index > -1 && index > 0) - { - loadnote(list[index - 1].title); - } -} - -function sortedlist() -{ - return Object - .values(metadata) - .sort( (i,j) => j.lastchanged - i.lastchanged); -} - -function grep(needle) -{ - var result = {}; - - sortedlist() - .forEach(item => - { - if (item.title.toLowerCase().includes(needle.toLowerCase())) - { - result[item.title] = {}; - } - var content = localStorage.getItem(getguid(item.title)); - content.split("\n") - .forEach((line, nb) => { - if (line.toLowerCase().includes(needle.toLowerCase())) - { - result[item.title] = result[item.title] || {}; - result[item.title][nb] = line; - } - }); - }); - - return result; -} - -function showgrepresult(needle, grepresult) -{ - var grepcontent = ["# Search results: \"" + needle + "\""]; - for (var file in grepresult) - { - grepcontent.push("[[" + file + "]]"); - for (var l in grepresult[file]) - { - grepcontent.push("[" + l + "] " + grepresult[file][l]); - } - grepcontent.push(""); - } - - if (grepcontent.length == 0) - { - grepcontent.push("No result."); - } - - bind("Search result", grepcontent.join("\n")); - - if (preview.hidden) - { - togglepreview(); - } -} - -function showgrep() -{ - var text = prompt("Search:", md.selectionEnd > md.selectionStart ? md.value.substr(md.selectionStart, md.selectionEnd - md.selectionStart).trim() : ""); - if (text) - { - showgrepresult(text, grep(text)); - } -} - -function commandpalette() -{ - searchinlist(commands - .filter(command => !command.excludepalette) - .map(command => - { - return { - prefix: "command ", - text: command.hint, - suffix: command.shortcut ? [command.shortcut.toLowerCase()] : null - }; - }) - .concat(snippets.map(s => - { - return { - prefix: "snippet ", - text: s.hint, - suffix: [s.command] - }; - })) - .concat( - sortedlist() - .map(item => - { - return { - prefix: "note ", - text: item.title, - suffix: item.header.tags.map(t => tagmark + t) - }; - })) - .concat(Object.keys(settings).map(s => - { - return { - prefix: "setting ", - text: s, - suffix: [settings[s]] - }; - }))) - .then(selected => - { - if (selected.prefix == "snippet ") - { - var snippet = snippets.find(s => s.hint == selected.text); - insert(snippet.insert, snippet.cursor); - md.focus(); - } - else if (selected.prefix == "note ") - { - loadnote(selected.text); - } - else if (selected.prefix == "setting ") - { - editsetting(selected.text); - } - else - { - var command = commands.find(c => c.hint == selected.text); - if (command) - { - executecommand(command); - } - else - { - // if unknown command, create a new note - loadnote(selected); - } - } - }); -} - -function insert(text, cursoroffset = 0, nbtodelete = 0) -{ - var pos = md.selectionStart; - var content = md.value; - seteditorcontent( - content.substring(0, pos - nbtodelete) - + text - + content.substring(pos)); - setpos(pos - nbtodelete + text.length + cursoroffset); -} - -function searchinlist(list) -{ - return new Promise(selectitem => - { - fileindex = 0; - searchdialog.hidden = false; - filteredlist.hidden = false; - - filteredlist.innerHTML = ""; - filter.value = ""; - - list.forEach(item => - { - var elt = document.createElement("div"); - elt.tag = item; - - if (typeof item === "string") - { - elt.textContent = item; - } - else - { - var ts = document.createElement("span"); - ts.setAttribute("class", "searchlistprefix"); - ts.innerHTML = item.prefix || ""; - elt.appendChild(ts); - - ts = document.createElement("span"); - ts.innerHTML = item.text; - elt.appendChild(ts); - - if (item.suffix) - { - item.suffix.forEach(t => - { - ts = document.createElement("span"); - ts.setAttribute("class", "searchlistsuffix"); - ts.innerHTML = " " + t; - elt.appendChild(ts); - }); - } - } - - elt.onclick = function() - { - searchdialog.hidden = true; - selectitem(elt.tag); - } - 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.tag : filter.value); - } - } - - filter.focus(); - }); -} - -function applyfileindex() -{ - var i = 0; - [...filteredlist.children].forEach(child => - { - if (child.nodeName == "DIV") - { - child.className = "searchitem"; - if(!child.hidden) - { - if (i++ == fileindex) - { - child.className = "selected"; - if (child.customevent) - { - child.customevent(child.textContent); - filter.focus(); - } - } - } - } - }); -} - -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) - { - md.style.height = md.scrollHeight + 'px'; - } -} - -function postpone() -{ - return new Promise(function(resolve) - { - clearTimeout(workerid); - workerid = setTimeout(resolve, settings.savedelay); - }); -} - -function flush() -{ - clearTimeout(workerid); - workerid = null; - - var guid = getguid(title.value); - if (guid) - { - var item = metadata[guid]; - item.title = title.value; - item.pos = md.selectionStart; - item.header = indexheader(md.value); - item.lastchanged = Date.now(); - - serializeindex(); - serialize(guid, md.value); - } - else if (title.value == "settings.json") - { - settings = JSON.parse(md.value); - savesettings(); - loadsettings(); - } - unsavedmark.hidden = true; -} - -function escapeHtml(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - -var languagekeywords = { - "sql": ["select", "from", "where", "and", "or", "set", "declare"], - "js": ["var", "for", "if", "else"], - "zsh": ["sudo"], - "bash": ["molecule", "cd"], - "python": ["from", "import"] -} - -function currentline() -{ - return (md.value.substring(0, md.selectionStart).match(/\n/g) || []).length; -} - -function lineatpos(pos) -{ - return (md.value.substring(0, pos).match(/\n/g) || []).length; -} - -function currentcol() -{ - return md.selectionStart - Math.max(0, md.value.lastIndexOf("\n", md.selectionStart - 1)) - 1; -} - -function rawline(index) -{ - return md.value.split("\n")[index]; -} - -var emptyline = "
"; -function rawline2html(line, index, options) -{ - line = escapeHtml(line); - - // headings - if (line.startsWith("#")) - { - line = line.replace(/(#* )/, "$1"); - line = "" + line + ""; - } - - // bold and italics - var temp = line; - if (line.startsWith("* ")) - { - temp = line.substring(2); - } - temp = temp.replace(/\*\*([^\*]*)\*\*/g, "**$1**"); - temp = temp.replace(/\*([^\*]*)\*/g, "*$1*"); - - if (line.startsWith("* ")) - { - line = "* " + temp; - } - else - { - line = temp; - } - - // lists - markerslist.forEach(marker => - { - if (line.startsWith(marker) && marker.trim()) - { - line = line.replace(marker, "" + marker + ""); - } - }); - - // md header - if (index == 0 && line == "---") - { - options.header = true; - } - if (options.header) - { - if (index > 0 && line == "---") - { - options.header = false; - } - line = line || emptyline; - line = "" + line + ""; - } - - // code blocks - if (line.startsWith("```") && !options.code) - { - options.code = true; - options.language = line.substring(3); - line = "
" + line.replace(new RegExp("(" + options.language + ")"), "$1") + "
"; - } - else if (line == "```" && options.code) - { - options.code = false; - options.language = ""; - line = "
" + line + "
"; - } - else if (options.code) - { - var comment = false; - if (line.match(/^\s*\/\//)) - { - line = "" + line + ""; - comment = true; - } - line = "
" + (line || emptyline) + "
"; - if (!comment && languagekeywords[options.language]) - { - var keywords = languagekeywords[options.language]; - keywords.forEach(keyword => - { - line = line.replace(new RegExp("\\b(" + keyword + ")\\b", "ig"), "$1"); - }); - } - } - - // internal links - line = line.replace(/\[\[(.*)\]\]/g, "[[$1]]"); - - // comments - line = line.replace(/<\!--(.*)/g, "<!--$1"); - - if (line.includes("<!--") && !line.includes("-->")) - { - options.comment = true; - } - else if (options.comment) - { - line = line || emptyline; - line = "" + line - if (line.includes("-->")) - { - options.comment = false; - } - else - { - line += ""; - } - } - - line = line.replace(/\-\->/g, "-->"); - - if (line.startsWith("// ")) - { - line = "" + line + ""; - } - - // autocomplete snippets - if (index == currentline()) - { - var raw = rawline(index); - var pos = currentcol(); - var slashpos = raw.substring(0, pos).lastIndexOf("/"); - if (slashpos > -1) - { - var spacepos = raw.substring(0, pos).lastIndexOf(" "); - if (slashpos > spacepos) - { - var snippetpart = raw.substring(slashpos); - var matching = snippets - .filter(s => s.command.startsWith(snippetpart)) - .map(s => s.command.substring(1)); - - if (matching.length) - { - line += ""; - line += matching.join().substr(pos - slashpos - 1); - line += ""; - } - } - } - } - - if (line.startsWith("x ")) - { - line = "" + line + ""; - } - - // inline code - line = line.replace(/`(.*)`/, "`$1`"); - - // links - line = line.replace(/(http[^\s]*)/, "$1"); - - return line; -} - -function applycolors(currentonly) -{ - if (!settings.colors) - { - return; - } - - var options = - { - header: false, - code: false, - comment: false, - language: "" - }; - - var linediv = null; - if (currentonly) - { - var index = currentline(); - linediv = document.getElementById("line" + index); - options = JSON.parse(linediv.getAttribute("tag")); - var line = rawline(index); - line = rawline2html(line, index, options); - linediv.innerHTML = line || emptyline; - } - else - { - console.log("redrawing all colored div"); - var lines = md.value.split("\n"); - var i = 0; - for (; i < lines.length; i++) - { - linediv = document.getElementById("line" + i); - if (!linediv) - { - linediv = document.createElement("div"); - colored.appendChild(linediv); - } - linediv.setAttribute("id", "line" + i); - linediv.setAttribute("tag", JSON.stringify(options)); - linediv.innerHTML = rawline2html(lines[i], i, options) || emptyline; - } - - // remove remanining - linediv = document.getElementById("line" + (i++)); - while (linediv) - { - colored.removeChild(linediv); - linediv = document.getElementById("line" + (i++)); - } - } -} - -function editorinput() -{ - unsavedmark.hidden = false; - - // criteria to improve. Or redraw only after? - var multiline = md.value.substring(md.selectionStart, md.selectionEnd).includes("\n"); - applycolors(!multiline && event.data && (event.inputType == "insertText" || event.inputType == "deleteContentBackward" || event.inputType == "deleteContentForward")); - - // todo: fix if current note change during postponing, the wrong one will be saved. Or prevent binding another note? - postpone().then(flush); - resize(); -} - -function timestamp() -{ - var utc = new Date(); - var loc = new Date(utc - utc.getTimezoneOffset() * 60 * 1000); - - return loc.toISOString().replace("T", " ").replace(/\..*/, "").replace(/:/g, "."); -} - -function quicknewnote() -{ - flush(); - loadnote(timestamp()); -} - -function startnewnote() -{ - flush(); - var title = prompt("Note title: ", timestamp()); - if (title) - { - loadnote(title); - } -} - -function showhelp() -{ - var help = ["# Notes"]; - help.push("## Shortcuts"); - - commands - .filter(command => Boolean(command.shortcut)) - .forEach(command => help.push(command.hint + ": " + command.shortcut.toLowerCase())); - - help.push("## Snippets"); - snippets.forEach(snippet => - { - help.push(snippet.hint + ": " + snippet.command); - }); - - help.push("## Libs"); - help.push("[Showdown](https://showdownjs.com/)"); - help.push("[vis-network](https://visjs.org/)"); - help.push("[jszip](https://stuk.github.io/jszip/)"); - help.push("[FileSaver](http://eligrey.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/)"); - help.push("[Notion](https://www.notion.so/)"); - help.push("[Calmly Writer](https://calmlywriter.com/)"); - help.push("[Cryptee](https://crypt.ee/)"); - - help.push("##Sources"); - help.push("https://git.ouvaton.coop/quenousimporte/notes"); - - bind("Help", 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( - sortedlist() - .map(item => - { - return { - text: item.title, - suffix: item.header.tags.map(t => tagmark + t) - } - })); -} - -function searchautocomplete() -{ - selectnote().then(selected => - { - insertautocomplete(selected.text || selected); - }); -} - -function searchandloadnote() -{ - selectnote().then(selected => - { - var title = selected.text || selected; - loadnote(title); - }); -} - -function istodo(title, content) -{ - return title.includes("todo") || gettags(content).includes("todo"); -} - -function searchandreplace() -{ - var oldvalue = prompt("Search:"); - if (!oldvalue) - { - return; - } - - var newvalue = prompt("Replace by:"); - if (!newvalue) - { - return; - } - - var doit = confirm(`Replace '${oldvalue}' by '${newvalue}'?`); - if (!doit) - { - return; - } - - seteditorcontent(md.value.replaceAll(oldvalue, newvalue)); -} - -function serializeindex() -{ - serialize("index", JSON.stringify(metadata)); -} - -function deletenote(title) -{ - var guid = getguid(title); - delete metadata[guid]; - renameinternallinks(title, title + " (deleted)"); - var content = localStorage.getItem(guid); - localStorage.setItem(guid, "Deleted. Title: " + title + ". Date: " + timestamp() + "\r\n" + content); - serializeindex(); -} - -function deletecurrentnote() -{ - if (confirm('delete "' + title.value + '"?')) - { - deletenote(title.value); - loadlast(); - } -} - -function restore() -{ - if (confirm('restore "' + title.value + '"?')) - { - seteditorcontent(backup); - } -} - -function insertheader() -{ - if (preview.hidden && !md.value.startsWith("---\n")) - { - var headers = defaultheaders(); - seteditorcontent(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 shortcutmatches(event, shortcut) -{ - var s = splitshortcut(shortcut); - return (event.key == s.key && !(s.ctrl && !event.ctrlKey && !event.altKey) && !(s.shift && !event.shiftKey) && !(s.alt && !event.altKey)) -} - -function executecommand(command) -{ - if (command.action) - { - command.action(); - } -} - -function esc(event) -{ - if (!searchdialog.hidden) - { - event.preventDefault(); - searchdialog.hidden = true; - filter.placeholder = "Search..."; - md.focus(); - } - else if (settings.uselinkpopup && typeof linkdialog != "undefined") - { - removelinkdialog(); - } - else if (title.value == "Help" || title.value == "Search result") - { - loadlast(); - } - else if (networkpage.hidden == false) - { - networkpage.hidden = true; - toggleeditor(false); - } - else if (preview.hidden == false) - { - togglepreview(); - } - else - { - commandpalette(); - } -} - -function boldify() -{ - var pos = currentrange(); - var newcontent; - var offset = 2; - if (md.value.substr(pos.start - 2, 2) == "**" && md.value.substr(pos.end, 2) == "**") - { - newcontent = md.value.substr(0, pos.start - 2) + md.value.substr(pos.start, pos.end - pos.start) + md.value.substr(pos.end + 2); - offset = -2; - } - else - { - newcontent = md.value.substr(0, pos.start) + "**" + md.value.substr(pos.start, pos.end - pos.start) + "**" + md.value.substr(pos.end); - } - - seteditorcontent(newcontent); - md.setSelectionRange(pos.start + offset, pos.end + offset); -} - -function foreachmetadata(callback) -{ - Object.keys(metadata).forEach(guid => - { - callback(guid, metadata[guid]); - }); -} - -function snippetautocomplete() -{ - var tocursor = md.value.substr(0, md.selectionStart) - var slashindex = tocursor.lastIndexOf("/"); - if (slashindex > tocursor.lastIndexOf(" ") && slashindex > tocursor.lastIndexOf("\n")) - { - var commandbegin = tocursor.substr(slashindex); - var snippet = snippets.find(s => s.command.startsWith(commandbegin)); - if (snippet) - { - insert(snippet.insert, snippet.cursor, commandbegin.length); - return true; - } - } - return false; -} - -function mainkeydownhandler() -{ - if (event.key == "Escape") - { - esc(event); - } - else 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(); - } - else if (event.ctrlKey && event.key == " " || event.key == "F1") - { - commandpalette(); - event.preventDefault(); - } - else if (event.ctrlKey && event.key == "b" && md.selectionStart < md.selectionEnd) - { - boldify(); - event.preventDefault(); - } - else if (event.ctrlKey && event.shiftKey && (event.keyCode == "40" || event.keyCode == "38")) - { - var pos = currentrange(); - var direction = event.keyCode == "40" ? 1 : -1; - var start = lineatpos(md.selectionStart); - var end = lineatpos(md.selectionEnd); - var lines = md.value.split("\n"); - if (direction > 0 && end < lines.length || direction < 0 && start > 0) - { - var block = lines.splice(start, end - start + 1); - lines.splice(start + direction, 0, ...block); - seteditorcontent(lines.join("\n")); - var posshift = direction > 0 ? lines[start].length + 1 : - 1 - lines[end].length; - md.setSelectionRange(pos.start + posshift, pos.end + posshift); - - } - event.preventDefault(); - } - else if (event.ctrlKey || event.altKey) - { - // notes shortcuts - var found = false; - foreachmetadata((guid, item) => - { - if (item.header.shortcut && shortcutmatches(event, item.header.shortcut)) - { - console.log("Loading note '" + item.title + "' from header shortcut " + item.header.shortcut); - event.preventDefault(); - loadnote(item.title); - found = true; - } - }); - - // commands shortcuts - if (!found) - { - commands.filter(c => c.shortcut) - .every(command => - { - if (shortcutmatches(event, command.shortcut)) - { - event.preventDefault(); - executecommand(command); - return false; - } - return true; - }); - } - } -} - -function setwindowtitle() -{ - document.title = title.value; -} - -function renameinternallinks(from, to) -{ - foreachmetadata( (guid, item) => - { - var content = localStorage.getItem(guid); - var newcontent = content.replaceAll("[[" + from + "]]", "[[" + to + "]]"); - if (content != newcontent) - { - serialize(guid, newcontent); - if (item.title == title.value) - { - seteditorcontent(newcontent); - } - } - }); -} - -function ontitlechange() -{ - var guid = getguid(title.value); - if (guid) - { - showtemporaryinfo(title.value + " alreday exists"); - title.value = previoustitle; - } - else - { - renameinternallinks(previoustitle, title.value); - - guid = getguid(previoustitle); - previoustitle = title.value; - metadata[guid].title = title.value; - setwindowtitle(); - flush(); - - if (!settings.titlebydefault) - { - toggletitle(); - } - } -} - -function simplifystring(str) -{ - return str.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ""); -} - -function applyfilter() -{ - [...filteredlist.children].forEach(div => - { - div.hidden = simplifystring(div.textContent).indexOf(simplifystring(filter.value)) < 0; - }); - - fileindex = 0; - applyfileindex(); -} - -function backspace(nb) -{ - var pos = getpos(); - var c = md.value; - seteditorcontent(c.substring(0, pos - nb) + c.substring(pos)); - setpos(pos - nb); -} - -function editorkeydown() -{ - if (event.key == "Enter") - { - var line = md.value.substring(0, getpos()).split("\n").pop(); - markerslist.filter(marker => line.startsWith(marker)) - .every(marker => - { - event.preventDefault(); - if (line != marker) - { - insert("\n" + marker.replace("[x]", "[ ]")); - } - else - { - backspace(marker.length); - } - return false; - }); - } - else if (event.key === "Tab") - { - event.preventDefault(); - if (!snippetautocomplete()) - { - var init = currentrange(); - var range = getlinesrange(); - range.start--; - range.end--; - var selection = md.value.substring(range.start, range.end); - var newtext; - if (event.shiftKey) - { - newtext = selection.replaceAll("\n ", "\n"); - } - else - { - newtext = selection.replaceAll("\n", "\n "); - } - seteditorcontent(md.value.substring(0, range.start) - + newtext - + md.value.substring(range.end)); - - var shift = 0; - if (newtext.length < selection.length) - { - shift = -4; - } - else if (newtext.length > selection.length) - { - shift = 4; - } - - md.selectionStart = init.start + shift; - md.selectionEnd = init.end + (newtext.length - selection.length); - } - } - else if (event.key === "[" && before(1) == "[") - { - event.preventDefault(); - insert("["); - searchautocomplete(); - } - else if (settings.tagautocomplete && event.key == " " && before(1) == "," && md.value.substring(0, getpos()).split("\n").pop().startsWith("tags: ")) - { - event.preventDefault(); - // search in tags list - console.log(event.key); - tagslist() - .then(tag => - { - insert(" " + tag); - md.focus(); - }) - } - else - { - var snippet = snippets.find(s => before(s.command.length - 1) + event.key == s.command); - if (snippet) - { - event.preventDefault(); - insert(snippet.insert, snippet.cursor, snippet.command.length - 1); - } - } -} - -function insertautocomplete(selectednote) -{ - md.focus(); - insert(selectednote + "]] "); -} - -function togglepreview() -{ - preview.innerHTML = md2html(md.value); - toggleeditor(!md.hidden); - preview.hidden = !preview.hidden; - - if (preview.hidden) - { - resize(); - md.focus(); - } -} - -function withsubs() -{ - // todo: handle loops - var currentguid = getguid(title.value); - - var content = md.value; - var kids = children(currentguid); - while (kids.length) - { - kids.forEach(guid => - { - var kidcontent = localStorage.getItem(guid); - if (kidcontent.startsWith("---\n")) - { - var pos = kidcontent.indexOf("---\n", 4); - kidcontent = kidcontent.substring(pos + 4); - } - content = content.replaceAll("[[" + metadata[guid].title + "]]", kidcontent); - }); - kids = children(currentguid); - } - - return content; -} - -function togglepreviewwithsubs() -{ - var note = withsubs(); - if (note) - { - preview.innerHTML = md2html(note.content); - toggleeditor(!md.hidden); - preview.hidden = !preview.hidden; - - if (preview.hidden) - { - resize(); - md.focus(); - } - } -} - -function bind(title, content, pos) -{ - if (workerid) - { - showtemporaryinfo("Cannot open '" + title + "' because current note not yet serialized"); - return; - } - - previoustitle = title; - backup = content; - - window.title.value = title; - setwindowtitle(); - - md.value = content || ""; - applycolors(); - - md.style.height = "0px"; - resize(); - - setpos(pos || 0); - - if (!issplit() && searchdialog.hidden) - { - // force to scroll to cursor pos - md.blur(); - md.focus(); - } -} - -function defaultheaders(tags = "") -{ - return [ - "---", - "date: " + timestamp().substr(0,10), - "tags: " + (tags || ""), - "---", - "",""].join("\n"); -} - -function loadnote(title) -{ - var guid = getguid(title); - if (!guid) - { - guid = createnote(title); - } - var content = localStorage.getItem(guid); - var item = metadata[guid]; - - if (item.header.tags.includes("journal")) - { - // remove empty entries - content = content.replace(/\d{4}-\d{2}-\d{2}\n*(\d{4}-\d{2}-\d{2})/g, "$1"); - - // create new entry for today - var hat = headerandtext(content); - var today = timestamp().substr(0,10); - if (!hat.text.startsWith(today) && !hat.text.startsWith("\n" + today)) - { - content = hat.header + "\n" + today + "\n\n" + hat.text; - item.pos = hat.header.length + 12; - } - } - - bind(item.title, content, item.pos); - item.lastchanged = Date.now(); - serializeindex(); - - if (!preview.hidden || (preview.hidden && (gettags(md.value).indexOf("preview") !== -1))) - { - togglepreview(); - } -} - -function focuseditor() -{ - if (document.documentElement == event.srcElement) - { - md.focus(); - console.log("Forced focus"); - } -} +var defaultsettings = +{ + fontsize: "16px", + lineheight: "24px", + margins: "20%", + fontfamily: "helvetica,system-ui", + savedelay: 2000, + defaultpreviewinsplit: false, + tagautocomplete: false, + enablenetwork: true, + titlebydefault: false, + linksinnewtab: true, + colors: true, + tagsinlists: true, + uselinkpopup: true, + downloadextension: ".md", + firstname: "" +}; + +//builtin +const markerslist = ["* ", "- ", " * ", " - ", ">> ", "> ", "=> ", "— ", "[ ] ", " ", "• ", "- [ ]", "[x] ", "- [x]"]; +const codelanguages = ["xml", "js", "sql"]; +const tagmark = "+"; + +// globals +var previoustitle = ""; +var metadata = null; +var fileindex = 0; +var workerid = null; +var backup = ""; +var settings = null; +var tags = null; + +var commands = [ +{ + hint: "Close menu" +}, +{ + shortcut: "ctrl+shift+P", + hint: "Command palette", + action: commandpalette, +}, +{ + shortcut: "ctrl+p", + hint: "Show notes list", + action: searchandloadnote +}, +{ + shortcut: "ctrl+n", + hint: "New note", + action: startnewnote +}, +{ + shortcut: "ctrl+shift+N", + hint: "Quick new note", + action: quicknewnote +}, +{ + hint: "Force save", + action: flush, + shortcut: "ctrl+s" +}, +{ + hint: "Share note", + action: share +}, +{ + hint: "Share note (html)", + action: sharehtml +}, +{ + shortcut: "ctrl+g", + hint: "Find in notes", + action: showgrep +}, +{ + shortcut: "ctrl+i", + hint: "Toggle title", + action: toggletitle +}, +{ + shortcut: "ctrl+m", + hint: "Toggle preview", + action: togglepreview +}, +{ + shortcut: "ctrl+shift+M", + hint: "Toggle preview with merged subnotes", + action: togglepreviewwithsubs +}, +{ + shortcut: "ctrl+d", + hint: "Delete note", + action: deletecurrentnote +}, +{ + hint: "Restore current note", + action: restore +}, +{ + hint: "Insert markdown header", + action: insertheader +}, +{ + hint: "Show help", + action: showhelp +}, +{ + hint: "Search tags", + action: searchtags, + shortcut: "ctrl+shift+T" +}, +{ + hint: "Toggle split view", + action: togglesplit +}, +{ + hint: "Load previous note", + action: loadprevious, + shortcut: "alt+ArrowLeft" +}, +{ + hint: "Load next note", + action: loadnext, + shortcut: "alt+ArrowRight" +}, +{ + hint: "Settings", + action: editsettings +}, +{ + hint: "Change a setting", + action: changesetting +}, +{ + hint: "Restore default settings", + action: restoresettings +}, +{ + hint: "Note outline", + action: showoutline, + shortcut: "ctrl+o" +}, +{ + hint: "Show connected notes", + action: shownotelinks, + shortcut: "ctrl+l" +}, +{ + hint: "Show stats", + action: showinfo, + shortcut: "ctrl+w" +}, +{ + hint: "Toggle spell check", + action: togglespellcheck, + shortcut: "F7" +}, +{ + hint: "Create subnote from selection", + action: createsubnote +}, +{ + hint: "Merge subnote", + action: includesub +}, +{ + hint: "Comment selection", + action: comment +}, +{ + hint: "Download current note", + action: downloadnote +}, +{ + hint: "Download current note with merged subnotes", + action: downloadnotewithsubs +}, +{ + hint: "Download all notes (md files in zip archive)", + action: downloadnotes, + shortcut: "ctrl+shift+S" +}, +{ + hint: "Download all notes of tag (md files in zip archive)", + action: downloadtag +}, +{ + hint: "Download all notes (html files in zip archive)", + action: downloadhtmlnotes +}, +{ + hint: "Insert text in todo", + action: promptinserttodo +}, +{ + hint: "Replace", + shortcut: "ctrl+h", + action: searchandreplace +}, +{ + hint: "Sort text", + action: sortselection +}, +{ + hint: "Show backlinks", + action: backlinks +}, +{ + hint: "Import notes", + action: importnotes +}]; + +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,\n", + cursor: -29 +}, +{ + command: "/hello", + hint: "Standard answer (en)", + insert: "Hello ,\n\n\n\nKind regards,\n", + cursor: -24 +}, +{ + command: "/-", + hint: "Dialog mark", + insert: "— ", + cursor: 0 +}, +{ + command: "/comment", + hint: "Comment", + insert: "", + cursor: -4 +}, +{ + command: "/x", + hint: "Mark todo entry done", + insert: "x " + (new Date).toISOString().substring(0, 10) + " " +}]; + +function importnotes() +{ + const input = document.createElement('input'); + input.type = "file"; + input.webkitdirectory = true; + + input.addEventListener("change", () => + { + let files = Array.from(input.files); + + files.forEach(file => + { + var title = file.name.replace(".md", ""); + if (getguid(title)) + { + console.warn(title + " already exists, skip import"); + } + else + { + file.text().then(content => + { + var guid = createnote(title, content); + }); + } + }); + }); + + if ('showPicker' in HTMLInputElement.prototype) + { + input.showPicker(); + } + else + { + input.click(); + } + } + +function genguid() +{ + return crypto.randomUUID(); +} + +function seteditorcontent(content) +{ + md.value = content; + applycolors(); + flush(); + resize(); +} + +function getrangecontent(range) +{ + return md.value.substring(range.start, range.end); +} + +function currentrange() +{ + return { + start: md.selectionStart, + end: md.selectionEnd + }; +} + +function serialize(key, value) +{ + localStorage.setItem(key, value); + + var name = metadata && metadata[key] ? metadata[key].title : key; + console.log("'" + name + "' serialized locally"); +} + +function createsubnote() +{ + var title = prompt("Subnote tite:"); + if (!title) + { + showtemporaryinfo("No title provided"); + setpos(md.selectionStart); + md.focus(); + } + else + { + var guid = getguid(title); + if (guid) + { + showtemporaryinfo("'" + title + "' already exists"); + setpos(md.selectionStart); + md.focus(); + } + else + { + var range = currentrange(); + var content = getrangecontent(range); + guid = createnote(title); + serialize(guid, content); + + seteditorcontent(md.value.substring(0, range.start) + + "[[" + title + "]]" + + md.value.substring(range.end)); + } + } +} + +function comment() +{ + seteditorcontent(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?")) + { + var subnote = getnote(title); + seteditorcontent(md.value.substring(0, range.start) + + subnote.content + + md.value.substring(range.end)); + + if (confirm("Delete '" + title + "'?")) + { + deletenote(title); + } + } + } +} + +function togglespellcheck() +{ + md.spellcheck = !md.spellcheck; +} + +function formatsize(size) +{ + var unit = "b"; + if (size > 1024) + { + size /= 1024; + unit = "kb"; + } + if (size > 1024) + { + size /= 1024; + unit = "mb"; + } + return size.toFixed(2) + " " + unit; +} + +function pospercent() +{ + return md.value.length > 0 ?(100 * md.selectionStart / md.value.length).toFixed(2) : 100; +} + +function showinfo() +{ + var tags = gettags(md.value); + showtemporaryinfo( + [ + "title: " + title.value, + "line count: " + md.value.split("\n").length, + "word count: " + getwords(), + "cursor position: " + md.selectionStart + " (" + pospercent() + "%)", + (tags ? "tags: " + tags : ""), + "notes count: " + sortedlist().length, + "spell check: " + (md.spellcheck ? "en" : "dis") + "abled" + ].join("\n")); +} + +function savesettings() +{ + localStorage.setItem("settings", JSON.stringify(settings)); +} + +function children(guid) +{ + var content = localStorage.getItem(guid); + return (content + .match(/\[\[([^\]]*)\]\]/g) || []) + .map(l => l.replace("[[", "").replace("]]", "")) + .filter(l => !l.includes("(deleted)")) + .map(l => getguid(l)); +} + +function parents(guid) +{ + return Object.keys(metadata) + .filter(g => localStorage.getItem(g).indexOf("[[" + metadata[guid].title + "]]") != -1); +} + +function connected(guid) +{ + var list = [guid]; + var result = []; + + while (list.length) + { + var current = list.shift(); + if (result.indexOf(current) == -1) + { + result.push(current); + list = list.concat(children(current)).concat(parents(current)); + } + } + return result; +} + +function toggleeditor(hidden) +{ + md.hidden = hidden; + + if (settings.colors) + { + colored.hidden = hidden; + } +} + +function shownotelinks() +{ + if (settings.enablenetwork) + { + networkpage.hidden = false; + toggleeditor(true); + + var nodes = []; + var edges = []; + + var list = [getguid(title.value)]; + + while (list.length) + { + var current = list.shift(); + if (!nodes.find(n => n.id == current)) + { + nodes.push( + { + id: current, + label: metadata[current].title + }); + + var buddies = children(current).concat(parents(current)); + + list = list.concat(buddies); + + buddies. + forEach(buddy => { + if (!edges.find(edge => (edge.to == current && edge.from == buddy) || (edge.to == buddy && edge.from == current))) + { + edges.push({ + from: current, + to: buddy + }); + } + }); + } + } + + var data = { + nodes: nodes, + edges: edges + }; + + var options = + { + nodes: + { + color: + { + background: "white", + border: "black", + }, + font: + { + color: "black", + size: 16 + } + } + }; + + var graph = new vis.Network(network, data, options); + graph.on("click", function(event) + { + networkpage.hidden = true; + toggleeditor(false); + loadnote(nodes.find(n => n.id == event.nodes[0]).label); + }); + graph.setSelection( + { + nodes : [getguid(title.value)] + }); + } + else + { + searchinlist(connected(getguid(title.value)).map(g => metadata[g].title)) + .then(loadnote); + } +} + + +function showoutline() +{ + var outline = {}; + var pos = 0; + md.value.split("\n").forEach((line, index, lines) => + { + pos += line.length + 1; + if (line.startsWith("#")) + { + line = line + .replace("# ", "") + .replace(/#/g, "\xa0".repeat(4)); + outline[line] = pos; + } + else if (line == "---" && index != 0 && index != 3) + { + var next = lines.find((current, i) => + { + return i > index && current != ""; + }); + if (next) + { + var nbcar = 80; + next = next.length < nbcar ? next : next.substring(0, nbcar) + "..."; + outline[next] = 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() +{ + if (md.value.lastIndexOf("tags: ", md.selectionStart) < md.value.lastIndexOf("\n", md.selectionStart) || md.selectionStart < 6) + { + return null; + } + + var start = md.value.lastIndexOf(" ", md.selectionStart); + if (start == -1 || md.value.substring(start, md.selectionStart).indexOf("\n") != -1) return ""; + + var eol = md.value.indexOf("\n", md.selectionStart); + var end = md.value.indexOf(",", md.selectionStart); + + if (end == -1 || eol < end) + { + end = eol; + } + + return md.value.substring(start + 1, end); +} + +function removelinkdialog() +{ + if (typeof linkdialog != "undefined") + { + notepage.removeChild(linkdialog); + } +} + +function showlinkdialog(link) +{ + var div = document.createElement("div"); + div.setAttribute("style", "top:" + event.pageY + "px;left:" + event.pageX + "px"); + div.setAttribute("id", "linkdialog"); + + var a = document.createElement("a"); + a.setAttribute("id", "linkelt"); + + if (link.startsWith("http")) + { + a.setAttribute("href", link); + a.setAttribute("target", "_blank"); + div.onclick = removelinkdialog; + a.innerText = "Open on " + + link.replace("https://", "") + .replace("http://", "") + .replace("www.", "") + .replace(/\/.*/, ""); + } + else + { + a.setAttribute("href", "#"); + a.innerText = link; + div.onclick = function() + { + removelinkdialog(); + loadnote(link); + }; + } + + div.appendChild(a); + + notepage.appendChild(div); +} + +function checkatpos(pos) +{ + if (pos > 0 && md.value[pos - 1] == "[" && md.value[pos + 1] == "]") + { + return { + pos: pos, + val: md.value[pos] + }; + } + return null; +} + +function clickedcheck() +{ + return checkatpos(md.selectionStart) || checkatpos(md.selectionStart + 1) || checkatpos(md.selectionStart - 1); +} + +function clickeditor() +{ + var word, link; + if (event.ctrlKey) + { + link = linkatpos(); + var tag = tagatpos(); + word = wordatpos(); + if (link) + { + loadnote(link); + } + else if (tag) + { + tagslist(); + searchinlist(tags[tag.toLowerCase()]) + .then(loadnote); + } + else if (word.startsWith("http")) + { + window.open(word, '_blank'); + } + } + else if (clickedcheck()) + { + var res = clickedcheck(); + seteditorcontent(md.value.substring(0, res.pos) + + (res.val == " " ? "x" : " ") + + md.value.substring(res.pos + 1)); + setpos(res.pos); + } + else if (settings.uselinkpopup) + { + removelinkdialog(); + link = linkatpos(); + if (link) + { + showlinkdialog(link); + } + else + { + word = wordatpos(); + if (word.startsWith("http")) + { + showlinkdialog(word); + } + } + } +} + +function restoresettings() +{ + if (confirm("Restore default settings?")) + { + settings = defaultsettings; + savesettings(); + loadsettings(); + } +} + +function editsettings() +{ + bind("settings.json", JSON.stringify(settings, null, " ")); +} + +function editsetting(name) +{ + if (typeof settings[name] != "undefined") + { + var value = settings[name]; + var type = typeof value; + if (type != "undefined") + { + value = prompt(name, value); + if (value !== null) + { + if (type == "number") + { + value = parseInt(value); + } + else if (type == "boolean") + { + value = value === "true"; + } + settings[name] = value; + savesettings(); + loadsettings(); + } + } + } +} + +function changesetting() +{ + searchinlist(Object.keys(settings).map(name => name + ": " + settings[name])) + .then(setting => + { + editsetting(setting.split(":").shift()); + }); +} + +function showtemporaryinfo(info) +{ + alert(info); +} + +function getwords() +{ + return md.value.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 tagslist() +{ + tags = {}; + + Object.values(metadata) + .forEach(n => + { + var ts = n.header.tags; + 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(content) +{ + var i = content.indexOf("tags: "); + if (i > -1) + { + var j = content.indexOf("\n", i); + return content.substring(i + 6, j) + .split(",") + .map(t => t.toLowerCase().trim()) + .filter(t => t.length); + } + return []; +} + +function share() +{ + if (navigator.share) + { + navigator.share( + { + text: md.value, + title: title.value + }); + } +} + +function sharehtml() +{ + if (navigator.share) + { + var file = new File(['' + md2html(md.value) + ''], + title.value + ".html", + { + type: "text/html", + }); + + navigator.share( + { + title: title.value, + files: [file] + }); + } +} + +function getfilename(title) +{ + return title.replace(/[\?\"<>|\*:\/\\]/g, "_") + settings.downloadextension; +} + +function download(filename, content) +{ + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/markdown;charset=utf-8,' + encodeURIComponent(content)); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + element = null; +} + +function downloadtag() +{ + tagslist() + .then(tag => + { + var zip = new JSZip(); + Object.keys(metadata).forEach(guid => + { + if (metadata[guid].header.tags.includes(tag)) + { + zip.file(getfilename(metadata[guid].title), localStorage.getItem(guid)); + } + }); + zip.generateAsync({type:"blob"}) + .then(function(content) + { + saveAs(content, "notes-" + tag + "-" + timestamp() + ".zip"); + }); + }); +} + +function downloadnotes() +{ + var zip = new JSZip(); + Object.keys(metadata).forEach(guid => + { + zip.file(getfilename(metadata[guid].title), localStorage.getItem(guid)); + }); + zip.generateAsync({type:"blob"}) + .then(function(content) + { + saveAs(content, "notes-" + timestamp() + ".zip"); + }); +} + +function downloadhtmlnotes() +{ + var zip = new JSZip(); + Object.keys(metadata).forEach(guid => + { + zip.file(getfilename(metadata[guid].title) + ".html", md2html(localStorage.getItem(guid))); + }); + zip.generateAsync({type:"blob"}) + .then(function(content) + { + saveAs(content, "notes-html-" + timestamp() + ".zip"); + }); +} + +function headerandtext(content) +{ + var result = + { + header: "", + text: content + }; + if (content.startsWith("---\n")) + { + var end = content.indexOf("---\n", 4); + if (end > -1) + { + result.header = content.substring(0, end + 4); + result.text = content.substring(end + 4); + } + } + return result; +} + +function inserttodo(text) +{ + var guid = getguid("todo"); + if (!guid) + { + guid = createnote("todo"); + } + var content = localStorage.getItem(guid); + var split = headerandtext(content); + content = split.header + text + "\n" + split.text; + if (title.value == "todo") + { + seteditorcontent(content); + } + else + { + serialize(guid, content); + metadata[guid].lastchanged = Date.now(); + flush(); + } +} + +function promptinserttodo() +{ + var text = prompt("Text:"); + if (text) + { + inserttodo(text); + } +} + +function downloadnotewithsubs() +{ + var note = withsubs(); + if (note) + { + download(getfilename(note.title), note.content); + } +} + +function downloadnote() +{ + download(getfilename(title.value), md.value); +} + +function getguid(title) +{ + return Object.keys(metadata).find(guid => metadata[guid].title === title); +} + +function gotoline(line) +{ + var i = 0; + var pos = 0; + while (i < line && pos > -1) + { + pos = md.value.indexOf("\n", pos + 1); + i++; + } + if (pos > -1) + { + setpos(pos + 1); + } +} + +function createnote(title, content) +{ + var guid = genguid(); + content = content || defaultheaders(); + var item = { + lastchanged: Date.now(), + title: title, + pos: content.length, + header: indexheader(content) + }; + metadata[guid] = item; + serializeindex() + serialize(guid, content); + return guid; +} + +function loadstorage() +{ + metadata = JSON.parse(localStorage.getItem("index")); + if (!metadata) + { + metadata = {}; + createnote(timestamp()); + } + + var params = new URLSearchParams(window.location.search); + var clip = params.get("c"); + + if (clip) + { + settings.savedelay = 0; + colored.hidden = true; + md.hidden = true; + var msg = document.createElement("div"); + msg.innerText = "Clipping..."; + notepage.appendChild(msg); + + inserttodo("@clip " + clip) + window.close(); + } + + var title = params.get("n") || params.get("name"); + if (window.title.value) + { + // reload current + loadnote(window.title.value); + } + else if (title) + { + loadnote(title); + var line = params.get("l"); + if (line) + { + gotoline(parseInt(line)); + } + } + else + { + loadlast(); + } +} + +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]; + } + } + } + + document.body.style.fontSize = settings.fontsize; + document.body.style.lineHeight = settings.lineheight; + document.body.style.marginLeft = settings.margins; + document.body.style.marginRight = settings.margins; + document.body.style.fontFamily = settings.fontfamily; + + if (settings.titlebydefault && title.hidden) + { + toggletitle(); + } + + colored.hidden = !settings.colors; + md.style.color = settings.colors ? "transparent" : "inherit"; + md.style.background = settings.colors ? "transparent" : "inherit"; + + snippets.find(s => s.command == "/bonjour").insert = "Bonjour ,\n\n\n\nBien cordialement,\n" + settings.firstname; + snippets.find(s => s.command == "/hello").insert = "Hello ,\n\n\n\nKind regards,\n" + settings.firstname; +} + +function checksaved() +{ + if (!unsavedmark.hidden) + { + 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 + }); + } + }); +} + +function indexheader(content) +{ + var indexedheader = { + tags: [] + }; + var hat = headerandtext(content); + var header = hat.header; + if (header) + { + header.split("\n").forEach(line => + { + if (line && line != "---") + { + var t = line.split(":"); + var val = t.pop(); + var key = t.pop(); + if (key == "tags") + { + val = val.split(",").map(t => t.trim()); + if (val.length == 1 && !val[0]) + { + val.pop(); + } + } + indexedheader[key] = val; + } + }); + } + return indexedheader; +} + +function migratelegacystorage() +{ + var legacy = localStorage.getItem("data"); + if (legacy) + { + alert("Hey! I am about to migrate your notes to the brand new data model. No worries, I will keep a backup somewhere in case things go wrong. Take a deep breath, and click ok when you're ready."); + localStorage.setItem("legacy", legacy); + legacy = JSON.parse(legacy); + var index = {}; + legacy.reverse().forEach( (note, i) => + { + var guid = genguid(); + localStorage.setItem(guid, note.content); + note.header = indexheader(note.content); + note.lastchanged = i; + delete note.content; + index[guid] = note; + }); + localStorage.setItem("index", JSON.stringify(index)); + localStorage.removeItem("data"); + } +} + +function init() +{ + migratelegacystorage(); + loadsettings(); + + window.onbeforeunload = checksaved; + window.onclick = focuseditor; + title.value = ""; + + initsnippets(); + + loadstorage(); + + if (issplit()) + { + if (settings.defaultpreviewinsplit && name == "right") + { + togglepreview(); + } + else if (name == "left") + { + md.focus(); + } + } +} + +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 backlinks() +{ + searchinlist(Object.keys(metadata) + .filter(guid => localStorage.getItem(guid).includes("[[" + title.value + "]]")) + .map(guid => metadata[guid].title)) + .then(loadnote); +} + +function sortselection() +{ + var content = md.value; + 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"); + seteditorcontent(content.substring(0, range.start) + sorted + content.substring(range.end)); +} + +function wordatpos() +{ + var words = md.value.split(/\s/); + var i = 0; + var word = ""; + while (i < md.selectionStart) + { + word = words.shift(); + i += word.length + 1; + } + return word; +} + +function ontopbarclick() +{ + if (title.hidden) + { + commandpalette(); + } +} + +function md2html(content) +{ + var converter = new showdown.Converter(); + converter.setOption("simplifiedAutoLink", true); + converter.setOption("simpleLineBreaks", true); + converter.setOption("metadata", true); + converter.setOption("tasklists", true); + converter.setOption("literalMidWordUnderscores", true); + + if (settings.linksinnewtab) + { + converter.setOption("openLinksInNewWindow", true); + } + + var html = converter.makeHtml(content); + + // internal links + html = html.replace(/\[\[([^\]]*)\]\]/g, "$1"); + + return html; +} + +function loadlast() +{ + loadnote(sortedlist()[0].title); +} + +function loadprevious() +{ + var list = sortedlist(); + var index = list.findIndex(i => i.title == title.value); + if (index > -1 && index < list.length - 1) + { + loadnote(list[index + 1].title); + } +} + +function loadnext() +{ + var list = sortedlist(); + var index = list.findIndex(i => i.title == title.value); + if (index > -1 && index > 0) + { + loadnote(list[index - 1].title); + } +} + +function sortedlist() +{ + return Object + .values(metadata) + .sort( (i,j) => j.lastchanged - i.lastchanged); +} + +function grep(needle) +{ + var result = {}; + + sortedlist() + .forEach(item => + { + if (item.title.toLowerCase().includes(needle.toLowerCase())) + { + result[item.title] = {}; + } + var content = localStorage.getItem(getguid(item.title)); + content.split("\n") + .forEach((line, nb) => { + if (line.toLowerCase().includes(needle.toLowerCase())) + { + result[item.title] = result[item.title] || {}; + result[item.title][nb] = line; + } + }); + }); + + return result; +} + +function showgrepresult(needle, grepresult) +{ + var grepcontent = ["# Search results: \"" + needle + "\""]; + for (var file in grepresult) + { + grepcontent.push("[[" + file + "]]"); + for (var l in grepresult[file]) + { + grepcontent.push("[" + l + "] " + grepresult[file][l]); + } + grepcontent.push(""); + } + + if (grepcontent.length == 0) + { + grepcontent.push("No result."); + } + + bind("Search result", grepcontent.join("\n")); + + if (preview.hidden) + { + togglepreview(); + } +} + +function showgrep() +{ + var text = prompt("Search:", md.selectionEnd > md.selectionStart ? md.value.substr(md.selectionStart, md.selectionEnd - md.selectionStart).trim() : ""); + if (text) + { + showgrepresult(text, grep(text)); + } +} + +function commandpalette() +{ + searchinlist(commands + .filter(command => !command.excludepalette) + .map(command => + { + return { + prefix: "command ", + text: command.hint, + suffix: command.shortcut ? [command.shortcut.toLowerCase()] : null + }; + }) + .concat(snippets.map(s => + { + return { + prefix: "snippet ", + text: s.hint, + suffix: [s.command] + }; + })) + .concat( + sortedlist() + .map(item => + { + return { + prefix: "note ", + text: item.title, + suffix: item.header.tags.map(t => tagmark + t) + }; + })) + .concat(Object.keys(settings).map(s => + { + return { + prefix: "setting ", + text: s, + suffix: [settings[s]] + }; + }))) + .then(selected => + { + if (selected.prefix == "snippet ") + { + var snippet = snippets.find(s => s.hint == selected.text); + insert(snippet.insert, snippet.cursor); + md.focus(); + } + else if (selected.prefix == "note ") + { + loadnote(selected.text); + } + else if (selected.prefix == "setting ") + { + editsetting(selected.text); + } + else + { + var command = commands.find(c => c.hint == selected.text); + if (command) + { + executecommand(command); + } + else + { + // if unknown command, create a new note + loadnote(selected); + } + } + }); +} + +function insert(text, cursoroffset = 0, nbtodelete = 0) +{ + var pos = md.selectionStart; + var content = md.value; + seteditorcontent( + content.substring(0, pos - nbtodelete) + + text + + content.substring(pos)); + setpos(pos - nbtodelete + text.length + cursoroffset); +} + +function searchinlist(list) +{ + return new Promise(selectitem => + { + fileindex = 0; + searchdialog.hidden = false; + filteredlist.hidden = false; + + filteredlist.innerHTML = ""; + filter.value = ""; + + list.forEach(item => + { + var elt = document.createElement("div"); + elt.tag = item; + + if (typeof item === "string") + { + elt.textContent = item; + } + else + { + var ts = document.createElement("span"); + ts.setAttribute("class", "searchlistprefix"); + ts.innerHTML = item.prefix || ""; + elt.appendChild(ts); + + ts = document.createElement("span"); + ts.innerHTML = item.text; + elt.appendChild(ts); + + if (item.suffix) + { + item.suffix.forEach(t => + { + ts = document.createElement("span"); + ts.setAttribute("class", "searchlistsuffix"); + ts.innerHTML = " " + t; + elt.appendChild(ts); + }); + } + } + + elt.onclick = function() + { + searchdialog.hidden = true; + selectitem(elt.tag); + } + 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.tag : filter.value); + } + } + + filter.focus(); + }); +} + +function applyfileindex() +{ + var i = 0; + [...filteredlist.children].forEach(child => + { + if (child.nodeName == "DIV") + { + child.className = "searchitem"; + if(!child.hidden) + { + if (i++ == fileindex) + { + child.className = "selected"; + if (child.customevent) + { + child.customevent(child.textContent); + filter.focus(); + } + } + } + } + }); +} + +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) + { + md.style.height = md.scrollHeight + 'px'; + } +} + +function postpone() +{ + return new Promise(function(resolve) + { + clearTimeout(workerid); + workerid = setTimeout(resolve, settings.savedelay); + }); +} + +function flush() +{ + clearTimeout(workerid); + workerid = null; + + var guid = getguid(title.value); + if (guid) + { + var item = metadata[guid]; + item.title = title.value; + item.pos = md.selectionStart; + item.header = indexheader(md.value); + item.lastchanged = Date.now(); + + serializeindex(); + serialize(guid, md.value); + } + else if (title.value == "settings.json") + { + settings = JSON.parse(md.value); + savesettings(); + loadsettings(); + } + unsavedmark.hidden = true; +} + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + +var languagekeywords = { + "sql": ["select", "from", "where", "and", "or", "set", "declare"], + "js": ["var", "for", "if", "else"], + "zsh": ["sudo"], + "bash": ["molecule", "cd"], + "python": ["from", "import"] +} + +function currentline() +{ + return (md.value.substring(0, md.selectionStart).match(/\n/g) || []).length; +} + +function lineatpos(pos) +{ + return (md.value.substring(0, pos).match(/\n/g) || []).length; +} + +function currentcol() +{ + return md.selectionStart - Math.max(0, md.value.lastIndexOf("\n", md.selectionStart - 1)) - 1; +} + +function rawline(index) +{ + return md.value.split("\n")[index]; +} + +var emptyline = "
"; +function rawline2html(line, index, options) +{ + line = escapeHtml(line); + + // headings + if (line.startsWith("#")) + { + line = line.replace(/(#* )/, "$1"); + line = "" + line + ""; + } + + // bold and italics + var temp = line; + if (line.startsWith("* ")) + { + temp = line.substring(2); + } + temp = temp.replace(/\*\*([^\*]*)\*\*/g, "**$1**"); + temp = temp.replace(/\*([^\*]*)\*/g, "*$1*"); + + if (line.startsWith("* ")) + { + line = "* " + temp; + } + else + { + line = temp; + } + + // lists + markerslist.forEach(marker => + { + if (line.startsWith(marker) && marker.trim()) + { + line = line.replace(marker, "" + marker + ""); + } + }); + + // md header + if (index == 0 && line == "---") + { + options.header = true; + } + if (options.header) + { + if (index > 0 && line == "---") + { + options.header = false; + } + line = line || emptyline; + line = "" + line + ""; + } + + // code blocks + if (line.startsWith("```") && !options.code) + { + options.code = true; + options.language = line.substring(3); + line = "
" + line.replace(new RegExp("(" + options.language + ")"), "$1") + "
"; + } + else if (line == "```" && options.code) + { + options.code = false; + options.language = ""; + line = "
" + line + "
"; + } + else if (options.code) + { + var comment = false; + if (line.match(/^\s*\/\//)) + { + line = "" + line + ""; + comment = true; + } + line = "
" + (line || emptyline) + "
"; + if (!comment && languagekeywords[options.language]) + { + var keywords = languagekeywords[options.language]; + keywords.forEach(keyword => + { + line = line.replace(new RegExp("\\b(" + keyword + ")\\b", "ig"), "$1"); + }); + } + } + + // internal links + line = line.replace(/\[\[(.*)\]\]/g, "[[$1]]"); + + // comments + line = line.replace(/<\!--(.*)/g, "<!--$1"); + + if (line.includes("<!--") && !line.includes("-->")) + { + options.comment = true; + } + else if (options.comment) + { + line = line || emptyline; + line = "" + line + if (line.includes("-->")) + { + options.comment = false; + } + else + { + line += ""; + } + } + + line = line.replace(/\-\->/g, "-->"); + + if (line.startsWith("// ")) + { + line = "" + line + ""; + } + + // autocomplete snippets + if (index == currentline()) + { + var raw = rawline(index); + var pos = currentcol(); + var slashpos = raw.substring(0, pos).lastIndexOf("/"); + if (slashpos > -1) + { + var spacepos = raw.substring(0, pos).lastIndexOf(" "); + if (slashpos > spacepos) + { + var snippetpart = raw.substring(slashpos); + var matching = snippets + .filter(s => s.command.startsWith(snippetpart)) + .map(s => s.command.substring(1)); + + if (matching.length) + { + line += ""; + line += matching.join().substr(pos - slashpos - 1); + line += ""; + } + } + } + } + + if (line.startsWith("x ")) + { + line = "" + line + ""; + } + + // inline code + line = line.replace(/`(.*)`/, "`$1`"); + + // links + line = line.replace(/(http[^\s]*)/, "$1"); + + return line; +} + +function applycolors(currentonly) +{ + if (!settings.colors) + { + return; + } + + var options = + { + header: false, + code: false, + comment: false, + language: "" + }; + + var linediv = null; + if (currentonly) + { + var index = currentline(); + linediv = document.getElementById("line" + index); + options = JSON.parse(linediv.getAttribute("tag")); + var line = rawline(index); + line = rawline2html(line, index, options); + linediv.innerHTML = line || emptyline; + } + else + { + console.log("redrawing all colored div"); + var lines = md.value.split("\n"); + var i = 0; + for (; i < lines.length; i++) + { + linediv = document.getElementById("line" + i); + if (!linediv) + { + linediv = document.createElement("div"); + colored.appendChild(linediv); + } + linediv.setAttribute("id", "line" + i); + linediv.setAttribute("tag", JSON.stringify(options)); + linediv.innerHTML = rawline2html(lines[i], i, options) || emptyline; + } + + // remove remanining + linediv = document.getElementById("line" + (i++)); + while (linediv) + { + colored.removeChild(linediv); + linediv = document.getElementById("line" + (i++)); + } + } +} + +function editorinput() +{ + unsavedmark.hidden = false; + + // criteria to improve. Or redraw only after? + var multiline = md.value.substring(md.selectionStart, md.selectionEnd).includes("\n"); + applycolors(!multiline && event.data && (event.inputType == "insertText" || event.inputType == "deleteContentBackward" || event.inputType == "deleteContentForward")); + + // todo: fix if current note change during postponing, the wrong one will be saved. Or prevent binding another note? + postpone().then(flush); + resize(); +} + +function timestamp() +{ + var utc = new Date(); + var loc = new Date(utc - utc.getTimezoneOffset() * 60 * 1000); + + return loc.toISOString().replace("T", " ").replace(/\..*/, "").replace(/:/g, "."); +} + +function quicknewnote() +{ + flush(); + loadnote(timestamp()); +} + +function startnewnote() +{ + flush(); + var title = prompt("Note title: ", timestamp()); + if (title) + { + loadnote(title); + } +} + +function showhelp() +{ + var help = ["# Notes"]; + help.push("## Shortcuts"); + + commands + .filter(command => Boolean(command.shortcut)) + .forEach(command => help.push(command.hint + ": " + command.shortcut.toLowerCase())); + + help.push("## Snippets"); + snippets.forEach(snippet => + { + help.push(snippet.hint + ": " + snippet.command); + }); + + help.push("## Libs"); + help.push("[Showdown](https://showdownjs.com/)"); + help.push("[vis-network](https://visjs.org/)"); + help.push("[jszip](https://stuk.github.io/jszip/)"); + help.push("[FileSaver](http://eligrey.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/)"); + help.push("[Notion](https://www.notion.so/)"); + help.push("[Calmly Writer](https://calmlywriter.com/)"); + help.push("[Cryptee](https://crypt.ee/)"); + + help.push("##Sources"); + help.push("https://git.ouvaton.coop/quenousimporte/notes"); + + bind("Help", 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( + sortedlist() + .map(item => + { + return { + text: item.title, + suffix: item.header.tags.map(t => tagmark + t) + } + })); +} + +function searchautocomplete() +{ + selectnote().then(selected => + { + insertautocomplete(selected.text || selected); + }); +} + +function searchandloadnote() +{ + selectnote().then(selected => + { + var title = selected.text || selected; + loadnote(title); + }); +} + +function istodo(title, content) +{ + return title.includes("todo") || gettags(content).includes("todo"); +} + +function searchandreplace() +{ + var oldvalue = prompt("Search:"); + if (!oldvalue) + { + return; + } + + var newvalue = prompt("Replace by:"); + if (!newvalue) + { + return; + } + + var doit = confirm(`Replace '${oldvalue}' by '${newvalue}'?`); + if (!doit) + { + return; + } + + seteditorcontent(md.value.replaceAll(oldvalue, newvalue)); +} + +function serializeindex() +{ + serialize("index", JSON.stringify(metadata)); +} + +function deletenote(title) +{ + var guid = getguid(title); + delete metadata[guid]; + renameinternallinks(title, title + " (deleted)"); + var content = localStorage.getItem(guid); + localStorage.setItem(guid, "Deleted. Title: " + title + ". Date: " + timestamp() + "\r\n" + content); + serializeindex(); +} + +function deletecurrentnote() +{ + if (confirm('delete "' + title.value + '"?')) + { + deletenote(title.value); + loadlast(); + } +} + +function restore() +{ + if (confirm('restore "' + title.value + '"?')) + { + seteditorcontent(backup); + } +} + +function insertheader() +{ + if (preview.hidden && !md.value.startsWith("---\n")) + { + var headers = defaultheaders(); + seteditorcontent(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 shortcutmatches(event, shortcut) +{ + var s = splitshortcut(shortcut); + return (event.key == s.key && !(s.ctrl && !event.ctrlKey && !event.altKey) && !(s.shift && !event.shiftKey) && !(s.alt && !event.altKey)) +} + +function executecommand(command) +{ + if (command.action) + { + command.action(); + } +} + +function esc(event) +{ + if (!searchdialog.hidden) + { + event.preventDefault(); + searchdialog.hidden = true; + filter.placeholder = "Search..."; + md.focus(); + } + else if (settings.uselinkpopup && typeof linkdialog != "undefined") + { + removelinkdialog(); + } + else if (title.value == "Help" || title.value == "Search result") + { + loadlast(); + } + else if (networkpage.hidden == false) + { + networkpage.hidden = true; + toggleeditor(false); + } + else if (preview.hidden == false) + { + togglepreview(); + } + else + { + commandpalette(); + } +} + +function boldify() +{ + var pos = currentrange(); + var newcontent; + var offset = 2; + if (md.value.substr(pos.start - 2, 2) == "**" && md.value.substr(pos.end, 2) == "**") + { + newcontent = md.value.substr(0, pos.start - 2) + md.value.substr(pos.start, pos.end - pos.start) + md.value.substr(pos.end + 2); + offset = -2; + } + else + { + newcontent = md.value.substr(0, pos.start) + "**" + md.value.substr(pos.start, pos.end - pos.start) + "**" + md.value.substr(pos.end); + } + + seteditorcontent(newcontent); + md.setSelectionRange(pos.start + offset, pos.end + offset); +} + +function foreachmetadata(callback) +{ + Object.keys(metadata).forEach(guid => + { + callback(guid, metadata[guid]); + }); +} + +function snippetautocomplete() +{ + var tocursor = md.value.substr(0, md.selectionStart) + var slashindex = tocursor.lastIndexOf("/"); + if (slashindex > tocursor.lastIndexOf(" ") && slashindex > tocursor.lastIndexOf("\n")) + { + var commandbegin = tocursor.substr(slashindex); + var snippet = snippets.find(s => s.command.startsWith(commandbegin)); + if (snippet) + { + insert(snippet.insert, snippet.cursor, commandbegin.length); + return true; + } + } + return false; +} + +function mainkeydownhandler() +{ + if (event.key == "Escape") + { + esc(event); + } + else 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(); + } + else if (event.ctrlKey && event.key == " " || event.key == "F1") + { + commandpalette(); + event.preventDefault(); + } + else if (event.ctrlKey && event.key == "b" && md.selectionStart < md.selectionEnd) + { + boldify(); + event.preventDefault(); + } + else if (event.ctrlKey && event.shiftKey && (event.keyCode == "40" || event.keyCode == "38")) + { + var pos = currentrange(); + var direction = event.keyCode == "40" ? 1 : -1; + var start = lineatpos(md.selectionStart); + var end = lineatpos(md.selectionEnd); + var lines = md.value.split("\n"); + if (direction > 0 && end < lines.length || direction < 0 && start > 0) + { + var block = lines.splice(start, end - start + 1); + lines.splice(start + direction, 0, ...block); + seteditorcontent(lines.join("\n")); + var posshift = direction > 0 ? lines[start].length + 1 : - 1 - lines[end].length; + md.setSelectionRange(pos.start + posshift, pos.end + posshift); + + } + event.preventDefault(); + } + else if (event.ctrlKey || event.altKey) + { + // notes shortcuts + var found = false; + foreachmetadata((guid, item) => + { + if (item.header.shortcut && shortcutmatches(event, item.header.shortcut)) + { + console.log("Loading note '" + item.title + "' from header shortcut " + item.header.shortcut); + event.preventDefault(); + loadnote(item.title); + found = true; + } + }); + + // commands shortcuts + if (!found) + { + commands.filter(c => c.shortcut) + .every(command => + { + if (shortcutmatches(event, command.shortcut)) + { + event.preventDefault(); + executecommand(command); + return false; + } + return true; + }); + } + } +} + +function setwindowtitle() +{ + document.title = title.value; +} + +function renameinternallinks(from, to) +{ + foreachmetadata( (guid, item) => + { + var content = localStorage.getItem(guid); + var newcontent = content.replaceAll("[[" + from + "]]", "[[" + to + "]]"); + if (content != newcontent) + { + serialize(guid, newcontent); + if (item.title == title.value) + { + seteditorcontent(newcontent); + } + } + }); +} + +function ontitlechange() +{ + var guid = getguid(title.value); + if (guid) + { + showtemporaryinfo(title.value + " alreday exists"); + title.value = previoustitle; + } + else + { + renameinternallinks(previoustitle, title.value); + + guid = getguid(previoustitle); + previoustitle = title.value; + metadata[guid].title = title.value; + setwindowtitle(); + flush(); + + if (!settings.titlebydefault) + { + toggletitle(); + } + } +} + +function simplifystring(str) +{ + return str.toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, ""); +} + +function applyfilter() +{ + [...filteredlist.children].forEach(div => + { + div.hidden = simplifystring(div.textContent).indexOf(simplifystring(filter.value)) < 0; + }); + + fileindex = 0; + applyfileindex(); +} + +function backspace(nb) +{ + var pos = getpos(); + var c = md.value; + seteditorcontent(c.substring(0, pos - nb) + c.substring(pos)); + setpos(pos - nb); +} + +function editorkeydown() +{ + if (event.key == "Enter") + { + var line = md.value.substring(0, getpos()).split("\n").pop(); + markerslist.filter(marker => line.startsWith(marker)) + .every(marker => + { + event.preventDefault(); + if (line != marker) + { + insert("\n" + marker.replace("[x]", "[ ]")); + } + else + { + backspace(marker.length); + } + return false; + }); + } + else if (event.key === "Tab") + { + event.preventDefault(); + if (!snippetautocomplete()) + { + var init = currentrange(); + var range = getlinesrange(); + range.start--; + range.end--; + var selection = md.value.substring(range.start, range.end); + var newtext; + if (event.shiftKey) + { + newtext = selection.replaceAll("\n ", "\n"); + } + else + { + newtext = selection.replaceAll("\n", "\n "); + } + seteditorcontent(md.value.substring(0, range.start) + + newtext + + md.value.substring(range.end)); + + var shift = 0; + if (newtext.length < selection.length) + { + shift = -4; + } + else if (newtext.length > selection.length) + { + shift = 4; + } + + md.selectionStart = init.start + shift; + md.selectionEnd = init.end + (newtext.length - selection.length); + } + } + else if (event.key === "[" && before(1) == "[") + { + event.preventDefault(); + insert("["); + searchautocomplete(); + } + else if (settings.tagautocomplete && event.key == " " && before(1) == "," && md.value.substring(0, getpos()).split("\n").pop().startsWith("tags: ")) + { + event.preventDefault(); + // search in tags list + console.log(event.key); + tagslist() + .then(tag => + { + insert(" " + tag); + md.focus(); + }) + } + else + { + var snippet = snippets.find(s => before(s.command.length - 1) + event.key == s.command); + if (snippet) + { + event.preventDefault(); + insert(snippet.insert, snippet.cursor, snippet.command.length - 1); + } + } +} + +function insertautocomplete(selectednote) +{ + md.focus(); + insert(selectednote + "]] "); +} + +function togglepreview() +{ + preview.innerHTML = md2html(md.value); + toggleeditor(!md.hidden); + preview.hidden = !preview.hidden; + + if (preview.hidden) + { + resize(); + md.focus(); + } +} + +function withsubs() +{ + // todo: handle loops + var currentguid = getguid(title.value); + + var content = md.value; + var kids = children(currentguid); + while (kids.length) + { + kids.forEach(guid => + { + var kidcontent = localStorage.getItem(guid); + if (kidcontent.startsWith("---\n")) + { + var pos = kidcontent.indexOf("---\n", 4); + kidcontent = kidcontent.substring(pos + 4); + } + content = content.replaceAll("[[" + metadata[guid].title + "]]", kidcontent); + }); + kids = children(currentguid); + } + + return content; +} + +function togglepreviewwithsubs() +{ + var note = withsubs(); + if (note) + { + preview.innerHTML = md2html(note.content); + toggleeditor(!md.hidden); + preview.hidden = !preview.hidden; + + if (preview.hidden) + { + resize(); + md.focus(); + } + } +} + +function bind(title, content, pos) +{ + if (workerid) + { + showtemporaryinfo("Cannot open '" + title + "' because current note not yet serialized"); + return; + } + + previoustitle = title; + backup = content; + + window.title.value = title; + setwindowtitle(); + + md.value = content || ""; + applycolors(); + + md.style.height = "0px"; + resize(); + + setpos(pos || 0); + + if (!issplit() && searchdialog.hidden) + { + // force to scroll to cursor pos + md.blur(); + md.focus(); + } +} + +function defaultheaders(tags = "") +{ + return [ + "---", + "date: " + timestamp().substr(0,10), + "tags: " + (tags || ""), + "---", + "",""].join("\n"); +} + +function loadnote(title) +{ + var guid = getguid(title); + if (!guid) + { + guid = createnote(title); + } + var content = localStorage.getItem(guid); + var item = metadata[guid]; + + if (item.header.tags.includes("journal")) + { + // remove empty entries + content = content.replace(/\d{4}-\d{2}-\d{2}\n*(\d{4}-\d{2}-\d{2})/g, "$1"); + + // create new entry for today + var hat = headerandtext(content); + var today = timestamp().substr(0,10); + if (!hat.text.startsWith(today) && !hat.text.startsWith("\n" + today)) + { + content = hat.header + "\n" + today + "\n\n" + hat.text; + item.pos = hat.header.length + 12; + } + } + + bind(item.title, content, item.pos); + item.lastchanged = Date.now(); + serializeindex(); + + if (!preview.hidden || (preview.hidden && (gettags(md.value).indexOf("preview") !== -1))) + { + togglepreview(); + } +} + +function focuseditor() +{ + if (document.documentElement == event.srcElement) + { + md.focus(); + console.log("Forced focus"); + } +}