UNPKG

7.42 kBJavaScriptView Raw
1const Asset = require('../Asset');
2const api = require('posthtml/lib/api');
3const urlJoin = require('../utils/urlJoin');
4const render = require('posthtml-render');
5const posthtmlTransform = require('../transforms/posthtml');
6const htmlnanoTransform = require('../transforms/htmlnano');
7const isURL = require('../utils/is-url');
8
9// A list of all attributes that may produce a dependency
10// Based on https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
11const ATTRS = {
12 src: [
13 'script',
14 'img',
15 'audio',
16 'video',
17 'source',
18 'track',
19 'iframe',
20 'embed'
21 ],
22 href: ['link', 'a', 'use'],
23 srcset: ['img', 'source'],
24 poster: ['video'],
25 'xlink:href': ['use', 'image'],
26 content: ['meta'],
27 data: ['object']
28};
29
30// A list of metadata that should produce a dependency
31// Based on:
32// - http://schema.org/
33// - http://ogp.me
34// - https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/markup
35// - https://msdn.microsoft.com/en-us/library/dn255024.aspx
36const META = {
37 property: [
38 'og:image',
39 'og:image:url',
40 'og:image:secure_url',
41 'og:audio',
42 'og:audio:secure_url',
43 'og:video',
44 'og:video:secure_url'
45 ],
46 name: [
47 'twitter:image',
48 'msapplication-square150x150logo',
49 'msapplication-square310x310logo',
50 'msapplication-square70x70logo',
51 'msapplication-wide310x150logo',
52 'msapplication-TileImage',
53 'msapplication-config'
54 ],
55 itemprop: [
56 'image',
57 'logo',
58 'screenshot',
59 'thumbnailUrl',
60 'contentUrl',
61 'downloadUrl'
62 ]
63};
64
65const SCRIPT_TYPES = {
66 'application/javascript': 'js',
67 'text/javascript': 'js',
68 'application/json': false,
69 'application/ld+json': 'jsonld',
70 'text/html': false
71};
72
73// Options to be passed to `addURLDependency` for certain tags + attributes
74const OPTIONS = {
75 a: {
76 href: {entry: true}
77 },
78 iframe: {
79 src: {entry: true}
80 }
81};
82
83class HTMLAsset extends Asset {
84 constructor(name, options) {
85 super(name, options);
86 this.type = 'html';
87 this.isAstDirty = false;
88 this.hmrPageReload = true;
89 }
90
91 async parse(code) {
92 let res = await posthtmlTransform.parse(code, this);
93 res.walk = api.walk;
94 res.match = api.match;
95 return res;
96 }
97
98 processSingleDependency(path, opts) {
99 let assetPath = this.addURLDependency(path, opts);
100 if (!isURL(assetPath)) {
101 assetPath = urlJoin(this.options.publicURL, assetPath);
102 }
103 return assetPath;
104 }
105
106 collectSrcSetDependencies(srcset, opts) {
107 const newSources = [];
108 for (const source of srcset.split(',')) {
109 const pair = source.trim().split(' ');
110 if (pair.length === 0) continue;
111 pair[0] = this.processSingleDependency(pair[0], opts);
112 newSources.push(pair.join(' '));
113 }
114 return newSources.join(',');
115 }
116
117 getAttrDepHandler(attr) {
118 if (attr === 'srcset') {
119 return this.collectSrcSetDependencies;
120 }
121 return this.processSingleDependency;
122 }
123
124 collectDependencies() {
125 let {ast} = this;
126
127 // Add bundled dependencies from plugins like posthtml-extend or posthtml-include, if any
128 if (ast.messages) {
129 ast.messages.forEach(message => {
130 if (message.type === 'dependency') {
131 this.addDependency(message.file, {
132 includedInParent: true
133 });
134 }
135 });
136 }
137
138 ast.walk(node => {
139 if (node.attrs) {
140 if (node.tag === 'meta') {
141 if (
142 !Object.keys(node.attrs).some(attr => {
143 let values = META[attr];
144
145 return (
146 values &&
147 values.includes(node.attrs[attr]) &&
148 node.attrs.content !== ''
149 );
150 })
151 ) {
152 return node;
153 }
154 }
155
156 if (
157 node.tag === 'link' &&
158 node.attrs.rel === 'manifest' &&
159 node.attrs.href
160 ) {
161 node.attrs.href = this.getAttrDepHandler('href').call(
162 this,
163 node.attrs.href,
164 {entry: true}
165 );
166 this.isAstDirty = true;
167 return node;
168 }
169
170 for (let attr in node.attrs) {
171 const attrVal = node.attrs[attr];
172
173 if (!attrVal) {
174 continue;
175 }
176
177 // Check for virtual paths
178 if (node.tag === 'a' && attrVal.lastIndexOf('.') < 1) {
179 continue;
180 }
181
182 let elements = ATTRS[attr];
183
184 if (elements && elements.includes(node.tag)) {
185 let depHandler = this.getAttrDepHandler(attr);
186 let options = OPTIONS[node.tag];
187 node.attrs[attr] = depHandler.call(
188 this,
189 attrVal,
190 options && options[attr]
191 );
192 this.isAstDirty = true;
193 }
194 }
195 }
196
197 return node;
198 });
199 }
200
201 async pretransform() {
202 await posthtmlTransform.transform(this);
203 }
204
205 async transform() {
206 if (this.options.minify) {
207 await htmlnanoTransform(this);
208 }
209 }
210
211 async generate() {
212 // Extract inline <script> and <style> tags for processing.
213 let parts = [];
214 if (this.ast) {
215 this.ast.walk(node => {
216 if (node.tag === 'script' || node.tag === 'style') {
217 let value = node.content && node.content.join('').trim();
218 if (value) {
219 let type;
220
221 if (node.tag === 'style') {
222 if (node.attrs && node.attrs.type) {
223 type = node.attrs.type.split('/')[1];
224 } else {
225 type = 'css';
226 }
227 } else if (node.attrs && node.attrs.type) {
228 // Skip JSON
229 if (SCRIPT_TYPES[node.attrs.type] === false) {
230 return node;
231 }
232
233 if (SCRIPT_TYPES[node.attrs.type]) {
234 type = SCRIPT_TYPES[node.attrs.type];
235 } else {
236 type = node.attrs.type.split('/')[1];
237 }
238 } else {
239 type = 'js';
240 }
241
242 parts.push({
243 type,
244 value,
245 inlineHTML: true,
246 meta: {
247 type: 'tag',
248 node
249 }
250 });
251 }
252 }
253
254 // Process inline style attributes.
255 if (node.attrs && node.attrs.style) {
256 parts.push({
257 type: 'css',
258 value: node.attrs.style,
259 meta: {
260 type: 'attr',
261 node
262 }
263 });
264 }
265
266 return node;
267 });
268 }
269
270 return parts;
271 }
272
273 async postProcess(generated) {
274 // Replace inline scripts and styles with processed results.
275 for (let rendition of generated) {
276 let {type, node} = rendition.meta;
277 if (type === 'attr' && rendition.type === 'css') {
278 node.attrs.style = rendition.value;
279 } else if (type === 'tag') {
280 if (rendition.isMain) {
281 node.content = rendition.value;
282 }
283
284 // Delete "type" attribute, since CSS and JS are the defaults.
285 // Unless it's application/ld+json
286 if (
287 node.attrs &&
288 (node.tag === 'style' ||
289 (node.attrs.type && SCRIPT_TYPES[node.attrs.type] === 'js'))
290 ) {
291 delete node.attrs.type;
292 }
293 }
294 }
295
296 return [
297 {
298 type: 'html',
299 value: render(this.ast)
300 }
301 ];
302 }
303}
304
305module.exports = HTMLAsset;