Skip to content

Commit ac18b3d

Browse files
feat(index): use posthtml for HTML processing
1 parent 6a52f85 commit ac18b3d

File tree

2 files changed

+104
-151
lines changed

2 files changed

+104
-151
lines changed

src/index.js

+89-151
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,100 @@
1-
/* eslint-disable
2-
import/order,
3-
import/first,
4-
arrow-parens,
5-
no-undefined,
6-
no-param-reassign,
7-
no-useless-escape,
8-
*/
9-
import LoaderError from './Error';
10-
import loaderUtils from 'loader-utils';
1+
/* eslint-disable */
2+
import { getOptions } from 'loader-utils';
113
import validateOptions from 'schema-utils';
124

13-
import url from 'url';
14-
import attrs from './lib/attrs';
15-
import minifier from 'html-minifier';
5+
import posthtml from 'posthtml';
6+
import urls from './lib/plugins/url';
7+
import imports from './lib/plugins/import';
8+
import minifier from 'htmlnano';
169

17-
const schema = require('./options');
10+
import schema from './options.json';
11+
import LoaderError from './lib/Error';
1812

19-
function randomize() {
20-
return `link__${Math.random()}`;
21-
}
13+
// Loader Defaults
14+
const defaults = {
15+
url: true,
16+
import: true,
17+
minimize: false,
18+
template: false,
19+
};
2220

