diff --git a/config/collections/index.js b/config/collections/index.js deleted file mode 100644 index ddb3764..0000000 --- a/config/collections/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/** All blog posts as a collection. */ -const getAllPosts = collection => { - const posts = collection.getFilteredByGlob('./src/posts/**/*.md'); - return posts.reverse(); -}; - -/** All markdown files as a collection for sitemap.xml */ -const onlyMarkdown = collection => { - return collection.getFilteredByGlob('./src/**/*.md'); -}; - -/** All tags from all posts as a collection. */ -const tagList = collection => { - const tagsSet = new Set(); - collection.getAll().forEach(item => { - if (!item.data.tags) return; - item.data.tags - .filter(tag => !['posts', 'docs', 'all'].includes(tag)) - .forEach(tag => tagsSet.add(tag)); - }); - return Array.from(tagsSet).sort(); -}; - -module.exports = { - getAllPosts, - onlyMarkdown, - tagList -}; diff --git a/config/filters/index.js b/config/filters/index.js deleted file mode 100644 index 1bb9ab4..0000000 --- a/config/filters/index.js +++ /dev/null @@ -1,90 +0,0 @@ -const dayjs = require('dayjs'); -const CleanCSS = require('clean-css'); -const site = require('../../src/_data/meta'); -const {throwIfNotType} = require('../utils'); -const esbuild = require('esbuild'); - -/** Removes all tags from an HTML string. */ -const stripHtml = str => { - throwIfNotType(str, 'string'); - return str.replace(/<[^>]+>/g, ''); -}; - -/** Formats the given string as an absolute url. */ -const toAbsoluteUrl = url => { - throwIfNotType(url, 'string'); - // Replace trailing slash, e.g., site.com/ => site.com - const siteUrl = site.url.replace(/\/$/, ''); - // Replace starting slash, e.g., /path/ => path/ - const relativeUrl = url.replace(/^\//, ''); - - return `${siteUrl}/${relativeUrl}`; -}; - -/** Converts the given date string to ISO8610 format. */ -const toISOString = dateString => dayjs(dateString).toISOString(); - -/** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */ -const formatDate = (date, format) => dayjs(date).format(format); - -const minifyCss = code => new CleanCSS({}).minify(code).styles; - -const minifyJs = async (code, ...rest) => { - const callback = rest.pop(); - const cacheKey = rest.length > 0 ? rest[0] : null; - - try { - if (cacheKey && jsminCache.hasOwnProperty(cacheKey)) { - const cacheValue = await Promise.resolve(jsminCache[cacheKey]); // Wait for the data, wrapped in a resolved promise in case the original value already was resolved - callback(null, cacheValue.code); // Access the code property of the cached value - } else { - const minified = esbuild.transform(code, { - minify: true - }); - if (cacheKey) { - jsminCache[cacheKey] = minified; // Store the promise which has the minified output (an object with a code property) - } - callback(null, (await minified).code); // Await and use the return value in the callback - } - } catch (err) { - console.error('jsmin error: ', err); - callback(null, code); // Fail gracefully. - } -}; - -// source: https://github.com/bnijenhuis/bnijenhuis-nl/blob/main/.eleventy.js -const splitlines = (input, maxCharLength) => { - const parts = input.split(' '); - const lines = parts.reduce(function (acc, cur) { - if (!acc.length) { - return [cur]; - } - - let lastOne = acc[acc.length - 1]; - - if (lastOne.length + cur.length > maxCharLength) { - return [...acc, cur]; - } - - acc[acc.length - 1] = lastOne + ' ' + cur; - - return acc; - }, []); - - return lines; -}; - -const shuffleArray = array => { - return array.sort(() => Math.random() - 0.5); -}; - -module.exports = { - toISOString, - formatDate, - toAbsoluteUrl, - stripHtml, - minifyCss, - minifyJs, - splitlines, - shuffleArray -}; diff --git a/config/shortcodes/includeRaw/index.js b/config/shortcodes/includeRaw/index.js deleted file mode 100644 index b5afd57..0000000 --- a/config/shortcodes/includeRaw/index.js +++ /dev/null @@ -1,17 +0,0 @@ -// Because Nunjucks's include doesn't like CSS with "{#". Source: https://github.com/nhoizey/pack11ty/blob/781248b92480701208f69e2161165e58d79a23ee/src/_11ty/shortcodes/include_raw.js - -const fs = require('fs'); - -let memoizedIncludes = {}; - -const includeRaw = file => { - if (file in memoizedIncludes) { - return memoizedIncludes[file]; - } else { - let content = fs.readFileSync(file, 'utf8'); - memoizedIncludes[file] = content; - return content; - } -}; - -module.exports = includeRaw; diff --git a/config/shortcodes/index.js b/config/shortcodes/index.js deleted file mode 100644 index 27bbd7e..0000000 --- a/config/shortcodes/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const imageShortcode = require('./image'); -const includeRaw = require('./includeRaw'); -const liteYoutube = require('./youtube-lite'); -module.exports = { - imageShortcode, - includeRaw, - liteYoutube -}; diff --git a/config/shortcodes/youtube-lite/index.js b/config/shortcodes/youtube-lite/index.js deleted file mode 100644 index dc2f925..0000000 --- a/config/shortcodes/youtube-lite/index.js +++ /dev/null @@ -1,10 +0,0 @@ -const liteYoutube = (id, label) => { - return ` -
- -
- `; -}; -module.exports = liteYoutube; diff --git a/config/template-languages/css-config.js b/config/template-languages/css-config.js deleted file mode 100644 index 6d32e4b..0000000 --- a/config/template-languages/css-config.js +++ /dev/null @@ -1,37 +0,0 @@ -// CSS and JavaScript as first-class citizens in Eleventy: https://pepelsbey.dev/articles/eleventy-css-js/ - -const postcss = require('postcss'); -const postcssImport = require('postcss-import'); -const postcssImportExtGlob = require('postcss-import-ext-glob'); -const tailwindcss = require('tailwindcss'); -const postcssRelativeColorSyntax = require('@csstools/postcss-relative-color-syntax'); -const autoprefixer = require('autoprefixer'); -const cssnano = require('cssnano'); - -module.exports = eleventyConfig => { - eleventyConfig.addTemplateFormats('css'); - - eleventyConfig.addExtension('css', { - outputFileExtension: 'css', - compile: async (content, path) => { - if (path !== './src/assets/css/global.css') { - return; - } - - return async () => { - let output = await postcss([ - postcssImportExtGlob, - postcssImport, - tailwindcss, - postcssRelativeColorSyntax({preserve: true}), - autoprefixer, - cssnano - ]).process(content, { - from: path - }); - - return output.css; - }; - } - }); -}; diff --git a/config/template-languages/js-config.js b/config/template-languages/js-config.js deleted file mode 100644 index ea16b9f..0000000 --- a/config/template-languages/js-config.js +++ /dev/null @@ -1,37 +0,0 @@ -const esbuild = require('esbuild'); - -module.exports = eleventyConfig => { - eleventyConfig.addTemplateFormats('js'); - - eleventyConfig.addExtension('js', { - outputFileExtension: 'js', - compile: async (content, path) => { - if (!path.startsWith('./src/assets/scripts/')) { - return; - } - - if (path === './src/assets/scripts/theme-toggle.js') { - await esbuild.build({ - target: 'es2020', - entryPoints: [path], - outfile: './src/_includes/theme-toggle-inline.js', - bundle: true, - minify: true - }); - return; - } - - return async () => { - let output = await esbuild.build({ - target: 'es2020', - entryPoints: [path], - minify: true, - bundle: true, - write: false - }); - - return output.outputFiles[0].text; - }; - } - }); -}; diff --git a/config/utils/index.js b/config/utils/index.js deleted file mode 100644 index 6403dbe..0000000 --- a/config/utils/index.js +++ /dev/null @@ -1,24 +0,0 @@ -const slugify = require('slugify'); - -/** Converts string to a slug form. */ -const slugifyString = str => { - return slugify(str, { - replacement: '-', - remove: /[#,&,+()$~%.'":*¿?¡!<>{}]/g, - lower: true - }); -}; - -/** throw an error if the provided argument is not of the expected. */ -const throwIfNotType = (arg, expectedType) => { - if (typeof arg !== expectedType) { - throw new Error( - `Expected argument of type ${expectedType} but instead got ${arg} (${typeof arg})` - ); - } -}; - -module.exports = { - slugifyString, - throwIfNotType -}; diff --git a/src/_config/collections.js b/src/_config/collections.js new file mode 100644 index 0000000..8e4be3a --- /dev/null +++ b/src/_config/collections.js @@ -0,0 +1,19 @@ +/** All blog posts as a collection. */ +export const getAllPosts = collection => { + return collection.getFilteredByGlob('./src/posts/**/*.md').reverse(); +}; + +/** All markdown files as a collection for sitemap.xml */ +export const onlyMarkdown = collection => { + return collection.getFilteredByGlob('./src/**/*.md'); +}; + +/** All tags from all posts as a collection - excluding custom collections */ +export const tagList = collection => { + const tagsSet = new Set(); + collection.getAll().forEach(item => { + if (!item.data.tags) return; + item.data.tags.filter(tag => !['posts', 'docs', 'all'].includes(tag)).forEach(tag => tagsSet.add(tag)); + }); + return Array.from(tagsSet).sort(); +}; diff --git a/src/_config/events.js b/src/_config/events.js new file mode 100644 index 0000000..80a9896 --- /dev/null +++ b/src/_config/events.js @@ -0,0 +1,5 @@ +import {svgToJpeg} from './events/svg-to-jpeg.js'; + +export default { + svgToJpeg +}; diff --git a/config/events/index.js b/src/_config/events/svg-to-jpeg.js similarity index 50% rename from config/events/index.js rename to src/_config/events/svg-to-jpeg.js index b6723c9..e9faad8 100644 --- a/config/events/index.js +++ b/src/_config/events/svg-to-jpeg.js @@ -1,24 +1,17 @@ -// https://bnijenhuis.nl/notes/automatically-generate-open-graph-images-in-eleventy/ -// https://github.com/sophiekoonin/localghost/blob/main/src/plugins/og-to-png.js -// converts SVG to JPEG for open graph images - -const fsPromises = require('fs/promises'); -const fs = require('fs'); -const path = require('path'); -const Image = require('@11ty/eleventy-img'); +import {promises as fsPromises, existsSync} from 'fs'; +import path from 'node:path'; +import Image from '@11ty/eleventy-img'; const ogImagesDir = './src/assets/og-images'; -const svgToJpeg = async function () { +export const svgToJpeg = async function () { const socialPreviewImagesDir = 'dist/assets/og-images/'; const files = await fsPromises.readdir(socialPreviewImagesDir); if (files.length > 0) { - files.forEach(function (filename) { + files.forEach(async function (filename) { const outputFilename = filename.substring(0, filename.length - 4); - if ( - filename.endsWith('.svg') & !fs.existsSync(path.join(ogImagesDir, outputFilename)) - ) { + if (filename.endsWith('.svg') & !existsSync(path.join(ogImagesDir, outputFilename))) { const imageUrl = socialPreviewImagesDir + filename; - Image(imageUrl, { + await Image(imageUrl, { formats: ['jpeg'], outputDir: ogImagesDir, filenameFormat: function (id, src, width, format, options) { @@ -31,7 +24,3 @@ const svgToJpeg = async function () { console.log('⚠ No social images found'); } }; - -module.exports = { - svgToJpeg -}; diff --git a/src/_config/filters.js b/src/_config/filters.js new file mode 100644 index 0000000..df0a556 --- /dev/null +++ b/src/_config/filters.js @@ -0,0 +1,20 @@ +import {toISOString, formatDate} from './filters/dates.js'; +import {markdownFormat} from './filters/markdown-format.js'; +import {shuffleArray} from './filters/sort-random.js'; +import {sortAlphabetically} from './filters/sort-alphabetic.js'; +import {splitlines} from './filters/splitlines.js'; +import {striptags} from './filters/striptags.js'; +import {toAbsoluteUrl} from './filters/to-absolute-url.js'; +import {slugifyString} from './filters/slugify.js'; + +export default { + toISOString, + formatDate, + markdownFormat, + splitlines, + striptags, + toAbsoluteUrl, + shuffleArray, + sortAlphabetically, + slugifyString +}; diff --git a/src/_config/filters/dates.js b/src/_config/filters/dates.js new file mode 100644 index 0000000..684fc58 --- /dev/null +++ b/src/_config/filters/dates.js @@ -0,0 +1,7 @@ +import dayjs from 'dayjs'; + +/** Converts the given date string to ISO8610 format. */ +export const toISOString = dateString => dayjs(dateString).toISOString(); + +/** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */ +export const formatDate = (date, format) => dayjs(date).format(format); diff --git a/src/_config/filters/markdown-format.js b/src/_config/filters/markdown-format.js new file mode 100644 index 0000000..2285d92 --- /dev/null +++ b/src/_config/filters/markdown-format.js @@ -0,0 +1,9 @@ +// by Chris Burnell: https://chrisburnell.com/article/some-eleventy-filters/#markdown-format + +import markdownParser from 'markdown-it'; + +const markdown = markdownParser(); + +export const markdownFormat = string => { + return markdown.render(string); +}; diff --git a/src/_config/filters/slugify.js b/src/_config/filters/slugify.js new file mode 100644 index 0000000..89d75b5 --- /dev/null +++ b/src/_config/filters/slugify.js @@ -0,0 +1,10 @@ +import slugify from 'slugify'; + +/** Converts string to a slug form. */ +export const slugifyString = str => { + return slugify(str, { + replacement: '-', + remove: /[#,&,+()$~%.'":*¿?¡!<>{}]/g, + lower: true + }); +}; diff --git a/src/_config/filters/sort-alphabetic.js b/src/_config/filters/sort-alphabetic.js new file mode 100644 index 0000000..0f12faf --- /dev/null +++ b/src/_config/filters/sort-alphabetic.js @@ -0,0 +1,7 @@ +export const sortAlphabetically = array => { + return array.sort((a, b) => { + if (a.data.title < b.data.title) return -1; + if (a.data.title > b.data.title) return 1; + return 0; + }); +}; diff --git a/src/_config/filters/sort-random.js b/src/_config/filters/sort-random.js new file mode 100644 index 0000000..baca2e0 --- /dev/null +++ b/src/_config/filters/sort-random.js @@ -0,0 +1,3 @@ +export const shuffleArray = array => { + return array.sort(() => Math.random() - 0.5); +}; diff --git a/src/_config/filters/splitlines.js b/src/_config/filters/splitlines.js new file mode 100644 index 0000000..507b72c --- /dev/null +++ b/src/_config/filters/splitlines.js @@ -0,0 +1,20 @@ +export const splitlines = (input, maxCharLength) => { + const parts = input.split(' '); + const lines = parts.reduce(function (acc, cur) { + if (!acc.length) { + return [cur]; + } + + let lastOne = acc[acc.length - 1]; + + if (lastOne.length + cur.length > maxCharLength) { + return [...acc, cur]; + } + + acc[acc.length - 1] = lastOne + ' ' + cur; + + return acc; + }, []); + + return lines; +}; diff --git a/src/_config/filters/striptags.js b/src/_config/filters/striptags.js new file mode 100644 index 0000000..b839112 --- /dev/null +++ b/src/_config/filters/striptags.js @@ -0,0 +1,3 @@ +export const striptags = string => { + return string.replace(/<[^>]*>?/gm, ''); +}; diff --git a/src/_config/filters/to-absolute-url.js b/src/_config/filters/to-absolute-url.js new file mode 100644 index 0000000..b01e71b --- /dev/null +++ b/src/_config/filters/to-absolute-url.js @@ -0,0 +1,12 @@ +import {throwIfNotType} from '../utils/throw-if-not-type.js'; +import {url as site} from '../../_data/meta.js'; + +/** Formats the given string as an absolute url. */ +export const toAbsoluteUrl = url => { + throwIfNotType(url, 'string'); + // Replace trailing slash, e.g., site.com/ => site.com + const siteUrl = site.url.replace(/\/$/, ''); + // Replace starting slash, e.g., /path/ => path/ + const relativeUrl = url.replace(/^\//, ''); + return `${siteUrl}/${relativeUrl}`; +}; diff --git a/src/_config/plugins.js b/src/_config/plugins.js new file mode 100644 index 0000000..a74642c --- /dev/null +++ b/src/_config/plugins.js @@ -0,0 +1,26 @@ +// Eleventy +import {EleventyRenderPlugin} from '@11ty/eleventy'; +import rss from '@11ty/eleventy-plugin-rss'; +import syntaxHighlight from '@11ty/eleventy-plugin-syntaxhighlight'; +import webc from '@11ty/eleventy-plugin-webc'; + +// custom +import {markdownLib} from './plugins/markdown.js'; + +// Custom transforms +import {htmlConfig} from './plugins/html-config.js'; + +// Custom template language +import {cssConfig} from './plugins/css-config.js'; +import {jsConfig} from './plugins/js-config.js'; + +export default { + EleventyRenderPlugin, + rss, + syntaxHighlight, + webc, + markdownLib, + htmlConfig, + cssConfig, + jsConfig +}; diff --git a/src/_config/plugins/css-config.js b/src/_config/plugins/css-config.js new file mode 100644 index 0000000..1c186ec --- /dev/null +++ b/src/_config/plugins/css-config.js @@ -0,0 +1,49 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import postcss from 'postcss'; +import postcssImport from 'postcss-import'; +import postcssImportExtGlob from 'postcss-import-ext-glob'; +import tailwindcss from 'tailwindcss'; +import autoprefixer from 'autoprefixer'; +import cssnano from 'cssnano'; + +export const cssConfig = eleventyConfig => { + eleventyConfig.addTemplateFormats('css'); + + eleventyConfig.addExtension('css', { + outputFileExtension: 'css', + compile: async (inputContent, inputPath) => { + const paths = []; + if (inputPath.endsWith('/src/assets/css/global/global.css')) { + paths.push('dist/assets/css/global.css'); + paths.push('src/_includes/css/global.css'); // Assuming you might want to keep naming consistent for ease + } else if (inputPath.includes('/src/assets/css/bundle/')) { + const baseName = path.basename(inputPath); + paths.push(`src/_includes/css/${baseName}`); + } else if (inputPath.includes('/src/assets/css/components/')) { + const baseName = path.basename(inputPath); + paths.push(`dist/assets/css/components/${baseName}`); + } else { + return; + } + + return async () => { + let result = await postcss([ + postcssImportExtGlob, + postcssImport, + tailwindcss, + autoprefixer, + cssnano + ]).process(inputContent, {from: inputPath}); + + // Write the output to all specified paths + for (const outputPath of paths) { + await fs.mkdir(path.dirname(outputPath), {recursive: true}); + await fs.writeFile(outputPath, result.css); + } + + return result.css; + }; + } + }); +}; diff --git a/config/transforms/html-config.js b/src/_config/plugins/html-config.js similarity index 83% rename from config/transforms/html-config.js rename to src/_config/plugins/html-config.js index dd8ffd1..288d451 100644 --- a/config/transforms/html-config.js +++ b/src/_config/plugins/html-config.js @@ -1,7 +1,8 @@ -const htmlmin = require('html-minifier-terser'); +import htmlmin from 'html-minifier-terser'; + const isProduction = process.env.ELEVENTY_ENV === 'production'; -module.exports = eleventyConfig => { +export const htmlConfig = eleventyConfig => { eleventyConfig.addTransform('html-minify', (content, path) => { if (path && path.endsWith('.html') && isProduction) { return htmlmin.minify(content, { diff --git a/src/_config/plugins/js-config.js b/src/_config/plugins/js-config.js new file mode 100644 index 0000000..e45a688 --- /dev/null +++ b/src/_config/plugins/js-config.js @@ -0,0 +1,45 @@ +import esbuild from 'esbuild'; +import path from 'node:path'; + +export const jsConfig = eleventyConfig => { + eleventyConfig.addTemplateFormats('js'); + + eleventyConfig.addExtension('js', { + outputFileExtension: 'js', + compile: async (content, inputPath) => { + // Skip processing if not in the designated scripts directories + if (!inputPath.startsWith('./src/assets/scripts/')) { + return; + } + + // Inline scripts processing + if (inputPath.startsWith('./src/assets/scripts/bundle/')) { + const filename = path.basename(inputPath); + const outputFilename = filename; + const outputPath = `./src/_includes/scripts/${outputFilename}`; + + await esbuild.build({ + target: 'es2020', + entryPoints: [inputPath], + outfile: outputPath, + bundle: true, + minify: true + }); + return; + } + + // Default handling for other scripts, excluding inline scripts + return async () => { + let output = await esbuild.build({ + target: 'es2020', + entryPoints: [inputPath], + bundle: true, + minify: true, + write: false + }); + + return output.outputFiles[0].text; + }; + } + }); +}; diff --git a/config/plugins/markdown.js b/src/_config/plugins/markdown.js similarity index 69% rename from config/plugins/markdown.js rename to src/_config/plugins/markdown.js index 6c3ee98..8d3a607 100644 --- a/config/plugins/markdown.js +++ b/src/_config/plugins/markdown.js @@ -1,17 +1,17 @@ -const markdownIt = require('markdown-it'); -const markdownItPrism = require('markdown-it-prism'); -const markdownItAnchor = require('markdown-it-anchor'); -const markdownItClass = require('@toycode/markdown-it-class'); -const markdownItLinkAttributes = require('markdown-it-link-attributes'); -const markdownItEmoji = require('markdown-it-emoji').full; -const markdownItEleventyImg = require('markdown-it-eleventy-img'); -const markdownItFootnote = require('markdown-it-footnote'); -const markdownitMark = require('markdown-it-mark'); -const markdownitAbbr = require('markdown-it-abbr'); -const {slugifyString} = require('../utils'); -const path = require('path'); +import markdownIt from 'markdown-it'; +import markdownItPrism from 'markdown-it-prism'; +import markdownItAnchor from 'markdown-it-anchor'; +import markdownItClass from '@toycode/markdown-it-class'; +import markdownItLinkAttributes from 'markdown-it-link-attributes'; +import {full as markdownItEmoji} from 'markdown-it-emoji'; +import markdownItEleventyImg from 'markdown-it-eleventy-img'; +import markdownItFootnote from 'markdown-it-footnote'; +import markdownitMark from 'markdown-it-mark'; +import markdownitAbbr from 'markdown-it-abbr'; +import {slugifyString} from '../filters/slugify.js'; +import path from 'node:path'; -const markdownLib = markdownIt({ +export const markdownLib = markdownIt({ html: true, breaks: true, linkify: true, @@ -84,5 +84,3 @@ const markdownLib = markdownIt({ .use(markdownItFootnote) .use(markdownitMark) .use(markdownitAbbr); - -module.exports = markdownLib; diff --git a/config/scripts/generate-screenshots.mjs b/src/_config/setup/generate-screenshots.js similarity index 92% rename from config/scripts/generate-screenshots.mjs rename to src/_config/setup/generate-screenshots.js index 3770c4e..ade44c2 100644 --- a/config/scripts/generate-screenshots.mjs +++ b/src/_config/setup/generate-screenshots.js @@ -5,13 +5,13 @@ import path from 'node:path'; const dataPath = './src/_data/builtwith.json'; const screenshotDir = path.join( path.dirname(new URL(import.meta.url).pathname), - '../../src/assets/images/screenshots' + '../../assets/images/screenshots' ); async function fetchScreenshot(url, filePath) { const waitCondition = 'wait:2'; const timeout = 'timeout:5'; - const apiUrl = `https://v1.screenshot.11ty.dev/${encodeURIComponent(url)}/opengraph/_${waitCondition}_${timeout}/`; + const apiUrl = `https://v1.screenshot.11ty.dev/${encodeURIComponent(url)}/large/_${waitCondition}_${timeout}/`; const buffer = await fetch(apiUrl, { duration: '1d', diff --git a/src/_config/shortcodes.js b/src/_config/shortcodes.js new file mode 100644 index 0000000..82bcec6 --- /dev/null +++ b/src/_config/shortcodes.js @@ -0,0 +1,4 @@ +import {imageShortcode} from './shortcodes/image.js'; +import {svgShortcode} from './shortcodes/svg.js'; + +export default {imageShortcode, svgShortcode}; diff --git a/config/shortcodes/image/index.js b/src/_config/shortcodes/image.js similarity index 57% rename from config/shortcodes/image/index.js rename to src/_config/shortcodes/image.js index 69b7633..03bfc02 100644 --- a/config/shortcodes/image/index.js +++ b/src/_config/shortcodes/image.js @@ -1,7 +1,12 @@ -const Image = require('@11ty/eleventy-img'); -const path = require('path'); -const htmlmin = require('html-minifier-terser'); +import Image from '@11ty/eleventy-img'; +import path from 'node:path'; +import htmlmin from 'html-minifier-terser'; +/** + * Converts an attribute map object to a string of HTML attributes. + * @param {Object} attributeMap - The attribute map object. + * @returns {string} - The string of HTML attributes. + */ const stringifyAttributes = attributeMap => { return Object.entries(attributeMap) .map(([attribute, value]) => { @@ -11,14 +16,26 @@ const stringifyAttributes = attributeMap => { .join(' '); }; -const imageShortcode = async ( +/** + * Generates an HTML image element with responsive images and optional caption. + * @param {string} src - The path to the image source file. + * @param {string} [alt=''] - The alternative text for the image. + * @param {string} [caption=''] - The caption for the image. + * @param {string} [loading='lazy'] - The loading attribute for the image. + * @param {string} [className] - The CSS class name for the image element. + * @param {string} [sizes='90vw'] - The sizes attribute for the image. + * @param {number[]} [widths=[440, 650, 960, 1200]] - The widths for generating responsive images. + * @param {string[]} [formats=['avif', 'webp', 'jpeg']] - The formats for generating responsive images. + * @returns {string} - The HTML image element. + */ +export const imageShortcode = async ( src, alt = '', - caption, + caption = '', loading = 'lazy', className, sizes = '90vw', - widths = [440, 880, 1024, 1360], + widths = [440, 650, 960, 1200], formats = ['avif', 'webp', 'jpeg'] ) => { const metadata = await Image(src, { @@ -62,7 +79,7 @@ const imageShortcode = async ( }); const imageElement = caption - ? `
+ ? `
${imageSources}
${caption}
` - : ` + : ` ${imageSources} @@ -78,5 +95,3 @@ const imageShortcode = async ( return htmlmin.minify(imageElement, {collapseWhitespace: true}); }; - -module.exports = imageShortcode; diff --git a/src/_config/shortcodes/svg.js b/src/_config/shortcodes/svg.js new file mode 100644 index 0000000..b46abe8 --- /dev/null +++ b/src/_config/shortcodes/svg.js @@ -0,0 +1,23 @@ +/** + * Generates an optimized SVG shortcode with optional attributes. + * + * @param {string} svgName - The name of the SVG file (without the .svg extension). + * @param {string} [ariaName=''] - The ARIA label for the SVG. + * @param {string} [className=''] - The CSS class name for the SVG. + * @param {string} [styleName=''] - The inline style for the SVG. + * @returns {Promise} The optimized SVG shortcode. + */ + +import {optimize} from 'svgo'; +import {readFileSync} from 'node:fs'; + +export const svgShortcode = async (svgName, ariaName = '', className = '', styleName = '') => { + const svgData = readFileSync(`./src/assets/svg/${svgName}.svg`, 'utf8'); + + const {data} = await optimize(svgData); + + return data.replace( + //, + `` + ); +}; diff --git a/src/_config/utils/clamp-generator.js b/src/_config/utils/clamp-generator.js new file mode 100644 index 0000000..0617e6b --- /dev/null +++ b/src/_config/utils/clamp-generator.js @@ -0,0 +1,43 @@ +/** + * Credits: + * - © Andy Bell - https://buildexcellentwebsit.es/ + */ + +/** + * Takes an array of tokens and sends back and array of name + * and clamp pairs for CSS fluid values. + * + * @param {array} tokens array of {name: string, min: number, max: number} + * @returns {array} {name: string, value: string} + */ + +import viewports from '../../_data/designTokens/viewports.json'; + +export const clampGenerator = tokens => { + const rootSize = 16; + + return tokens.map(({name, min, max}) => { + if (min === max) { + return `${min / rootSize}rem`; + } + + // Convert the min and max sizes to rems + const minSize = min / rootSize; + const maxSize = max / rootSize; + + // Convert the pixel viewport sizes into rems + const minViewport = viewports.min / rootSize; + const maxViewport = viewports.max / rootSize; + + // Slope and intersection allow us to have a fluid value but also keep that sensible + const slope = (maxSize - minSize) / (maxViewport - minViewport); + const intersection = -1 * minViewport * slope + minSize; + + return { + name, + value: `clamp(${minSize}rem, ${intersection.toFixed(2)}rem + ${(slope * 100).toFixed( + 2 + )}vw, ${maxSize}rem)` + }; + }); +}; diff --git a/src/_config/utils/throw-if-not-type.js b/src/_config/utils/throw-if-not-type.js new file mode 100644 index 0000000..1ed0912 --- /dev/null +++ b/src/_config/utils/throw-if-not-type.js @@ -0,0 +1,13 @@ +/** + * Throws an error if the argument is not of the expected type. + * + * @param {*} arg - The argument to check the type of. + * @param {string} expectedType - The expected type of the argument. + * @throws {Error} If the argument is not of the expected type. + */ + +export const throwIfNotType = (arg, expectedType) => { + if (typeof arg !== expectedType) { + throw new Error(`Expected argument of type ${expectedType} but instead got ${arg} (${typeof arg})`); + } +}; diff --git a/src/_config/utils/tokens-to-tailwind.js b/src/_config/utils/tokens-to-tailwind.js new file mode 100644 index 0000000..2944744 --- /dev/null +++ b/src/_config/utils/tokens-to-tailwind.js @@ -0,0 +1,24 @@ +/** + * Credits: + * - © Andy Bell - https://buildexcellentwebsit.es/ + */ + +/** + * Converts human readable tokens into tailwind config friendly ones + * + * @param {array} tokens {name: string, value: any} + * @return {object} {key, value} + */ + +import slugify from 'slugify'; + +export const tokensToTailwind = tokens => { + const nameSlug = text => slugify(text, {lower: true}); + let response = {}; + + tokens.forEach(({name, value}) => { + response[nameSlug(name)] = value; + }); + + return response; +};