@adobe/helix-html-pipeline 1.1.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,323 +0,0 @@
1
- /*
2
- * Copyright 2018 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
-
13
- import { selectAll } from 'unist-util-select';
14
- import { toHast as mdast2hast, defaultHandlers } from 'mdast-util-to-hast';
15
- import { toHtml as hast2html } from 'hast-util-to-html';
16
- import { JSDOM } from 'jsdom';
17
-
18
- import toDOM from './hast-util-to-dom.js';
19
- import HeadingHandler from './heading-handler.js';
20
- import link from './link-handler.js';
21
- import table from './table-handler.js';
22
- import icon from './icon-handler.js';
23
- import section from './section-handler.js';
24
-
25
- const types = [
26
- 'root',
27
- 'paragraph',
28
- 'text',
29
- 'heading',
30
- 'thematicBreak',
31
- 'blockquote',
32
- 'list',
33
- 'listItem',
34
- 'table',
35
- 'tableRow',
36
- 'tableCell',
37
- 'html',
38
- 'code',
39
- 'yaml',
40
- 'definition',
41
- 'footnoteDefinition',
42
- 'emphasis',
43
- 'strong',
44
- 'delete',
45
- 'inlineCode',
46
- 'break',
47
- 'link',
48
- 'image',
49
- 'linkReference',
50
- 'imageReference',
51
- 'footnote',
52
- 'footnoteReference',
53
- 'embed',
54
- 'dataEmbed',
55
- 'section',
56
- 'icon',
57
- ];
58
-
59
- /**
60
- * @typedef {function(parent, tagName, attributes, children)} handlerFunction
61
- * @param {Node} parent the root node to append the new dom node to
62
- * @param {string} tagName name of the new tag
63
- * @param {object} attributes HTML attributes as key-value pairs
64
- * @param {Node[]} children list of children
65
- */
66
-
67
- /**
68
- * Utility class that transforms an MDAST (Markdown) node into a (virtual) DOM
69
- * representation of the same content.
70
- */
71
- export default class VDOMTransformer {
72
- /**
73
- * Initializes the transformer with a Markdown document or fragment of a document
74
- * @param {Node} mdast the markdown AST node to start the transformation from.
75
- * @param {object} options options for custom transformers
76
- */
77
- constructor(mdast, options) {
78
- this._matchers = [];
79
- this._handlers = {};
80
- this._options = {};
81
-
82
- // go over all handlers that have been defined
83
-
84
- const that = this;
85
- // use our own handle function for every known node type
86
- types.map((type) => {
87
- this._handlers[type] = (cb, node, parent) => VDOMTransformer.handle(cb, node, parent, that);
88
- return true;
89
- });
90
-
91
- if (mdast) {
92
- this.withMdast(mdast);
93
- }
94
- this.withOptions(options);
95
- }
96
-
97
- withOptions(options = {}) {
98
- this._options = Object.assign(this._options, options);
99
-
100
- this._headingHandler = new HeadingHandler(options.slugger);
101
- this.match('heading', this._headingHandler.handler());
102
- this.match('link', link(this._options));
103
- this.match('icon', icon());
104
- this.match('table', table());
105
- this.match('section', section());
106
- this.match('html', (h, node) => {
107
- if (node.value.startsWith('<!--')) {
108
- return h.augment(node, {
109
- type: 'comment',
110
- value: node.value.substring(4, node.value.length - 3),
111
- });
112
- }
113
- this._hasRaw = true;
114
- const frag = JSDOM.fragment(node.value);
115
- return h.augment(node, {
116
- type: 'raw',
117
- value: '', // we ignore the value here and treat it later in hast-util-to-dom
118
- frag,
119
- html: node.value,
120
- });
121
- });
122
-
123
- return this;
124
- }
125
-
126
- withMdast(mdast) {
127
- this._root = mdast;
128
-
129
- return this;
130
- }
131
-
132
- /**
133
- * A mdast-util-to-hast handler function that applies matchers and
134
- * falls back to the default mdast-util-to-hast handlers if no matchers
135
- * apply
136
- * @private
137
- * @param {handlerFunction} cb
138
- * @param {Node} node the MDAST node to transofrm
139
- * @param {Node} parent the MDAST parent or root node for select expressions
140
- * @param {VDOMTransformer} that the MDAST to VDOM transformer
141
- * @returns {Node} a HTAST representation of the `node` input
142
- */
143
- static handle(cb, node, parent, that) {
144
- // get the function that handles this node type
145
- // this will fall back to the default if none matches
146
- const handlefn = that.matches(node);
147
-
148
- // process the node
149
-
150
- const result = handlefn(cb, node, parent);
151
- if (result && typeof result === 'string') {
152
- throw new Error('returning string from a handler is not supported yet.');
153
- } else if (result && typeof result === 'object' && result.outerHTML) {
154
- throw new Error('returning a DOM element from a handler is not supported yet.');
155
- }
156
- return result;
157
- }
158
-
159
- /**
160
- * Returns the default handler for a given node type
161
- * @param {Node} node an MDAST node
162
- * @returns {handlerFunction} the default handler function
163
- */
164
- static default(node) {
165
- // use the default handler from mdast-util-to-hast
166
- return defaultHandlers[node.type];
167
- }
168
-
169
- /**
170
- * A predicate function that filters MDAST nodes
171
- * @typedef {function(node)} matcherFunction
172
- * @param {Node} node an MDAST node
173
- * @returns {boolean} true for matching nodes
174
- */
175
-
176
- /**
177
- * Registers a handler function for nodes that match either a select expression
178
- * or a matcher predicate function. The `matcher` will be evaluated against every
179
- * node in the MDAST. In cases where the `matcher` matches (returns true), the
180
- * processor will be called with the current node.
181
- * @param {(string|matcherFunction)} matcher either an unist-util-select expression
182
- * or a predicate function
183
- * @param {handlerFunction} processor the appropriate handler function to handle matching types.
184
- * @returns {VDOMTransformer} this, enabling chaining
185
- */
186
- match(matcher, processor) {
187
- const matchfn = typeof matcher === 'function' ? matcher : VDOMTransformer.matchfn(this._root, matcher);
188
- this._matchers.push([matchfn, processor]);
189
- return this;
190
- }
191
-
192
- /**
193
- * Finds an appropriate handler for a given MDAST node
194
- * @private
195
- * @param {Node} node an MDAST node
196
- * @returns {handlerFunction} a handler function to process the node with
197
- */
198
- matches(node) {
199
- // go through all matchers to find processors where matchfn matches
200
- // start with most recently added processors
201
- for (let i = this._matchers.length - 1; i >= 0; i -= 1) {
202
- const [matchfn, processor] = this._matchers[i];
203
- if (matchfn(node, this._root)) {
204
- // return the first processor that matches
205
- return processor;
206
- }
207
- }
208
- // add the fallback processors
209
- return VDOMTransformer.default(node);
210
- }
211
-
212
- /**
213
- * Turns an unist-util-select expression into a matcher predicate function
214
- * @private
215
- * @param {Node} ast the MDAST root node to evaluated expressions against
216
- * @param {string} pattern a CSS-like unist-util-select expression
217
- * @returns {matcherFunction} a corresponding matcher function that returns true
218
- * for nodes matching the pattern
219
- */
220
- static matchfn(ast, pattern) {
221
- // evaluating selectAll on a large tree for each handler is very expensive.
222
- // use node name for simple element selectors
223
- if (/^\w+$/.test(pattern)) {
224
- return function match(node) {
225
- return node.type === pattern;
226
- };
227
- }
228
-
229
- return function match(node, myast = ast) {
230
- return selectAll(pattern, myast).indexOf(node) >= 0;
231
- };
232
- }
233
-
234
- /**
235
- * Tries to sanitize inline HTML elements. The remark parser creates `raw` nodes for html.
236
- * In case of inline html, those are not closed elements. since we generated the DOM already
237
- * in the HTML handler, we get incomplete fragments, missing the inner HTML. This method tries
238
- * to fix this. It doesn't support inner markdown-elements, though.
239
- *
240
- * @param {object} node HAST node
241
- */
242
- static sanitizeInlineHTML(node) {
243
- const stack = [];
244
- for (let i = 0; i < node.children.length; i += 1) {
245
- const child = node.children[i];
246
- if (child.type === 'raw') {
247
- if (child.frag.firstElementChild === null) {
248
- if (stack.length === 0) {
249
- // ignore unmatched inline elements
250
- } else {
251
- const last = stack.pop();
252
- let html = '';
253
- for (let j = last; j <= i; j += 1) {
254
- const innerChild = node.children[j];
255
- if (innerChild.type === 'raw') {
256
- html += innerChild.html;
257
- } else {
258
- html += hast2html(innerChild);
259
- }
260
- }
261
- node.children[last].frag = JSDOM.fragment(html);
262
- node.children[last].html = html;
263
- node.children.splice(last + 1, i - last);
264
- i = last;
265
- }
266
- } else {
267
- stack.push(i);
268
- }
269
- } else if (child.children && child.children.length) {
270
- VDOMTransformer.sanitizeInlineHTML(child);
271
- }
272
- }
273
- }
274
-
275
- /**
276
- * Turns the MDAST into a full DOM-like structure using JSDOM
277
- * @returns {Document} a full DOM document
278
- */
279
- getDocument() {
280
- // mdast -> hast; hast -> DOM using JSDOM
281
- const hast = mdast2hast(this._root, {
282
- handlers: this._handlers,
283
- allowDangerousHtml: true,
284
- });
285
-
286
- if (this._hasRaw) {
287
- VDOMTransformer.sanitizeInlineHTML(hast);
288
- }
289
-
290
- const dom = new JSDOM();
291
- const doc = dom.window.document;
292
- const frag = toDOM(doc, hast, { fragment: true });
293
-
294
- if (frag.nodeName === '#document') {
295
- // this only happens if it's an empty markdown document, so just ignore
296
- } else {
297
- doc.body.appendChild(frag);
298
- }
299
-
300
- // add convenience function to serialize entire document. this is to make it similar to the
301
- // document created in html-to-vdom.
302
- doc.serialize = dom.serialize.bind(dom);
303
-
304
- // this is a bit a hack to pass the JSDOM instance along, so that other module can use it.
305
- // this ensures that other modules can parse documents and fragments that are compatible
306
- // with this document
307
- Object.defineProperty(doc, 'JSDOM', {
308
- enumerable: false,
309
- writable: false,
310
- value: JSDOM,
311
- });
312
-
313
- // this is another hack to pass the respective window instance along. this is to ensure that
314
- // the shared prototypes can be used to check node instances (see jsdom 16.x release)
315
- Object.defineProperty(doc, 'window', {
316
- enumerable: false,
317
- writable: false,
318
- value: dom.window,
319
- });
320
-
321
- return doc;
322
- }
323
- }