Since I moved my Wordpress- and Medium-based blogs to this standalone platform, compiled by Eleventy and deployed by Netlify, I was wondering whether there is a way to persist original Markdown formatting even if someone copies the pre-rendered text from the browser.

Initially I was considering two approaches:

  • Create an invisible layer and map its content to the actual text using Selection objects, or
  • Somehow intercept with copy board buffer and replace html tags with their Markdown equivalents.

So the former option should be better performance-wise, as doesn't bother users every time they copy a few characters, and yet mapping two layers with different ranges (think hello word vs <b>hello <i>world</i></b>) might be pretty difficult. The latter sounds more error-prone, but requires fetching an html equivalent for any copied text, as well as raises another question: what happens, if someone copies only a part of the string?

Let's think about that a bit more. If you have a bold string of text and want to copy just of text, its html version will be of text</b>, and while mapping adding a missing tag should be relatively straightforward, it stays an open question.

I've decided to focus on some pure functionality in lieu of performance and went for the second option. Let's see how it was done.

First things first, let's prepare Eleventy for some Markdown magic:

<script src="https://unpkg.com/turndown/dist/turndown.js"></script>
{% set js %}{% include "scripts.js" %}{% endset %}
<script>{{ js | jsmin | safe }}</script>

If you put the above lines into your main template engine (in my case base.njk), it will fetch turndown.js, a small tool for converting html into Markdown, and prepare your template to fetch and minify contents of _include/scripts.js.

Now create the file scripts.js in the folder _include and get user's selection in HTML:

function getSelectionHtml() {
var html = "";
if (typeof window.getSelection != "undefined") {
var sel = window.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
} else if (typeof document.selection != "undefined") {
if (document.selection.type == "Text") {
html = document.selection.createRange().htmlText;
}
}
return html;
}

Once it's done, let's convert it to Markdown and replace whatever is already copied:

function addLink() {
var turndownService = new TurndownService(); // create html to md service

var selection = getSelectionHtml(); // get the html selection
var windowSelection = window.getSelection() // get the normal selection so we can override its content
var converted = turndownService.turndown(selection), // convert html to md
newdiv = document.createElement('div'); // magic starts here
newdiv.style.position = 'absolute';
newdiv.style.left = '-99999px';
document.body.appendChild(newdiv);
newdiv.innerHTML = converted; // and ends here
windowSelection.selectAllChildren(newdiv); // let's override the existing selection

window.setTimeout(function () {
document.body.removeChild(newdiv); // and remove the redundant magic garbage as it is not needed
}, 100);
}

So to elaborate on some magic bits above, we can't just forcely replace user's copy board with our random variables, thus we have to simulate a fair process of copying some web page content.

And that's done. Feel free to grab the plain source code in the repo of my blog, or try to copy some text on this page and see how it magically turns into Markdown.