From a3562bf599d11cdcc3ac399726a240a1db21864d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 8 May 2024 01:20:03 +1200 Subject: [PATCH] basic implementation of converting micron to html --- public/assets/js/micron-parser.js | 174 ++++++++++++++++++++++++++++++ public/index.html | 133 ++++++++++++++++++++--- 2 files changed, 291 insertions(+), 16 deletions(-) create mode 100644 public/assets/js/micron-parser.js diff --git a/public/assets/js/micron-parser.js b/public/assets/js/micron-parser.js new file mode 100644 index 0000000..98fbb02 --- /dev/null +++ b/public/assets/js/micron-parser.js @@ -0,0 +1,174 @@ +class MicronParser { + + /** + * Set a custom scheme for urls set on html links. + * This just makes the browser show a pretty url when hovering links, but in the future, I may implement url + * interception, rather than using an onclick script on the link element itself. + */ + static formatNomadnetworkUrl(url) { + return `nomadnetwork://${url}`; + } + + /** + * Converts micron markup to html. + * FIXME: make sure you can't inject javascript into Reticulum WebChat browser from micron page content! + * + * Known URL Examples: + * - : + * - :/page/index.mu + * - :/page/somefolder/somefile.mu + * - :/file/somefile.ext + * - 00000000000000000000000000000000 + * - 00000000000000000000000000000000:/page/index.mu + * - 00000000000000000000000000000000:/page/somefolder/somefile.mu + * - 00000000000000000000000000000000:/file/somefile.ext + * - lxmf@00000000000000000000000000000000 + * + * References: + * - https://github.com/markqvist/NomadNet/blob/6a4f2026249b22a00f6ff98c12d06fd5b160a90d/nomadnet/ui/textui/MicronParser.py + */ + static convertMicronToHtml(markup) { + + console.log(markup); + + // split by line + var lines = markup.split("\n"); + + // parse each line + lines = lines.map((line) => { + + // skip comments + if(line.startsWith("#")){ + return null; + } + + // skip section heading reset + // FIXME: implement + if(line === "<"){ + return null; + } + + // skip background colours + // FIXME: implement + line = line.replaceAll(/`B([0-9A-Fa-f]+)/g, function(match, colourCode) { + return ""; + }); + + // skip default background colour + // FIXME: implement + line = line.replaceAll(/`b/g, function(match) { + return ""; + }); + + // skip literal + // FIXME: implement + line = line.replaceAll(/`=/g, function(match) { + return ""; + }); + + // skip reset formatting + // FIXME: implement + if(line.startsWith("``")){ + line = line.replace("``", ""); + } + + // parse micron links + // `[ ◈ The Future of RNode: A New Model`:/page/posts/2022_06_27.mu] + line = line.replaceAll(/`\[(.*?)`(.*?)\]/g, function(match, linkText, linkUrl) { + const url = MicronParser.formatNomadnetworkUrl(linkUrl); + return `${linkText}` + }); + + // parse markdown links + // [Reticulum](https://github.com/markqvist/Reticulum) + line = line.replaceAll(/\[(.*?)\]\((.*?)\)/g, function(match, linkText, linkUrl) { + const url = MicronParser.formatNomadnetworkUrl(linkUrl); + return `${linkText}` + }); + + // parse bold + // `!Bold Text`! + line = line.replaceAll(/`!(.*?)`!/g, function(match, text) { + return `${text}` + }); + + // parse italics + // `*Italic Text`* + line = line.replaceAll(/`\*(.*?)`\*/g, function(match, text) { + return `${text}` + }); + + // parse underline + // `_Underlined Text`_ + line = line.replaceAll(/`_(.*?)`_/g, function(match, text) { + return `${text}` + }); + + // parse divider + // FIXME: use raw css, not tailwind classes for styling + if(line.trim() === "-"){ + line = "
" + } + + // parse divider (custom character) + // FIXME: use raw css, not tailwind classes for styling + // FIXME: use custom divider character + if(line.startsWith("-") && line.length === 2){ + line = "
" + } + + // parse depth (should be full width) + // FIXME: use raw css, not tailwind classes for styling + if(line.startsWith(">>>")){ + line = line.replace(">>>", ""); + line = `${line}` + } else if(line.startsWith(">>")){ + line = line.replace(">>", ""); + line = `${line}` + } else if(line.startsWith(">")){ + line = line.replace(">", ""); + line = `${line}` + } + + // align (default: using left) + if(line.startsWith("`a")){ + line = line.replace("`a", ""); + line = `${line}`; + } + + // align center + if(line.startsWith("`c")){ + line = line.replace("`c", ""); + line = `${line}`; + } + + // align left + if(line.startsWith("`l")){ + line = line.replace("`l", ""); + line = `${line}`; + } + + // align right + if(line.startsWith("`r")){ + line = line.replace("`l", ""); + line = `${line}`; + } + + // formatted content + // `Ff00 red content `f + line = line.replaceAll(/`F([0-9A-Fa-f]+)(.*?)`f/g, function(match, colourCode, content) { + return `${content}` + }); + + return line; + + }); + + // filter out null items, and join with line break + return lines.filter((line) => { + return line != null; + }).join("
"); + + } + +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 6ac9e9a..2555538 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,7 @@ + @@ -206,8 +207,8 @@ - -
+ +
-
+
- -
-
+
{{ selectedNode.name }}
-
@<{{ selectedNode.destination_hash }}>
@@ -472,15 +470,27 @@
-
- {{ nodePagePath }} +
+
+ + + +
+
{{ nodePagePath }}
-
-
- {{ nodePageContent }} +
+
+
+ + + + +
+
Loading {{ nodePageProgress }}%
+

                 
@@ -526,9 +536,11 @@ lxmfMessagesRequestSequence: 0, chatItems: [], + isLoadingNodePage: false, nodePageRequestSequence: 0, nodePagePath: null, nodePageContent: null, + nodePageProgress: 0, nomadnetPageDownloadCallbacks: {}, nomadnetFileDownloadCallbacks: {}, @@ -539,6 +551,9 @@ this.connectWebsocket(); this.getLxmfDeliveryAnnounces(); this.getNomadnetworkNodeAnnounces(); + window.onNodePageUrlClick = (url) => { + this.onNodePageUrlClick(url); + }; }, methods: { connectWebsocket: function() { @@ -1006,8 +1021,10 @@ const seq = ++this.nodePageRequestSequence; // update ui - this.nodePagePath = pagePath; - this.nodePageContent = `Loading page`; + this.isLoadingNodePage = true; + this.nodePagePath = `${destinationHash}:${pagePath}`; + this.nodePageContent = null; + this.nodePageProgress = 0; this.downloadNomadNetPage(destinationHash, pagePath, (pageContent) => { @@ -1017,8 +1034,16 @@ return; } + // convert micron to html if page ends with .mu extension + // otherwise, we will just serve the content as is + if(pagePath.endsWith(".mu")){ + this.nodePageContent = MicronParser.convertMicronToHtml(pageContent); + } else { + this.nodePageContent = pageContent; + } + // update page content - this.nodePageContent = pageContent; + this.isLoadingNodePage = false; }, (failureReason) => { @@ -1030,6 +1055,7 @@ // update page content this.nodePageContent = `Failed loading page: ${failureReason}`; + this.isLoadingNodePage = false; }, (progress) => { @@ -1040,10 +1066,85 @@ } // update page content - this.nodePageContent = `Loading page: ${progress}`; + this.nodePageProgress = Math.round(progress * 100); }); }, + parseNomadnetworkUrl: function(url) { + + // parse relative urls + if(url.startsWith(":")){ + return { + destination_hash: null, // node hash was not in provided url + path: url.substring(1), // remove leading ":" + }; + } + + // parse absolute urls such as 00000000000000000000000000000000:/page/index.mu + if(url.includes(":")){ + + // parse destination hash and url + const [destinationHash, relativeUrl] = url.split(":"); + + // ensure destination is expected length + if(destinationHash.length === 32){ + return { + destination_hash: destinationHash, + path: relativeUrl, + }; + } + + } + + // parse node id only + if(url.length === 32){ + return { + destination_hash: url, + path: "/page/index.mu", + }; + } + + // unsupported url + return null; + + }, + onNodePageUrlClick: function(url) { + + // open http urls in new tab + if(url.startsWith("http://") || url.startsWith("https://")){ + window.open(url, "_blank"); + return; + } + + // attempt to parse url + const parsedUrl = this.parseNomadnetworkUrl(url); + if(parsedUrl != null){ + + // file urls are not supported yet + if(parsedUrl.path.startsWith("/file/")){ + alert("file urls are not supported yet") + return; + } + + // use parsed destination hash, or fallback to selected node destination hash + const destinationHash = parsedUrl.destination_hash || this.selectedNode.destination_hash; + + // update selected node, so relative urls work correctly when returned by the new node + this.selectedNode = this.nodes[destinationHash] || { + name: "Unknown Node", + destination_hash: destinationHash, + }; + + // navigate to node page + this.loadNodePage(destinationHash, parsedUrl.path); + return; + + } + + // unsupported url + alert("unsupported url: " + url); + + }, async deleteChatItem(chatItem) { try {