basic implementation of converting micron to html
This commit is contained in:
174
public/assets/js/micron-parser.js
Normal file
174
public/assets/js/micron-parser.js
Normal 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/>");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// update page content
|
||||
// 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.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 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user