basic implementation of converting micron to html

This commit is contained in:
liamcottle
2024-05-08 01:20:03 +12:00
parent bfc613497b
commit a3562bf599
2 changed files with 291 additions and 16 deletions

View File

@@ -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 `<a href="${url}" onclick="event.preventDefault(); onNodePageUrlClick('${linkUrl}')">${linkText}</a>`
});
// parse markdown links
// [Reticulum](https://github.com/markqvist/Reticulum)
line = line.replaceAll(/\[(.*?)\]\((.*?)\)/g, function(match, linkText, linkUrl) {
const url = MicronParser.formatNomadnetworkUrl(linkUrl);
return `<a href="${url}" onclick="event.preventDefault(); onNodePageUrlClick('${linkUrl}')" style="text-decoration:underline;">${linkText}</a>`
});
// parse bold
// `!Bold Text`!
line = line.replaceAll(/`!(.*?)`!/g, function(match, text) {
return `<span style="font-weight:bold;">${text}</span>`
});
// parse italics
// `*Italic Text`*
line = line.replaceAll(/`\*(.*?)`\*/g, function(match, text) {
return `<span style="font-style:italic;">${text}</span>`
});
// parse underline
// `_Underlined Text`_
line = line.replaceAll(/`_(.*?)`_/g, function(match, text) {
return `<span style="text-decoration:underline;">${text}</span>`
});
// parse divider
// FIXME: use raw css, not tailwind classes for styling
if(line.trim() === "-"){
line = "<hr class='border-gray-500'/>"
}
// 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 = "<hr class='border-gray-500'/>"
}
// parse depth (should be full width)
// FIXME: use raw css, not tailwind classes for styling
if(line.startsWith(">>>")){
line = line.replace(">>>", "");
line = `<span class='inline-block w-full bg-gray-100 text-black pl-3'>${line}</span>`
} else if(line.startsWith(">>")){
line = line.replace(">>", "");
line = `<span class='inline-block w-full bg-gray-100 text-black pl-2'>${line}</span>`
} else if(line.startsWith(">")){
line = line.replace(">", "");
line = `<span class='inline-block w-full bg-gray-100 text-black pl-1'>${line}</span>`
}
// align (default: using left)
if(line.startsWith("`a")){
line = line.replace("`a", "");
line = `<span style="text-align:left;">${line}</span>`;
}
// align center
if(line.startsWith("`c")){
line = line.replace("`c", "");
line = `<span style="text-align:center;">${line}</span>`;
}
// align left
if(line.startsWith("`l")){
line = line.replace("`l", "");
line = `<span style="text-align:left;">${line}</span>`;
}
// align right
if(line.startsWith("`r")){
line = line.replace("`l", "");
line = `<span style="text-align:right;">${line}</span>`;
}
// formatted content
// `Ff00 red content `f
line = line.replaceAll(/`F([0-9A-Fa-f]+)(.*?)`f/g, function(match, colourCode, content) {
return `<span style="color:#${colourCode}">${content}</span>`
});
return line;
});
// filter out null items, and join with line break
return lines.filter((line) => {
return line != null;
}).join("<br/>");
}
}

View File

@@ -10,6 +10,7 @@
<script src="assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
<script src="assets/js/axios@1.6.8/dist/axios.min.js"></script>
<script src="assets/js/vue@3.4.26/dist/vue.global.js"></script>
<script src="assets/js/micron-parser.js"></script>
</head>
<body class="bg-gray-100">
@@ -206,8 +207,8 @@
</div>
<!-- chat view -->
<div class="flex flex-col flex-1 py-2">
<!-- main view -->
<div class="flex flex-col flex-1 py-2 overflow-hidden">
<!-- peer -->
<template v-if="!selectedNode">
@@ -443,17 +444,14 @@
</template>
<!-- node -->
<div v-if="selectedNode" class="flex flex-col h-full border rounded-xl bg-white shadow">
<div v-if="selectedNode" class="flex flex-col h-full border rounded-xl bg-white shadow overflow-hidden">
<!-- header -->
<!-- todo: peer icon -->
<!-- todo: node icon -->
<div class="flex p-2 border-b border-gray-300">
<!-- node info -->
<div>
<div class="my-auto">
<div class="font-semibold">{{ selectedNode.name }}</div>
<div class="text-sm">@<{{ selectedNode.destination_hash }}></div>
</div>
<!-- close button -->
@@ -472,15 +470,27 @@
</div>
<!-- browser navigation -->
<div class="w-full border-gray-300 border-b p-2">
{{ nodePagePath }}
<div class="flex w-full border-gray-300 border-b p-2">
<div @click="loadNodePage(selectedNode.destination_hash, '/page/index.mu')" class="my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" clip-rule="evenodd" />
</svg>
</div>
<div class="my-auto ml-2">{{ nodePagePath }}</div>
</div>
<!-- page content -->
<div class="h-full overflow-y-scroll px-3 sm:px-0">
<div class="flex flex-col space-y-3 p-3">
{{ nodePageContent }}
<div class="h-full overflow-y-scroll p-3 bg-black text-white">
<div class="flex" v-if="isLoadingNodePage">
<div class="my-auto">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<div class="my-auto">Loading {{ nodePageProgress }}%</div>
</div>
<pre v-else v-html="nodePageContent" class="h-full text-wrap"></pre>
</div>
</div>
@@ -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 {