Skip to content

Commit

Permalink
Replace minifier (wilsonzlin/minify-html#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandrHovhannisyan committed Nov 11, 2024
1 parent cc5daed commit da3bfaa
Show file tree
Hide file tree
Showing 7 changed files with 708 additions and 640 deletions.
4 changes: 2 additions & 2 deletions .eleventy.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const { EleventyPluginCodeDemo } = require('./src');
import { EleventyPluginCodeDemo } from './src/index.js';

/**
* @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig
*/
module.exports = (eleventyConfig) => {
export default function(eleventyConfig) {
eleventyConfig.addPlugin(EleventyPluginCodeDemo, {
renderDocument: ({ html, css, js }) => `
<!DOCTYPE html>
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ This plugin adds a paired shortcode to your 11ty site that converts HTML, CSS, a

## Getting Started

### Installation
### Requirements

- Eleventy v3.0+
- ESM

> **Note**: If you are on a [macOS ARM64 device](https://github.com/wilsonzlin/minify-html/issues/119#issuecomment-1688113448), you may run into problems with installing the plugin due to a package dependency. See the linked issue for a known workaround.
### Installation

Install the package using your preferred package manager. Example command in yarn:

Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,13 @@
"test:watch": "pnpm run test --watch"
},
"dependencies": {
"@minify-html/node": "0.10.3",
"clsx": "1.2.1",
"lodash.escape": "4.0.1",
"html-minifier-terser": "^7.2.0",
"markdown-it": "13.0.1",
"outdent": "0.8.0"
},
"devDependencies": {
"@11ty/eleventy": "2.0.0",
"@11ty/eleventy": "3.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
Expand Down
1,149 changes: 594 additions & 555 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 8 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { makeCodeDemoShortcode } from './utils.js';
* @param {import('@11ty/eleventy/src/UserConfig')} eleventyConfig
* @param {import('./typedefs').EleventyPluginCodeDemoOptions} options
*/
export const EleventyPluginCodeDemo = (eleventyConfig, options) => {
export async function EleventyPluginCodeDemo(eleventyConfig, options) {
try {
eleventyConfig.versionCheck('>=3.0');
} catch (e) {
console.log(`[eleventy-plugin-code-demo] WARN Eleventy plugin compatibility: ${e.message}`);
}
const name = options.name ?? 'codeDemo';
eleventyConfig.addPairedShortcode(name, makeCodeDemoShortcode({ ...options, name }));
};
eleventyConfig.addPairedAsyncShortcode(name, makeCodeDemoShortcode({ ...options, name }));
}
47 changes: 31 additions & 16 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,63 @@
import escape from 'lodash.escape';
import minifyHtml from '@minify-html/node';
import markdownIt from 'markdown-it';
import outdent from 'outdent';
import clsx from 'clsx';
import { minify } from 'html-minifier-terser';

const charactersToEscape = new Map([
['&', '&amp;'],
['<', '&lt;'],
['>', '&gt;'],
['"', '&quot;'],
["'", '&#39;'],
]);

/**
* Ecapes special symbols in the given HTML string, returning a new string.
* @param {string} string
*/
function escapeHtml(string) {
return string.replace(/[&<>"']/g, (char) => charactersToEscape.get(char));
}

/**
* Given an array of tokens and a type of token to look up, finds all such matching tokens and returns one
* big string concatenating all of those tokens' content.
* @param {import('markdown-it/lib/token')[]} tokens
* @param {string} type
*/
const parseCode = (tokens, type) =>
tokens
function parseCode(tokens, type) {
return tokens
.filter((token) => token.info === type)
.map((token) => token.content)
.join('');
}

/** Maps a config of attribute-value pairs to an HTML string representing those same attribute-value pairs.
* There's also this, but it's ESM only: https://github.com/sindresorhus/stringify-attributes
* @param {Record<string, unknown>} attributeMap
*/
const stringifyAttributes = (attributeMap) => {
function stringifyAttributes(attributeMap) {
return Object.entries(attributeMap)
.map(([attribute, value]) => {
if (typeof value === 'undefined') return '';
return `${attribute}="${value}"`;
})
.join(' ');
};
}

/**
* Higher-order function that takes user configuration options and returns the plugin shortcode.
* @param {import('./typedefs').EleventyPluginCodeDemoOptions} options
*/
export const makeCodeDemoShortcode = (options) => {
export function makeCodeDemoShortcode(options) {
const sharedIframeAttributes = options.iframeAttributes;

/**
* @param {string} source The children of this shortcode, as Markdown code blocks.
* @param {string} title The title to set on the iframe.
* @param {Record<string, unknown>} props HTML attributes to set on this specific `<iframe>`.
*/
const codeDemoShortcode = (source, title, props = {}) => {
return async function codeDemoShortcode(source, title, props = {}) {
if (!title) {
throw new Error(`${options.name}: you must provide a non-empty title for the iframe.`);
}
Expand All @@ -61,14 +77,15 @@ export const makeCodeDemoShortcode = (options) => {
// We have to check this or Buffer.from will throw segfaults
if (srcdoc) {
// Convert all the HTML/CSS/JS into one long string with zero non-essential white space, comments, etc.
srcdoc = minifyHtml.minify(Buffer.from(srcdoc), {
keep_spaces_between_attributes: false,
srcdoc = await minify(srcdoc, {
preserveLineBreaks: false,
collapseWhitespace: true,
// Only need to minify these two if they're present
minify_css: !!css,
minify_js: !!js,
minifyCSS: !!css,
minifyJS: !!js,
});
}
srcdoc = escape(srcdoc);
srcdoc = escapeHtml(srcdoc);

let iframeAttributes = { ...sharedIframeAttributes, ...props };
/* Do this separately to allow for multiple class names. Note that this should
Expand All @@ -84,6 +101,4 @@ export const makeCodeDemoShortcode = (options) => {
iframeAttributes ? ` ${iframeAttributes}` : ''
}></iframe>`;
};

return codeDemoShortcode;
};
}
125 changes: 66 additions & 59 deletions src/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { outdent } from 'outdent';
import { makeCodeDemoShortcode } from './utils.js';

describe('makeCodeDemoShortcode', () => {
test('includes html, css, and js', () => {
test('includes html, css, and js', async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: ({ html, css, js }) => `
<!doctype html>
Expand All @@ -29,103 +29,110 @@ describe('makeCodeDemoShortcode', () => {
console.log("test");
\`\`\`
`;
const result = await shortcode(source, 'title');

assert.deepStrictEqual(
shortcode(source, 'title'),
`<iframe title="title" srcdoc="&lt;!doctypehtml&gt;&lt;style&gt;button{padding:0}&lt;/style&gt;&lt;body&gt;&lt;button&gt;Click me&lt;/button&gt;&lt;script&gt;console.log(&quot;test&quot;)&lt;/script&gt;"></iframe>`
result,
`<iframe title="title" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;button{padding:0}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;button&gt;Click me&lt;/button&gt;&lt;script&gt;console.log(&quot;test&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;"></iframe>`
);
});
describe('merges multiple code blocks of the same type', () => {
test('html', () => {
test('html', async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: ({ html }) => `
<!doctype html>
<html>
<head></head>
<body>${html}</body>
</html>`,
<!doctype html>
<html>
<head></head>
<body>${html}</body>
</html>`,
});
const source = outdent`
\`\`\`html
<button>1</button>
\`\`\`
\`\`\`html
<button>2</button>
\`\`\`
`;
\`\`\`html
<button>1</button>
\`\`\`
\`\`\`html
<button>2</button>
\`\`\`
`;
const result = await shortcode(source, 'title');
assert.deepStrictEqual(
shortcode(source, 'title'),
`<iframe title="title" srcdoc="&lt;!doctypehtml&gt;&lt;body&gt;&lt;button&gt;1&lt;/button&gt;&lt;button&gt;2&lt;/button&gt;"></iframe>`
result,
`<iframe title="title" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;button&gt;1&lt;/button&gt; &lt;button&gt;2&lt;/button&gt;&lt;/body&gt;&lt;/html&gt;"></iframe>`
);
});
test('css', () => {
test('css', async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: ({ css }) => `
<!doctype html>
<html>
<head><style>${css}</style></head>
<body></body>
</html>`,
<!doctype html>
<html>
<head><style>${css}</style></head>
<body></body>
</html>`,
});
const source = outdent`
\`\`\`css
* {
padding: 0;
}
\`\`\`
\`\`\`css
* {
margin: 0;
}
\`\`\`
`;
\`\`\`css
* {
padding: 0;
}
\`\`\`
\`\`\`css
* {
margin: 0;
}
\`\`\`
`;
const result = await shortcode(source, 'title');
assert.deepStrictEqual(
shortcode(source, 'title'),
`<iframe title="title" srcdoc="&lt;!doctypehtml&gt;&lt;style&gt;*{padding:0;margin:0}&lt;/style&gt;&lt;body&gt;"></iframe>`
result,
`<iframe title="title" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;style&gt;*{padding:0}*{margin:0}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;"></iframe>`
);
});
test('js', () => {
test('js', async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: ({ js }) => `
<!doctype html>
<html>
<head></head>
<body><script>${js}</script></body>
</html>`,
<!doctype html>
<html>
<head></head>
<body><script>${js}</script></body>
</html>`,
});
const source = outdent`
\`\`\`js
console.log("one");
\`\`\`
\`\`\`js
console.log("two");
\`\`\`
`;
\`\`\`js
console.log("one");
\`\`\`
\`\`\`js
console.log("two");
\`\`\`
`;
const result = await shortcode(source, 'title');
assert.deepStrictEqual(
shortcode(source, 'title'),
`<iframe title="title" srcdoc="&lt;!doctypehtml&gt;&lt;body&gt;&lt;script&gt;console.log(&quot;one&quot;);console.log(&quot;two&quot;)&lt;/script&gt;"></iframe>`
result,
`<iframe title="title" srcdoc="&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;script&gt;console.log(&quot;one&quot;),console.log(&quot;two&quot;)&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;"></iframe>`
);
});
});
test('respects global and per-usage attributes', () => {
test('respects global and per-usage attributes', async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: () => ``,
iframeAttributes: { class: 'one', width: '300', height: '600' },
});
const result = await shortcode(``, 'title', { class: 'two' });
assert.deepStrictEqual(
shortcode(``, 'title', { class: 'two' }),
result,
`<iframe title="title" srcdoc="" class="one two" width="300" height="600"></iframe>`
);
});
test(`removes __keywords from Nunjucks keyword argument props`, () => {
test(`removes __keywords from Nunjucks keyword argument props`, async () => {
const shortcode = makeCodeDemoShortcode({
renderDocument: () => ``,
});
assert.deepStrictEqual(shortcode(``, 'title', { __keywords: true }), `<iframe title="title" srcdoc=""></iframe>`);
const result = await shortcode(``, 'title', { __keywords: true });
assert.deepStrictEqual(result, `<iframe title="title" srcdoc=""></iframe>`);
});
test('throws an error if title is empty or undefined', () => {
const shortcode = makeCodeDemoShortcode({ renderDocument: () => `` });
assert.throws(() => shortcode(''));
assert.throws(() => shortcode('', ''));
assert.doesNotThrow(() => shortcode('', 'Non-empty title'));
assert.rejects(shortcode(''));
assert.rejects(shortcode('', ''));
assert.doesNotReject(shortcode('', 'Non-empty title'));
});
});

0 comments on commit da3bfaa

Please sign in to comment.