diff --git a/travis.yaml b/.travis.yml similarity index 100% rename from travis.yaml rename to .travis.yml diff --git a/choo.js b/choo.js index 752b773..8785a87 100644 --- a/choo.js +++ b/choo.js @@ -1,8 +1,10 @@ -var Page = require('./lib') +var File = require('./lib/file') +var Page = require('./lib/page') module.exports = plugin function plugin (state, emit) { state.content = state.content || { } + state.file = new File(state) state.page = new Page(state) } diff --git a/file.js b/file.js new file mode 100644 index 0000000..e60c77b --- /dev/null +++ b/file.js @@ -0,0 +1 @@ +module.exports = require('./lib/file') diff --git a/index.js b/index.js new file mode 100644 index 0000000..ea346c7 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/page') diff --git a/lib/file/index.js b/lib/file/index.js new file mode 100644 index 0000000..8727e5d --- /dev/null +++ b/lib/file/index.js @@ -0,0 +1,65 @@ +var NanoTemplate = require('../template') +var utilsPage = require('../page/utils') +var objectKeys = require('object-keys') +var joinPath = require('join-path') +var methods = require('../methods') +var utils = require('./utils') + +var minimatch = require('minimatch') +var isGlob = require('is-glob') + +module.exports = wrapper + +class File extends NanoTemplate { + constructor (state, value) { + super(state, value) + var self = this + + // add utilities + objectKeys(utils).forEach(function (key) { + self[key] = function (...args) { + this._value = utils[key](this._state, this._value, ...args) + return this + } + }) + } +} + +function wrapper (state) { + return function (value) { + return new File(state, parseInitialValue(state, value)) + } +} + +function parseInitialValue (state, value) { + // defaults + state = state || { } + state.content = state.content || { } + + // grab our content + var href = state.href || '/' + var page = state.content[methods.parseHref(href)] || { } + + // set the value + switch (typeof value) { + case 'string': + if (isGlob(value)) return getContentGlob(state, page, value) + else return utilsPage.file(state, page, methods.parseHref(joinPath(href, value))) + case 'object': + // if an object and it contains value grab that value + if (typeof value.value === 'function') return value.value() + else return value + default: + // if no value, pass our page + return value || page + } +} + +function getContentGlob (state, page, value) { + return objectKeys(state.content) + .filter(minimatch.filter(value, { matchBase: true })) + .reduce(function (res, cur) { + res[cur] = state.content[cur] + return res + }, { }) +} diff --git a/lib/file/utils.js b/lib/file/utils.js new file mode 100644 index 0000000..6012e98 --- /dev/null +++ b/lib/file/utils.js @@ -0,0 +1,14 @@ +module.exports = { + size, + source: noop +} + +function size (state, value, size) { + try { + return value.sizes[size.toString()] + } catch (err) { + return '' + } +} + +function noop () { } diff --git a/lib/methods.js b/lib/methods.js index dcd14ab..feaa0ef 100644 --- a/lib/methods.js +++ b/lib/methods.js @@ -1,8 +1,38 @@ +var parsePath = require('parse-filepath') + module.exports = { - parseHref + parseHref, + isDate, + isFile, + isPage, + hasUrl } function parseHref (str) { if (str.length > 1) return str.replace(/\/+$/, '') else return str } + +function isDate (str) { + return /^\d{4}-\d{1,2}-\d{1,2}$/.test(str) +} + +function isFile (value) { + return isPage(value) === false +} + +function isPage (value) { + try { + var url = hasUrl(value) ? value.url : value + return parsePath(url).ext === '' + } catch (err) { + return false + } +} + +function hasUrl (value) { + return ( + typeof value === 'object' && + typeof value.url === 'string' + ) +} diff --git a/lib/page/index.js b/lib/page/index.js new file mode 100644 index 0000000..222936f --- /dev/null +++ b/lib/page/index.js @@ -0,0 +1,65 @@ +var NanoTemplate = require('../template') +var utilsPage = require('../page/utils') +var objectKeys = require('object-keys') +var joinPath = require('join-path') +var methods = require('../methods') +var utils = require('./utils') + +var minimatch = require('minimatch') +var isGlob = require('is-glob') + +module.exports = wrapper + +class Page extends NanoTemplate { + constructor (state, value) { + super(state, value) + var self = this + + // add utilities + objectKeys(utils).forEach(function (key) { + self[key] = function (...args) { + this._value = utils[key](this._state, this._value, ...args) + return this + } + }) + } +} + +function wrapper (state) { + return function (value) { + return new Page(state, parseInitialValue(state, value)) + } +} + +function parseInitialValue (state, value) { + // defaults + state = state || { } + state.content = state.content || { } + + // grab our content + var href = state.href || '/' + var page = state.content[methods.parseHref(href)] || { } + + // set the value + switch (typeof value) { + case 'string': + if (isGlob(value)) return getContentGlob(state, page, value) + else return utilsPage.file(state, page, methods.parseHref(joinPath(href, value))) + case 'object': + // if an object and it contains value grab that value + if (typeof value.value === 'function') return value.value() + else return value + default: + // if no value, pass our page + return value || page + } +} + +function getContentGlob (state, page, value) { + return objectKeys(state.content) + .filter(minimatch.filter(value, { matchBase: true })) + .reduce(function (res, cur) { + res[cur] = state.content[cur] + return res + }, { }) +} diff --git a/lib/page/utils.js b/lib/page/utils.js new file mode 100644 index 0000000..e609edf --- /dev/null +++ b/lib/page/utils.js @@ -0,0 +1,132 @@ +var objectValues = require('object-values') +var objectKeys = require('object-keys') +var resolve = require('resolve-path') +var methods = require('../methods') + +module.exports = { + children, + file, + files, + find, + hasView, + images, + pages, + sortBy +} + +function children (state, value) { + // alias for pages + return pages(state, value) +} + +function file (state, value, key) { + return find(state, value, key) +} + +function files (state, value) { + try { + var base = value.url.replace(/^\//, '') ? value.url : '' + var regex = new RegExp('^' + base + '/[^/]+/?$') + return objectKeys(state.content) + .filter(key => regex.test(key.trim())) + .reduce(readFile, { }) + } catch (err) { + return { } + } + + function readFile (result, key) { + var file = state.content[key] + if (methods.isFile(file)) result[key] = file + return result + } +} + +function find (state, value, url) { + try { + // normalize + url = methods.parseHref(url) + + // grab from root + if (url.indexOf('/') === 0) { + return state.content[url] + } + + // if on a page grab relative + if (typeof value.url === 'string') { + return state.content[resolve(value.url, url)] + } + + // fall back to href + return state.content[resolve(state.href || '/', url)] + } catch (err) { + return { } + } +} + +function hasView (state, value) { + try { + return typeof value.view !== 'undefined' + } catch (err) { + return false + } +} + +function images (state, value) { + try { + var _files = files(state, value) + return objectKeys(_files).reduce(function (res, key) { + if (_files[key].type === 'image') res[key] = _files[key] + return res + }, { }) + } catch (err) { + return { } + } +} + +function pages (state, value) { + try { + var base = value.url.replace(/^\//, '') ? value.url : '' + var regex = new RegExp('^' + base + '/[^/]+/?$') + return objectKeys(state.content) + .filter(key => regex.test(key.trim())) + .reduce(readPage, { }) + } catch (err) { + return { } + } + + function readPage (result, key) { + var page = state.content[key] + if (methods.isPage(page)) result[key] = page + return result + } +} + +function sortBy (state, value, key, order) { + try { + return objectValues(value).sort(sort) + } catch (err) { + return [ ] + } + + function sort (a, b) { + try { + var alpha = a[key] + var beta = b[key] + + // reverse order + if (order === 'desc') { + alpha = b[key] + beta = a[key] + } + + // date or string + if (methods.isDate(alpha) && methods.isDate(beta)) { + return new Date(alpha) - new Date(beta) + } else { + return alpha.localeCompare(beta) + } + } catch (err) { + return 0 + } + } +} diff --git a/lib/index.js b/lib/template.js similarity index 51% rename from lib/index.js rename to lib/template.js index 11e67af..adc9da0 100644 --- a/lib/index.js +++ b/lib/template.js @@ -3,10 +3,10 @@ var objectKeys = require('object-keys') var methods = require('./methods') var utils = require('./utils') -module.exports = wrapper - -class Page { +module.exports = class NanoTemplate { constructor (state, value) { + var self = this + // defaults state = state || { } state.content = state.content || { } @@ -17,16 +17,14 @@ class Page { // private data this._state = state || { } this._value = value || { } - } - url () { - try { - // file or page - if (this._value.extension) return this._value.source || '' - else return this._value.url || '' - } catch (err) { - return '' - } + // add utilities + objectKeys(utils).forEach(function (key) { + self[key] = function (...args) { + this._value = utils[key](this._state, this._value, ...args) + return this + } + }) } keys (key) { @@ -38,16 +36,19 @@ class Page { } } + source () { + return this.value('source') + } + toArray (key) { return this.values(key) } - values (key) { - var obj = (typeof key !== 'undefined') ? this._value[key] : this._value - if (typeof obj === 'object') { - return objectValues(obj) || [ ] - } else { - return obj || [ ] + url () { + try { + return this._value.url || '' + } catch (err) { + return '' } } @@ -64,43 +65,13 @@ class Page { return undefined } } -} - -// dynamically add tools -objectKeys(utils).forEach(function (key) { - Page.prototype[key] = function (...args) { - this._value = utils[key](this._state, this._value, ...args) - return this - } -}) - -function wrapper (state) { - // defaults - state = state || { } - state.content = state.content || { } - - // compose and get on with it - return function (value) { - return new Page(state, parseValue(value)) - } - - function parseValue (value) { - // grab our content - var href = state.href || '/' - var page = state.content[methods.parseHref(href)] || { } - // set the value - switch (typeof value) { - case 'string': - // if passing a string assume we want a url - return utils.find(state, page, value) - case 'object': - // if an object and it contains value grab that value - if (typeof value.value === 'function') return value.value() - else return value - default: - // if no value, pass our page - return value || page + values (key) { + var obj = (typeof key !== 'undefined') ? this._value[key] : this._value + if (typeof obj === 'object') { + return objectValues(obj) || [ ] + } else { + return obj || [ ] } } } diff --git a/lib/utils.js b/lib/utils.js index ae70d1d..b609858 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,103 +4,50 @@ var resolve = require('resolve-path') var methods = require('./methods') module.exports = { - children, - file, - files, - find, first, - hasView, - images, + isFile, + isPage, isActive, // isFirst, // isLast, last, - pages, parent, shuffle, - sortBy, visible } -function children (state, value) { - return pages(state, value) -} - -function file (state, value, key) { - try { - return value.files[key] || { } - } catch (err) { - return { } - } -} - -function files (state, value) { - try { - return value.files || { } - } catch (err) { - return { } - } -} - -function find (state, value, url) { - try { - // normalize - url = methods.parseHref(url) - - // grab from root - if (url.indexOf('/') === 0) { - return state.content[url] - } - - // if on a page grab relative - if (typeof value.url === 'string') { - return state.content[resolve(value.url, url)] - } - - // fall back to href - return state.content[resolve(state.href || '/', url)] - } catch (err) { - return { } - } -} - function first (state, value) { try { - var obj = value || { } - return obj[objectKeys(obj)[0]] || { } + if (typeof value === 'object') { + var obj = value || { } + return obj[objectKeys(obj)[0]] || { } + } else { + return value[0] + } } catch (err) { return { } } } -function hasView (state, value) { +function isActive (state, value) { try { - return typeof this._value.view !== 'undefined' + return value.url === state.href } catch (err) { return false } } -function images (state, value) { +function isFile (state, value) { try { - return objectKeys(value.files || { }) - .reduce(function (res, cur) { - if ( - typeof value.files[cur] === 'object' && - value.files[cur].type === 'image' - ) { - res[cur] = value.files[cur] - } - return res - }, { }) + return methods.isFile(value) } catch (err) { - return { } + return false } } -function isActive (state, value) { +function isPage (state, value) { try { - return value.url === state.href + return methods.isPage(value) } catch (err) { return false } @@ -139,30 +86,16 @@ function isLast (state, value) { function last (state, value) { try { - var obj = value || { } - var keys = objectKeys(obj) - return obj[keys[keys.length - 1]] || { } - } catch (err) { - return { } - } -} - -function pages (state, value) { - try { - var base = value.url.replace(/^\//, '') ? value.url : '' - var regex = new RegExp('^' + base + '/[^/]+/?$') - return objectKeys(state.content) - .filter(key => regex.test(key.trim())) - .reduce(readPage, { }) + if (typeof value === 'object') { + var obj = value || { } + var keys = objectKeys(obj) + return obj[keys[keys.length - 1]] || { } + } else { + return value[value.length - 1] + } } catch (err) { return { } } - - function readPage (result, key) { - var page = state.content[key] - if (page) result[key] = page - return result - } } function parent (state, value) { @@ -194,39 +127,9 @@ function shuffle (state, value) { } } -function sortBy (state, value, key, order) { - try { - return objectValues(value).sort(sort) - } catch (err) { - return [ ] - } - - function sort (a, b) { - try { - var alpha = a[key] - var beta = b[key] - - // reverse order - if (order === 'desc') { - alpha = b[key] - beta = a[key] - } - - // date or string - if (isDate(alpha) && isDate(beta)) { - return new Date(alpha) - new Date(beta) - } else { - return alpha.localeCompare(beta) - } - } catch (err) { - return 0 - } - } -} - function visible (state, value) { try { - if (isPage(value)) { + if (methods.isPage(value)) { return value.visible !== false } else { return objectValues(value).filter(obj => obj.visible !== false) @@ -235,15 +138,3 @@ function visible (state, value) { return false } } - -function isPage (value) { - return ( - typeof value === 'object' && - typeof value.url === 'string' && - !value.extension - ) -} - -function isDate (str) { - return /^\d{4}-\d{1,2}-\d{1,2}$/.test(str) -} diff --git a/package.json b/package.json index b03a5c7..232dfc3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "nanopage", - "version": "0.2.0", + "version": "0.2.0-next2", "description": "handy utility methods for traversing content state", - "main": "lib/index.js", + "main": "index.js", "scripts": { - "test": "standard --fix && node test.js" + "test": "standard; node test.js" }, "repository": { "type": "git", @@ -20,8 +20,12 @@ }, "homepage": "https://github.com/jondashkyle/nanopage#readme", "dependencies": { + "is-glob": "^4.0.0", + "join-path": "^1.1.1", + "minimatch": "^3.0.4", "object-keys": "^1.0.11", "object-values": "^2.0.0", + "parse-filepath": "^1.0.2", "resolve-path": "^1.4.0", "xtend": "^4.0.1" }, diff --git a/readme.md b/readme.md index 1294a6f..77e11c5 100644 --- a/readme.md +++ b/readme.md @@ -19,14 +19,18 @@ var state = { title: 'Just a site', text: 'With some text', tags: ['and', 'some', 'tags'] + url: '/' }, '/example': { title: 'Example', text: 'Scope it out', - great: { - demo: 'Am I right?', - nah: 'Ok nbd.' - } + url: '/example' + }, + '/image.jpg': { + filename: 'image.jpg', + extension: 'jpg', + type: 'image', + url: '/content/image.jpg' } } } @@ -57,7 +61,7 @@ var lastTitle = page(last).value().title // or like this - End a query and return it’s value by calling `.value()` or `.v()` - Values can be reused in new queries by doing `page(oldQuery)` -Want to transform a directory of files and folders into flat content state? Try [Nanocontent](https://github.com/jondashkyle/nanocontent)! Super handy to use with [Enoki](https://github.com/enokidotsite/enoki) and [Choo](https://github.com/choojs/choo). If using Choo, you might [not even need it](#extra). +Want to transform a directory of files and folders into flat content state? Try [Nanocontent](https://github.com/jondashkyle/nanocontent)! Super handy to use with [Enoki](https://github.com/enokidotsite/enoki) and [Choo](https://github.com/choojs/choo) and the provided [plugin](#choo). ## Philosophy @@ -65,15 +69,51 @@ State is really handy. Especially global state. We ended up with state when weba After a while, we started using state for sites too. That is, sites that look more like sites and less like apps. Mostly because state is super handy. However, state can get really messy as sites get larger. Where do you store data? How do you reference it? If you have ten nested pages, do you have ten nested objects? -Instead of all of this complexity, lets reintroduce the URL. Each page url of your site is a key in a flat object. We can simply use the `window.location` to grab the data/content for the current page. Or, we can use any arbitrary url, like `/members/nelson`. +Instead of all of this complexity, lets reintroduce the URL. Each page url of your site is a key in a flat object. We can simply use the `window.location` to grab the data/content for the current page. Or, we can use any arbitrary url, like `/foo/bar`. This way of organizing state for sites as a flat object of page urls makes it super trivial to access content in your views and pass it down into components, or whatever. Ok cool! -## Extra +## Files + +Nanopage provides utilities for files via `file()` addition to `page()`. + +```js +var File = require('nanopage/file') + +var state = { + '/': { + title: 'Index', + url: '/' + }, + '/example.jpg': { + title: 'Example', + filename: 'example.jpg', + extension: 'jpg', + url: '/content/example.jpg' + } +} + +// instantiate the file +var file = new File(state) -Using Choo? Try the plugin! Don’t need all the bells and whistles? Try creating your own basic Choo plugin from scratch. +// grab some metadata +var example = file('/example.jpg').value() +``` + +## Globs -
Use the Choo plugin +Both the `page()` and `file()` methods also accept [globs](https://github.com/isaacs/minimatch) and relative urls to easily traverse directories and files. + +```js +var imagesParent = page('../*.jpg').toArray() +var imagesAll = page('*.jpg').toArray() +var parentTitle = page('../').value('title') +``` + + +## Choo + +Using Choo? Try the plugin! ```js var html = require('choo/html') @@ -84,102 +124,97 @@ app.use(require('nanopage/choo')) app.route('*', function (state, emit) { return html` - ${state.page().value('title')} + +

${state.page().value('title')}

+ + ` }) if (module.parent) module.exports = app else app.mount('body') ``` -
-
Basic vanilla Choo plugin +## Global utilities -```js -app.use(function (state, emitter) { - state.page = function (key) { - key = key || (state.href || '/') - return state.content[key] - } -}) -``` -
+These utilities work for both `page()` and `file()` -## Methods +#### `.first()` -#### `.children()` +Returns the first `page` or `file`. -Alias for `.pages()`. +#### `.isActive()` -#### `.file(filename)` +Is the current value active? Returns boolean. -Grab an individual file. For example, `.file('example.jpg')`. +#### `.isFile()` -#### `.files()` +Is the current value a file? Returns boolean. -Files of the current `page`. +#### `.isPage()` -#### `.find(href)` +Is the current value a page? Returns boolean. -Locate a `sub-page` of the `current page` based on the `href`. +#### `.last()` -#### `.first()` +Returns the last `page` or `file`. -Returns the first `page` or `file`. +#### `.parent()` -#### `.hasView()` +The parent of the current page. -Does the current page have a custom view? +#### `.toArray()` -#### `.images()` +Converts the values of an object to an array. Not chainable. -Images of the current page. +#### `.v()` -#### `.isActive()` +Alias for `.value()`. -Is the current page active? Returns boolean. +#### `.value()` -#### `.last()` +Return the current value. Not chainable. -Returns the last `page` or `file`. +#### `.visible()` -#### `.page()` +Returns if the current value key `visible` is not `false`. -The current page. +## Page utilities -#### `.pages()` +#### `.children()` -Sub-pages of the current page. +Alias for `.pages()`. -#### `.parent()` +#### `.file(url)` -The parent of the current page. +Locate a child file of the current page based on the `url`. -#### `.shuffle()` +#### `.files()` -Shuffle values of an array. +Child files of the current page. -#### `.sort()` +#### `.find(url)` -Sorts the current value’s `.pages` by `.order`. Formatting of `.order` follows the arguments of `.sortBy` separated by a space. For example, `date asc`. +Locate a child page of the current page based on the `url`. -#### `.sortBy(key, order)` +#### `.hasView()` -Sort the `files` or `pages` based by a certain key. Order can be either `asc` or `desc`. For example, `.sortBy('name', 'desc')` or `.sortBy('date', 'asc')`. +Does the current page have a custom view? -#### `.toArray()` +#### `.images()` -Converts the values of an object to an array. +Child images of the current page. -#### `.v()` +#### `.pages()` -Alias for `.value()`. +Child pages of the current page. -#### `.value()` +#### `.shuffle()` -Return the current value. Not chainable. +Shuffle values of an array. -#### `.visible()` +#### `.sortBy(key, order)` -Returns if the current value key `visible` is not `false`. - +Sort the `files` or `pages` based by a certain key. Order can be either `asc` or `desc`. For example, `.sortBy('name', 'desc')` or `.sortBy('date', 'asc')`. + +## File utilities \ No newline at end of file diff --git a/test.js b/test.js index 0a4fdb6..399ad1e 100644 --- a/test.js +++ b/test.js @@ -1,13 +1,25 @@ var test = require('tape') -var Page = require('./lib') +var Page = require('./lib/page') +var File = require('./lib/file') var page = new Page(createState()) +var file = new File(createState()) test('output', function (t) { t.ok(typeof page === 'function', 'outputs function') t.end() }) +test('page', function (t) { + var parentPage = new Page(createState()) + parentPage.href = '/example' + + t.ok(page('/example').value('title') === 'Example', 'select page by id') + t.ok(page('/example/*').keys()[0] === '/example/child', 'select page(s) by glob') + t.ok(page('../').value('title') === 'Index', 'select page by relative path') + t.end() +}) + test('value', function (t) { t.ok(typeof page().value() === 'object', 'value is a type object') t.ok(typeof page().v() === 'object', 'v alias for value') @@ -63,6 +75,15 @@ test('parent', function (t) { t.end() }) +test('file', function (t) { + t.ok(typeof file('/image.jpg').value() === 'object', 'value is type object') + t.ok(file('/image.jpg').value('url') === '/content/image.jpg', 'url exists') + t.ok(typeof file('/example/child/image.png').value() === 'object', 'deep value is type object') + t.ok(typeof page('/example/child').file('image.png').value() === 'object', 'page file value is type object') + t.ok(file('/image.jpg').size('500').v() === 'content/image-s500.jpg', 'file size') + t.end() +}) + function createState () { return { href: '/', @@ -86,11 +107,27 @@ function createState () { text: 'This is a test', url: '/example' }, + '/image.jpg': { + filename: 'image.jpg', + extension: '.jpg', + type: 'image', + url: '/content/image.jpg', + sizes: { + '100': 'content/image-s100.jpg', + '500': 'content/image-s500.jpg' + } + }, '/example/child': { title: 'Child', date: '2018-04-01', text: 'Look ma I’m nested', url: '/example/child' + }, + '/example/child/image.png': { + filename: 'image.png', + extension: '.png', + type: 'image', + url: '/content/example/child/image.png' } } }