I am looking for a tool to preview contents of a Markdown file, as I edit it. After checking plenty of existing tools, all of them appear to have different shortcomings:
- subjectively, too slow when editing huge .md files (thousands of words; for example, https://stackedit.io and https://hackmd.io),
- some of them need to be installed and are resource hungry (seems they attempt to bring full Electron),
- no longer developed actively (for example, GitHub - pandao/editor.md: The open source embeddable online markdown editor (component).),
- not conforming to CommonMark or GFM, and having weird default preview styling (e.g. Obsidian).
(I got to be honest I haven’t check each and every Markdown editor out there)
I’ve also tried using an online “Demo” page for Markedjs, only to have lost a couple of hours of writing when my Chrome crashed But that’s a lesson for me, I guess.
Instead of using a dedicated “Markdown editor” tool for this, I’d like to use:
- plain text .md files on my hard drive,
- my editor of choice, e.g. neovim or Zed, or even a TextEdit on macOS,
- a “small” server that would notice changes to the file, then render an .html out of it and cue browser to reload the page (see next item),
- a browser, that would render an .html version of the .md file.
I’ve largely achieved this by using a node
HTTP server, with a combination of marked
, highlight.js
and socket.io
libraries:
I know this is ugly
const http = require('http');
const fs = require('fs');
const socketIo = require('socket.io');
const { Marked } = require('marked');
const { markedHighlight } = require('marked-highlight');
const hljs = require('highlight.js');
const server = http.createServer((req, res) => {
if (req.url === '/') {
fs.readFile('./index.html', 'utf8', (err, content) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Internal server error');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content);
});
} else if (req.url === '/socket.io/socket.io.js') {
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(socketIo.clientSource);
} else if (req.url === '/highlight.js/styles/github.css') {
res.writeHead(200, { 'Content-Type': 'text/css' });
res.end("node_modules/highlightjs/styles/obsidian.css")
}
});
const io = socketIo(server);
let htmlContent = '';
const updateHtmlContent = () => {
const mdContent = fs.readFileSync('post-1.md', 'utf8');
const marked = new Marked(
markedHighlight({
langPrefix: 'hljs language-',
highlight(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
}
})
);
htmlContent = marked.parse(mdContent);
};
// Initial read
updateHtmlContent();
io.on('connection', (socket) => {
console.log('Client connected');
socket.emit('updateContent', htmlContent);
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
fs.watchFile('article.md', () => {
console.log('article.md changed');
updateHtmlContent();
io.emit('refreshContent');
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000/');
});
With the above, I’m able to edit the file locally and see it’s preview in Safari. This works good enough, but as the length of article grows, there’s a noticeable lag between saving the file and the contents appearing on the web page. Perceivably, the lag comes from 2 sources:
- there’s some kind of delay between when the file is changed, and when the change is “noticed”,
- on top of that, there’s a full page re-render needs to happen every time the new contents is about to appear on page.
Because in Elixir ecosystem there exist tools like earmark, phoenix_live_view and phoenix_live_reload - couldn’t the above script in node
be implemented much more optimally? E.g.:
- (server) on start,
- load .md file from disk,
- convert .md to .html, “memoize” it,
- serve .html,
- establish a WebSocket connection with browser,
- (server) listen for changes on .md file; when a change occurs:
- convert .md to .html,
- figure out diff between existing .html by using a previous “memoized” version of .html,
- send diff over the WebSocket connection to browser,
- let JavaScript in browser apply up the diff.
Conceptually, it would look like this:
Or more precisely, like this:
I don’t know a lot about LiveView, and not sure where to start about this one. I’ve noticed that LiveView actually is dependent on Phoenix itself, so actually a full-blown phoenix project is likely be needed here, but I am ok with that.
The benefits of such a server as I see it:
- all .md files are local, so chances of loosing changes is small,
- the preview generation is blazing fast,
- I’d be using tools from Elixir ecosystem, and potentially contribute back (for example if
earcut
is lacking support for some things from CommonMark).
Where would one start on something like this?