From dd819c45b58088f9a98384ab237268d865836fe0 Mon Sep 17 00:00:00 2001 From: Alan Pearce Date: Sat, 24 Jun 2017 22:05:18 +0200 Subject: refactor: re-architect to be closer to ADR --- config/default.toml | 2 +- src/actions.js | 56 +++++++++++++++++++++++++++++ src/app.js | 54 ++++++---------------------- src/domain/posts.js | 74 ++++++++++++++++++++++++++++++++++++++ src/modules/markdown.js | 12 +++++++ src/modules/posts.js | 88 ---------------------------------------------- src/responders.js | 3 +- test/domain/posts.test.js | 23 ++++++++++++ test/modules/posts.test.js | 49 -------------------------- 9 files changed, 179 insertions(+), 182 deletions(-) create mode 100644 src/actions.js create mode 100644 src/domain/posts.js create mode 100644 src/modules/markdown.js delete mode 100644 src/modules/posts.js create mode 100644 test/domain/posts.test.js delete mode 100644 test/modules/posts.test.js diff --git a/config/default.toml b/config/default.toml index 67bb521..acda778 100644 --- a/config/default.toml +++ b/config/default.toml @@ -7,7 +7,7 @@ title = "Test Site" [posts] folder = "./posts" -[taxonomies] +[posts.taxonomies] tag = "tags" category = "categories" diff --git a/src/actions.js b/src/actions.js new file mode 100644 index 0000000..c6e04c0 --- /dev/null +++ b/src/actions.js @@ -0,0 +1,56 @@ +'use strict' + +const send = require('koa-send') +const streamify = require('stream-array') +const responders = require('./responders') + +function toArrayStream (iterator) { + return streamify(Array.from(iterator.entries())) +} + +function home (config, posts) { + const postsStream = toArrayStream(posts) + return async function (ctx, next) { + responders.home(ctx, config, postsStream) + } +} + +function post (config, posts) { + return async function (ctx, next) { + ctx.assert(posts.has(ctx.params.filename), 404, 'Post not found') + const post = posts.get(ctx.params.filename) + + responders.post(ctx, config, post) + } +} + +function taxonGenerator (config, term, items) { + return async function (ctx, next) { + const value = ctx.params.value + ctx.assert( + items.has(ctx.params.value), + 404, + `Could not find ${term} ${value}` + ) + + const taxonItems = toArrayStream(items.get(value)) + + responders.taxon(ctx, config, taxonItems) + } +} + +const prefix = /^\/static\// +async function serveFiles (ctx) { + if (prefix.test(ctx.path)) { + await send(ctx, ctx.path.replace(prefix, ''), { + root: './static' + }) + } +} + +module.exports = { + home, + post, + taxonGenerator, + serveFiles +} diff --git a/src/app.js b/src/app.js index 84722fc..46ebd2e 100644 --- a/src/app.js +++ b/src/app.js @@ -3,10 +3,7 @@ const Koa = require('koa') const app = new Koa() -const streamify = require('stream-array') - -const send = require('koa-send') -const responders = require('./responders.js') +const actions = require('./actions.js') const config = require('./modules/config.js') @@ -15,51 +12,22 @@ const router = new Router() app.context.getURL = router.url.bind(router) -const Posts = require('./modules/posts.js') -const posts = Posts.getFolder(config.posts.folder) - -function toArrayStream (iterator) { - return streamify(Array.from(iterator.entries())) -} - -const postsStream = toArrayStream(posts) -router.get('home', '/', async function (ctx, next) { - responders.home(ctx, config, postsStream) -}) - -router.get('post', '/post/:filename', async function (ctx, next) { - ctx.assert(posts.has(ctx.params.filename), 404, 'Post not found') - const post = posts.get(ctx.params.filename) - post.body = Posts.render(post) - - responders.post(ctx, config, post) -}) +const Posts = require('./domain/posts.js')(config.posts) -const taxonomies = Posts.taxonomise(config.taxonomies, posts) -for (let [term, items] of taxonomies) { - router.get(`taxon-${term}`, `/${term}/:value`, async function (ctx, next) { - const value = ctx.params.value - ctx.assert( - items.has(ctx.params.value), - 404, - `Could not find ${term} ${value}` - ) +router.get('home', '/', actions.home(config, Posts.posts)) - const taxonItems = toArrayStream(items.get(value)) +router.get('post', '/post/:filename', actions.post(config, Posts.posts)) - responders.taxon(ctx, config, taxonItems) - }) +for (let [term, items] of Posts.taxonomies) { + router.get( + `taxon-${term}`, + `/${term}/:value`, + actions.taxonGenerator(config, term, items) + ) } app.use(router.routes()).use(router.allowedMethods()) -const prefix = /^\/static\// -app.use(async function (ctx) { - if (prefix.test(ctx.path)) { - await send(ctx, ctx.path.replace(prefix, ''), { - root: './static' - }) - } -}) +app.use(actions.serveFiles) module.exports = app diff --git a/src/domain/posts.js b/src/domain/posts.js new file mode 100644 index 0000000..14bc7cc --- /dev/null +++ b/src/domain/posts.js @@ -0,0 +1,74 @@ +'use strict' + +const fs = require('fs') +const path = require('path') +const matter = require('gray-matter') + +const grayMatterOptions = { + lang: 'toml', + delims: '+++' +} + +function* lowercaseKeys (iterator) { + for (let [k, v] of iterator) { + yield [String(k).toLowerCase(), v] + } +} + +function canonicaliseMetadata (meta) { + if (meta.data) { + meta.data = new Map(lowercaseKeys(Object.entries(meta.data))) + } else { + meta.data = new Map() + } + return meta +} + +function getTitle (file) { + return path.basename(file.path, path.extname(file.path)) +} + +function get (filename) { + const fileMatter = matter.read(filename, grayMatterOptions) + fileMatter.basename = getTitle(fileMatter) + return canonicaliseMetadata(fileMatter) +} + +function getFolder (folder) { + return new Map( + fs + .readdirSync(folder) + .map(f => path.resolve(folder, f)) + .map(get) + .map(f => [getTitle(f), f]) + ) +} + +function taxonomise (taxonomies, posts) { + const taxons = new Map(Object.keys(taxonomies).map(t => [t, new Map()])) + + for (let [, post] of posts) { + for (let [singularName, pluralName] of Object.entries(taxonomies)) { + if (post.data.has(pluralName)) { + for (let term of post.data.get(pluralName)) { + const current = taxons.get(singularName).get(term) + taxons + .get(singularName) + .set(term, current ? current.concat(post) : [post]) + } + } + } + } + + return taxons +} + +module.exports = function (config) { + const posts = getFolder(config.folder) + const taxonomies = taxonomise(config.taxonomies, posts) + return { + posts, + taxonomies, + get + } +} diff --git a/src/modules/markdown.js b/src/modules/markdown.js new file mode 100644 index 0000000..9f0af45 --- /dev/null +++ b/src/modules/markdown.js @@ -0,0 +1,12 @@ +'use strict' + +const Markdown = require('markdown-it') + +const markdownOptions = { + html: true, + typographer: true +} + +const markdown = new Markdown(markdownOptions) + +module.exports = markdown.render.bind(markdown) diff --git a/src/modules/posts.js b/src/modules/posts.js deleted file mode 100644 index cf2d80e..0000000 --- a/src/modules/posts.js +++ /dev/null @@ -1,88 +0,0 @@ -'use strict' - -const fs = require('fs') -const path = require('path') -const matter = require('gray-matter') -const Markdown = require('markdown-it') - -const grayMatterOptions = { - lang: 'toml', - delims: '+++' -} - -const markdownOptions = { - html: true, - typographer: true -} - -const markdown = new Markdown(markdownOptions) - -function* lowercaseKeys (iterator) { - for (let [k, v] of iterator) { - yield [String(k).toLowerCase(), v] - } -} - -function canonicaliseMetadata (meta) { - if (meta.data) { - meta.data = new Map(lowercaseKeys(Object.entries(meta.data))) - } else { - meta.data = new Map() - } - return meta -} - -function getTitle (file) { - return path.basename(file.path, path.extname(file.path)) -} - -function render (post) { - return markdown.render(post.content) -} - -function get (filename) { - const fileMatter = matter.read(filename, grayMatterOptions) - fileMatter.basename = getTitle(fileMatter) - return canonicaliseMetadata(fileMatter) -} - -function getFolder (folder) { - return new Map( - fs - .readdirSync(folder) - .map(f => path.resolve(folder, f)) - .map(get) - .map(f => [getTitle(f), f]) - ) -} - -function taxonomise (taxonomies, posts) { - const taxons = new Map(Object.keys(taxonomies).map(t => [t, new Map()])) - - for (let [, post] of posts) { - for (let [singularName, pluralName] of Object.entries(taxonomies)) { - if (post.data.has(pluralName)) { - for (let term of post.data.get(pluralName)) { - const current = taxons.get(singularName).get(term) - taxons - .get(singularName) - .set(term, current ? current.concat(post) : [post]) - } - } - } - } - - return taxons -} - -function toTags (posts) { - return taxonomise({ tag: 'tags' }, posts).get('tag') -} - -module.exports = { - get, - getFolder, - toTags, - taxonomise, - render -} diff --git a/src/responders.js b/src/responders.js index 1a9e8ce..a11e598 100644 --- a/src/responders.js +++ b/src/responders.js @@ -2,6 +2,7 @@ const fs = require('fs') const rheo = require('rheo') +const markdown = require('./modules/markdown.js') const templateReader = template => () => fs.createReadStream(`${__dirname}/templates/${template}.html`) @@ -55,7 +56,7 @@ module.exports = { .pipe(rheo()) .inner('main', showPage('post')) .inner('article h1', rheo(post.data.get('title'))) - .inner('article main', rheo(post.body)) + .inner('article main', rheo(markdown(post.content))) .pipe(setTitle(config.site.title, post.data.get('title'))) .render() }, diff --git a/test/domain/posts.test.js b/test/domain/posts.test.js new file mode 100644 index 0000000..8866f40 --- /dev/null +++ b/test/domain/posts.test.js @@ -0,0 +1,23 @@ +const test = require('ava') +const path = require('path') + +const Posts = require('../../src/domain/posts.js')({ + folder: path.resolve('../data', __dirname), + taxonomies: { + tag: 'tags', + category: 'categories' + } +}) + +test('get', t => { + const expected = new Map( + Object.entries({ + title: 'This is a test', + description: 'Test file', + tags: ['a', 'b'] + }) + ) + const post = Posts.get(path.resolve(__dirname, '../data/testfile.md')) + t.deepEqual(post.data, expected) + t.is(post.basename, 'testfile', 'must include basename') +}) diff --git a/test/modules/posts.test.js b/test/modules/posts.test.js deleted file mode 100644 index 454488f..0000000 --- a/test/modules/posts.test.js +++ /dev/null @@ -1,49 +0,0 @@ -const test = require('ava') -const path = require('path') - -const Posts = require('../../src/modules/posts.js') - -test('get', t => { - const expected = new Map( - Object.entries({ - title: 'This is a test', - description: 'Test file', - tags: ['a', 'b'] - }) - ) - const post = Posts.get(path.resolve(__dirname, '../data/testfile.md')) - t.deepEqual(post.data, expected) - t.is(post.basename, 'testfile', 'must include basename') -}) - -test('getFolder', t => { - const expected = new Map( - Object.entries({ - title: 'This is a test', - description: 'Test file', - tags: ['a', 'b'] - }) - ) - const actual = Posts.getFolder(path.resolve(__dirname, '../data/')) - t.true(actual.size > 0, 'must return a non-empty map') - t.is( - actual.get('testfile').path, - path.resolve(__dirname, '../data/testfile.md') - ) - t.deepEqual(actual.get('testfile').data, expected) -}) - -test('toTags', t => { - const posts = new Map([ - [ - 'testfile', - { - data: new Map([['title', 'Test Post'], ['tags', ['a', 'b']]]) - } - ] - ]) - const actual = Posts.toTags(posts) - t.is(actual.size, 2) - t.is(actual.get('a')[0].data.get('title'), 'Test Post') - t.deepEqual(actual.get('a'), actual.get('b')) -}) -- cgit 1.4.1