diff --git a/.prettierignore b/.prettierignore index dac576a..9d58b19 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ package-lock.json CHANGELOG.md .github/ .backstage/ +toolproof_tests/test_sites/ diff --git a/src/index.js b/src/index.js index 1961a6c..b8e1742 100644 --- a/src/index.js +++ b/src/index.js @@ -27,7 +27,7 @@ function filterPaths(filePaths, source) { * @param options {import('./types').GenerateOptions=} Options to aid generation. * @returns {Promise} */ -export async function generate(filePaths, options) { +export async function generateConfiguration(filePaths, options) { const ssg = options?.buildConfig?.ssg ? ssgs[options.buildConfig.ssg] : guessSsg(filterPaths(filePaths, options?.config?.source)); @@ -50,10 +50,35 @@ export async function generate(filePaths, options) { source, collections_config: options?.config?.collections_config || - ssg.generateCollectionsConfig(collectionPaths, source), + ssg.generateCollectionsConfig(collectionPaths, { source, config }), paths: options?.config?.paths ?? undefined, timezone: options?.config?.timezone ?? ssg.getTimezone(), markdown: options?.config?.markdown ?? ssg.generateMarkdown(config), }, }; } + +/** + * Generates a baseline CloudCannon configuration based on the file path provided. + * + * @param filePaths {string[]} List of input file paths. + * @param options {import('./types').GenerateOptions=} Options to aid generation. + * @returns {Promise} + */ +export async function generateBuildCommands(filePaths, options) { + const ssg = options?.buildConfig?.ssg + ? ssgs[options.buildConfig.ssg] + : guessSsg(filterPaths(filePaths, options?.config?.source)); + + const source = options?.config?.source ?? ssg.getSource(filePaths); + filePaths = filterPaths(filePaths, source); + + const files = ssg.groupFiles(filePaths); + + const configFilePaths = files.groups.config.map((fileSummary) => fileSummary.filePath); + const config = options?.readFile + ? await ssg.parseConfig(configFilePaths, options.readFile) + : undefined; + + return ssg.generateBuildCommands(filePaths, { config, source, readFile: options?.readFile }); +} diff --git a/src/ssgs/eleventy.js b/src/ssgs/eleventy.js index daeae5d..7dfba69 100644 --- a/src/ssgs/eleventy.js +++ b/src/ssgs/eleventy.js @@ -40,4 +40,26 @@ export default class Eleventy extends Ssg { }, }; } + + /** + * Generates a list of build suggestions. + * + * @param filePaths {string[]} List of input file paths. + * @param options {{ config?: Record; source?: string; readFile?: (path: string) => Promise; }} + * @returns {Promise} + */ + async generateBuildCommands(filePaths, options) { + const commands = await super.generateBuildCommands(filePaths, options); + + commands.build.unshift({ + value: 'npx @11ty/eleventy', + attribution: 'most common for 11ty sites', + }); + commands.output.unshift({ + value: '_site', + attribution: 'most common for 11ty sites', + }); + + return commands; + } } diff --git a/src/ssgs/hugo.js b/src/ssgs/hugo.js index a162ced..ebcadce 100644 --- a/src/ssgs/hugo.js +++ b/src/ssgs/hugo.js @@ -33,13 +33,13 @@ export default class Hugo extends Ssg { * * @param key {string} * @param path {string} - * @param basePath {string} + * @param options {{ basePath: string; }=} * @returns {import('@cloudcannon/configuration-types').CollectionConfig} */ - generateCollectionConfig(key, path, basePath) { - const collectionConfig = super.generateCollectionConfig(key, path, basePath); + generateCollectionConfig(key, path, options) { + const collectionConfig = super.generateCollectionConfig(key, path, options); - if (path !== basePath) { + if (path !== options?.basePath) { collectionConfig.glob = typeof collectionConfig.glob === 'string' ? [collectionConfig.glob] @@ -92,4 +92,26 @@ export default class Hugo extends Ssg { options, }; } + + /** + * Generates a list of build suggestions. + * + * @param filePaths {string[]} List of input file paths. + * @param options {{ config?: Record; source?: string; readFile?: (path: string) => Promise; }} + * @returns {Promise} + */ + async generateBuildCommands(filePaths, options) { + const commands = await super.generateBuildCommands(filePaths, options); + + commands.build.unshift({ + value: 'hugo', + attribution: 'most common for Hugo sites', + }); + commands.output.unshift({ + value: 'public', + attribution: 'most common for Hugo sites', + }); + + return commands; + } } diff --git a/src/ssgs/jekyll.js b/src/ssgs/jekyll.js index b7c3c17..7007ef9 100644 --- a/src/ssgs/jekyll.js +++ b/src/ssgs/jekyll.js @@ -1,4 +1,4 @@ -import { decodeEntity } from '../utility.js'; +import { decodeEntity, joinPaths, stripTopPath } from '../utility.js'; import Ssg from './ssg.js'; /** @@ -21,6 +21,85 @@ function isPostsPath(path) { return !!path?.match(/\b_posts$/); } +/** + * Transforms a Jekyll drafts collection path into a posts path. + * + * @param path {string} The drafts path. + * @returns {string} + */ +function toDraftsPath(path) { + return path.replace(/\b_posts$/, '_drafts'); +} + +/** + * Transforms a Jekyll posts collection path into a drafts path. + * + * @param path {string} The posts path. + * @returns {string} + */ +function toPostsPath(path) { + return path.replace(/\b_drafts$/, '_posts'); +} + +/** + * Transforms a drafts collection key into a posts key. + * + * @param key {string} The drafts key. + * @returns {string} + */ +function toDraftsKey(key) { + return key.replace('posts', 'drafts'); +} + +/** + * Transforms a posts collection key into a drafts key. + * + * @param key {string} The posts key. + * @returns {string} + */ +function toPostsKey(key) { + return key.replace('drafts', 'posts'); +} + +/** + * Gets `collections` from Jekyll configuration in a standard format. + * + * @param collections {Record | undefined} The `collections` object from Jekyll config + * @returns {Record} + */ +function getJekyllCollections(collections) { + /** @type {Record} */ + let formatted = {}; + + if (Array.isArray(collections)) { + return collections.reduce((memo, key) => { + memo[key] = {}; + return memo; + }, formatted); + } + + if (typeof collections === 'object') { + return collections; + } + + return formatted; +} + +/** + * Checks if a Jekyll collection is output. + * + * @param key {string} + * @param collection {Record | undefined} + * @returns {boolean} + */ +function isCollectionOutput(key, collection) { + if (key === 'data' || key === 'drafts' || key.endsWith('_drafts')) { + return false; + } + + return key === 'pages' || key === 'posts' || key.endsWith('_posts') || !!collection?.output; +} + export default class Jekyll extends Ssg { constructor() { super('jekyll'); @@ -99,12 +178,17 @@ export default class Jekyll extends Ssg { * * @param key {string} * @param path {string} + * @param options {{ basePath?: string; collection: Record | undefined; }} * @returns {import('@cloudcannon/configuration-types').CollectionConfig} */ - generateCollectionConfig(key, path) { + generateCollectionConfig(key, path, options) { const collectionConfig = super.generateCollectionConfig(key, path); - // TODO: read contents of _config.yml to find which collections are output - collectionConfig.output = key !== 'data'; + + collectionConfig.output = isCollectionOutput(key, options.collection); + + if (options.collection?.sort_by) { + collectionConfig.sort = { key: options.collection.sort_by }; + } if (isPostsPath(collectionConfig.path)) { collectionConfig.create ||= { @@ -112,20 +196,15 @@ export default class Jekyll extends Ssg { }; collectionConfig.add_options ||= [ - { - name: `Add ${collectionConfig.singular_name || 'Post'}`, - }, - { - name: 'Add Draft', - collection: key.replace('posts', 'drafts'), - }, + { name: `Add ${collectionConfig.singular_name || 'Post'}` }, + { name: 'Add Draft', collection: toDraftsKey(key) }, ]; } if (isDraftsPath(collectionConfig.path)) { collectionConfig.create ||= { path: '', // TODO: this should not be required if publish_to is set - publish_to: key.replace('drafts', 'posts'), + publish_to: toPostsKey(key), }; } @@ -136,30 +215,54 @@ export default class Jekyll extends Ssg { * Generates collections config from a set of paths. * * @param collectionPaths {{ basePath: string, paths: string[] }} - * @param source {string | undefined} + * @param options {{ config?: Record; source?: string; }=} * @returns {import('../types').CollectionsConfig} */ - generateCollectionsConfig(collectionPaths, source) { - const collectionsConfig = super.generateCollectionsConfig(collectionPaths, source); + generateCollectionsConfig(collectionPaths, options) { + /** @type {import('../types').CollectionsConfig} */ + const collectionsConfig = {}; + const collectionsDir = options?.config?.collections_dir || ''; + const collections = getJekyllCollections(options?.config?.collections); + + // Content folder to collections_config mapping. + for (const fullPath of collectionPaths.paths) { + const path = stripTopPath(fullPath, options?.source); + const pathInCollectionsDir = collectionsDir ? stripTopPath(path, collectionsDir) : path; + const key = this.generateCollectionsConfigKey(pathInCollectionsDir, collectionsConfig); + const collection = collections[stripTopPath(path, collectionsDir).replace(/^\/?_/, '')]; + collectionsConfig[key] = this.generateCollectionConfig(key, path, { collection }); + } - const keys = Object.keys(collectionsConfig); + // Handle defined collections without files. + for (const key of Object.keys(collections)) { + collectionsConfig[key] ||= { + path: joinPaths([collectionsDir, `_${key}`]), + output: isCollectionOutput(key, collections[key]), + }; + } - for (const key of keys) { + // Add matching post/draft collections + for (const key of Object.keys(collectionsConfig)) { const collectionConfig = collectionsConfig[key]; + if (!collectionConfig.path) { + continue; + } - if (isDraftsPath(collectionConfig.path) && collectionConfig.path) { + if (isDraftsPath(collectionConfig.path)) { // Ensure there is a matching posts collection - const postsKey = key.replace('drafts', 'posts'); + const postsKey = toPostsKey(key); collectionsConfig[postsKey] ||= this.generateCollectionConfig( postsKey, - collectionConfig.path?.replace(/\b_drafts$/, '_posts'), + toPostsPath(collectionConfig.path), + { collection: collections?.posts }, ); - } else if (isPostsPath(collectionConfig.path) && collectionConfig.path) { + } else if (isPostsPath(collectionConfig.path)) { // Ensure there is a matching drafts collection - const draftsKey = key.replace('posts', 'drafts'); + const draftsKey = toDraftsKey(key); collectionsConfig[draftsKey] ||= this.generateCollectionConfig( draftsKey, - collectionConfig.path?.replace(/\b_posts$/, '_drafts'), + toDraftsPath(collectionConfig.path), + { collection: collections?.drafts || collections?.posts }, ); } } @@ -233,4 +336,38 @@ export default class Jekyll extends Ssg { options, }; } + + /** + * Generates a list of build suggestions. + * + * @param filePaths {string[]} List of input file paths. + * @param options {{ config?: Record; source?: string; readFile?: (path: string) => Promise; }} + * @returns {Promise} + */ + async generateBuildCommands(filePaths, options) { + const commands = await super.generateBuildCommands(filePaths, options); + + if (filePaths.includes(joinPaths([options.source, 'Gemfile']))) { + commands.install.unshift({ + value: 'bundle install', + attribution: 'because of your Gemfile', + }); + commands.build.unshift({ + value: 'bundle exec jekyll build', + attribution: 'because of your Gemfile', + }); + } else { + commands.build.unshift({ + value: 'jekyll build', + attribution: 'most common for Jekyll sites', + }); + } + + commands.output.unshift({ + value: '_site', + attribution: 'most common for Jekyll sites', + }); + + return commands; + } } diff --git a/src/ssgs/ssg.js b/src/ssgs/ssg.js index be48e11..4995564 100644 --- a/src/ssgs/ssg.js +++ b/src/ssgs/ssg.js @@ -3,7 +3,7 @@ import { basename } from 'path'; import slugify from '@sindresorhus/slugify'; import titleize from 'titleize'; import { findIcon } from '../icons.js'; -import { last, parseDataFile, stripTopPath } from '../utility.js'; +import { joinPaths, last, parseDataFile, stripTopPath } from '../utility.js'; import { getCollectionPaths } from '../collections.js'; export default class Ssg { @@ -93,7 +93,7 @@ export default class Ssg { * Returns the parsed contents of the first readable configuration file provided. * * @param configFilePaths {string[]} List of config files. - * @param readFile {(path: string) => Promise} Function to read files. + * @param readFile {(path: string) => Promise} Function to read files. * @returns {Promise | undefined>} */ async parseConfig(configFilePaths, readFile) { @@ -304,10 +304,10 @@ export default class Ssg { * * @param key {string} * @param path {string} - * @param _basePath {string=} + * @param _options {{ basePath?: string; }=} * @returns {import('@cloudcannon/configuration-types').CollectionConfig} */ - generateCollectionConfig(key, path, _basePath) { + generateCollectionConfig(key, path, _options) { const name = titleize( basename(path || key) .replace(/[_-]/g, ' ') @@ -321,25 +321,43 @@ export default class Ssg { }; } + /** + * Generates a collections config key from a path, avoiding existing keys. + * + * @param path {string} + * @param collectionsConfig {Record} + * @returns {string} + */ + generateCollectionsConfigKey(path, collectionsConfig) { + let key = slugify(path, { separator: '_' }) || 'pages'; + let suffix = 1; + + while (Object.prototype.hasOwnProperty.call(collectionsConfig, key)) { + key = `${key.replace(/_\d+$/, '')}_${suffix++}`; + } + + return key; + } + /** * Generates collections config from a set of paths. * * @param collectionPaths {{ basePath: string, paths: string[] }} - * @param source {string | undefined} + * @param options {{ config?: Record; source?: string; }=} * @returns {import('../types').CollectionsConfig} */ - generateCollectionsConfig(collectionPaths, source) { + generateCollectionsConfig(collectionPaths, options) { /** @type {import('../types').CollectionsConfig} */ const collectionsConfig = {}; - const basePath = source - ? stripTopPath(collectionPaths.basePath, source) + const basePath = options?.source + ? stripTopPath(collectionPaths.basePath, options.source) : collectionPaths.basePath; for (const fullPath of collectionPaths.paths) { - const path = source ? stripTopPath(fullPath, source) : fullPath; - const key = slugify(path, { separator: '_' }) || 'pages'; + const path = stripTopPath(fullPath, options?.source); + const key = this.generateCollectionsConfigKey(path, collectionsConfig); - collectionsConfig[key] = this.generateCollectionConfig(key, path, basePath); + collectionsConfig[key] = this.generateCollectionConfig(key, path, { basePath }); } return collectionsConfig; @@ -355,4 +373,38 @@ export default class Ssg { options: {}, }; } + + /** + * Generates a list of build suggestions. + * + * @param filePaths {string[]} List of input file paths. + * @param options {{ config?: Record; source?: string; readFile?: (path: string) => Promise; }} + * @returns {Promise} + */ + async generateBuildCommands(filePaths, options) { + /** @type {import('../types').BuildCommands} */ + const commands = { install: [], build: [], output: [] }; + + const packageJsonPath = joinPaths([options.source, 'package.json']); + if (filePaths.includes(packageJsonPath)) { + commands.install.push({ + value: 'npm i', + attribution: 'because of your `package.json` file', + }); + + try { + const raw = options.readFile ? await options.readFile(packageJsonPath) : undefined; + const parsed = raw ? JSON.parse(raw) : undefined; + + if (parsed?.scripts?.build) { + commands.build.push({ + value: 'npm run build', + attribution: 'found in your `package.json` file', + }); + } + } catch (_e) {} + } + + return commands; + } } diff --git a/src/ssgs/static.js b/src/ssgs/static.js index 28c03a7..138fbad 100644 --- a/src/ssgs/static.js +++ b/src/ssgs/static.js @@ -15,17 +15,13 @@ export default class Static extends Ssg { /** * Generates collections config from a set of paths. * - * @param collectionPaths {{ basePath: string, paths: string[] }} - * @param source {string | undefined} + * @param _collectionPaths {{ basePath: string, paths: string[] }} + * @param options {{ config?: Record; source?: string; }=} * @returns {import('../types').CollectionsConfig} */ - generateCollectionsConfig(collectionPaths, source) { - const collectionsConfig = super.generateCollectionsConfig(collectionPaths, source); - - if (!Object.keys(collectionsConfig).length) { - collectionsConfig.pages = this.generateCollectionConfig('pages', ''); - } - - return collectionsConfig; + generateCollectionsConfig(_collectionPaths, options) { + return { + pages: this.generateCollectionConfig('pages', options?.source ?? ''), + }; } } diff --git a/src/types.d.ts b/src/types.d.ts index bbbcc9d..65718ee 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -20,7 +20,7 @@ export interface GenerateOptions { ssg?: SsgKey; }; /** Function to access the source contents a file. */ - readFile?: (path: string) => Promise; + readFile?: (path: string) => Promise; } export interface GenerateResult { @@ -36,3 +36,15 @@ export interface GroupedFileSummaries { } export type CollectionsConfig = Record; + +export interface BuildCommandSuggestion { + value: string; + /** Describes why this build suggestion was made */ + attribution: string; +} + +export interface BuildCommands { + install: BuildCommandSuggestion[]; + build: BuildCommandSuggestion[]; + output: BuildCommandSuggestion[]; +} diff --git a/src/utility.js b/src/utility.js index fc20038..f1cb6bb 100644 --- a/src/utility.js +++ b/src/utility.js @@ -16,17 +16,17 @@ export function last(array) { /** * Joins strings with slashes. * - * @param {string[]} paths + * @param {(string | undefined)[]} paths */ export function joinPaths(paths) { - return paths.join('/').replace(/\/+/g, '/'); + return normalisePath(paths.filter(Boolean).join('/')); } /** * Removes the first section of a path if it exists. * * @param path {string} - * @param stripPath {string} + * @param stripPath {string | undefined} * @returns {string} */ export function stripTopPath(path, stripPath) { @@ -34,12 +34,26 @@ export function stripTopPath(path, stripPath) { return ''; } + if (!stripPath) { + return path; + } + return path.startsWith(`${stripPath}/`) ? path.substring(stripPath.length + 1) : path; } +/** + * Removes duplicate, leading, and trailing slashes. + * + * @param path {string} + * @returns {string} + */ +export function normalisePath(path) { + return path.replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, ''); +} + /** * @param path {string} - * @param readFile {(path: string) => Promise} + * @param readFile {(path: string) => Promise} * @returns {Promise | undefined>} * @throws {Error} When file fails to read or parse. */ diff --git a/test/ssgs/bridgetown.test.js b/test/ssgs/bridgetown.test.js index 1caa26a..7c32bc1 100644 --- a/test/ssgs/bridgetown.test.js +++ b/test/ssgs/bridgetown.test.js @@ -7,13 +7,11 @@ const readFileMock = async (path) => { } return ''; -} +}; test('bridge', async (t) => { const bridgetown = new Bridgetown(); - const filePaths = [ - 'bridgetown.config.yml', - ]; + const filePaths = ['bridgetown.config.yml']; const config = await bridgetown.parseConfig(filePaths, readFileMock); t.deepEqual(config, { path: 'bridgetown.config.yml' }); diff --git a/test/ssgs/jekyll.test.js b/test/ssgs/jekyll.test.js index c54ebaa..0c32c71 100644 --- a/test/ssgs/jekyll.test.js +++ b/test/ssgs/jekyll.test.js @@ -24,23 +24,18 @@ const readFileMock = async (path) => { } return ''; -} +}; test('reads config', async (t) => { const jekyll = new Jekyll(); - const filePaths = [ - '_config.toml', - ]; + const filePaths = ['_config.toml']; const config = await jekyll.parseConfig(filePaths, readFileMock); t.deepEqual(config, { path: '_config.toml' }); }); test('prefers yaml over toml config', async (t) => { const jekyll = new Jekyll(); - const filePaths = [ - '_config.toml', - '_config.yaml', - ]; + const filePaths = ['_config.toml', '_config.yaml']; const config = await jekyll.parseConfig(filePaths, readFileMock); t.deepEqual(config, { path: '_config.yaml' }); @@ -48,11 +43,7 @@ test('prefers yaml over toml config', async (t) => { test('prefers yml over yaml config', async (t) => { const jekyll = new Jekyll(); - const filePaths = [ - '_config.toml', - '_config.yml', - '_config.yaml', - ]; + const filePaths = ['_config.toml', '_config.yml', '_config.yaml']; const config = await jekyll.parseConfig(filePaths, readFileMock); t.deepEqual(config, { path: '_config.yml' }); diff --git a/test/utility.test.js b/test/utility.test.js index 971c542..8b257c2 100644 --- a/test/utility.test.js +++ b/test/utility.test.js @@ -10,7 +10,7 @@ test('gets last element', (t) => { test('joins paths', (t) => { t.is(joinPaths(['first']), 'first'); t.is(joinPaths(['first', 'final']), 'first/final'); - t.is(joinPaths(['/first/', '//fi///nal']), '/first/fi/nal'); + t.is(joinPaths(['/first/', '//fi///nal']), 'first/fi/nal'); t.is(joinPaths([]), ''); }); diff --git a/toolproof_tests/core/run_gadget.toolproof.yml b/toolproof_tests/core/run_gadget.toolproof.yml index 104c764..62b2a4b 100644 --- a/toolproof_tests/core/run_gadget.toolproof.yml +++ b/toolproof_tests/core/run_gadget.toolproof.yml @@ -17,13 +17,13 @@ steps: js: |- import { fdir } from 'fdir'; import { readFile } from 'fs/promises'; - import { generate } from '@cloudcannon/gadget'; + import { generateConfiguration } from '@cloudcannon/gadget'; const crawler = new fdir().withRelativePaths().withFullPaths(); const filePaths = await crawler.crawl("src").withPromise(); filePaths.sort(); - const config = await generate(filePaths, { - readFile, + const config = await generateConfiguration(filePaths, { + readFile: (path) => path ? readFile('src/' + path) : '', config: { timezone: 'Pacific/Auckland' } diff --git a/toolproof_tests/jekyll/base.toolproof.yml b/toolproof_tests/jekyll/base.toolproof.yml new file mode 100644 index 0000000..1cc5882 --- /dev/null +++ b/toolproof_tests/jekyll/base.toolproof.yml @@ -0,0 +1,164 @@ +name: Jekyll with collections and data + +steps: + - step: I have a "src/_config.yml" file with the content {yaml} + yaml: |- + collections_dir: collections + collections: + - staff + - step: I have a "src/collections/_posts/1999-12-31-partying.md" file with the content {md} + md: |- + --- + title: Partying + --- + Yee haw + - step: I have a "src/collections/news/_posts/2000-01-01-a-new-age.md" file with the content {md} + md: |- + --- + title: No more partying + --- + - step: I have a "src/collections/data/clash.md" file with the content {md} + md: |- + --- + title: Not site.data + --- + - step: I have a "src/collections/staff/jane-doe.md" file with the content {md} + md: |- + --- + _uuid: 05589684-8d33-4d2f-8fde-460f9922d319 + name: Jane Doe + image: "https://placekitten.com/440/440?a=.png" + description: Jane has 19 years of experience in law, and specialises in property and business. + credentials: LLB + phone_extension: "02" + --- + - step: I have a "src/index.html" file with the content {html} + html: |- + + + + Home + + +

Home

+

Hello.

+ + + - step: I have a "src/_data/tags.yml" file with the content {yaml} + yaml: |- + - party + - news + - step: I have a "src/_data/animals/cat.yml" file with the content {yaml} + yaml: |- + name: Cat + - step: I have a "src/_data/animals/dog.yml" file with the content {yaml} + yaml: |- + name: Dog + - ref: ./../core/run_gadget.toolproof.yml + - snapshot: stdout + snapshot_content: |- + ╎{ + ╎ "ssg": "jekyll", + ╎ "config": { + ╎ "source": "", + ╎ "collections_config": { + ╎ "data_animals": { + ╎ "path": "_data/animals", + ╎ "name": "Animals", + ╎ "icon": "animation", + ╎ "output": false + ╎ }, + ╎ "data": { + ╎ "path": "_data", + ╎ "name": "Data", + ╎ "icon": "data_usage", + ╎ "output": false + ╎ }, + ╎ "posts": { + ╎ "path": "collections/_posts", + ╎ "name": "Posts", + ╎ "icon": "event_available", + ╎ "output": true, + ╎ "create": { + ╎ "path": "[relative_base_path]/{date|year}-{date|month}-{date|day}-{title|slugify}.[ext]" + ╎ }, + ╎ "add_options": [ + ╎ { + ╎ "name": "Add Post" + ╎ }, + ╎ { + ╎ "name": "Add Draft", + ╎ "collection": "drafts" + ╎ } + ╎ ] + ╎ }, + ╎ "data_1": { + ╎ "path": "collections/data", + ╎ "name": "Data", + ╎ "icon": "data_usage", + ╎ "output": false + ╎ }, + ╎ "news_posts": { + ╎ "path": "collections/news/_posts", + ╎ "name": "Posts", + ╎ "icon": "event_available", + ╎ "output": true, + ╎ "create": { + ╎ "path": "[relative_base_path]/{date|year}-{date|month}-{date|day}-{title|slugify}.[ext]" + ╎ }, + ╎ "add_options": [ + ╎ { + ╎ "name": "Add Post" + ╎ }, + ╎ { + ╎ "name": "Add Draft", + ╎ "collection": "news_drafts" + ╎ } + ╎ ] + ╎ }, + ╎ "staff": { + ╎ "path": "collections/staff", + ╎ "name": "Staff", + ╎ "icon": "people", + ╎ "output": false + ╎ }, + ╎ "pages": { + ╎ "path": "", + ╎ "name": "Pages", + ╎ "icon": "wysiwyg", + ╎ "output": true + ╎ }, + ╎ "drafts": { + ╎ "path": "collections/_drafts", + ╎ "name": "Drafts", + ╎ "icon": "event", + ╎ "output": false, + ╎ "create": { + ╎ "path": "", + ╎ "publish_to": "posts" + ╎ } + ╎ }, + ╎ "news_drafts": { + ╎ "path": "collections/news/_drafts", + ╎ "name": "Drafts", + ╎ "icon": "event", + ╎ "output": false, + ╎ "create": { + ╎ "path": "", + ╎ "publish_to": "news_posts" + ╎ } + ╎ } + ╎ }, + ╎ "timezone": "Pacific/Auckland", + ╎ "markdown": { + ╎ "engine": "kramdown", + ╎ "options": { + ╎ "heading_ids": false, + ╎ "gfm": false, + ╎ "breaks": false, + ╎ "typographer": false, + ╎ "treat_indentation_as_code": true + ╎ } + ╎ } + ╎ } + ╎} diff --git a/toolproof_tests/jekyll/default-collections.toolproof.yml b/toolproof_tests/jekyll/default-collections.toolproof.yml new file mode 100644 index 0000000..a5703a6 --- /dev/null +++ b/toolproof_tests/jekyll/default-collections.toolproof.yml @@ -0,0 +1,131 @@ +name: Jekyll with default collections and data + +steps: + - step: I have a "src/_config.yml" file with the content {yaml} + yaml: '' + - step: I have a "src/_posts/1999-12-31-partying.md" file with the content {md} + md: |- + --- + title: Partying + --- + Yee haw + - step: I have a "src/news/_posts/2000-01-01-a-new-age.md" file with the content {md} + md: |- + --- + title: No more partying + --- + - step: I have a "src/index.html" file with the content {html} + html: |- + + + + Home + + +

Home

+

Hello.

+ + + - step: I have a "src/_data/tags.yml" file with the content {yaml} + yaml: |- + - party + - news + - step: I have a "src/_data/animals/cat.yml" file with the content {yaml} + yaml: |- + name: Cat + - ref: ./../core/run_gadget.toolproof.yml + - snapshot: stdout + snapshot_content: |- + ╎{ + ╎ "ssg": "jekyll", + ╎ "config": { + ╎ "source": "", + ╎ "collections_config": { + ╎ "data_animals": { + ╎ "path": "_data/animals", + ╎ "name": "Animals", + ╎ "icon": "animation", + ╎ "output": false + ╎ }, + ╎ "data": { + ╎ "path": "_data", + ╎ "name": "Data", + ╎ "icon": "data_usage", + ╎ "output": false + ╎ }, + ╎ "posts": { + ╎ "path": "_posts", + ╎ "name": "Posts", + ╎ "icon": "event_available", + ╎ "output": true, + ╎ "create": { + ╎ "path": "[relative_base_path]/{date|year}-{date|month}-{date|day}-{title|slugify}.[ext]" + ╎ }, + ╎ "add_options": [ + ╎ { + ╎ "name": "Add Post" + ╎ }, + ╎ { + ╎ "name": "Add Draft", + ╎ "collection": "drafts" + ╎ } + ╎ ] + ╎ }, + ╎ "pages": { + ╎ "path": "", + ╎ "name": "Pages", + ╎ "icon": "wysiwyg", + ╎ "output": true + ╎ }, + ╎ "news_posts": { + ╎ "path": "news/_posts", + ╎ "name": "Posts", + ╎ "icon": "event_available", + ╎ "output": true, + ╎ "create": { + ╎ "path": "[relative_base_path]/{date|year}-{date|month}-{date|day}-{title|slugify}.[ext]" + ╎ }, + ╎ "add_options": [ + ╎ { + ╎ "name": "Add Post" + ╎ }, + ╎ { + ╎ "name": "Add Draft", + ╎ "collection": "news_drafts" + ╎ } + ╎ ] + ╎ }, + ╎ "drafts": { + ╎ "path": "_drafts", + ╎ "name": "Drafts", + ╎ "icon": "event", + ╎ "output": false, + ╎ "create": { + ╎ "path": "", + ╎ "publish_to": "posts" + ╎ } + ╎ }, + ╎ "news_drafts": { + ╎ "path": "news/_drafts", + ╎ "name": "Drafts", + ╎ "icon": "event", + ╎ "output": false, + ╎ "create": { + ╎ "path": "", + ╎ "publish_to": "news_posts" + ╎ } + ╎ } + ╎ }, + ╎ "timezone": "Pacific/Auckland", + ╎ "markdown": { + ╎ "engine": "kramdown", + ╎ "options": { + ╎ "heading_ids": false, + ╎ "gfm": false, + ╎ "breaks": false, + ╎ "typographer": false, + ╎ "treat_indentation_as_code": true + ╎ } + ╎ } + ╎ } + ╎} diff --git a/toolproof_tests/jekyll/empty-collection.toolproof.yml b/toolproof_tests/jekyll/empty-collection.toolproof.yml new file mode 100644 index 0000000..8a0104d --- /dev/null +++ b/toolproof_tests/jekyll/empty-collection.toolproof.yml @@ -0,0 +1,33 @@ +name: Jekyll with defined, empty collections + +steps: + - step: I have a "src/_config.yml" file with the content {yaml} + yaml: |- + collections: + has_no_files: + output: true + - ref: ./../core/run_gadget.toolproof.yml + - snapshot: stdout + snapshot_content: |- + ╎{ + ╎ "ssg": "jekyll", + ╎ "config": { + ╎ "collections_config": { + ╎ "has_no_files": { + ╎ "path": "_has_no_files", + ╎ "output": true + ╎ } + ╎ }, + ╎ "timezone": "Pacific/Auckland", + ╎ "markdown": { + ╎ "engine": "kramdown", + ╎ "options": { + ╎ "heading_ids": false, + ╎ "gfm": false, + ╎ "breaks": false, + ╎ "typographer": false, + ╎ "treat_indentation_as_code": true + ╎ } + ╎ } + ╎ } + ╎} diff --git a/toolproof_tests/jekyll/sort.toolproof.yml b/toolproof_tests/jekyll/sort.toolproof.yml new file mode 100644 index 0000000..9bc292e --- /dev/null +++ b/toolproof_tests/jekyll/sort.toolproof.yml @@ -0,0 +1,48 @@ +name: Jekyll collection using sort + +steps: + - step: I have a "src/_config.yml" file with the content {yaml} + yaml: |- + collections: + staff: + sort_by: credentials + - step: I have a "src/staff/jane-doe.md" file with the content {md} + md: |- + --- + _uuid: 05589684-8d33-4d2f-8fde-460f9922d319 + name: Jane Doe + image: "https://placekitten.com/440/440?a=.png" + description: Jane has 19 years of experience in law, and specialises in property and business. + credentials: LLB + phone_extension: "02" + --- + - ref: ./../core/run_gadget.toolproof.yml + - snapshot: stdout + snapshot_content: |- + ╎{ + ╎ "ssg": "jekyll", + ╎ "config": { + ╎ "collections_config": { + ╎ "staff": { + ╎ "path": "staff", + ╎ "name": "Staff", + ╎ "icon": "people", + ╎ "output": false, + ╎ "sort": { + ╎ "key": "credentials" + ╎ } + ╎ } + ╎ }, + ╎ "timezone": "Pacific/Auckland", + ╎ "markdown": { + ╎ "engine": "kramdown", + ╎ "options": { + ╎ "heading_ids": false, + ╎ "gfm": false, + ╎ "breaks": false, + ╎ "typographer": false, + ╎ "treat_indentation_as_code": true + ╎ } + ╎ } + ╎ } + ╎} diff --git a/toolproof_tests/vonge_jekyll.toolproof.yml b/toolproof_tests/vonge_jekyll.toolproof.yml index 38d8de2..8009d7e 100644 --- a/toolproof_tests/vonge_jekyll.toolproof.yml +++ b/toolproof_tests/vonge_jekyll.toolproof.yml @@ -16,13 +16,13 @@ steps: ╎ "icon": "data_usage", ╎ "output": false ╎ }, - ╎ "collections_pages": { + ╎ "pages": { ╎ "path": "collections/_pages", ╎ "name": "Pages", ╎ "icon": "wysiwyg", ╎ "output": true ╎ }, - ╎ "collections_posts": { + ╎ "posts": { ╎ "path": "collections/_posts", ╎ "name": "Posts", ╎ "icon": "event_available", @@ -36,36 +36,36 @@ steps: ╎ }, ╎ { ╎ "name": "Add Draft", - ╎ "collection": "collections_drafts" + ╎ "collection": "drafts" ╎ } ╎ ] ╎ }, - ╎ "collections_projects": { + ╎ "projects": { ╎ "path": "collections/_projects", ╎ "name": "Projects", ╎ "icon": "eject", ╎ "output": true ╎ }, - ╎ "collections_testimonials": { + ╎ "testimonials": { ╎ "path": "collections/_testimonials", ╎ "name": "Testimonials", ╎ "icon": "festival", - ╎ "output": true + ╎ "output": false ╎ }, - ╎ "pages": { + ╎ "pages_1": { ╎ "path": "", - ╎ "name": "Pages", - ╎ "icon": "wysiwyg", - ╎ "output": true + ╎ "name": "Pages 1", + ╎ "icon": "pages", + ╎ "output": false ╎ }, - ╎ "collections_drafts": { + ╎ "drafts": { ╎ "path": "collections/_drafts", ╎ "name": "Drafts", ╎ "icon": "event", - ╎ "output": true, + ╎ "output": false, ╎ "create": { ╎ "path": "", - ╎ "publish_to": "collections_posts" + ╎ "publish_to": "posts" ╎ } ╎ } ╎ },