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