Skip to content

Instantly share code, notes, and snippets.

@T4rk1n
Last active August 12, 2024 11:57
Show Gist options
  • Save T4rk1n/9a44e6d0a2da88cd3b54d3328df29bb8 to your computer and use it in GitHub Desktop.
Save T4rk1n/9a44e6d0a2da88cd3b54d3328df29bb8 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<!--
Static markdown document viewer.
Markdown renderer: remarkable
-->
<head>
<title>Markdown document viewer</title>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/remarkable/1.7.1/remarkable.min.js"></script>
</head>
<body>
<main style="display: flex; justify-content: center; font-family: Calibri;">
<div style="width: 80%;">
<div id="content" style="display:none;">
<div id="title"></div>
<div id="toc-header" style="display:none;">Table of content</div>
<div id="toc"></div>
<div id="render"></div>
</div>
<div id="error"></div>
<div id="uri" style="display:none;">
<label for="uri-input">Markdown uri</label>
<input id="uri-input" type="text" />
<button type="button" id="uri-render-btn">render</button>
</div>
</div>
</main>
<script>
(() => {
const promiseWrap = (func, options={rejectNull: false}) => new Promise((resolve, reject) => {
const { rejectNull } = options;
let result;
try {
result = func();
} catch(e) {
return reject(e);
}
if (rejectNull && !result) reject('Expected promise result is null');
else resolve(result);
});
const selectElement = (selector) => promiseWrap(() => document.querySelector(selector), {rejectNull: true});
const changeHtml = (selector, text) => selectElement(selector).then(elem => elem.innerHTML = text);
const createElement = (containerSelector, elementId, tagName='div', attributes={}, innerHtml='') => selectElement(containerSelector)
.then(elem => {
const element = document.createElement(tagName);
element.id = elementId;
elem.appendChild(element);
Object.keys(attributes)
.filter(k => attributes.hasOwnProperty(k))
.forEach(k => element.setAttribute(k, attributes[k]));
element.innerHTML = innerHtml;
});
const toggleDisplay = (selector, displayMode='block') => selectElement(selector)
.then(elem => elem.style.display = elem.style.display === 'none' ? displayMode : 'none');
const buildToc = (contentSelector, tocSelector, options={headings: ['h1', 'h2', 'h3', 'h4', 'h5']}) => selectElement(contentSelector).then(element => {
const { headings } = options;
let currentNode;
const nodeIterator = document.createNodeIterator(document.documentElement, NodeFilter.SHOW_ELEMENT,
(node) => headings.map(h => node.nodeName.toLowerCase().match(h)).reduce((a, e) => a || e) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT);
let lastNodeId = 0;
while(currentNode = nodeIterator.nextNode()) {
const nodeId = 'toc-' + lastNodeId++;
const nodeRefId = nodeId + '-ref';
const currentLevel = headings.indexOf(currentNode.nodeName.toLowerCase());
const nodeClass = 'toc-level-'+currentLevel;
const nodeContent = currentNode.textContent;
console.log(currentNode.attributes);
const idAttr = document.createAttribute('id');
idAttr.value = nodeRefId;
currentNode.attributes.setNamedItem(idAttr);
createElement(tocSelector, nodeId, 'a', {href: '#'+nodeRefId, className:nodeClass}, nodeContent);
}
});
const defaultMarkdownParams = {
html: false,
breaks: false,
linkify: false,
typographer: false
};
const fetchMDDefaultOptions = {
displayTOC: false,
displayTitle: false,
remarkableParams: defaultMarkdownParams
};
const fetchMarkdown = (uri, options=fetchMDDefaultOptions) => {
const { displayTOC, displayTitle, remarkableParams } = options;
const md = new Remarkable('full', remarkableParams);
fetch(uri, {
method: 'get',
mode: 'cors'
}).then(value => {
toggleDisplay('#content');
if (displayTitle) changeHtml('#title', uri);
value.text().then(t => changeHtml('#render', md.render(t))).then(() => {
if (displayTOC) {
toggleDisplay('#toc-header');
buildToc('#render','#toc')
}
});
});
};
const pageParams = window.location.search.substring(1)
.split('&')
.map(p => p.split('='))
.reduce((m, [k, v]) => m.set(k, decodeURIComponent(v)), new Map());
const filename = pageParams.get('f') || pageParams.get('file');
const mdParams = Object.keys(defaultMarkdownParams)
.map(k => [k, pageParams.get(k) === 'true'])
.reduce((o, [k, v]) => {o[k] = v; return o;}, {});
const fetchMarkdownOptions = {
displayTOC: pageParams.get('displayTOC'),
displayTitle: pageParams.get('displayTitle'),
remarkableParams: mdParams
};
if (filename) {
fetchMarkdown(filename, fetchMarkdownOptions);
} else {
toggleDisplay('#uri');
selectElement('#uri-render-btn')
.then(elem => elem.onclick = () => selectElement('#uri-input')
.then(uriInput => fetchMarkdown(uriInput.value, fetchMarkdownOptions))
.then(()=> toggleDisplay('#uri')))
}
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment