From 132f140ce077e2324f3d303b3c95601239fe3929 Mon Sep 17 00:00:00 2001 From: Ross Phillips <12723297+rphillips-cc@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:50:58 +1200 Subject: [PATCH] Exclude data/non-content folder when choosing top level content folders --- src/defaults.js | 30 +- src/ssgs/eleventy.js | 18 +- src/ssgs/hugo.js | 66 ++-- src/ssgs/jekyll.js | 1 - src/ssgs/ssg.js | 48 ++- src/utility.js | 4 +- test/markdown.test.js | 286 +++++++++--------- test/utility.test.js | 9 +- .../templates-in-source.toolproof.yml | 49 +++ 9 files changed, 287 insertions(+), 224 deletions(-) create mode 100644 toolproof_tests/eleventy/templates-in-source.toolproof.yml diff --git a/src/defaults.js b/src/defaults.js index 83eda22..0c1f4d8 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -1,20 +1,20 @@ /** @type {import('@cloudcannon/configuration-types').MarkdownAttributeElementOptions} */ export const commonmarkAttributeElementOptions = { - inline: 'none', - block: 'space right', - img: 'right', - ul: 'below', - ol: 'below', - li: 'space right', - table: 'newline below', - blockquote: 'below', -} + inline: 'none', + block: 'space right', + img: 'right', + ul: 'below', + ol: 'below', + li: 'space right', + table: 'newline below', + blockquote: 'below', +}; /** @type {import('@cloudcannon/configuration-types').MarkdownAttributeElementOptions} */ export const kramdownAttributeElementOptions = { - inline: 'right', - block: 'below', - tr: 'none', - td: 'none', - li: 'right-of-prefix', -} \ No newline at end of file + inline: 'right', + block: 'below', + tr: 'none', + td: 'none', + li: 'right-of-prefix', +}; diff --git a/src/ssgs/eleventy.js b/src/ssgs/eleventy.js index 5efcfae..2cdc762 100644 --- a/src/ssgs/eleventy.js +++ b/src/ssgs/eleventy.js @@ -1,4 +1,4 @@ -import { normalisePath, popPathSection } from '../utility.js'; +import { joinPaths, stripBottomPath } from '../utility.js'; import Ssg from './ssg.js'; export default class Eleventy extends Ssg { @@ -55,7 +55,7 @@ export default class Eleventy extends Ssg { const configFilePath = filePaths.find(this.isConfigPath.bind(this)); if (configFilePath) { - return normalisePath(popPathSection(configFilePath)) || undefined; + return stripBottomPath(configFilePath) || undefined; } } @@ -73,6 +73,20 @@ export default class Eleventy extends Ssg { return collectionConfig; } + /** + * Filters out collection paths that are collections, but exist in isolated locations. + * Used when a data folder (or similar) is causing all collections to group under one + * `collections_config` entry. + * + * @param collectionPaths {string[]} + * @param basePath {string} + * @returns {string[]} + */ + filterContentCollectionPaths(collectionPaths, basePath) { + const dataPath = joinPaths([basePath, '_data']); + return collectionPaths.filter((path) => path !== dataPath && !path.startsWith(`${dataPath}/`)); + } + /** * @param _config {Record} * @returns {import('@cloudcannon/configuration-types').MarkdownSettings} diff --git a/src/ssgs/hugo.js b/src/ssgs/hugo.js index b3cc274..d82492c 100644 --- a/src/ssgs/hugo.js +++ b/src/ssgs/hugo.js @@ -1,5 +1,5 @@ import { findBasePath } from '../collections.js'; -import { decodeEntity, joinPaths, stripTopPath } from '../utility.js'; +import { decodeEntity, joinPaths } from '../utility.js'; import Ssg from './ssg.js'; export default class Hugo extends Ssg { @@ -83,6 +83,20 @@ export default class Hugo extends Ssg { return collectionConfig; } + /** + * Filters out collection paths that are collections, but exist in isolated locations. + * Used when a data folder (or similar) is causing all collections to group under one + * `collections_config` entry. + * + * @param collectionPaths {string[]} + * @param basePath {string} + * @returns {string[]} + */ + filterContentCollectionPaths(collectionPaths, basePath) { + const dataPath = joinPaths([basePath, 'data']); + return collectionPaths.filter((path) => path !== dataPath && !path.startsWith(`${dataPath}/`)); + } + /** * Generates collections config from a set of paths. * @@ -91,10 +105,6 @@ export default class Hugo extends Ssg { * @returns {import('../types').CollectionsConfig} */ generateCollectionsConfig(collectionPaths, options) { - /** @type {import('../types').CollectionsConfig} */ - const collectionsConfig = {}; - let basePath = options.basePath; - const collectionPathsOutsideExampleSite = collectionPaths.filter( (path) => !path.includes('exampleSite/'), ); @@ -103,49 +113,15 @@ export default class Hugo extends Ssg { collectionPathsOutsideExampleSite.length && collectionPathsOutsideExampleSite.length !== collectionPaths.length; - // Exclude collections found inside the exampleSite folder, unless they are the only collections if (hasNonExampleSiteCollections) { - basePath = findBasePath(collectionPathsOutsideExampleSite); - collectionPaths = collectionPathsOutsideExampleSite; - } - - const dataPath = joinPaths([basePath, 'data']); - const collectionPathsOutsideData = collectionPaths.filter((path) => !path.startsWith(dataPath)); - const hasDataCollection = - collectionPathsOutsideData.length && - collectionPathsOutsideData.length !== collectionPaths.length; - - // Reprocess basePath to exclude the data folder - if (hasDataCollection) { - basePath = findBasePath(collectionPathsOutsideData); - } - - basePath = stripTopPath(basePath, options.source); - - const sortedPaths = collectionPaths.sort((a, b) => a.length - b.length); - /** @type {string[]} */ - const seenPaths = []; - - for (const fullPath of sortedPaths) { - const path = stripTopPath(fullPath, options.source); - const pathInBasePath = stripTopPath(path, basePath); - - if ( - !path.startsWith(dataPath) && - seenPaths.some((seenPath) => pathInBasePath.startsWith(seenPath)) - ) { - // Skip collection if not data, or a top-level content collection (i.e. seen before) - continue; - } else if (pathInBasePath) { - seenPaths.push(pathInBasePath + '/'); - } - - const key = this.generateCollectionsConfigKey(pathInBasePath, collectionsConfig); - - collectionsConfig[key] = this.generateCollectionConfig(key, path, { basePath }); + // Exclude collections found inside the exampleSite folder, unless they are the only collections + return super.generateCollectionsConfig(collectionPathsOutsideExampleSite, { + ...options, + basePath: findBasePath(collectionPathsOutsideExampleSite), + }); } - return collectionsConfig; + return super.generateCollectionsConfig(collectionPaths, options); } /** diff --git a/src/ssgs/jekyll.js b/src/ssgs/jekyll.js index 8136ca2..26c79db 100644 --- a/src/ssgs/jekyll.js +++ b/src/ssgs/jekyll.js @@ -290,7 +290,6 @@ export default class Jekyll extends Ssg { options.attributes = true; options.attribute_elements = kramdownAttributeElementOptions; - } else if (config) { const commonmarkConfig = config?.['commonmark'] || {}; diff --git a/src/ssgs/ssg.js b/src/ssgs/ssg.js index e177aca..e840f8c 100644 --- a/src/ssgs/ssg.js +++ b/src/ssgs/ssg.js @@ -4,7 +4,7 @@ import slugify from '@sindresorhus/slugify'; import titleize from 'titleize'; import { findIcon } from '../icons.js'; import { joinPaths, last, parseDataFile, stripTopPath } from '../utility.js'; -import { getCollectionPaths } from '../collections.js'; +import { findBasePath, getCollectionPaths } from '../collections.js'; export default class Ssg { /** @type {import('@cloudcannon/configuration-types').SsgKey} */ @@ -380,6 +380,19 @@ export default class Ssg { return key; } + /** + * Filters out collection paths that are collections, but exist in isolated locations. + * Used when a data folder (or similar) is causing all collections to group under one + * `collections_config` entry. + * + * @param collectionPaths {string[]} + * @param _basePath {string} + * @returns {string[]} + */ + filterContentCollectionPaths(collectionPaths, _basePath) { + return collectionPaths; + } + /** * Generates collections config from a set of paths. * @@ -388,28 +401,35 @@ export default class Ssg { * @returns {import('../types').CollectionsConfig} */ generateCollectionsConfig(collectionPaths, options) { - /** @type {import('../types').CollectionsConfig} */ - const collectionsConfig = {}; - const basePath = options.source - ? stripTopPath(options.basePath, options.source) - : options.basePath; + const contentCollectionPaths = this.filterContentCollectionPaths( + collectionPaths, + options.basePath, + ); + + const hasNonContentCollection = + collectionPaths.length && collectionPaths.length !== contentCollectionPaths.length; + + const basePath = stripTopPath( + hasNonContentCollection ? findBasePath(contentCollectionPaths) : options.basePath, + options.source, + ); const sortedPaths = collectionPaths.sort((a, b) => a.length - b.length); - /** @type {string[]} */ - const seenPaths = []; + const seenPaths = /** @type {string[]} */ ([]); + const collectionsConfig = /** @type {import('../types').CollectionsConfig} */ ({}); for (const fullPath of sortedPaths) { const path = stripTopPath(fullPath, options.source); + const pathInBasePath = stripTopPath(path, basePath); - if (seenPaths.some((seenPath) => path.startsWith(seenPath))) { - // Skip collection if parent path seen before + if (seenPaths.some((seenPath) => pathInBasePath.startsWith(seenPath))) { + // Skip collection if higher level path seen before continue; - } else if (path) { - // add to seen paths if not the root folder - seenPaths.push(path + '/'); + } else if (pathInBasePath) { + seenPaths.push(pathInBasePath + '/'); } - const key = this.generateCollectionsConfigKey(path, collectionsConfig); + const key = this.generateCollectionsConfigKey(pathInBasePath, collectionsConfig); collectionsConfig[key] = this.generateCollectionConfig(key, path, { basePath }); } diff --git a/src/utility.js b/src/utility.js index a81c333..8b750bd 100644 --- a/src/utility.js +++ b/src/utility.js @@ -47,9 +47,9 @@ export function stripTopPath(path, stripPath) { * @param path {string} * @returns {string} */ -export function popPathSection(path) { +export function stripBottomPath(path) { const index = path.lastIndexOf('/'); - return index > 0 ? path.substring(path.lastIndexOf('/')) : ''; + return index > 0 ? path.substring(0, path.lastIndexOf('/')) : ''; } /** diff --git a/test/markdown.test.js b/test/markdown.test.js index 15cc8bd..ef0128d 100644 --- a/test/markdown.test.js +++ b/test/markdown.test.js @@ -4,8 +4,6 @@ import Hugo from '../src/ssgs/hugo.js'; import Jekyll from '../src/ssgs/jekyll.js'; import Ssg from '../src/ssgs/ssg.js'; - - test('Defaults to CommonMark', (t) => { t.deepEqual(new Ssg().generateMarkdown({}), { engine: 'commonmark', @@ -14,169 +12,169 @@ test('Defaults to CommonMark', (t) => { }); test('Respects Jekyll Kramdown options enabled', (t) => { - const markdown = new Jekyll().generateMarkdown({ - kramdown: { - input: 'GFM', - hard_wrap: true, - gfm_quirks: [], - auto_ids: true, - smart_quotes: 'lsquo,rsquo,ldquo,rdquo', - }, - }); - t.is(markdown.engine, 'kramdown'); - t.is(markdown.options.quotes, '‘’“”'); - t.true(markdown.options.breaks); - t.true(markdown.options.gfm); - t.true(markdown.options.heading_ids); - t.true(markdown.options.typographer); - t.true(markdown.options.treat_indentation_as_code); + const markdown = new Jekyll().generateMarkdown({ + kramdown: { + input: 'GFM', + hard_wrap: true, + gfm_quirks: [], + auto_ids: true, + smart_quotes: 'lsquo,rsquo,ldquo,rdquo', + }, + }); + t.is(markdown.engine, 'kramdown'); + t.is(markdown.options.quotes, '‘’“”'); + t.true(markdown.options.breaks); + t.true(markdown.options.gfm); + t.true(markdown.options.heading_ids); + t.true(markdown.options.typographer); + t.true(markdown.options.treat_indentation_as_code); }); test('Respects Jekyll Kramdown options disabled', (t) => { - const markdown = new Jekyll().generateMarkdown({ - kramdown: { - input: 'not_gfm', - hard_wrap: false, - gfm_quirks: ['no_auto_typographic'], - auto_ids: false, - }, - }); - t.is(markdown.engine, 'kramdown'); - t.false(markdown.options.breaks); - t.false(markdown.options.gfm); - t.false(markdown.options.heading_ids); - t.false(markdown.options.typographer); - t.true(markdown.options.treat_indentation_as_code); + const markdown = new Jekyll().generateMarkdown({ + kramdown: { + input: 'not_gfm', + hard_wrap: false, + gfm_quirks: ['no_auto_typographic'], + auto_ids: false, + }, + }); + t.is(markdown.engine, 'kramdown'); + t.false(markdown.options.breaks); + t.false(markdown.options.gfm); + t.false(markdown.options.heading_ids); + t.false(markdown.options.typographer); + t.true(markdown.options.treat_indentation_as_code); }); test('Respects Jekyll CommonMark options', (t) => { - const markdown = new Jekyll().generateMarkdown({ - markdown: 'CommonMark', - commonmark: { - options: ['HARDBREAKS', 'GFM_QUIRKS'], - extensions: ['strikethrough', 'table', 'autolink', 'superscript', 'header_ids'], - }, - }); - t.is(markdown.engine, 'commonmark'); - t.true(markdown.options.breaks); - t.true(markdown.options.gfm); - t.true(markdown.options.strikethrough); - t.true(markdown.options.superscript); - t.true(markdown.options.linkify); - t.true(markdown.options.heading_ids); - t.true(markdown.options.table); - t.true(markdown.options.treat_indentation_as_code); + const markdown = new Jekyll().generateMarkdown({ + markdown: 'CommonMark', + commonmark: { + options: ['HARDBREAKS', 'GFM_QUIRKS'], + extensions: ['strikethrough', 'table', 'autolink', 'superscript', 'header_ids'], + }, + }); + t.is(markdown.engine, 'commonmark'); + t.true(markdown.options.breaks); + t.true(markdown.options.gfm); + t.true(markdown.options.strikethrough); + t.true(markdown.options.superscript); + t.true(markdown.options.linkify); + t.true(markdown.options.heading_ids); + t.true(markdown.options.table); + t.true(markdown.options.treat_indentation_as_code); }); test('Respects Hugo options', (t) => { - const markdown = new Hugo().generateMarkdown({ - markup: { - goldmark: { - extensions: { - linkify: true, - strikethrough: true, - table: true, - extras: { - delete: { enable: true }, - subscript: { enable: true }, - superscript: { enable: true }, - }, - typographer: { - disable: false, - leftDoubleQuote: '“', - leftSingleQuote: '‘', - rightDoubleQuote: '”', - rightSingleQuote: '’', - }, - }, - parser: { - autoHeadingID: true, - attribute: { block: false, title: true }, - }, - renderer: { hardWraps: true, xhtml: true }, - }, - }, - }); - t.is(markdown.engine, 'commonmark'); - t.is(markdown.options.quotes, '‘’“”'); - t.true(markdown.options.attributes); - t.true(markdown.options.linkify); - t.true(markdown.options.strikethrough); - t.true(markdown.options.table); - t.true(markdown.options.treat_indentation_as_code); - t.true(markdown.options.typographer); - t.true(markdown.options.breaks); - t.true(markdown.options.gfm); - t.true(markdown.options.subscript); - t.true(markdown.options.superscript); - t.true(markdown.options.heading_ids); - t.true(markdown.options.xhtml); + const markdown = new Hugo().generateMarkdown({ + markup: { + goldmark: { + extensions: { + linkify: true, + strikethrough: true, + table: true, + extras: { + delete: { enable: true }, + subscript: { enable: true }, + superscript: { enable: true }, + }, + typographer: { + disable: false, + leftDoubleQuote: '“', + leftSingleQuote: '‘', + rightDoubleQuote: '”', + rightSingleQuote: '’', + }, + }, + parser: { + autoHeadingID: true, + attribute: { block: false, title: true }, + }, + renderer: { hardWraps: true, xhtml: true }, + }, + }, + }); + t.is(markdown.engine, 'commonmark'); + t.is(markdown.options.quotes, '‘’“”'); + t.true(markdown.options.attributes); + t.true(markdown.options.linkify); + t.true(markdown.options.strikethrough); + t.true(markdown.options.table); + t.true(markdown.options.treat_indentation_as_code); + t.true(markdown.options.typographer); + t.true(markdown.options.breaks); + t.true(markdown.options.gfm); + t.true(markdown.options.subscript); + t.true(markdown.options.superscript); + t.true(markdown.options.heading_ids); + t.true(markdown.options.xhtml); }); test('Respects Hugo options with attributes disabled', (t) => { - const noAttrConfig = new Hugo().generateMarkdown( - { "markup": { - "goldmark": { - "parser": { - "attribute": { "block": false, "title": false } - }, - } - }} - ); - t.false(noAttrConfig.options.attributes); - t.is(noAttrConfig.options.attribute_elements, undefined); + const noAttrConfig = new Hugo().generateMarkdown({ + markup: { + goldmark: { + parser: { + attribute: { block: false, title: false }, + }, + }, + }, + }); + t.false(noAttrConfig.options.attributes); + t.is(noAttrConfig.options.attribute_elements, undefined); }); test('Respects Hugo options with heading attributes enabled', (t) => { - const noAttrConfig = new Hugo().generateMarkdown( - { "markup": { - "goldmark": { - "parser": { - "attribute": { "block": false, "title": true }, - }, - } - }} - ); - t.true(noAttrConfig.options.attributes); - t.is(noAttrConfig.options.attribute_elements.h1, 'space right'); - t.is(noAttrConfig.options.attribute_elements.h6, 'space right'); - t.is(noAttrConfig.options.attribute_elements.blockquote, 'none'); - t.is(noAttrConfig.options.attribute_elements.table, 'none'); + const noAttrConfig = new Hugo().generateMarkdown({ + markup: { + goldmark: { + parser: { + attribute: { block: false, title: true }, + }, + }, + }, + }); + t.true(noAttrConfig.options.attributes); + t.is(noAttrConfig.options.attribute_elements.h1, 'space right'); + t.is(noAttrConfig.options.attribute_elements.h6, 'space right'); + t.is(noAttrConfig.options.attribute_elements.blockquote, 'none'); + t.is(noAttrConfig.options.attribute_elements.table, 'none'); }); test('Respects Hugo options with block attributes enabled', (t) => { - const noAttrConfig = new Hugo().generateMarkdown( - { "markup": { - "goldmark": { - "parser": { - "attribute": { "block": true, "title": false }, - }, - } - }} - ); - t.true(noAttrConfig.options.attributes); - t.is(noAttrConfig.options.attribute_elements.h1, 'none'); - t.is(noAttrConfig.options.attribute_elements.img, 'none'); - t.is(noAttrConfig.options.attribute_elements.blockquote, 'below'); - t.is(noAttrConfig.options.attribute_elements.ul, 'below'); - t.is(noAttrConfig.options.attribute_elements.ol, 'below'); - t.is(noAttrConfig.options.attribute_elements.table, 'below'); - t.is(noAttrConfig.options.attribute_elements.p, 'below'); + const noAttrConfig = new Hugo().generateMarkdown({ + markup: { + goldmark: { + parser: { + attribute: { block: true, title: false }, + }, + }, + }, + }); + t.true(noAttrConfig.options.attributes); + t.is(noAttrConfig.options.attribute_elements.h1, 'none'); + t.is(noAttrConfig.options.attribute_elements.img, 'none'); + t.is(noAttrConfig.options.attribute_elements.blockquote, 'below'); + t.is(noAttrConfig.options.attribute_elements.ul, 'below'); + t.is(noAttrConfig.options.attribute_elements.ol, 'below'); + t.is(noAttrConfig.options.attribute_elements.table, 'below'); + t.is(noAttrConfig.options.attribute_elements.p, 'below'); }); test('Respects Hugo options to enable attributes on standalone image', (t) => { - const noAttrConfig = new Hugo().generateMarkdown( - { "markup": { - "goldmark": { - "parser": { - "attribute": { "block": true }, - "wrapStandAloneImageWithinParagraph": false - }, - } - }} - ); - t.true(noAttrConfig.options.attributes); - t.is(noAttrConfig.options.attribute_elements.img, 'below'); + const noAttrConfig = new Hugo().generateMarkdown({ + markup: { + goldmark: { + parser: { + attribute: { block: true }, + wrapStandAloneImageWithinParagraph: false, + }, + }, + }, + }); + t.true(noAttrConfig.options.attributes); + t.is(noAttrConfig.options.attribute_elements.img, 'below'); }); test('Has good 11ty defaults', (t) => { diff --git a/test/utility.test.js b/test/utility.test.js index ba1288f..aad68f9 100644 --- a/test/utility.test.js +++ b/test/utility.test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import { last, joinPaths, stripTopPath, decodeEntity } from '../src/utility.js'; +import { last, joinPaths, stripTopPath, decodeEntity, stripBottomPath } from '../src/utility.js'; test('gets last element', (t) => { t.is(last(['first', 'final']), 'final'); @@ -24,6 +24,13 @@ test('strips top path', (t) => { t.is(stripTopPath('sauce/content', 'sauce'), 'content'); }); +test('strips bottom path', (t) => { + t.is(stripBottomPath('src/content/index.html'), 'src/content'); + t.is(stripBottomPath('src/content'), 'src'); + t.is(stripBottomPath('src'), ''); + t.is(stripBottomPath(''), ''); +}); + test('decodes entities', (t) => { t.is(decodeEntity('&'), '&'); t.is(decodeEntity('©'), '©'); diff --git a/toolproof_tests/eleventy/templates-in-source.toolproof.yml b/toolproof_tests/eleventy/templates-in-source.toolproof.yml new file mode 100644 index 0000000..9ac0581 --- /dev/null +++ b/toolproof_tests/eleventy/templates-in-source.toolproof.yml @@ -0,0 +1,49 @@ +name: Eleventy templates in source + +steps: + - step: I have a "src/eleventy.config.js" file with the content {js} + js: |- + export default function(eleventyConfig) { + return { dir: { input: "sauce" } }; + } + - step: I have a "src/sauce/content/index.html" file with the content "" + - step: I have a "src/sauce/content/blog.html" file with the content "" + - step: I have a "src/sauce/content/blog/partying.md" file with the content "" + - step: I have a "src/sauce/content/blog/follow-up/no-more-partying.md" file with the content "" + - step: I have a "src/sauce/_data/party-locations.csv" file with the content "" + - ref: ./../core/run_gadget.toolproof.yml + - snapshot: stdout + snapshot_content: |- + ╎{ + ╎ "ssg": "eleventy", + ╎ "config": { + ╎ "source": "sauce", + ╎ "collections_config": { + ╎ "data": { + ╎ "path": "_data", + ╎ "name": "Data", + ╎ "icon": "data_usage", + ╎ "output": false + ╎ }, + ╎ "pages": { + ╎ "path": "content", + ╎ "name": "Pages", + ╎ "icon": "wysiwyg", + ╎ "output": true + ╎ }, + ╎ "blog": { + ╎ "path": "content/blog", + ╎ "name": "Blog", + ╎ "icon": "event_available", + ╎ "output": true + ╎ } + ╎ }, + ╎ "timezone": "Pacific/Auckland", + ╎ "markdown": { + ╎ "engine": "commonmark", + ╎ "options": { + ╎ "html": true + ╎ } + ╎ } + ╎ } + ╎}