diff --git a/eleventy.config.js b/eleventy.config.js index 94b9421..ce47910 100644 --- a/eleventy.config.js +++ b/eleventy.config.js @@ -1,24 +1,16 @@ 'use strict'; -const path = require('path'); - const { EleventyRenderPlugin } = require('@11ty/eleventy'); const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight'); -const formatDistanceToNow = require('date-fns/formatDistanceToNow'); const yaml = require('js-yaml'); -const { Liquid } = require('liquidjs'); -const { LoremIpsum } = require('lorem-ipsum'); -const markdown = require('markdown-it'); -const markdownItAttrs = require('markdown-it-attrs'); -const markdownDefList = require('markdown-it-deflist'); -const Prism = require('prismjs'); -const PrismLoader = require('prismjs/components/index.js'); -const typogrify = require('typogr'); -const { - generateCodeExample, - getImplStatus, -} = require('./source/helpers/sass_helpers.ts'); +const codeExample = require('./source/helpers/codeExample.ts').default; +const compatibility = require('./source/helpers/compatibility.ts'); +const components = require('./source/helpers/components.ts'); +const dates = require('./source/helpers/dates.ts'); +const { liquidEngine, markdownEngine } = require('./source/helpers/engines.ts'); +const page = require('./source/helpers/page.ts'); +const type = require('./source/helpers/type.ts'); /** @param {import('@11ty/eleventy').UserConfig} eleventyConfig */ module.exports = (eleventyConfig) => { @@ -26,129 +18,49 @@ module.exports = (eleventyConfig) => { eleventyConfig.addPassthroughCopy('source/assets/img'); eleventyConfig.addPassthroughCopy('source/favicon.ico'); - const liquidEngine = new Liquid({ - root: [ - path.resolve(__dirname, 'source/_includes/'), - path.resolve(__dirname, 'source/'), - ], - extname: '.liquid', - strictFilters: true, - jsTruthy: true, - }); - - eleventyConfig.setLibrary('liquid', liquidEngine); eleventyConfig.setUseGitIgnore(false); eleventyConfig.watchIgnores.add('source/_data/versionCache.json'); - const mdown = markdown({ - html: true, - typographer: true, - }) - .use(markdownDefList) - .use(markdownItAttrs); - - eleventyConfig.setLibrary('md', mdown); + eleventyConfig.setLibrary('liquid', liquidEngine); + eleventyConfig.setLibrary('md', markdownEngine); eleventyConfig.addDataExtension('yml, yaml', (contents) => yaml.load(contents), ); - // Shortcodes... - const lorem = new LoremIpsum(); - eleventyConfig.addLiquidShortcode('lorem', (type, number = 1) => { - switch (type) { - case 'sentence': - case 'sentences': - return lorem.generateSentences(number); - case 'paragraph': - case 'paragraphs': - return lorem.generateParagraphs(number); - case 'word': - case 'words': - return lorem.generateWords(number); - } - return ''; - }); - - // Paired shortcodes... - + // Components + eleventyConfig.addPairedLiquidShortcode('code', components.codeBlock); + eleventyConfig.addPairedLiquidShortcode('codeExample', codeExample); // Ideally this could be used with named args, but that's not supported yet in // 11ty's implementation of LiquidJS: // https://github.com/11ty/eleventy/issues/2679 // In the meantime, the args are: `dart`, `libsass`, `ruby`, `feature` eleventyConfig.addPairedLiquidShortcode( 'compatibility', - async (details, dart = null, libsass = null, ruby = null, feature = null) => - liquidEngine.renderFile('compatibility', { - details, - dart, - libsass, - ruby, - feature, - }), + compatibility.compatibility, ); + eleventyConfig.addPairedLiquidShortcode('funFact', components.funFact); + eleventyConfig.addLiquidFilter('implStatus', compatibility.implStatus); + // Type + eleventyConfig.addLiquidShortcode('lorem', type.getLorem); + eleventyConfig.addPairedLiquidShortcode('markdown', type.markdown); + eleventyConfig.addLiquidFilter('markdown', type.markdown); eleventyConfig.addPairedLiquidShortcode( - 'codeExample', - async (contents, exampleName, autogenCSS = true, syntax = null) => { - const code = generateCodeExample(contents, autogenCSS, syntax); - return liquidEngine.renderFile('code_examples/code_example', { - code, - exampleName, - }); - }, + 'markdownInline', + type.markdownInline, + ); + eleventyConfig.addLiquidFilter('markdownInline', type.markdownInline); + eleventyConfig.addPairedLiquidShortcode('typogr', type.typogr); + eleventyConfig.addLiquidFilter('typogr', type.typogr); + + // Dates + eleventyConfig.addLiquidFilter( + 'formatDistanceToNow', + dates.formatDistanceToNow, ); - eleventyConfig.addPairedLiquidShortcode('funFact', async (contents) => - liquidEngine.renderFile('fun_fact', { - contents, - }), - ); - - eleventyConfig.addPairedLiquidShortcode('markdown', (content) => - mdown.render(content), - ); - - eleventyConfig.addPairedLiquidShortcode('markdownInline', (content) => - mdown.renderInline(content), - ); - - eleventyConfig.addPairedLiquidShortcode('typogr', (content) => - typogrify.typogrify(content), - ); - - eleventyConfig.addPairedLiquidShortcode('code', (content, language) => { - if (!Prism.languages[language]) { - PrismLoader(language); - } - const html = Prism.highlight(content, Prism.languages[language], language); - const attr = `language-${language}`; - return `
${html}
`; - }); - - // Filters... - eleventyConfig.addLiquidFilter('formatDistanceToNow', (date) => { - return formatDistanceToNow(new Date(date)); - }); - - eleventyConfig.addLiquidFilter('markdown', (content) => - mdown.render(content), - ); - - eleventyConfig.addLiquidFilter('markdownInline', (content) => - mdown.renderInline(content), - ); - - eleventyConfig.addLiquidFilter('typogr', (content) => - typogrify.typogrify(content), - ); - - eleventyConfig.addLiquidFilter('isTypedoc', (page) => - page.url.startsWith('/documentation/js-api/'), - ); - - eleventyConfig.addLiquidFilter('implStatus', (status) => - getImplStatus(status), - ); + // Page + eleventyConfig.addLiquidFilter('isTypedoc', page.isTypedoc); // plugins eleventyConfig.addPlugin(EleventyRenderPlugin); diff --git a/package.json b/package.json index fb6e571..65ab9d1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,10 @@ "@rollup/plugin-terser": "^0.4.0", "@types/jquery": "^3.5.16", "@types/jqueryui": "^1.12.16", + "@types/markdown-it": "^12.2.3", + "@types/markdown-it-attrs": "^4.1.0", "@types/node": "^16", + "@types/prismjs": "^1.26.0", "@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/parser": "^5.54.0", "date-fns": "^2.29.3", diff --git a/source/@types/markdown-it-deflist.d.ts b/source/@types/markdown-it-deflist.d.ts new file mode 100644 index 0000000..a444f5d --- /dev/null +++ b/source/@types/markdown-it-deflist.d.ts @@ -0,0 +1,5 @@ +declare module 'markdown-it-deflist' { + import MarkdownIt from 'markdown-it'; + + export default function deflist(md: MarkdownIt): void; +} diff --git a/source/@types/typogr.d.ts b/source/@types/typogr.d.ts new file mode 100644 index 0000000..dcb1874 --- /dev/null +++ b/source/@types/typogr.d.ts @@ -0,0 +1,3 @@ +declare module 'typogr' { + export function typogrify(src: string): string; +} diff --git a/source/_data/releases.js b/source/_data/releases.js index b6f957f..eac3d05 100644 --- a/source/_data/releases.js +++ b/source/_data/releases.js @@ -6,8 +6,10 @@ const chalk = require('kleur'); const VERSION_CACHE_PATH = './source/_data/versionCache.json'; -// Promise version of `spawn` to avoid blocking the main thread while waiting -// for the child processes +/** + * Promise version of `spawn` to avoid blocking the main thread while waiting + * for the child processes. + */ function spawn(cmd, args, options) { return new Promise((resolve, reject) => { const child = nodeSpawn(cmd, args, options); @@ -26,6 +28,9 @@ function spawn(cmd, args, options) { }); } +/** + * Retrieves cached version object from cache file. + */ async function getCacheFile() { if (process.env.NETLIFY || process.env.REBUILD_VERSION_CACHE) return {}; let versionCache; @@ -41,13 +46,18 @@ async function getCacheFile() { return versionCache; } +/** + * Writes version object to cache file. + */ async function writeCacheFile(cache) { // eslint-disable-next-line no-console console.info(chalk.green(`[11ty] Writing version cache file...`)); await fs.writeFile(VERSION_CACHE_PATH, JSON.stringify(cache)); } -// Retrieve the highest stable version of `repo`, based on its git tags +/** + * Retrieves the highest stable version of `repo`, based on its git tags. + */ async function getLatestVersion(repo) { // eslint-disable-next-line no-console console.info(chalk.cyan(`[11ty] Fetching version information for ${repo}`)); @@ -78,6 +88,10 @@ async function getLatestVersion(repo) { return version; } +/** + * Returns the version and URL for the latest release of the given + * implementation. + */ module.exports = async () => { const repos = ['sass/libsass', 'sass/dart-sass', 'sass/migrator']; const cache = await getCacheFile(); diff --git a/source/helpers/sass_helpers.ts b/source/helpers/codeExample.ts similarity index 59% rename from source/helpers/sass_helpers.ts rename to source/helpers/codeExample.ts index c2da816..6a23d87 100644 --- a/source/helpers/sass_helpers.ts +++ b/source/helpers/codeExample.ts @@ -1,6 +1,65 @@ import sass from 'sass'; -export function generateCodeExample( +import { liquidEngine } from './engines'; + +/** + * Renders a code example. + * + * This takes a block of SCSS and/or indented syntax code, and emits HTML that + * (combined with JS) will allow users to choose which to display. + * + * The SCSS should be separated from the Sass with `===`. For example, in + * LiquidJS: + * + * {% codeExample 'unique-id-string' %} + * .foo { + * color: blue; + * } + * === + * .foo + * color: blue + * {% endcodeExample %} + * + * Different sections can be separated within one syntax (for example, to + * indicate different files) with `---`. For example, in LiquidJS: + * + * {% codeExample 'unique-id-string' %} + * // _reset.scss + * * {margin: 0} + * --- + * // base.scss + * @import 'reset'; + * === + * // _reset.sass + * * + * margin: 0; + * --- + * // base.sass + * @import reset + * {% endcodeExample %} + * + * A third section may optionally be provided to represent compiled CSS. If it's + * not passed and `autogenCSS` is `true`, it's generated from the SCSS source. + * If the autogenerated CSS is empty, it's omitted entirely. + * + * If `syntax` is either `sass` or `scss`, the first section will be + * interpreted as that syntax and the second will be interpreted (or + * auto-generated) as the CSS output. + */ +export default async function codeExample( + contents: string, + exampleName: string, + autogenCSS = true, + syntax: 'sass' | 'scss' | null = null, +) { + const code = generateCodeExample(contents, autogenCSS, syntax); + return liquidEngine.renderFile('code_examples/code_example', { + code, + exampleName, + }); +} + +function generateCodeExample( contents: string, autogenCSS: boolean, syntax: 'sass' | 'scss' | null, @@ -69,20 +128,6 @@ export function generateCodeExample( }; } -export function getImplStatus(status: string | boolean | null) { - switch (status) { - case true: - return '✓'; - case false: - return '✗'; - case 'partial': - case null: - return status; - default: - return `since ${status}`; - } -} - function getCanSplit( scssExamples: string[], sassExamples: string[], diff --git a/source/helpers/compatibility.ts b/source/helpers/compatibility.ts new file mode 100644 index 0000000..5db58ed --- /dev/null +++ b/source/helpers/compatibility.ts @@ -0,0 +1,54 @@ +import { liquidEngine } from './engines'; + +/** + * Renders a status dashboard for each implementation's support for a feature. + * + * Each implementation's value can be: + * + * - `true`, indicating that that implementation fully supports the feature; + * - `false`, indicating that it does not yet support the feature at all; + * - `'partial'`, indicating that it has limited or incorrect support for the + * feature; + * - or a string, indicating the version it started supporting the feature. + * + * When possible, prefer using the start version rather than `true`. + * + * If `feature` is passed, it should be a terse (one- to three-word) description + * of the particular feature whose compatibility is described. This should be + * used whenever the status isn't referring to the entire feature being + * described by the surrounding prose. + * + * This takes an optional Markdown block (`details`) that should provide more + * information about the implementation differences or the old behavior. + */ +export const compatibility = async ( + details: string, + dart: string | boolean | null = null, + libsass: string | boolean | null = null, + ruby: string | boolean | null = null, + feature: string | null = null, +) => + liquidEngine.renderFile('compatibility', { + details, + dart, + libsass, + ruby, + feature, + }); + +/** + * Renders a single row for `compatibility`. + */ +export const implStatus = (status: string | boolean | null) => { + switch (status) { + case true: + return '✓'; + case false: + return '✗'; + case 'partial': + case null: + return status; + default: + return `since ${status}`; + } +}; diff --git a/source/helpers/components.ts b/source/helpers/components.ts new file mode 100644 index 0000000..2977116 --- /dev/null +++ b/source/helpers/components.ts @@ -0,0 +1,29 @@ +import { highlight, languages } from 'prismjs'; +import PrismLoader from 'prismjs/components/index'; + +import { liquidEngine } from './engines'; + +/** + * Returns HTML for a fun fact that's not directly relevant to the main + * documentation. + */ +export const funFact = async (contents: string) => + liquidEngine.renderFile('fun_fact', { + contents, + }); + +/** + * Returns HTML for a code block with syntax highlighting via [Prism][]. + * + * [Prism]: https://prismjs.com/ + * + * @see https://prismjs.com/ + */ +export const codeBlock = (contents: string, language: string) => { + if (!languages[language]) { + PrismLoader(language); + } + const html = highlight(contents, languages[language], language); + const attr = `language-${language}`; + return `
${html}
`; +}; diff --git a/source/helpers/dates.ts b/source/helpers/dates.ts new file mode 100644 index 0000000..25cb361 --- /dev/null +++ b/source/helpers/dates.ts @@ -0,0 +1,9 @@ +import formatDistanceToNowBase from 'date-fns/formatDistanceToNow'; + +/** + * Returns the distance between the given date and now in words. + * + * @see https://date-fns.org/docs/formatDistanceToNow + */ +export const formatDistanceToNow = (date: string) => + formatDistanceToNowBase(new Date(date)); diff --git a/source/helpers/engines.ts b/source/helpers/engines.ts new file mode 100644 index 0000000..b6fc6a8 --- /dev/null +++ b/source/helpers/engines.ts @@ -0,0 +1,34 @@ +import { Liquid } from 'liquidjs'; +import markdown from 'markdown-it'; +import markdownItAttrs from 'markdown-it-attrs'; +import markdownDefList from 'markdown-it-deflist'; +import path from 'path'; + +/** + * Returns Markdown engine with custom configuration and plugins. + * + * @see https://github.com/markdown-it/markdown-it + * @see https://github.com/markdown-it/markdown-it-deflist + * @see https://github.com/arve0/markdown-it-attrs + */ +export const markdownEngine = markdown({ + html: true, + typographer: true, +}) + .use(markdownDefList) + .use(markdownItAttrs); + +/** + * Returns LiquidJS engine with custom configuration. + * + * @see https://liquidjs.com/ + */ +export const liquidEngine = new Liquid({ + root: [ + path.resolve(__dirname, '../_includes/'), + path.resolve(__dirname, '../'), + ], + extname: '.liquid', + strictFilters: true, + jsTruthy: true, +}); diff --git a/source/helpers/page.ts b/source/helpers/page.ts new file mode 100644 index 0000000..ccd6016 --- /dev/null +++ b/source/helpers/page.ts @@ -0,0 +1,15 @@ +interface Page { + url: string | false; + fileSlug: string; + filePathStem: string; + date: Date; + inputPath: string; + outputPath: string | false; + outputFileExtension: string; +} + +/** + * Indicates whether the given page is part of the JS API documentation. + */ +export const isTypedoc = (page: Page) => + page.url ? page.url.startsWith('/documentation/js-api/') : false; diff --git a/source/helpers/type.ts b/source/helpers/type.ts new file mode 100644 index 0000000..a154ea4 --- /dev/null +++ b/source/helpers/type.ts @@ -0,0 +1,45 @@ +import { LoremIpsum } from 'lorem-ipsum'; +import { typogrify } from 'typogr'; + +import { markdownEngine } from './engines'; + +const lorem = new LoremIpsum(); + +/** + * Returns block of generated `lorem ipsum` text. + * + * @see https://github.com/knicklabs/lorem-ipsum.js + */ +export const getLorem = (type: string, number = 1) => { + switch (type) { + case 'sentence': + case 'sentences': + return lorem.generateSentences(number); + case 'paragraph': + case 'paragraphs': + return lorem.generateParagraphs(number); + case 'word': + case 'words': + return lorem.generateWords(number); + } + return ''; +}; + +/** + * Renders block of Markdown into HTML. + */ +export const markdown = (content: string) => markdownEngine.render(content); + +/** + * Renders single line of Markdown into HTML, without wrapping `

