Organize and document 11ty filters.

This commit is contained in:
Jonny Gerig Meyer 2023-03-08 15:59:19 -05:00
parent 35d13b27f9
commit 943a958faa
No known key found for this signature in database
GPG Key ID: FB602F738A872F7F
14 changed files with 352 additions and 138 deletions

View File

@ -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 `<pre class="${attr}"><code class="${attr}">${html}</code></pre>`;
});
// 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);

View File

@ -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",

View File

@ -0,0 +1,5 @@
declare module 'markdown-it-deflist' {
import MarkdownIt from 'markdown-it';
export default function deflist(md: MarkdownIt): void;
}

3
source/@types/typogr.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'typogr' {
export function typogrify(src: string): string;
}

View File

@ -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();

View File

@ -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[],

View File

@ -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}`;
}
};

View File

@ -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 `<pre class="${attr}"><code class="${attr}">${html}</code></pre>`;
};

9
source/helpers/dates.ts Normal file
View File

@ -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));

34
source/helpers/engines.ts Normal file
View File

@ -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,
});

15
source/helpers/page.ts Normal file
View File

@ -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;

45
source/helpers/type.ts Normal file
View File

@ -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 `<p>`.
*/
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);

View File

@ -1,4 +1,7 @@
{
"ts-node": {
"transpileOnly": true
},
"compilerOptions": {
"module": "node16",
"target": "es2022",

View File

@ -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