From 7ddd3da4a15ff09cc7fa0708e6bca74e8d71a442 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 18:23:04 -0400 Subject: [PATCH 1/8] Add .eslintrc.json to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 01fb50d..a47ef31 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ node_modules test/fixtures/build/* # JetBrains project files -.idea \ No newline at end of file +.idea + +# Exclude ESLint config as long as it has not been officially adopted for project +.eslintrc.json From 150171c93d1813b66420e6cb63c6e5eeca0d03e8 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 18:24:13 -0400 Subject: [PATCH 2/8] Remove expected files from test fixtures --- test/fixtures/expected/no-pagination/topics/hello.html | 10 ---------- test/fixtures/expected/no-pagination/topics/tag.html | 8 -------- .../expected/no-pagination/topics/this-is.html | 4 ---- test/fixtures/expected/no-pagination/topics/this.html | 4 ---- test/fixtures/expected/no-pagination/topics/world.html | 4 ---- .../expected/pagination/topics/hello/2/index.html | 4 ---- .../expected/pagination/topics/hello/3/index.html | 4 ---- .../expected/pagination/topics/hello/4/index.html | 4 ---- .../expected/pagination/topics/hello/index.html | 4 ---- .../expected/pagination/topics/tag/2/index.html | 4 ---- .../expected/pagination/topics/tag/3/index.html | 4 ---- .../fixtures/expected/pagination/topics/tag/index.html | 4 ---- .../expected/pagination/topics/this-is/index.html | 4 ---- .../expected/pagination/topics/this/index.html | 4 ---- .../expected/pagination/topics/world/index.html | 4 ---- test/fixtures/tag.hbt | 4 ---- 16 files changed, 74 deletions(-) delete mode 100644 test/fixtures/expected/no-pagination/topics/hello.html delete mode 100644 test/fixtures/expected/no-pagination/topics/tag.html delete mode 100644 test/fixtures/expected/no-pagination/topics/this-is.html delete mode 100644 test/fixtures/expected/no-pagination/topics/this.html delete mode 100644 test/fixtures/expected/no-pagination/topics/world.html delete mode 100644 test/fixtures/expected/pagination/topics/hello/2/index.html delete mode 100644 test/fixtures/expected/pagination/topics/hello/3/index.html delete mode 100644 test/fixtures/expected/pagination/topics/hello/4/index.html delete mode 100644 test/fixtures/expected/pagination/topics/hello/index.html delete mode 100644 test/fixtures/expected/pagination/topics/tag/2/index.html delete mode 100644 test/fixtures/expected/pagination/topics/tag/3/index.html delete mode 100644 test/fixtures/expected/pagination/topics/tag/index.html delete mode 100644 test/fixtures/expected/pagination/topics/this-is/index.html delete mode 100644 test/fixtures/expected/pagination/topics/this/index.html delete mode 100644 test/fixtures/expected/pagination/topics/world/index.html delete mode 100644 test/fixtures/tag.hbt diff --git a/test/fixtures/expected/no-pagination/topics/hello.html b/test/fixtures/expected/no-pagination/topics/hello.html deleted file mode 100644 index 3d3cbbb..0000000 --- a/test/fixtures/expected/no-pagination/topics/hello.html +++ /dev/null @@ -1,10 +0,0 @@ -

Posts for hello

- \ No newline at end of file diff --git a/test/fixtures/expected/no-pagination/topics/tag.html b/test/fixtures/expected/no-pagination/topics/tag.html deleted file mode 100644 index 7dbc3ea..0000000 --- a/test/fixtures/expected/no-pagination/topics/tag.html +++ /dev/null @@ -1,8 +0,0 @@ -

Posts for tag

- \ No newline at end of file diff --git a/test/fixtures/expected/no-pagination/topics/this-is.html b/test/fixtures/expected/no-pagination/topics/this-is.html deleted file mode 100644 index 0ac07f6..0000000 --- a/test/fixtures/expected/no-pagination/topics/this-is.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for this is

- \ No newline at end of file diff --git a/test/fixtures/expected/no-pagination/topics/this.html b/test/fixtures/expected/no-pagination/topics/this.html deleted file mode 100644 index 1a29c48..0000000 --- a/test/fixtures/expected/no-pagination/topics/this.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for this

- \ No newline at end of file diff --git a/test/fixtures/expected/no-pagination/topics/world.html b/test/fixtures/expected/no-pagination/topics/world.html deleted file mode 100644 index 5d211a4..0000000 --- a/test/fixtures/expected/no-pagination/topics/world.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for world

- \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/hello/2/index.html b/test/fixtures/expected/pagination/topics/hello/2/index.html deleted file mode 100644 index 91fe865..0000000 --- a/test/fixtures/expected/pagination/topics/hello/2/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for hello

-PreviousNext \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/hello/3/index.html b/test/fixtures/expected/pagination/topics/hello/3/index.html deleted file mode 100644 index 89c7b94..0000000 --- a/test/fixtures/expected/pagination/topics/hello/3/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for hello

-PreviousNext \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/hello/4/index.html b/test/fixtures/expected/pagination/topics/hello/4/index.html deleted file mode 100644 index e0b21f4..0000000 --- a/test/fixtures/expected/pagination/topics/hello/4/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for hello

-Previous \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/hello/index.html b/test/fixtures/expected/pagination/topics/hello/index.html deleted file mode 100644 index cf13e5d..0000000 --- a/test/fixtures/expected/pagination/topics/hello/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for hello

-Next \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/tag/2/index.html b/test/fixtures/expected/pagination/topics/tag/2/index.html deleted file mode 100644 index 5251ba3..0000000 --- a/test/fixtures/expected/pagination/topics/tag/2/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for tag

-PreviousNext \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/tag/3/index.html b/test/fixtures/expected/pagination/topics/tag/3/index.html deleted file mode 100644 index 57dcc31..0000000 --- a/test/fixtures/expected/pagination/topics/tag/3/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for tag

-Previous \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/tag/index.html b/test/fixtures/expected/pagination/topics/tag/index.html deleted file mode 100644 index 1bab27a..0000000 --- a/test/fixtures/expected/pagination/topics/tag/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for tag

-Next \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/this-is/index.html b/test/fixtures/expected/pagination/topics/this-is/index.html deleted file mode 100644 index 0ac07f6..0000000 --- a/test/fixtures/expected/pagination/topics/this-is/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for this is

- \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/this/index.html b/test/fixtures/expected/pagination/topics/this/index.html deleted file mode 100644 index 1a29c48..0000000 --- a/test/fixtures/expected/pagination/topics/this/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for this

- \ No newline at end of file diff --git a/test/fixtures/expected/pagination/topics/world/index.html b/test/fixtures/expected/pagination/topics/world/index.html deleted file mode 100644 index 5d211a4..0000000 --- a/test/fixtures/expected/pagination/topics/world/index.html +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for world

- \ No newline at end of file diff --git a/test/fixtures/tag.hbt b/test/fixtures/tag.hbt deleted file mode 100644 index f1bc4a1..0000000 --- a/test/fixtures/tag.hbt +++ /dev/null @@ -1,4 +0,0 @@ -

Posts for {{tag}}

-{{#if pagination.previous}}Previous{{/if}}{{#if pagination.next}}Next{{/if}} \ No newline at end of file From a63e578993ebe3825af914f50201c42490733482 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 18:25:10 -0400 Subject: [PATCH 3/8] Test fixtures do not require content for tests --- test/fixtures/src/about.html | 4 +--- test/fixtures/src/index.html | 4 +--- test/fixtures/src/json.html | 5 +---- test/fixtures/src/page.html | 4 +--- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/test/fixtures/src/about.html b/test/fixtures/src/about.html index dcad0e2..af34377 100644 --- a/test/fixtures/src/about.html +++ b/test/fixtures/src/about.html @@ -1,7 +1,5 @@ --- title: about tags: hello -date: 2014-02-02T17:39:06.157Z +date: 2016-01-01 --- - -Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt odio provident at, cum, quasi perferendis suscipit aliquam dolores alias sequi vel ullam! Corrupti libero aliquid, tempora esse cupiditate impedit consectetur. diff --git a/test/fixtures/src/index.html b/test/fixtures/src/index.html index 835fa91..6a1b852 100644 --- a/test/fixtures/src/index.html +++ b/test/fixtures/src/index.html @@ -1,7 +1,5 @@ --- title: test tags: hello, world, this is, tag -date: 2014-03-02T17:39:06.157Z +date: 2015-01-01 --- - -Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt odio provident at, cum, quasi perferendis suscipit aliquam dolores alias sequi vel ullam! Corrupti libero aliquid, tempora esse cupiditate impedit consectetur. \ No newline at end of file diff --git a/test/fixtures/src/json.html b/test/fixtures/src/json.html index 9176a73..ae60b91 100644 --- a/test/fixtures/src/json.html +++ b/test/fixtures/src/json.html @@ -2,9 +2,6 @@ { "title": "test json", "tags": ["hello", "tag"], - "date": "2014-01-07T17:39:06.157Z" + "date": "2014-01-07" } --- - -Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt odio provident at, cum, quasi perferendis suscipit aliquam dolores alias sequi vel ullam! Corrupti libero aliquid, tempora esse cupiditate impedit consectetur. - diff --git a/test/fixtures/src/page.html b/test/fixtures/src/page.html index f81163d..1c5650f 100644 --- a/test/fixtures/src/page.html +++ b/test/fixtures/src/page.html @@ -1,7 +1,5 @@ --- title: test page 2 tags: hello, this, tag -date: 2014-02-07T17:39:06.157Z +date: 2014-01-01 --- - -Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deserunt odio provident at, cum, quasi perferendis suscipit aliquam dolores alias sequi vel ullam! Corrupti libero aliquid, tempora esse cupiditate impedit consectetur. \ No newline at end of file From 00f21ab7a2e4ec5acadc794aa000ab9ceaf77db7 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 18:28:44 -0400 Subject: [PATCH 4/8] Use self-explanatory variable names and do not clone metadata --- lib/index.js | 234 +++++++++++++++++++-------------------------------- package.json | 4 +- 2 files changed, 88 insertions(+), 150 deletions(-) diff --git a/lib/index.js b/lib/index.js index b7d640d..07f5761 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,210 +1,146 @@ - +var _ = require('lodash'); var slug = require('slug'); +var sortObj = require('sort-object'); /** - * A metalsmith plugin to create dedicated pages for tags in posts or pages. + * A metalsmith plugin to create tag pages for tagged pages (usually posts). * * @return {Function} */ -function plugin(opts) { - /** - * Holds a mapping of tag names to an array of files with that tag. - * @type {Object} - */ - var tagList = {}; - +module.exports = function plugin(opts) { opts = opts || {}; - opts.path = opts.path || 'tags/:tag/index.html'; + opts.path = opts.path || 'tags/:tag/index.html'; // default accomodates pagination opts.pathPage = opts.pathPage || 'tags/:tag/:num/index.html'; - opts.layout = opts.layout || 'partials/tag.hbt'; opts.handle = opts.handle || 'tags'; - var handleUrlSafe = opts.handle + 'UrlSafe'; opts.metadataKey = opts.metadataKey || 'tags'; opts.sortBy = opts.sortBy || 'title'; opts.reverse = opts.reverse || false; - opts.perPage = opts.perPage || 0; - opts.skipMetadata = opts.skipMetadata || false; + opts.perPage = opts.perPage || 0; opts.slug = opts.slug || {mode: 'rfc3986'}; - return function(files, metalsmith, done) { - /** - * Get a safe tag - * @param {string} a tag name - * @return {string} safe tag - */ - function safeTag(tag) { - if (typeof opts.slug === 'function') { - return opts.slug(tag); - } - - return slug(tag, opts.slug); + /** + * Get a safe tag + * @param {string} a tag name + * @return {string} safe tag + */ + function safeTag(tag) { + if (typeof opts.slug === 'function') { + return opts.slug(tag); } - /** - * Sort tags by property given in opts.sortBy. - * @param {Object} a Post object. - * @param {Object} b Post object. - * @return {number} sort value. - */ - function sortBy(a, b) { - a = a[opts.sortBy]; - b = b[opts.sortBy]; - if (!a && !b) { - return 0; - } - if (!a) { - return -1; - } - if (!b) { - return 1; - } - if (b > a) { - return -1; - } - if (a > b) { - return 1; - } - return 0; - } + return slug(tag, opts.slug); + } - function getFilePath(path, opts) { - return path - .replace(/:num/g, opts.num) - .replace(/:tag/g, safeTag(opts.tag)); - } + function getFilePath(path, opts) { + return path + .replace(/:num/g, opts.num) + .replace(/:tag/g, safeTag(opts.tag)); + } - // Find all tags and their associated files. - // Using a for-loop so we don't incur the cost of creating a large array - // of file names that we use to loop over the files object. + return function(files, metalsmith, done) { + var metadata = metalsmith.metadata(); + var allTags = {}; // map tag names to array of tagged pages + + // Check all files in files object for tags. for (var fileName in files) { - var data = files[fileName]; - if (!data) { + var file = files[fileName]; + if (!file) { continue; } - var tagsData = data[opts.handle]; - - // If we have tag data for this file then turn it into an array of - // individual tags where each tag has been sanitized. - if (tagsData) { - // Convert data into array. - if (typeof tagsData === 'string') { - tagsData = tagsData.split(','); - } - - // Re-initialize tag array. - data[opts.handle] = []; - - // Add url safe version of every tag as well. - data[handleUrlSafe] = []; + var fileTags = file[opts.handle]; - tagsData.forEach(function(rawTag) { - // Trim leading + trailing white space from tag. - var tag = String(rawTag).trim(); + // If tags for this file are defined, convert them into array of tags. + if (fileTags && typeof fileTags === 'string') { + // TODO in current implementation page tags need to be a comma separated string. + // They should be defined as array. + fileTags = fileTags.split(','); - // Save formatted tag data. - data[opts.handle].push(tag); + // Re-initialize tags as array. + file[opts.handle] = []; - // Save url safe formatted tag data. - data[handleUrlSafe].push(safeTag(tag)); + fileTags.forEach(function(rawTag) { + var tag = rawTag.trim(); + file[opts.handle].push({ + name: tag, + urlSafe: safeTag(tag) + }); - // Add each tag to our overall tagList and initialize array if it - // doesn't exist. - if (!tagList[tag]) { - tagList[tag] = []; + // Add file tag to allTags and initialize array of tagged pages. + if (!allTags[tag]) { + allTags[tag] = []; } - // Store a reference to where the file data exists to reduce our - // overhead. - tagList[tag].push(fileName); + // Store reference to tagged file. + allTags[tag].push(file); }); + + // sort file tags by name + file[opts.handle] = _.sortBy(file[opts.handle], 'name'); } } - // Add to metalsmith.metadata for access outside of the tag files. - if (!opts.skipMetadata) { - var metadata = metalsmith.metadata(); - metadata[opts.metadataKey] = metadata[opts.metadataKey] || {}; - } + // At his point object properties (tags) in allTags are not sorted. + // Sort properties. + allTags = sortObj(allTags); - for (var tag in tagList) { - // Map the array of tagList names back to the actual data object. - // Sort tags via opts.sortBy property value. - var posts = tagList[tag].map(function(fileName) { - return files[fileName]; - }).sort(sortBy); + // At this point the array of files for each tag in allTags is not sorted. + // Sort by property defined in opts.sortBy. + for (var tag in allTags) { + allTags[tag] = _.sortBy(allTags[tag], opts.sortBy); // Reverse posts if desired. - if (opts.reverse) { - posts.reverse(); - } + if (opts.reverse) allTags[tag].reverse(); - if (!opts.skipMetadata) { - metadata[opts.metadataKey][tag] = posts; - metadata[opts.metadataKey][tag].urlSafe = safeTag(tag); - } + // Add urlSafe property + allTags[tag].urlSafe = safeTag(tag); - // If we set opts.perPage to 0 then we don't want to paginate and as such - // we should have all posts shown on one page. - var postsPerPage = opts.perPage === 0 ? posts.length : opts.perPage; - var numPages = Math.ceil(posts.length / postsPerPage); - var pages = []; + // Compute paginations: create 1 or more tag pages for current tag. - for (var i = 0; i < numPages; i++) { - var pageFiles = posts.slice(i * postsPerPage, (i + 1) * postsPerPage); + // If opts.perPage === 0 create only 1 page with all posts. + var perTagPage = opts.perPage === 0 ? allTags[tag].length : opts.perPage; + var numTagPages = Math.ceil(allTags[tag].length / perTagPage); + var tagPages = []; - // Generate a new file based on the filename with correct metadata. - var page = { + for (var i = 0; i < numTagPages; i++) { + var tagPageFiles = allTags[tag].slice(i * perTagPage, (i + 1) * perTagPage); + + // New paginated tag page. + var tagPage = { layout: opts.layout, - // TODO: remove this property when metalsmith-templates usage - // declines. - template: opts.template, contents: '', - tag: tag, pagination: { - num: i + 1, - pages: pages, + num: i + 1, // current page number tag: tag, - files: pageFiles + pages: tagPages, // all tag pages for current tag + files: tagPageFiles } }; // Render the non-first pages differently to the rest, when set. if (i > 0 && opts.pathPage) { - page.path = getFilePath(opts.pathPage, page.pagination); + tagPage.path = getFilePath(opts.pathPage, tagPage.pagination); } else { - page.path = getFilePath(opts.path, page.pagination); + tagPage.path = getFilePath(opts.path, tagPage.pagination); } - // Add new page to files object. - files[page.path] = page; + // Add tag page to files. + files[tagPage.path] = tagPage; // Update next/prev references. - var previousPage = pages[i - 1]; - if (previousPage) { - page.pagination.previous = previousPage; - previousPage.pagination.next = page; + var prevTagPage = tagPages[i - 1]; + if (prevTagPage) { + tagPage.pagination.previous = prevTagPage; + prevTagPage.pagination.next = tagPage; } - pages.push(page); + tagPages.push(tagPage); } } - // update metadata - if (!opts.skipMetadata) { - metalsmith.metadata(metadata); - } + // Add allTags to metadata + metadata[opts.metadataKey] = allTags; - /* clearing this after each pass avoids - * double counting when using metalsmith-watch - */ - tagList = {}; done(); - }; -} - -/** - * Expose `plugin`. - */ -module.exports = plugin; +}; diff --git a/package.json b/package.json index 5f5bd43..818a418 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "moment": "^2.10.3" }, "dependencies": { - "slug": "^0.9.1" + "lodash": "^4.15.0", + "slug": "^0.9.1", + "sort-object": "^3.0.2" } } From 32a83ca59190fa5e744b9fa560d8a4d40ff2750a Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Sat, 3 Sep 2016 22:13:39 -0400 Subject: [PATCH 5/8] Cleanup tests and test with files object instead of generated pages --- package.json | 11 +- test/index.js | 343 +++++++++++++++++++++++++++++++------------------- 2 files changed, 220 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index 818a418..cb67310 100644 --- a/package.json +++ b/package.json @@ -16,18 +16,17 @@ "tags" ], "author": "Toto Tvalavadze ", + "contributors": [ + "Thilo Maier " + ], "license": "MIT", "bugs": { "url": "https://github.com/totocaster/metalsmith-tags/issues" }, "homepage": "https://github.com/totocaster/metalsmith-tags", "devDependencies": { - "assert-dir-equal": "1.0.1", - "handlebars": "*", - "metalsmith": "1.x", - "metalsmith-layouts": "1.x", - "mocha": "2.x", - "moment": "^2.10.3" + "metalsmith": "^2.2.0", + "mocha": "^3.0.2" }, "dependencies": { "lodash": "^4.15.0", diff --git a/test/index.js b/test/index.js index 7ec39be..aa1100a 100644 --- a/test/index.js +++ b/test/index.js @@ -1,166 +1,253 @@ var assert = require('assert'); -var equal = require('assert-dir-equal'); var Metalsmith = require('metalsmith'); -var layouts = require('metalsmith-layouts'); var tags = require('../lib'); -var Handlebars = require('handlebars'); -var moment = require('moment'); -var slug = require('slug'); - -Handlebars.registerHelper('dateFormat', function(context, format) { - var f = format || 'DD/MM/YYYY'; - return moment(new Date(context)).format(f); -}); describe('metalsmith-tags', function() { - - it('should modify comma separated tags into dehumanized array', function(done) { - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path:'topics' - })) - .build(function(err,files){ + describe('default behavior', function() { + it('should convert comma separated page tags into sorted tag objects array', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags()); + builder.build(function(err, files) { if (err) return done(err); - assert.equal(files['index.html'].tags.toString(),['hello', 'world', 'this is', 'tag'].toString()); - assert.equal(files['index.html'].tagsUrlSafe.toString(),['hello', 'world', 'this-is', 'tag'].toString()); + assert.deepEqual(files['index.html'].tags, [ + { name: 'hello', urlSafe: 'hello' }, + { name: 'tag', urlSafe: 'tag' }, + { name: 'this is', urlSafe: 'this-is' }, + { name: 'world', urlSafe: 'world' } + ]); done(); }); - }); + }); - it('should create a tags property to metalsmith.metadata', function(done) { - var tagList; + it('should add tags object to metadata with tags sorted in ascending order', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags()); + builder.build(function(err, files) { + if (err) return done(err); + var tags = builder.metadata().tags; - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics' - })) - .use(function(files, metalsmith, done) { - tagList = metalsmith.metadata().tags; + // check tags are sorted in ascending order + assert.deepEqual(Object.keys(tags), ['hello', 'tag', 'this', 'this is', 'world']); done(); - }) - .build(function(err, files){ + }); + }); + + it('should sort tagged files by title ascending', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags()); + builder.build(function(err, files) { if (err) return done(err); - var tagListKeys = Object.keys(tagList).sort(); - assert.deepEqual(tagListKeys, ['hello', 'tag', 'this', 'this is', 'world']); - // Ensure every object in the metadata tags array is a data object. - tagListKeys.forEach(function(tagName) { - var tagPostsArray = tagList[tagName]; - tagPostsArray.forEach(function(fileData) { - assert.equal(typeof fileData, 'object'); - assert.ok(fileData.stats); - assert.ok(fileData.contents); - assert.ok(fileData.tags); - }); - }); + var tags = builder.metadata().tags; + + // check sort order for tagged files for 'hello' tag + // title from json.html not included since it defines tags as array instead of comma separated string + assert.deepEqual(tags['hello'].map(function(file) { return file.title; }), [ + 'about', + 'test', + 'test page 2' + ]); + + // check sort order for tagged files for 'tag' tag + // title from json.html not included since it defines tags as array instead of comma separated string + assert.deepEqual(tags['tag'].map(function(file) { return file.title; }), [ + 'test', + 'test page 2' + ]); + + // check sort order for tagged files for 'this' tag + assert.deepEqual(tags['this'].map(function(file) { return file.title; }), [ + 'test page 2' + ]); + + // check sort order for tagged files for 'this is' tag + assert.deepEqual(tags['this is'].map(function(file) { return file.title; }), [ + 'test' + ]); + + // check sort order for tagged files for 'world' tag + assert.deepEqual(tags['world'].map(function(file) { return file.title; }), [ + 'test' + ]); + done(); }); - }); + }); + + it('should add property \'urlSafe\' to each tag in tag object', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags()); + builder.build(function(err, files) { + if (err) return done(err); + var tags = builder.metadata().tags; - it('should add a urlSafe property to each tag post list', function(done) { - var tagList; + // check urlSafe property for all tags + assert.equal(tags['hello'].urlSafe, 'hello'); + assert.equal(tags['tag'].urlSafe, 'tag'); + assert.equal(tags['this'].urlSafe, 'this'); + assert.equal(tags['this is'].urlSafe, 'this-is'); + assert.equal(tags['world'].urlSafe, 'world'); - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics' - })) - .use(function(files, metalsmith, done) { - tagList = metalsmith.metadata().tags; done(); - }) - .build(function(err, files){ + }); + }); + + it('should create tag pages with tagged pages sorted asc in page metadata', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags()); + builder.build(function(err, files) { if (err) return done(err); - var tagListKeys = Object.keys(tagList).sort(); - assert.deepEqual(tagListKeys, ['hello', 'tag', 'this', 'this is', 'world']); - // Ensure every object in the metadata tags array is a data object. - tagListKeys.forEach(function(tagName) { - var tagPostsArray = tagList[tagName]; - assert.ok(tagList[tagName].urlSafe); - assert.equal(typeof tagList[tagName].urlSafe, 'string'); - assert.equal(slug(tagName), tagList[tagName].urlSafe); - }); + + // check tag page for 'hello' tag + var taggedFiles = files['tags/hello/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'about', + 'test', + 'test page 2' + ]); + + // check tag page for `tag` tag + taggedFiles = files['tags/tag/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test', + 'test page 2' + ]); + + // check tag page for `this` tag + taggedFiles = files['tags/this/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test page 2' + ]); + + // check tag page for 'this is' tag + taggedFiles = files['tags/this-is/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test' + ]); + + // check tag page for 'world' tag + taggedFiles = files['tags/world/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test' + ]); + done(); }); - }); + }); - it('should skip creating a tags property on metalsmith.metadata', function(done) { - var tagList; - - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics', - skipMetadata: true - })) - .use(function(files, metalsmith, done) { - tagList = metalsmith.metadata().tags; - done(); - }) - .build(function(err, files){ + it('should create paginated tag pages', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags({ + path: 'topics/:tag/index.html', + pathPage: 'topics/:tag/:num/index.html', + perPage: 2 + })); + builder.build(function(err, files) { if (err) return done(err); - assert.equal(typeof tagList, 'undefined'); + + // check tag pages for 'hello' tag + + // first tag page + var taggedFiles = files['topics/hello/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'about', + 'test' + ]); + + // second tag page + taggedFiles = files['topics/hello/2/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test page 2' + ]); + + // check tag pages for 'tag' tag + + // only one tag page + taggedFiles = files['topics/tag/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test', + 'test page 2' + ]); + + // check tag pages for 'this' tag + + // only one tag page + taggedFiles = files['topics/this/index.html'].pagination.files; + assert.deepEqual(taggedFiles.map(function(file) { return file.title; }), [ + 'test page 2' + ]); + done(); }); + }); }); - var templateConfig = { - engine: 'handlebars', - directory: './', - pattern: "topics/**/*.html" - }; - - it('should create tag page with post lists according to template and sorted by date decreasing', function(done) { - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics/:tag.html', - layout: './tag.hbt', - sortBy: 'date', - reverse: true - })) - .use(layouts(templateConfig)) - .build(function(err){ + describe('configuration options', function() { + it('should sort tagged files by date ascending', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags({ + sortBy: 'date' + })); + builder.build(function(err, files) { if (err) return done(err); - equal('test/fixtures/expected/no-pagination/topics', 'test/fixtures/build/topics'); + var tags = builder.metadata().tags; + + assert.deepEqual(tags['hello'].map(function(file) { return file.title; }), [ + 'test page 2', + 'test', + 'about' + ]); + + assert.deepEqual(tags['tag'].map(function(file) { return file.title; }), [ + 'test page 2', + 'test' + ]); + done(); }); - }); + }); - it('should create tag pages with pagination with post lists according to template and sorted by date decreasing', function(done) { - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics/:tag/index.html', - pathPage: 'topics/:tag/:num/index.html', - perPage: 1, - layout: './tag.hbt', - sortBy: 'date', - reverse: true - })) - .use(layouts(templateConfig)) - .build(function(err){ + it('should sort tagged files by date descending', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags({ + reverse: true, + sortBy: 'date' + })); + builder.build(function(err, files) { if (err) return done(err); - equal('test/fixtures/expected/pagination/topics', 'test/fixtures/build/topics'); + var tags = builder.metadata().tags; + + assert.deepEqual(tags['hello'].map(function(file) { return file.title; }), [ + 'about', + 'test', + 'test page 2' + ]); + + assert.deepEqual(tags['tag'].map(function(file) { return file.title; }), [ + 'test', + 'test page 2' + ]); + done(); }); - }); + }); - it('should support custom slug functions', function(done) { - Metalsmith('test/fixtures') - .use(tags({ - handle: 'tags', - path: 'topics', - slug: function(tag) { - return tag.toUpperCase(); - } - })) - .build(function(err, files) { + it('should support a custom slug function', function(done) { + var builder = Metalsmith('test/fixtures'); + builder.use(tags({ + slug: function(tag) { return tag.toUpperCase(); } + })); + builder.build(function(err, files) { if (err) return done(err); - assert.equal(files['index.html'].tags.toString(),['hello', 'world', 'this is', 'tag'].toString()); - assert.equal(files['index.html'].tagsUrlSafe.toString(),['HELLO', 'WORLD', 'THIS IS', 'TAG'].toString()); + + assert.deepEqual(files['index.html'].tags, [ + { name: 'hello', urlSafe: 'HELLO' }, + { name: 'tag', urlSafe: 'TAG' }, + { name: 'this is', urlSafe: 'THIS IS' }, + { name: 'world', urlSafe: 'WORLD' } + ]); + done(); }); - }) + }); + }); }); From f648eb37efafd042b643c5f18c1ba7389e385fb3 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 19:00:37 -0400 Subject: [PATCH 6/8] Describe better what plugin does --- README.md | 136 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index be926be..d7081f5 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,52 @@ # metalsmith-tags - A metalsmith plugin to create dedicated pages for tags in provided in metalsmith pages. + A metalsmith plugin to add tag pages to Metalsmith's `files` object. - **NOTE**: looking for new maintainers of this project. please consult [this issue for discussion.](https://github.com/totocaster/metalsmith-tags/issues/26) + **NOTE**: We are looking for new maintainers for this project. Please consult [this issue for discussion.](https://github.com/totocaster/metalsmith-tags/issues/26) ## Installation - $ npm install metalsmith-tags + $ npm install --save metalsmith-tags -## Description in Pages +## How to tag pages - In your pages: +Add your tags to the frontmatter as comma separated strings using the `tags` handle: ``` --- -title: This is page with tags +title: This is a tagged page tags: tagged, page, metalsmith, plugin --- Hello World ``` -You can use different handle for the tags, by configuring the `handle` option. `tags` is the default. +You can configure a different tags handle with the `handle` option. +This plugin currently does not support tagging using arrays. With a YAML frontmatter tags defined like this -## CLI Usage +``` +--- +tags: + - tag 1 + - tag 2 + - tag 3 +--- +``` + +are ignored. Likewise, tags defined with an array in a JSON frontmatter like this + +``` +--- +{ + "tags": ["tag 1", "tag 2", "tag 2"], +} +--- +``` - Install the node modules and then add the `metalsmith-tags` key to your `metalsmith.json` plugins. The simplest use case just requires tag handle you want to use: +are ignored as well. + +## JSON configuration ```json { @@ -35,12 +55,8 @@ You can use different handle for the tags, by configuring the `handle` option. ` "handle": "tags", "path": "topics/:tag.html", "layout": "/partials/tag.hbt", - /* Can also use deprecated template property. - "template": "/partials/tag.hbt", - */ "sortBy": "date", "reverse": true, - "skipMetadata": false, "slug": { "mode": "rfc3986" } @@ -49,33 +65,23 @@ You can use different handle for the tags, by configuring the `handle` option. ` } ``` -## JavaScript Usage - - Pass the plugin to `Metalsmith#use`: +## Programmatic configuration ```js var tags = require('metalsmith-tags'); metalsmith .use(tags({ - // yaml key for tag list in you pages + // YAML key for tag list in you pages handle: 'tags', // path for result pages path:'topics/:tag.html', // layout to use for tag listing layout:'/partials/tag.hbt', - // Can also use `template` property for use with the (deprecated) - // metalsmith-templates plugin. The `template` property is deprecated here - // as well but still available for use. - // template:'/partials/tag.hbt', // provide posts sorted by 'date' (optional) sortBy: 'date', // sort direction (optional) reverse: true, - // skip updating metalsmith's metadata object. - // useful for improving performance on large blogs - // (optional) - skipMetadata: false, // Any options you want to pass to the [slug](https://github.com/dodo/node-slug) package. // Can also supply a custom slug function. // slug: function(tag) { return tag.toLowerCase() } @@ -83,53 +89,73 @@ metalsmith })); ``` -## Result +## Metadata - This will generate `topics/[tagname].html` pages in your `build` directory with array of `pagination.files` objects on which you can iterate on. You can use `tag` for tag name in your layouts. (You can refer to tests folder for tags layout.) +This plugin alters the following metadata. - The `tags` property on your pages will remain but it will be modified to an array of String containing the tags. +### Page metadata - There will also be a `tagsUrlSafe` array created that will contain an array of url safe tag names for use in url creation. +The `tags` handle on tagged pages is converted into an array of sorted tag objects. The following tags - You can use `metalsmith-permalink` to customize the permalink of the tag pages as you would do with anything else. +``` +tags: tag, another tag, yet another tag +``` - It is possible to use `opts.metadataKey` for defining the name of the global tag list. - By default it is `'tags'`. +results in -## Pagination +``` +tags: [ + { name: 'another tag', urlSafe: 'another-tag' }, + { name: 'tag', urlSafe: 'tag' }, + { name: 'yet another tag', 'yet-another-tag' } +] +``` - Additionally you can paginate your tag pages. To do so add two additional properties to your configuration object, `pathPage` and `perPage`, and modify `path` to point to the root pagination location: +You can use the resulting `tags` array to create links to tag pages. -```json +### Global tags object + +This plugin adds object `tags` with all tags to the global metadata. `tags` has the following structure: + +``` { - "handle": "tags", - "path": "topics/:tag/index.html", - "pathPage": "topics/:tag/:num/index.html", - "perPage": 6, - "layout": "/partials/tag.hbt", - /* Can also use deprecated template property. - "template": "/partials/tag.hbt", - */ - "sortBy": "date", - "reverse": true, - "skipMetadata": false, - "slug": { - "mode": "rfc3986" - } + tag 1: [page objects of pages tagged with tag 1], + tag 2: [page objects of pages tagged with tag 2], + ... } ``` - This will paginate your array of tags so that 6 appear per page, with additional tag pages being nested underneath the first page of tags. For additional details please look at the tests. +Page objects are references to page objects in Metalsmith's `files`. You can access the `urlSafe` version like this: + +``` +tags['tag 1'].urlSafe +``` + +Note: the global `tags` object is not accessible on pages that have a `tags` handle. You can use `opts.metadataKey` to rename global tags object. + +## Pagination + +Tag pages can be paginated. Pagination requires the following pagination properties in the plugin configuration: + +- `path` should be set to the location of the first pagination page, e.g. `tags/:tag/index.html`. +- `pathPage` should be set to the location of following pagination pages, e.g. `tags/:tag/:num/index.html`. +- `perPage` indicated the number of tagged files per page. + +The default for `perPage` is `0`, which turns off pagination. This means that only one tag page per tag is generated. + +Pagination does two things: + +1. It adds the pagination pages to `files` so subsequent plugins such as [`metalsmith-layouts`](https://github.com/superwolff/metalsmith-layouts) can process them. +2. It adds a `pagination` object to each generated page. You can access assigned files via `pagination.files`, the previous pagination page via `pagination.previous` and the following pagination page via `pagination.next`. ## Contribution - Feel free to contribute to this plug-in. Fork, commit, send pull request. - Issues, suggestions and bugs are more than welcome. +Feel free to contribute to this plug-in. Fork, commit, send pull request. Issues, suggestions and bugs are more than welcome. Please make sure that `npm test` passes before submitting a pull request. - In case you add functionality, please write corresponding test. Test using `npm test`. +In case you add functionality, please write corresponding tests. - Thanks! +Thanks! ## License - MIT +MIT From b0cd48a26905dd86d9956f9b4c131073f5d373eb Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 21:28:04 -0400 Subject: [PATCH 7/8] Make data structures for page tags and global tags compatible --- README.md | 19 +++++++++++++------ lib/index.js | 11 ++++------- test/index.js | 24 ++++++++++++------------ 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d7081f5..0635cd1 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,21 @@ tags: tag, another tag, yet another tag results in ``` -tags: [ - { name: 'another tag', urlSafe: 'another-tag' }, - { name: 'tag', urlSafe: 'tag' }, - { name: 'yet another tag', 'yet-another-tag' } -] +tags: { + 'another tag': { urlSafe: 'another-tag' }, + tag: { urlSafe: 'tag' }, + 'yet another tag': { urlSafe: 'yet-another-tag' } +} ``` -You can use the resulting `tags` array to create links to tag pages. +The structure of the converted page tags is compatible with the structure in the global `tags` object and ensures that you can reuse layout partials no matter if they display page tags or all tags from the global tags object. + +In a [Pug](https://pugjs.org/) partial, for example, you can access `tags` like this + + each obj, tag in tags + a(href=`${site.baseUrl}tags/${obj.urlSafe}/`)= tag + +and `tags` can be the page tags or the global `tags`. ### Global tags object diff --git a/lib/index.js b/lib/index.js index 07f5761..e555b32 100644 --- a/lib/index.js +++ b/lib/index.js @@ -56,15 +56,12 @@ module.exports = function plugin(opts) { // They should be defined as array. fileTags = fileTags.split(','); - // Re-initialize tags as array. - file[opts.handle] = []; + // Re-initialize tags handle as object. + file[opts.handle] = {}; fileTags.forEach(function(rawTag) { var tag = rawTag.trim(); - file[opts.handle].push({ - name: tag, - urlSafe: safeTag(tag) - }); + file[opts.handle][tag] = { urlSafe: safeTag(tag) }; // Add file tag to allTags and initialize array of tagged pages. if (!allTags[tag]) { @@ -76,7 +73,7 @@ module.exports = function plugin(opts) { }); // sort file tags by name - file[opts.handle] = _.sortBy(file[opts.handle], 'name'); + file[opts.handle] = sortObj(file[opts.handle]); } } diff --git a/test/index.js b/test/index.js index aa1100a..c8c1ba5 100644 --- a/test/index.js +++ b/test/index.js @@ -9,12 +9,12 @@ describe('metalsmith-tags', function() { builder.use(tags()); builder.build(function(err, files) { if (err) return done(err); - assert.deepEqual(files['index.html'].tags, [ - { name: 'hello', urlSafe: 'hello' }, - { name: 'tag', urlSafe: 'tag' }, - { name: 'this is', urlSafe: 'this-is' }, - { name: 'world', urlSafe: 'world' } - ]); + assert.deepEqual(files['index.html'].tags, { + hello: { urlSafe: 'hello' }, + tag: { urlSafe: 'tag' }, + 'this is': { urlSafe: 'this-is' }, + world: { urlSafe: 'world' } + }); done(); }); }); @@ -239,12 +239,12 @@ describe('metalsmith-tags', function() { builder.build(function(err, files) { if (err) return done(err); - assert.deepEqual(files['index.html'].tags, [ - { name: 'hello', urlSafe: 'HELLO' }, - { name: 'tag', urlSafe: 'TAG' }, - { name: 'this is', urlSafe: 'THIS IS' }, - { name: 'world', urlSafe: 'WORLD' } - ]); + assert.deepEqual(files['index.html'].tags, { + hello: { urlSafe: 'HELLO' }, + tag: { urlSafe: 'TAG' }, + 'this is': { urlSafe: 'THIS IS' }, + world: { urlSafe: 'WORLD' } + }); done(); }); From 2858827e5ca0d79815e47db0161a0a18fb550c78 Mon Sep 17 00:00:00 2001 From: Thilo Maier Date: Mon, 5 Sep 2016 21:34:12 -0400 Subject: [PATCH 8/8] Update changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 864b85e..0da286e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +* Fix #21 and #28 +* Align data structures of page tags and global tags to make reusing partials for accessing either easier. +* Remove `skipMetadata` option. +* Cleanup tests. + # 1.2.0 @@ -61,4 +66,4 @@ reducing overhead. * Support metalsmith `1.x` [#13](https://github.com/totocaster/metalsmith-tags/pull/13) * Use `metalsmith.data()` so you can now manipulate the data later as you desire. [#13](https://github.com/totocaster/metalsmith-tags/pull/13) -* Unified code style. \ No newline at end of file +* Unified code style.