diff --git a/package.json b/package.json index b01e7075b..2af563215 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@11ty/eleventy", - "version": "1.0.0-beta.0", + "version": "1.0.0-twok.0", "description": "Transform a directory of templates into HTML.", "publishConfig": { "access": "public" diff --git a/src/Plugins/Pagination.js b/src/Plugins/Pagination.js index 6fa062d3b..53c58672b 100755 --- a/src/Plugins/Pagination.js +++ b/src/Plugins/Pagination.js @@ -6,98 +6,73 @@ const config = require("../Config"); class PaginationError extends EleventyBaseError {} -class Pagination { - constructor(data) { - this.config = config.getConfig(); +class PaginationGroup { + constructor(options) { + if (!("size" in options)) { + throw new Error("Missing pagination size in front matter data."); + } + this.size = options.size; + this.alias = options.alias; + this.before = options.before; + this.reverse = options.reverse; + this.filter = options.filter; + this.resolve = options.resolve; + this.data = options.data; + this.target = null; + this.items = null; + } - this.setData(data); + _getDataKey() { + return this.data; } - static hasPagination(data) { - return "pagination" in data; + resolveItems(data) { + this.target = this._resolveItems(data); + this.items = this.pageItems(this.target); } - hasPagination() { - if (!this.data) { - throw new Error("Missing `setData` call for Pagination object."); + isFiltered(value) { + if (this.filter) { + let filtered = this.filter; + if (Array.isArray(filtered)) { + return filtered.indexOf(value) > -1; + } + + return filtered === value; } - return Pagination.hasPagination(this.data); + + return false; } - circularReferenceCheck(data) { + circularReferenceCheck(data, template) { if (data.eleventyExcludeFromCollections) { return; } - let key = data.pagination.data; + let key = this.data; let tags = data.tags || []; for (let tag of tags) { if (`collections.${tag}` === key) { throw new PaginationError( `Pagination circular reference${ - this.template ? ` on ${this.template.inputPath}` : "" + template ? ` on ${template.inputPath}` : "" }, data:\`${key}\` iterates over both the \`${tag}\` tag and also supplies pages to that tag.` ); } } } - setData(data) { - this.data = data || {}; - this.target = []; - - if (!this.hasPagination()) { - return; - } - - if (!data.pagination) { - throw new Error( - "Misconfigured pagination data in template front matter (YAML front matter precaution: did you use tabs and not spaces for indentation?)." - ); - } else if (!("size" in data.pagination)) { - throw new Error("Missing pagination size in front matter data."); - } - this.circularReferenceCheck(data); - - this.size = data.pagination.size; - this.alias = data.pagination.alias; - - this.target = this._resolveItems(); - this.items = this.pagedItems; - } - - setTemplate(tmpl) { - this.template = tmpl; - } - - _getDataKey() { - return this.data.pagination.data; - } - doResolveToObjectValues() { - if ("resolve" in this.data.pagination) { - return this.data.pagination.resolve === "values"; + if (this.resolve) { + return this.resolve === "values"; } return false; } - isFiltered(value) { - if ("filter" in this.data.pagination) { - let filtered = this.data.pagination.filter; - if (Array.isArray(filtered)) { - return filtered.indexOf(value) > -1; - } - - return filtered === value; - } - - return false; - } - - _resolveItems() { + _resolveItems(data) { let notFoundValue = "__NOT_FOUND_ERROR__"; let key = this._getDataKey(); - let fullDataSet = lodashGet(this.data, key, notFoundValue); + let fullDataSet = lodashGet(data, key, notFoundValue); if (fullDataSet === notFoundValue) { throw new Error( `Could not resolve pagination key in template data: ${key}` @@ -115,190 +90,288 @@ class Pagination { let result = keys.slice(); - if ( - this.data.pagination.before && - typeof this.data.pagination.before === "function" - ) { + if (this.before && typeof this.before === "function") { // we don’t need to make a copy of this because we already .filter() above - result = this.data.pagination.before(result); + result = this.before(result); } - if (this.data.pagination.reverse === true) { + if (this.reverse === true) { result = result.reverse(); } - if (this.data.pagination.filter) { + if (this.filter) { result = result.filter((value) => !this.isFiltered(value)); } return result; } - get pagedItems() { - if (!this.data) { - throw new Error("Missing `setData` call for Pagination object."); - } - - return lodashChunk(this.target, this.size); - } - - // TODO this name is not good - // “To cancel” means to not write the original root template - cancel() { - return this.hasPagination(); - } - - getPageCount() { - if (!this.hasPagination()) { - return 0; - } - - return this.items.length; + pageItems(data) { + return lodashChunk(data, this.size); } - async getPageTemplates() { - if (!this.data) { - throw new Error("Missing `setData` call for Pagination object."); - } - - if (!this.hasPagination()) { - return []; - } - - if (this.pagesCache) { - return this.pagesCache; + getPaginationData(prevPaginationData) { + const items = this.items; + if (!items) { + throw new Error( + `Cannot get pagination data before items have been resolved.` + ); } - - let pages = []; - let items = this.items; - let tmpl = this.template; - let templates = []; - let links = []; - let hrefs = []; - let overrides = []; - - for (let pageNumber = 0, k = items.length; pageNumber < k; pageNumber++) { - let cloned = tmpl.clone(); - - // TODO maybe also move this permalink additions up into the pagination class - if (pageNumber > 0 && !this.data[this.config.keys.permalink]) { - cloned.setExtraOutputSubdirectory(pageNumber); - } - - templates.push(cloned); - - let override = { - pagination: { - data: this.data.pagination.data, - size: this.data.pagination.size, - alias: this.alias, - - pages: this.size === 1 ? items.map((entry) => entry[0]) : items, - - // See Issue #345 for more examples - page: { - previous: - pageNumber > 0 - ? this.size === 1 - ? items[pageNumber - 1][0] - : items[pageNumber - 1] - : null, - next: - pageNumber < items.length - 1 - ? this.size === 1 - ? items[pageNumber + 1][0] - : items[pageNumber + 1] - : null, - first: items.length + return this.items.map((item, pageNumber, items) => { + const pagination = { + data: this.data, + size: this.size, + alias: this.alias, + + pages: this.size === 1 ? items.map((entry) => entry[0]) : items, + + // See Issue #345 for more examples + page: { + previous: + pageNumber > 0 ? this.size === 1 - ? items[0][0] - : items[0] + ? items[pageNumber - 1][0] + : items[pageNumber - 1] : null, - last: items.length + next: + pageNumber < items.length - 1 ? this.size === 1 - ? items[items.length - 1][0] - : items[items.length - 1] + ? items[pageNumber + 1][0] + : items[pageNumber + 1] : null, - }, - - items: items[pageNumber], - pageNumber: pageNumber, + first: items.length + ? this.size === 1 + ? items[0][0] + : items[0] + : null, + last: items.length + ? this.size === 1 + ? items[items.length - 1][0] + : items[items.length - 1] + : null, }, + + items: items[pageNumber], + pageNumber, + }; + const obj = { + ...prevPaginationData, + pagination: prevPaginationData + ? [...[].concat(prevPaginationData.pagination), pagination] + : pagination, }; if (this.alias) { lodashSet( - override, + obj, this.alias, this.size === 1 ? items[pageNumber][0] : items[pageNumber] ); } - overrides.push(override); - cloned.setPaginationData(override); + return obj; + }); + } +} - // TO DO subdirectory to links if the site doesn’t live at / - let [outputLink, outputHref] = await Promise.all([ - cloned.getOutputLink(), - cloned.getOutputHref(), - ]); - links.push("/" + outputLink); - hrefs.push(outputHref); - } +class Pagination { + constructor(data) { + this.config = config.getConfig(); - // we loop twice to pass in the appropriate prev/next links (already full generated now) - templates.forEach( - function (cloned, pageNumber) { - let pageObj = {}; + this.setData(data); + } - // links are okay but hrefs are better - pageObj.previousPageLink = - pageNumber > 0 ? links[pageNumber - 1] : null; - pageObj.previous = pageObj.previousPageLink; + static hasPagination(data) { + return "pagination" in data; + } - pageObj.nextPageLink = - pageNumber < templates.length - 1 ? links[pageNumber + 1] : null; - pageObj.next = pageObj.nextPageLink; + hasPagination() { + if (!this.data) { + throw new Error("Missing `setData` call for Pagination object."); + } + return Pagination.hasPagination(this.data); + } - pageObj.firstPageLink = links.length > 0 ? links[0] : null; - pageObj.lastPageLink = - links.length > 0 ? links[links.length - 1] : null; + setData(data) { + this.data = data || {}; - pageObj.links = links; - // todo deprecated, consistency with collections and use links instead - pageObj.pageLinks = links; + if (!this.hasPagination()) { + return; + } - // hrefs are better than links - pageObj.previousPageHref = - pageNumber > 0 ? hrefs[pageNumber - 1] : null; - pageObj.nextPageHref = - pageNumber < templates.length - 1 ? hrefs[pageNumber + 1] : null; + if (!data.pagination) { + throw new Error( + "Misconfigured pagination data in template front matter (YAML front matter precaution: did you use tabs and not spaces for indentation?)." + ); + } - pageObj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null; - pageObj.lastPageHref = - hrefs.length > 0 ? hrefs[hrefs.length - 1] : null; + this.dataPaginationGroups = Array.isArray(data.pagination) + ? data.pagination.map((item) => new PaginationGroup(item)) + : [new PaginationGroup(data.pagination)]; - pageObj.hrefs = hrefs; + this.dataPaginationGroups.forEach((group) => { + group.circularReferenceCheck(this.data, this.template); + group.resolveItems(this.data); + }); + } - // better names - pageObj.href = { - previous: pageObj.previousPageHref, - next: pageObj.nextPageHref, - first: pageObj.firstPageHref, - last: pageObj.lastPageHref, - }; + get pagedItems() { + return this.dataPaginationGroups + ? this.dataPaginationGroups.length === 1 + ? this.dataPaginationGroups[0].items + : this.dataPaginationGroups.map((group) => group.items) + : []; + } - Object.assign(overrides[pageNumber].pagination, pageObj); + _resolveItems() { + return this.dataPaginationGroups + ? this.dataPaginationGroups.length === 1 + ? this.dataPaginationGroups[0].target + : this.dataPaginationGroups.map((group) => group.target) + : []; + } - cloned.setPaginationData(overrides[pageNumber]); + setTemplate(tmpl) { + this.template = tmpl; + } - pages.push(cloned); - }.bind(this) + // TODO this name is not good + // “To cancel” means to not write the original root template + cancel() { + return this.hasPagination(); + } + + getPageCount() { + if (!this.hasPagination()) { + return 0; + } + + return this.dataPaginationGroups.reduce( + (acc, curr) => acc * curr.items.length, + 1 ); + } - this.pagesCache = pages; + async getPageTemplates() { + if (!this.data) { + throw new Error("Missing `setData` call for Pagination object."); + } - return pages; + if (!this.hasPagination()) { + return []; + } + + return ( + this.pagesCache || + (this.pagesCache = ( + await Promise.all( + this.dataPaginationGroups + .reduce((overrides, group) => { + return !overrides + ? group.getPaginationData() + : overrides.flatMap((pageData) => + group.getPaginationData(pageData) + ); + }, null) + .map(async (data, ii) => { + // const pageNumber = pageData.pagination.pageNumber; //wont work + let clonedTemplate = this.template.clone(); + + // TODO maybe also move this permalink additions up into the pagination class + if (ii > 0 && !this.data[this.config.keys.permalink]) { + clonedTemplate.setExtraOutputSubdirectory(ii); + } + + clonedTemplate.setPaginationData(data); + + // TO DO subdirectory to links if the site doesn’t live at / + var [outputLink, outputHref] = await Promise.all([ + clonedTemplate.getOutputLink(), + clonedTemplate.getOutputHref(), + ]); + + return { + outputLink: "/" + outputLink, + outputHref, + clonedTemplate, + data, + }; + }) + ) + ).map(({ clonedTemplate, outputHref, outputLink, data }, i, pages) => { + function fixupPagination(pagination, paginationGroup = 0) { + const pageNumber = pagination.pageNumber; + const pagesInGroup = pages + .filter((page) => { + return Array.isArray(page.data.pagination) + ? page.data.pagination.every( + (otherPaginationItem, index) => + index === paginationGroup || + data.pagination[index].pageNumber === + otherPaginationItem.pageNumber + ) + : true; + }) + .sort((a, b) => { + return Array.isArray(data.pagination) + ? a.data.pagination[paginationGroup].pageNumber - + b.data.pagination[paginationGroup].pageNumber + : 0; + }); + const links = pagesInGroup.map((item) => item.outputLink); + const hrefs = pagesInGroup.map((item) => item.outputHref); + let pageObj = {}; + + // links are okay but hrefs are better + pageObj.previous = pageObj.previousPageLink = + pageNumber > 0 ? links[pageNumber - 1] : null; + + pageObj.next = pageObj.nextPageLink = + pageNumber < pages.length - 1 ? links[pageNumber + 1] : null; + + pageObj.firstPageLink = links.length > 0 ? links[0] : null; + pageObj.lastPageLink = + links.length > 0 ? links[links.length - 1] : null; + + // todo pageLinks is deprecated, consistency with collections and use links instead + pageObj.pageLinks = pageObj.links = links; + + // hrefs are better than links + pageObj.previousPageHref = + pageNumber > 0 ? hrefs[pageNumber - 1] : null; + pageObj.nextPageHref = + pageNumber < pages.length - 1 ? hrefs[pageNumber + 1] : null; + + pageObj.firstPageHref = hrefs.length > 0 ? hrefs[0] : null; + pageObj.lastPageHref = + hrefs.length > 0 ? hrefs[hrefs.length - 1] : null; + + pageObj.hrefs = hrefs; + + // better names + pageObj.href = { + previous: pageObj.previousPageHref, + next: pageObj.nextPageHref, + first: pageObj.firstPageHref, + last: pageObj.lastPageHref, + }; + + return Object.assign({}, pagination, pageObj); + } + + if (Array.isArray(data.pagination)) { + data.pagination = data.pagination.map((item, index) => + fixupPagination(item, index) + ); + } else { + data.pagination = fixupPagination(data.pagination); + } + + clonedTemplate.setPaginationData(data); + + return clonedTemplate; + })) + ); } } diff --git a/src/TemplateMap.js b/src/TemplateMap.js index 6d1b81807..d1967eca9 100644 --- a/src/TemplateMap.js +++ b/src/TemplateMap.js @@ -253,7 +253,17 @@ class TemplateMap { if ( counter === 0 || (map.data.pagination && - map.data.pagination.addAllPagesToCollections) + (map.data.pagination.addAllPagesToCollections || + (Array.isArray(map.data.pagination) && + (Array.isArray(page.data.pagination) + ? page.data.pagination.every( + (pagination, ii) => + !pagination.pageNumber || + map.data.pagination[ii].addAllPagesToCollections + ) + : map.data.pagination.every( + (pagination) => pagination.addAllPagesToCollections + ))))) ) { if (!map.data.eleventyExcludeFromCollections) { // TODO do we need .template in collection entries? diff --git a/test/PaginationTest.js b/test/PaginationTest.js index 0d36ab94c..a247a37d9 100644 --- a/test/PaginationTest.js +++ b/test/PaginationTest.js @@ -714,3 +714,232 @@ test("Pagination make sure pageNumber is numeric for {{ pageNumber + 1 }} Issue t.is(templates[0].data.pagination.pageNumber, 0); t.not(templates[0].data.pagination.pageNumber, "0"); }); + +test("Paginate multiple data in frontmatter", async (t) => { + let tmpl = new Template( + "./test/stubs/paged/pagedinlinemultipledata.njk", + "./test/stubs/", + "./dist" + ); + + let data = await tmpl.getData(); + let pages = await tmpl.getTemplates(data); + t.is(pages.length, 4); + + t.is(pages[0].outputPath, "./dist/paged/pagedinlinemultipledata/index.html"); + t.is( + (await pages[0].template.render(pages[0].data)).trim(), + `