`. + */ +export const markdownInline = (content: string) => + markdownEngine.renderInline(content); + +/** + * Applies various transformations to plain text in order to yield + * typographically-improved HTML. + * + * @see https://github.com/ekalinin/typogr.js + */ +export const typogr = (content: string) => typogrify(content); diff --git a/tsconfig.json b/tsconfig.json index 5941de1..fccc0ee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "transpileOnly": true + }, "compilerOptions": { "module": "node16", "target": "es2022", diff --git a/yarn.lock b/yarn.lock index 316386a..1661734 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1837,6 +1837,39 @@ __metadata: languageName: node linkType: hard +"@types/linkify-it@npm:*": + version: 3.0.2 + resolution: "@types/linkify-it@npm:3.0.2" + checksum: dff8f10fafb885422474e456596f12d518ec4cdd6c33cca7a08e7c86b912d301ed91cf5a7613e148c45a12600dc9ab3d85ad16d5b48dc1aaeda151a68f16b536 + languageName: node + linkType: hard + +"@types/markdown-it-attrs@npm:^4.1.0": + version: 4.1.0 + resolution: "@types/markdown-it-attrs@npm:4.1.0" + dependencies: + "@types/markdown-it": "*" + checksum: a8bc1f8176ddeea8ac3f66958683a02bd84ba46ccb47fc941d10b83ac52e83595a03ff39efdf594c6614ae3f71f63b88fc62f706681c48630ec5ac9fc411b387 + languageName: node + linkType: hard + +"@types/markdown-it@npm:*, @types/markdown-it@npm:^12.2.3": + version: 12.2.3 + resolution: "@types/markdown-it@npm:12.2.3" + dependencies: + "@types/linkify-it": "*" + "@types/mdurl": "*" + checksum: 868824a3e4d00718ba9cd4762cf16694762a670860f4b402e6e9f952b6841a2027488bdc55d05c2b960bf5078df21a9d041270af7e8949514645fe88fdb722ac + languageName: node + linkType: hard + +"@types/mdurl@npm:*": + version: 1.0.2 + resolution: "@types/mdurl@npm:1.0.2" + checksum: 79c7e523b377f53cf1f5a240fe23d0c6cae856667692bd21bf1d064eafe5ccc40ae39a2aa0a9a51e8c94d1307228c8f6b121e847124591a9a828c3baf65e86e2 + languageName: node + linkType: hard + "@types/minimatch@npm:^3.0.3": version: 3.0.5 resolution: "@types/minimatch@npm:3.0.5" @@ -1865,6 +1898,13 @@ __metadata: languageName: node linkType: hard +"@types/prismjs@npm:^1.26.0": + version: 1.26.0 + resolution: "@types/prismjs@npm:1.26.0" + checksum: cd5e7a6214c1f4213ec512a5fcf6d8fe37a56b813fc57ac95b5ff5ee074742bfdbd2f2730d9fd985205bf4586728e09baa97023f739e5aa1c9735a7c1ecbd11a + languageName: node + linkType: hard + "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -6566,7 +6606,10 @@ __metadata: "@rollup/plugin-terser": ^0.4.0 "@types/jquery": ^3.5.16 "@types/jqueryui": ^1.12.16 + "@types/markdown-it": ^12.2.3 + "@types/markdown-it-attrs": ^4.1.0 "@types/node": ^16 + "@types/prismjs": ^1.26.0 "@typescript-eslint/eslint-plugin": ^5.54.0 "@typescript-eslint/parser": ^5.54.0 date-fns: ^2.29.3