diff --git a/share/js/DomRenderer.js b/share/js/DomRenderer.js new file mode 100644 index 0000000..465bc2f --- /dev/null +++ b/share/js/DomRenderer.js @@ -0,0 +1,98 @@ +import * as Dom from UnitJS.Dom; +import * as Fun from UnitJS.Fun; +import Metadata; +import Template; +import Remarkable; + +return { + article: article, + articlesList: articlesList, + replaceMarkdown: replaceMarkdown +}; + +function replaceMarkdown() { + var div = document.getElementById('contents'); + if(div.children[0] && div.children[0].tagName.toLowerCase() == 'article') { + convertArticle(div.children[0], true); + } else { + var articles = div.getElementsByClassName('articles')[0]; + if(articles != undefined) { + for(var i = 0; i < articles.children.length; i++) { + convertArticle(articles.children[i]); + } + } else { + console.log('No articles found for this page'); + } + } +} + +function convertArticle(article, comments) { + var header = article.getElementsByTagName('header')[0]; + header.appendChild(Metadata.get(article.id)); + var text = article.getElementsByTagName('pre')[0]; + if(text != undefined) { + article.replaceChild(getDiv(text.innerText), text); + if(comments) { + Metadata.getComments(article.id) + .forEach(article.appendChild.bind(article)); + } + } else { + console.log('No content found for this article'); + } +} + +function getDiv(markdown) { + var d= Dom.make('div', { + innerHTML: Remarkable.md.render(markdown) + }); + var scripts = d.getElementsByTagName('script'); + for(var i = 0; i < scripts.length; i++) { + var run = Dom.make('script', + {type: 'text/javascript', src: scripts[i].src, textContent: scripts[i].textContent} + ); + scripts[i].parentNode.replaceChild(run, scripts[i]); + } + return d; +} + +function article(key, markdown, limit) { + var url = ["", blog.path.articlesPath, key + (limit != undefined ? '.html' : '.md')].join('/'); + var lines = markdown.split(/\n/).slice(blog.articles[key].bodyOffset); + var div = getDiv(lines.slice(0, limit).join('\n')); + return Dom.make('article', {}, [ + Dom.make('header', {}, [ + Dom.make('a', {href: url}, [ + Dom.make('h1', {innerText: blog.articles[key].title}) + ]), + Metadata.get(key) + ]), + div + ].concat(limit != undefined ? [] : Metadata.getComments(key))); +} + +function pageTitle(tag, all) { + if(tag != undefined) { + var template = all ? 'allTaggedPage' : 'latestTaggedPage'; + return Template.render(template, {tag: tag}); + } else { + return blog.wording[all ? 'allPage' : 'latestPage']; + } +} + +function otherUrl(tag, all) { + var path = [tag, all ? null : 'all.html']; + return '/' + path.filter(Fun.defined).join('/'); +} + +function articlesList(tag, all) { + return function(articlePreviews) { + return [ + Dom.make('h2', {innerText: pageTitle(tag, all)}), + Dom.make('a', { + innerText: all ? blog.wording.latestLink : blog.wording.allLink, + href: otherUrl(tag, all) + }), + Dom.make('div', {class: 'articles'}, articlePreviews.filter(Fun.defined)) + ]; + }; +} diff --git a/share/js/Main.js b/share/js/Main.js new file mode 100644 index 0000000..4384422 --- /dev/null +++ b/share/js/Main.js @@ -0,0 +1,5 @@ +import replaceMarkdown from DomRenderer; +import hijackLinks from Navigation; + +replaceMarkdown(); +hijackLinks(); diff --git a/share/js/Metadata.js b/share/js/Metadata.js new file mode 100644 index 0000000..bec1b72 --- /dev/null +++ b/share/js/Metadata.js @@ -0,0 +1,157 @@ +import * as Async from UnitJS.Async; +import * as Cache from UnitJS.Cache; +import * as Dom from UnitJS.Dom; + +var comments = Cache.make(function(threadId) { + return Async.bind( + Async.parallel( + getJSON(url(threadId)), + getJSON(url(threadId) + '/context'), + ), + Async.map(function(t) { + return [renderLink(t[0]), renderAnswers(t[1])]; + }) + ); +}); +return { + get: get, + getComments: getComments +}; + +function url(threadId) { + return blog.urls.comments + '/api/v1/statuses/' + threadId; +} + +function getJSON(url) { + return Async.bind( + Async.http({method: 'GET', url: url}), + function(queryResult) { + if(queryResult.status == 200) { + try { + return Async.wrap(JSON.parse(queryResult.responseText)); + } catch(e) { + return Async.fail('Server returned invalid JSON for ' + url); + } + } else { + return Async.fail('Could not load page ' + url); + } + } + ); +} + +function getComments(articleKey) { + var threadId = blog.articles[articleKey].metadata.comments; + if(blog.urls.comments != undefined && threadId != undefined) { + var ul = Dom.make('ul'); + var div = emptySection(ul); + Async.run( + Async.bind( + comments.get(threadId), Async.map(populateComments(div, ul)) + ) + ); + return [div]; + } else { + return []; + } +} + +function populateComments(div, ul) { + return function(apiResults) { + var post = apiResults[0], comments = apiResults[1]; + div.appendChild(post); + comments.forEach(function(comment) {ul.appendChild(comment);}); + }; +} + +function emptySection(ul) { + return Dom.make('div', {class: 'comments'}, [ + Dom.make('h2', {innerText: blog.wording.commentsSection}), + ul + ]); +} + +function renderLink(post) { + return Dom.make('a', { + href: post.url, + innerText: blog.wording.commentsLink + }); +} + +function getContent(descendant) { + return descendant.content.replace(/:([^: ]+):/g, function(pattern, shortcode) { + var emoji = descendant.emojis.find(function(e) {return e.shortcode == shortcode;}); + if(emoji) { + return [ + ', shortcode, ' + ].join('"'); + } else { + return pattern; + } + }); +} + +function renderAnswers(comments) { + return comments.descendants.map(function(descendant) { + return Dom.make('li', {}, [ + Dom.make('a', {href: descendant.account.url}, [ + Dom.make('img', { + src: descendant.account.avatar, + alt: descendant.account.username + "'s profile picture" + }) + ]), + Dom.make('div', { + class: "metadata", + innerHTML: modules.template.render('metadata', { + author: author(descendant.account.url, descendant.account.username), + date: date(descendant.created_at) + }) + }), + Dom.make('div', {innerHTML: getContent(descendant)}) + ]); + }); +} + +function author(key, name) { + var authorUrl = key; + if(blog.articles[key] != undefined) { + authorUrl = blog.articles[key].metadata.author; + } + if(authorUrl) { + var author = name || authorUrl.replace(/.*\//, ''); + return '' + author + ''; + } +} + +function date(key) { + if(blog.articles[key] != undefined) { + var date = new Date(blog.articles[key].metadata.date * 1000); + } else { + var date = new Date(key); + } + var format = blog.wording.dateFormat; + if(format[0] != '[') { + if(format[0] != '"') { + format = '"' + format + '"'; + } + format = '[' + format + ']'; + } + return Date.prototype.toLocaleDateString.apply(date, JSON.parse(format)); +} + +function tags(key) { + var tags = blog.articles[key].tagged; + return tags.length < 1 ? null : tags.map(function(tag) { + return '' + tag + ''; + }).join(', '); +} + +function get(key) { + return Dom.make('div', { + class: "metadata", + innerHTML: modules.template.render('metadata', { + author: author(key), + date: date(key), + tags: tags(key) + }) + }); +} diff --git a/share/js/Navigation.js b/share/js/Navigation.js new file mode 100644 index 0000000..7b0c7f3 --- /dev/null +++ b/share/js/Navigation.js @@ -0,0 +1,117 @@ +import * as Async from UnitJS.Async; +import * as Cache from UnitJS.Cache; +import * as Dom from UnitJS.Dom; +import * as Fun from UnitJS.Fun; +import {article, articlesList} from DomRenderer; + +var articles = Cache.make(function(key) { + var url = ["", blog.path.articlesPath, key + '.md'].join('/'); + return Async.bind( + Async.http({method: 'GET', url: url}), + function(queryResult) { + if(queryResult.status == 200) { + return Async.wrap(queryResult.responseText); + } else { + return Async.fail( + "Could not load article " + url + " (" + queryResult.status + " " + queryResult.statusText + ")" + ); + } + } + ); +}); +window.addEventListener('popstate', function(e) { + if(e.state != undefined) { + navigate(e.state.url); + } +}); +history.replaceState({url: window.location.pathname}, 'Blog - title', window.location.pathname); +return { + hijackLinks: hijackLinks +}; + +function hijackLinks(domElem) { + domElem = domElem || document; + var links = domElem.getElementsByTagName('a'); + for(var i = 0; i < links.length; i++) { + var a = links[i]; + var href = a.getAttribute("href"); + if((href[0] == "/" && href.slice(-3) != ".md") || href[0] == "#") { + a.addEventListener('click', visit(a.getAttribute("href"))); + } + } +} + +function visit(url) { + return function(e) { + e.preventDefault(); + if(url[0] == '#') { + window.location = url; + history.replaceState({url: window.location.pathname}, 'Blog - title', url); + } else { + navigate(url); + history.pushState({url: url}, 'Blog - title', url); + } + }; +} + +function navigate(url) { + var path = decodeURI(url).split("/").slice(1); + if(blog.tags[path[0]] != undefined) { + show(getArticlesList(path[0], path[1] == "all.html")); + } else if(path[0] == blog.path.articlesPath) { + show(getArticle(path[1].replace(/\.html$/, ''))); + } else { + show(getArticlesList(null, path[0] == "all.html")); + } +} + +function getArticle(key) { + return Async.bind( + articles.get(key), + Async.map( + function(contents) {return [article(key, contents)];} + ) + ); +} + +function preview(key) { + return Async.bind( + articles.get(key), + function(contents) { + return Async.wrap( + article(key, contents, blog.skin.previewLinesCount) + ); + } + ); +} + +function articleIds(tag, all) { + var ids = tag != undefined ? blog.tags[tag] : Object.keys(blog.articles); + var reverseDate = function (id) {return -blog.articles[id].metadata.date;}; + ids.sort(Fun.compare(reverseDate)); + return ids.slice(0, all ? undefined : blog.skin.previewArticlesCount); +} + +function getArticlesList(tag, all) { + return Async.bind( + Async.parallel.apply(null, articleIds(tag, all).map(preview)), + Async.map(articlesList(tag, all)) + ); +} + +function show(contents) { + Async.run( + Async.bind( + contents, + Async.map(function (domElems) { + domElems = domElems.filter(Fun.defined); + var div = document.getElementById('contents'); + Dom.clear(div); + for(var i = 0; i < domElems.length; i++) { + div.appendChild(domElems[i]); + } + hijackLinks(div); + }) + ) + ); +} diff --git a/share/js/Remarkable.js b/share/js/Remarkable.js new file mode 100644 index 0000000..d5e6125 --- /dev/null +++ b/share/js/Remarkable.js @@ -0,0 +1,8 @@ +import remarkableConfig from Hablo.Config; + +var md = new Remarkable(remarkableConfig); +md.block.ruler.enable(['footnote']); + +return { + md: md +}; diff --git a/share/js/Template.js b/share/js/Template.js new file mode 100644 index 0000000..f13cecd --- /dev/null +++ b/share/js/Template.js @@ -0,0 +1,33 @@ +return { + render: render +}; + +function render(template, environment) { + if(blog.wording[template] != undefined) { + var template = blog.wording[template]; + } + template = template.replace(/{\?((?:[^?]|\?[^}])*)\?}/g, renderSub(environment)); + var failed = [false]; + var result = template.replace( + /([^$]|^)\$(?:{(\w+)}|(\w+)\b)/g, + substitute(environment, failed) + ); + return failed[0] ? null : result; +} + +function renderSub(environment) { + return function(_, sub) { + return render(sub, environment) || ''; + }; +} + +function substitute(environment, failed) { + return function(_, before, bracketed, raw) { + var replaced = environment[bracketed || raw]; + if(replaced != undefined) { + return before + replaced; + } else { + failed[0] = true; + } + } +} diff --git a/share/js/domRenderer.js b/share/js/domRenderer.js deleted file mode 100644 index aa4a57b..0000000 --- a/share/js/domRenderer.js +++ /dev/null @@ -1,94 +0,0 @@ -function DomRenderer(modules) { - return { - article: article, - articlesList: articlesList, - replaceMarkdown: replaceMarkdown - }; - - function replaceMarkdown() { - var div = document.getElementById('contents'); - if(div.children[0] && div.children[0].tagName.toLowerCase() == 'article') { - convertArticle(div.children[0], true); - } else { - var articles = div.getElementsByClassName('articles')[0]; - if(articles != undefined) { - for(var i = 0; i < articles.children.length; i++) { - convertArticle(articles.children[i]); - } - } else { - console.log('No articles found for this page'); - } - } - } - - function convertArticle(article, comments) { - var header = article.getElementsByTagName('header')[0]; - header.appendChild(modules.metadata.get(article.id)); - var text = article.getElementsByTagName('pre')[0]; - if(text != undefined) { - article.replaceChild(getDiv(text.innerText), text); - if(comments) { - modules.metadata.getComments(article.id) - .forEach(article.appendChild.bind(article)); - } - } else { - console.log('No content found for this article'); - } - } - - function getDiv(markdown) { - var d= modules.dom.make('div', { - innerHTML: modules.md.render(markdown) - }); - var scripts = d.getElementsByTagName('script'); - for(var i = 0; i < scripts.length; i++) { - var run = modules.dom.make('script', - {type: 'text/javascript', src: scripts[i].src, textContent: scripts[i].textContent} - ); - scripts[i].parentNode.replaceChild(run, scripts[i]); - } - return d; - } - - function article(key, markdown, limit) { - var url = ["", blog.path.articlesPath, key + (limit != undefined ? '.html' : '.md')].join('/'); - var lines = markdown.split(/\n/).slice(blog.articles[key].bodyOffset); - var div = getDiv(lines.slice(0, limit).join('\n')); - return modules.dom.make('article', {}, [ - modules.dom.make('header', {}, [ - modules.dom.make('a', {href: url}, [ - modules.dom.make('h1', {innerText: blog.articles[key].title}) - ]), - modules.metadata.get(key) - ]), - div - ].concat(limit != undefined ? [] : modules.metadata.getComments(key))); - } - - function pageTitle(tag, all) { - if(tag != undefined) { - var template = all ? 'allTaggedPage' : 'latestTaggedPage'; - return modules.template.render(template, {tag: tag}); - } else { - return blog.wording[all ? 'allPage' : 'latestPage']; - } - } - - function otherUrl(tag, all) { - var path = [tag, all ? null : 'all.html']; - return '/' + path.filter(modules.fun.defined).join('/'); - } - - function articlesList(tag, all) { - return function(articlePreviews) { - return [ - modules.dom.make('h2', {innerText: pageTitle(tag, all)}), - modules.dom.make('a', { - innerText: all ? blog.wording.latestLink : blog.wording.allLink, - href: otherUrl(tag, all) - }), - modules.dom.make('div', {class: 'articles'}, articlePreviews.filter(modules.fun.defined)) - ]; - }; - } -} diff --git a/share/js/main.js b/share/js/main.js deleted file mode 100644 index d9fd9d1..0000000 --- a/share/js/main.js +++ /dev/null @@ -1,14 +0,0 @@ -window.addEventListener('load', function() { - var async = unitJS.Async(); - var cache = unitJS.Cache(); - var dom = unitJS.Dom(); - var fun = unitJS.Fun(); - var md = new Remarkable(remarkableConfig); - md.block.ruler.enable(['footnote']); - var template = Template(); - var metadata = Metadata({async: async, cache: cache, dom: dom, fun:fun, template: template}); - var domRenderer = DomRenderer({dom: dom, fun: fun, md: md, metadata: metadata, template: template}); - var navigation = Navigation({async: async, cache: cache, dom: dom, domRenderer: domRenderer, fun: fun, md: md}); - domRenderer.replaceMarkdown(); - navigation.hijackLinks(); -}); diff --git a/share/js/metadata.js b/share/js/metadata.js deleted file mode 100644 index c37c2f3..0000000 --- a/share/js/metadata.js +++ /dev/null @@ -1,155 +0,0 @@ -function Metadata(modules) { - var comments = modules.cache.make(function(threadId) { - return modules.async.bind( - modules.async.parallel( - getJSON(url(threadId)), - getJSON(url(threadId) + '/context'), - ), - modules.async.map(function(t) { - return [renderLink(t[0]), renderAnswers(t[1])]; - }) - ); - }); - return { - get: get, - getComments: getComments - }; - - function url(threadId) { - return blog.urls.comments + '/api/v1/statuses/' + threadId; - } - - function getJSON(url) { - return modules.async.bind( - modules.async.http({method: 'GET', url: url}), - function(queryResult) { - if(queryResult.status == 200) { - try { - return modules.async.wrap(JSON.parse(queryResult.responseText)); - } catch(e) { - return modules.async.fail('Server returned invalid JSON for ' + url); - } - } else { - return modules.async.fail('Could not load page ' + url); - } - } - ); - } - - function getComments(articleKey) { - var threadId = blog.articles[articleKey].metadata.comments; - if(blog.urls.comments != undefined && threadId != undefined) { - var ul = modules.dom.make('ul'); - var div = emptySection(ul); - modules.async.run( - modules.async.bind( - comments.get(threadId), modules.async.map(populateComments(div, ul)) - ) - ); - return [div]; - } else { - return []; - } - } - - function populateComments(div, ul) { - return function(apiResults) { - var post = apiResults[0], comments = apiResults[1]; - div.appendChild(post); - comments.forEach(function(comment) {ul.appendChild(comment);}); - }; - } - - function emptySection(ul) { - return modules.dom.make('div', {class: 'comments'}, [ - modules.dom.make('h2', {innerText: blog.wording.commentsSection}), - ul - ]); - } - - function renderLink(post) { - return modules.dom.make('a', { - href: post.url, - innerText: blog.wording.commentsLink - }); - } - - function getContent(descendant) { - return descendant.content.replace(/:([^: ]+):/g, function(pattern, shortcode) { - var emoji = descendant.emojis.find(function(e) {return e.shortcode == shortcode;}); - if(emoji) { - return [ - ', shortcode, ' - ].join('"'); - } else { - return pattern; - } - }); - } - - function renderAnswers(comments) { - return comments.descendants.map(function(descendant) { - return modules.dom.make('li', {}, [ - modules.dom.make('a', {href: descendant.account.url}, [ - modules.dom.make('img', { - src: descendant.account.avatar, - alt: descendant.account.username + "'s profile picture" - }) - ]), - modules.dom.make('div', { - class: "metadata", - innerHTML: modules.template.render('metadata', { - author: author(descendant.account.url, descendant.account.username), - date: date(descendant.created_at) - }) - }), - modules.dom.make('div', {innerHTML: getContent(descendant)}) - ]); - }); - } - - function author(key, name) { - var authorUrl = key; - if(blog.articles[key] != undefined) { - authorUrl = blog.articles[key].metadata.author; - } - if(authorUrl) { - var author = name || authorUrl.replace(/.*\//, ''); - return '' + author + ''; - } - } - - function date(key) { - if(blog.articles[key] != undefined) { - var date = new Date(blog.articles[key].metadata.date * 1000); - } else { - var date = new Date(key); - } - var format = blog.wording.dateFormat; - if(format[0] != '[') { - if(format[0] != '"') { - format = '"' + format + '"'; - } - format = '[' + format + ']'; - } - return Date.prototype.toLocaleDateString.apply(date, JSON.parse(format)); - } - - function tags(key) { - var tags = blog.articles[key].tagged; - return tags.length < 1 ? null : tags.map(function(tag) { - return '' + tag + ''; - }).join(', '); - } - - function get(key) { - return modules.dom.make('div', { - class: "metadata", - innerHTML: modules.template.render('metadata', { - author: author(key), - date: date(key), - tags: tags(key) - }) - }); - } -} diff --git a/share/js/navigation.js b/share/js/navigation.js deleted file mode 100644 index b48c34e..0000000 --- a/share/js/navigation.js +++ /dev/null @@ -1,117 +0,0 @@ -function Navigation(modules) { - var articles = modules.cache.make(function(key) { - var url = ["", blog.path.articlesPath, key + '.md'].join('/'); - return modules.async.bind( - modules.async.http({method: 'GET', url: url}), - function(queryResult) { - if(queryResult.status == 200) { - return modules.async.wrap(queryResult.responseText); - } else { - return modules.async.fail( - "Could not load article " + url + " (" + queryResult.status + " " + queryResult.statusText + ")" - ); - } - } - ); - }); - window.addEventListener('popstate', function(e) { - if(e.state != undefined) { - navigate(e.state.url); - } - }); - history.replaceState({url: window.location.pathname}, 'Blog - title', window.location.pathname); - return { - hijackLinks: hijackLinks - }; - - function hijackLinks(domElem) { - domElem = domElem || document; - var links = domElem.getElementsByTagName('a'); - for(var i = 0; i < links.length; i++) { - var a = links[i]; - var href = a.getAttribute("href"); - if((href[0] == "/" && href.slice(-3) != ".md") || href[0] == "#") { - a.addEventListener('click', visit(a.getAttribute("href"))); - } - } - } - - function visit(url) { - return function(e) { - e.preventDefault(); - if(url[0] == '#') { - window.location = url; - history.replaceState({url: window.location.pathname}, 'Blog - title', url); - } else { - navigate(url); - history.pushState({url: url}, 'Blog - title', url); - } - }; - } - - function navigate(url) { - var path = decodeURI(url).split("/").slice(1); - if(blog.tags[path[0]] != undefined) { - show(getArticlesList(path[0], path[1] == "all.html")); - } else if(path[0] == blog.path.articlesPath) { - show(getArticle(path[1].replace(/\.html$/, ''))); - } else { - show(getArticlesList(null, path[0] == "all.html")); - } - } - - function getArticle(key) { - return modules.async.bind( - articles.get(key), - modules.async.map( - function(contents) {return [modules.domRenderer.article(key, contents)];} - ) - ); - } - - function preview(key) { - return modules.async.bind( - articles.get(key), - function(contents) { - return modules.async.wrap( - modules.domRenderer.article( - key, - contents, - blog.skin.previewLinesCount - ) - ); - } - ); - } - - function articleIds(tag, all) { - var ids = tag != undefined ? blog.tags[tag] : Object.keys(blog.articles); - var reverseDate = function (id) {return -blog.articles[id].metadata.date;}; - ids.sort(modules.fun.compare(reverseDate)); - return ids.slice(0, all ? undefined : blog.skin.previewArticlesCount); - } - - function getArticlesList(tag, all) { - return modules.async.bind( - modules.async.parallel.apply(null, articleIds(tag, all).map(preview)), - modules.async.map(modules.domRenderer.articlesList(tag, all)) - ); - } - - function show(contents) { - modules.async.run( - modules.async.bind( - contents, - modules.async.map(function (domElems) { - domElems = domElems.filter(modules.fun.defined); - var div = document.getElementById('contents'); - modules.dom.clear(div); - for(var i = 0; i < domElems.length; i++) { - div.appendChild(domElems[i]); - } - hijackLinks(div); - }) - ) - ); - } -} diff --git a/share/js/template.js b/share/js/template.js deleted file mode 100644 index 1bac571..0000000 --- a/share/js/template.js +++ /dev/null @@ -1,35 +0,0 @@ -function Template() { - return { - render: render - }; - - function render(template, environment) { - if(blog.wording[template] != undefined) { - var template = blog.wording[template]; - } - template = template.replace(/{\?((?:[^?]|\?[^}])*)\?}/g, renderSub(environment)); - var failed = [false]; - var result = template.replace( - /([^$]|^)\$(?:{(\w+)}|(\w+)\b)/g, - substitute(environment, failed) - ); - return failed[0] ? null : result; - } - - function renderSub(environment) { - return function(_, sub) { - return render(sub, environment) || ''; - }; - } - - function substitute(environment, failed) { - return function(_, before, bracketed, raw) { - var replaced = environment[bracketed || raw]; - if(replaced != undefined) { - return before + replaced; - } else { - failed[0] = true; - } - } - } -} diff --git a/src/DOM.hs b/src/DOM.hs index 4955f53..a5c8bff 100644 --- a/src/DOM.hs +++ b/src/DOM.hs @@ -77,7 +77,6 @@ page aPage = head_ (do meta_ [charset_ "utf-8"] title_ . toHtml =<< Blog.get name - script_ [src_ "/js/unit.js"] empty script_ [src_ "/js/remarkable.min.js"] empty script_ [src_ "/js/hablo.js"] empty optional faviconLink =<< (Blog.get $skin.$favicon)