23-
export default function loader(html) {
24-
const options = loaderUtils.getOptions(this) || {};
21+
export default function loader(html, map, meta) {
22+
// Loader Options
23+
const options = Object.assign(defaults, getOptions(this));
2524

2625
validateOptions(schema, options, 'HTML Loader');
27-
28-
// eslint-disable-next-line
29-
const root = options.root;
30-
31-
let attributes = ['img:src'];
32-
33-
if (options.attrs !== undefined) {
34-
if (typeof options.attrs === 'string') attributes = options.attrs.split(' ');
35-
else if (Array.isArray(options.attrs)) attributes = options.attrs;
36-
else if (options.attrs === false) attributes = [];
37-
else {
38-
throw new LoaderError({
39-
name: 'AttributesError',
40-
message: 'Invalid attribute value found',
41-
});
42-
}
43-
}
44-
45-
const links = attrs(html, (tag, attr) => {
46-
const item = `${tag}:${attr}`;
47-
48-
const result = attributes.find((a) => item.indexOf(a) >= 0);
49-
50-
return !!result;
51-
});
52-
53-
links.reverse();
54-
55-
const data = {};
56-
57-
html = [html];
58-
59-
links.forEach((link) => {
60-
if (!loaderUtils.isUrlRequest(link.value, root)) return;
61-
62-
const uri = url.parse(link.value);
63-
64-
if (uri.hash !== null && uri.hash !== undefined) {
65-
uri.hash = null;
66-
67-
link.value = uri.format();
68-
link.length = link.value.length;
69-
}
70-
71-
let ident;
72-
do { ident = randomize(); } while (data[ident]);
73-
data[ident] = link.value;
74-
75-
const item = html.pop();
76-
77-
html.push(item.substr(link.start + link.length));
78-
// eslint-disable-next-line
79-
html.push(ident);
80-
html.push(item.substr(0, link.start));
81-
});
82-
83-
html = html.reverse().join('');
84-
85-
if (options.interpolate === 'require') {
86-
const regex = /\$\{require\([^)]*\)\}/g;
87-
// eslint-disable-next-line
88-
let result;
89-
90-
const requires = [];
91-
92-
// eslint-disable-next-line
93-
while (result = regex.exec(html)) {
94-
requires.push({
95-
length: result[0].length,
96-
start: result.index,
97-
value: result[0],
98-
});
99-
}
100-
101-
requires.reverse();
102-
103-
html = [html];
104-
105-
requires.forEach((link) => {
106-
const item = html.pop();
107-
108-
let ident;
109-
do { ident = randomize(); } while (data[ident]);
110-
data[ident] = link.value.substring(11, link.length - 3);
111-
112-
html.push(item.substr(link.start + link.length));
113-
// eslint-disable-next-line
114-
html.push(ident);
115-
html.push(item.substr(0, link.start));
116-
});
117-
118-
html = html.reverse().join('');
26+
// Make the loader async
27+
const cb = this.async();
28+
const file = this.resourcePath;
29+
30+
// HACK add Module.type
31+
this._module.type = 'text/html';
32+
33+
const template = options.template
34+
? typeof options.template === 'string'
35+
? options.template
36+
: '_'
37+
: false;
38+
39+
const plugins = [];
40+
41+
if (options.url) plugins.push(urls());
42+
if (options.import) plugins.push(imports({ template }));
43+
// TODO(michael-ciniawsky)
44+
// <imports src=""./file.html"> aren't minified (#160)
45+
if (options.minimize) plugins.push(minifier());
46+
47+
// Reuse HTML AST (PostHTML AST) if available
48+
// (e.g posthtml-loader) to avoid HTML reparsing
49+
if (meta && meta.ast && meta.ast.type === 'posthtml') {
50+
html = meta.ast.root;
11951
}
12052

121-
if (options.minimize || this.minimize) {
122-
let minimize = Object.create({
123-
collapseWhitespace: true,
124-
conservativeCollapse: true,
125-
useShortDoctype: true,
126-
keepClosingSlash: true,
127-
minifyJS: true,
128-
minifyCSS: true,
129-
removeComments: true,
130-
removeAttributeQuotes: true,
131-
removeStyleTypeAttributes: true,
132-
removeScriptTypeAttributes: true,
133-
removeCommentsFromCDATA: true,
134-
removeCDATASectionsFromCDATA: true,
53+
posthtml(plugins)
54+
.process(html, { from: file, to: file })
55+
.then(({ html, messages }) => {
56+
let urls = messages[0];
57+
let imports = messages[1];
58+
59+
// TODO(michael-ciniawsky) revisit
60+
// Ensure to cleanup/reset messages
61+
// during recursive resolving of imports
62+
messages.length = 0;
63+
64+
// <img src="./file.png">
65+
// => import HTML__URL__${idx} from './file.png';
66+
if (urls) {
67+
urls = Object.keys(urls)
68+
.map(url => `import ${url} from '${urls[url]}';`)
69+
.join('\n');
70+
}
71+
// <import src="./file.html">
72+
// => import HTML__IMPORT__${idx} from './file.html';
73+
if (imports) {
74+
imports = Object.keys(imports)
75+
.map(i => `import ${i} from '${imports[i]}';`)
76+
.join('\n');
77+
}
78+
79+
html = options.template
80+
? `function (${template}) { return \`${html}\`; }`
81+
: `\`${html}\``;
82+
83+
const result = [
84+
urls ? `// HTML URLs\n${urls}\n` : false,
85+
imports ? `// HTML Imports\n${imports}\n` : false,
86+
`// HTML\nexport default ${html}`,
87+
]
88+
.filter(Boolean)
89+
.join('\n');
90+
91+
cb(null, result);
92+
93+
return null;
94+
})
95+
.catch((err) => {
96+
cb(new LoaderError(err));
97+
98+
return null;
13599
});
136-
137-
if (typeof options.minimize === 'object') {
138-
minimize = Object.assign(minimize, options.minimize);
139-
}
140-
141-
html = minifier.minify(html, minimize);
142-
}
143-
144-
// TODO
145-
// #120 - Support exporting a template function
146-
//
147-
// import template from 'file.html'
148-
//
149-
// const html = template({...locals})
150-
if (options.interpolate && options.interpolate !== 'require') {
151-
html = `${html}`;
152-
} else {
153-
html = JSON.stringify(html);
154-
}
155-
156-
html = html.replace(/link__[0-9\.]+/g, (match) => {
157-
if (!data[match]) return match;
158-
return `"require('${JSON.stringify(loaderUtils.urlToRequest(data[match], root))}')"`;
159-
});
160-
161-
return `export default ${html};`;
162100
}

src/lib/Error.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class LoaderError extends Error {
2+
constructor(err) {
3+
super(err);
4+
5+
this.name = 'HTML Loader';
6+
this.message = `\n\n${err.message}\n`;
7+
8+
// TODO(michael-ciniawsky)
9+
// add 'SyntaxError', 'PluginError', 'PluginWarning'
10+
11+
Error.captureStackTrace(this, this.constructor);
12+
}
13+
}
14+
15+
export default LoaderError;

0 commit comments

Comments
 (0)