@adobe/helix-html-pipeline 1.0.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.
- package/.eslintrc.cjs +33 -0
- package/.husky/pre-commit +4 -0
- package/.mocha-multi.json +6 -0
- package/.nycrc.json +10 -0
- package/.releaserc.cjs +16 -0
- package/CHANGELOG.md +6 -0
- package/CODE_OF_CONDUCT.md +74 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE.txt +264 -0
- package/README.md +45 -0
- package/docs/API.md +12 -0
- package/package.json +101 -0
- package/src/PipelineContent.d.ts +69 -0
- package/src/PipelineContent.js +26 -0
- package/src/PipelineRequest.d.ts +26 -0
- package/src/PipelineRequest.js +36 -0
- package/src/PipelineResponse.d.ts +32 -0
- package/src/PipelineResponse.js +44 -0
- package/src/PipelineState.d.ts +72 -0
- package/src/PipelineState.js +42 -0
- package/src/PipelineStatusError.d.ts +14 -0
- package/src/PipelineStatusError.js +17 -0
- package/src/html-pipe.js +100 -0
- package/src/index.d.ts +98 -0
- package/src/index.js +18 -0
- package/src/json-pipe.js +87 -0
- package/src/steps/add-heading-ids.js +32 -0
- package/src/steps/create-page-blocks.js +78 -0
- package/src/steps/create-pictures.js +35 -0
- package/src/steps/extract-metadata.js +257 -0
- package/src/steps/fetch-config.js +42 -0
- package/src/steps/fetch-content.js +83 -0
- package/src/steps/fetch-metadata.js +53 -0
- package/src/steps/fix-sections.js +36 -0
- package/src/steps/folder-mapping.js +61 -0
- package/src/steps/get-metadata.js +170 -0
- package/src/steps/make-html.js +34 -0
- package/src/steps/parse-markdown.js +42 -0
- package/src/steps/removeHlxProps.js +34 -0
- package/src/steps/render-code.js +25 -0
- package/src/steps/render.js +158 -0
- package/src/steps/rewrite-blob-images.js +44 -0
- package/src/steps/rewrite-icons.js +93 -0
- package/src/steps/set-custom-response-headers.js +41 -0
- package/src/steps/set-x-surrogate-key-header.js +35 -0
- package/src/steps/split-sections.js +57 -0
- package/src/steps/stringify-response.js +39 -0
- package/src/steps/utils.js +107 -0
- package/src/utils/hast-util-to-dom.js +190 -0
- package/src/utils/heading-handler.js +42 -0
- package/src/utils/icon-handler.js +40 -0
- package/src/utils/json-filter.js +143 -0
- package/src/utils/last-modified.js +48 -0
- package/src/utils/link-handler.js +25 -0
- package/src/utils/mdast-to-vdom.js +323 -0
- package/src/utils/mdast-util-gfm-nolink.js +93 -0
- package/src/utils/path.js +103 -0
- package/src/utils/remark-gfm-nolink.js +128 -0
- package/src/utils/section-handler.js +69 -0
- package/src/utils/table-handler.js +27 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2021 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
|
+
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
13
|
+
import { PipelineResponse } from '../PipelineResponse.js';
|
|
14
|
+
|
|
15
|
+
const TYPE_KEY = ':type';
|
|
16
|
+
|
|
17
|
+
const VERSION_KEY = ':version';
|
|
18
|
+
|
|
19
|
+
const NAMES_KEY = ':names';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a json response from the given data and query
|
|
23
|
+
* @param {PipelineState} state
|
|
24
|
+
* @param {object} data
|
|
25
|
+
* @param {object} query
|
|
26
|
+
* @returns {PipelineResponse}
|
|
27
|
+
*/
|
|
28
|
+
export default function jsonFilter(state, data, query) {
|
|
29
|
+
const {
|
|
30
|
+
limit = 1000,
|
|
31
|
+
offset = 0,
|
|
32
|
+
sheet = [],
|
|
33
|
+
raw,
|
|
34
|
+
} = query;
|
|
35
|
+
const { log } = state;
|
|
36
|
+
|
|
37
|
+
function filter(dataStruct) {
|
|
38
|
+
const len = Math.min(limit, dataStruct.data.length - offset);
|
|
39
|
+
const filtered = dataStruct.data.slice(offset, offset + len);
|
|
40
|
+
return {
|
|
41
|
+
total: dataStruct.total,
|
|
42
|
+
offset,
|
|
43
|
+
limit: filtered.length,
|
|
44
|
+
data: filtered,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let json;
|
|
49
|
+
try {
|
|
50
|
+
state.timer?.update('json-parse');
|
|
51
|
+
json = JSON.parse(data);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const msg = `failed to parse json: ${e.message}`;
|
|
54
|
+
if (raw) {
|
|
55
|
+
log.warn(msg);
|
|
56
|
+
return new PipelineResponse(data, {
|
|
57
|
+
headers: {
|
|
58
|
+
'content-type': 'text/plain',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
log.error(msg);
|
|
64
|
+
return new PipelineResponse('', {
|
|
65
|
+
status: 502,
|
|
66
|
+
headers: {
|
|
67
|
+
'x-error': cleanupHeaderValue(msg),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// when raw request, only handle multisheets.
|
|
73
|
+
if (raw && !(NAMES_KEY in json)) {
|
|
74
|
+
return new PipelineResponse(data, {
|
|
75
|
+
headers: {
|
|
76
|
+
'content-type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// if single sheet, convert it to multisheet
|
|
82
|
+
if (Array.isArray(json.data)) {
|
|
83
|
+
json = {
|
|
84
|
+
[NAMES_KEY]: ['default'],
|
|
85
|
+
default: json,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!json[NAMES_KEY]) {
|
|
90
|
+
const msg = 'multisheet data invalid. missing ":names" property.';
|
|
91
|
+
log.error(msg);
|
|
92
|
+
return new PipelineResponse('', {
|
|
93
|
+
status: 502,
|
|
94
|
+
headers: {
|
|
95
|
+
'x-error': cleanupHeaderValue(msg),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
state.timer?.update('json-filter');
|
|
101
|
+
const requestedSheets = Array.isArray(sheet) ? sheet : [sheet];
|
|
102
|
+
if (requestedSheets.length === 0 && 'default' in json) {
|
|
103
|
+
requestedSheets.push('default');
|
|
104
|
+
}
|
|
105
|
+
const sheetNames = [];
|
|
106
|
+
const sheets = {};
|
|
107
|
+
json[NAMES_KEY]
|
|
108
|
+
.filter((name) => requestedSheets.indexOf(name) >= 0 || requestedSheets.length === 0)
|
|
109
|
+
.forEach((name) => {
|
|
110
|
+
sheets[name] = filter(json[name]);
|
|
111
|
+
sheetNames.push(name);
|
|
112
|
+
});
|
|
113
|
+
if (sheetNames.length === 0) {
|
|
114
|
+
const msg = `filtered result does not contain selected sheet(s): ${requestedSheets.join(',')}`;
|
|
115
|
+
log.info(msg);
|
|
116
|
+
return new PipelineResponse('', {
|
|
117
|
+
status: 404,
|
|
118
|
+
headers: {
|
|
119
|
+
'x-error': cleanupHeaderValue(msg),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let body;
|
|
125
|
+
let type = 'sheet';
|
|
126
|
+
if (sheetNames.length === 1) {
|
|
127
|
+
body = sheets[sheetNames[0]];
|
|
128
|
+
} else {
|
|
129
|
+
type = 'multi-sheet';
|
|
130
|
+
body = {
|
|
131
|
+
...sheets,
|
|
132
|
+
[VERSION_KEY]: 3,
|
|
133
|
+
[NAMES_KEY]: sheetNames,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
body[TYPE_KEY] = type;
|
|
137
|
+
state.timer?.update('json-stringify');
|
|
138
|
+
return new PipelineResponse(JSON.stringify(body), {
|
|
139
|
+
headers: {
|
|
140
|
+
'content-type': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2022 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
|
+
/**
|
|
14
|
+
* Updates the context.content.lastModified if the time in `timeString` is newer than the existing
|
|
15
|
+
* one if none exists yet. please note that it generates helper property `lastModifiedTime` in
|
|
16
|
+
* unix epoch format.
|
|
17
|
+
*
|
|
18
|
+
* the date string will be a "http-date": https://httpwg.org/specs/rfc7231.html#http.date
|
|
19
|
+
*
|
|
20
|
+
* @param {PipelineState} state
|
|
21
|
+
* @param {PipelineResponse} res the pipeline context
|
|
22
|
+
* @param {string} httpDate http-date string
|
|
23
|
+
*/
|
|
24
|
+
export function updateLastModified(state, res, httpDate) {
|
|
25
|
+
if (!httpDate) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { log } = state;
|
|
29
|
+
const time = new Date(httpDate).getTime();
|
|
30
|
+
if (Number.isNaN(time)) {
|
|
31
|
+
log.warn(`updateLastModified date is invalid: ${httpDate}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (time > (res.lastModifiedTime ?? 0)) {
|
|
36
|
+
res.lastModifiedTime = time;
|
|
37
|
+
res.headers.set('last-modified', httpDate);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the last modified date from response headers, giving 'x-amz-meta-x-source-last-modified'
|
|
43
|
+
* preference.
|
|
44
|
+
* @param {object} headers
|
|
45
|
+
*/
|
|
46
|
+
export function extractLastModified(headers) {
|
|
47
|
+
return headers.get('x-amz-meta-x-source-last-modified') ?? headers.get('last-modified');
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
import { link as fallback } from 'mdast-util-to-hast/lib/handlers/link.js';
|
|
13
|
+
import { parse, serialize } from 'uri-js';
|
|
14
|
+
|
|
15
|
+
export default function link({ extension = 'html' } = {}) {
|
|
16
|
+
return function handler(h, node) {
|
|
17
|
+
const n = { ...node };
|
|
18
|
+
const uriParts = parse(n.url);
|
|
19
|
+
if (!uriParts.scheme && uriParts.path) {
|
|
20
|
+
uriParts.path = uriParts.path.replace(/\.md$/, `.${extension}`);
|
|
21
|
+
n.url = serialize(uriParts, { absolutePath: true });
|
|
22
|
+
}
|
|
23
|
+
return fallback(h, n);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2021 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
|
+
/*
|
|
14
|
+
copied from
|
|
15
|
+
https://github.com/micromark/micromark-extension-gfm/blob/7bc6b6f3baf941f877d7b2111e9257b21b13b37e/index.js
|
|
16
|
+
|
|
17
|
+
(The MIT License)
|
|
18
|
+
|
|
19
|
+
Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
|
|
20
|
+
|
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
22
|
+
a copy of this software and associated documentation files (the
|
|
23
|
+
'Software'), to deal in the Software without restriction, including
|
|
24
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
25
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
26
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
27
|
+
the following conditions:
|
|
28
|
+
|
|
29
|
+
The above copyright notice and this permission notice shall be
|
|
30
|
+
included in all copies or substantial portions of the Software.
|
|
31
|
+
|
|
32
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
33
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
34
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
35
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
36
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
37
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
38
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension
|
|
43
|
+
* @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
|
|
44
|
+
*
|
|
45
|
+
* @typedef {import('mdast-util-gfm-table').Options} Options
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
// import {
|
|
49
|
+
// gfmAutolinkLiteralFromMarkdown,
|
|
50
|
+
// gfmAutolinkLiteralToMarkdown
|
|
51
|
+
// } from 'mdast-util-gfm-autolink-literal'
|
|
52
|
+
import {
|
|
53
|
+
gfmFootnoteFromMarkdown,
|
|
54
|
+
gfmFootnoteToMarkdown,
|
|
55
|
+
} from 'mdast-util-gfm-footnote';
|
|
56
|
+
import {
|
|
57
|
+
gfmStrikethroughFromMarkdown,
|
|
58
|
+
gfmStrikethroughToMarkdown,
|
|
59
|
+
} from 'mdast-util-gfm-strikethrough';
|
|
60
|
+
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
|
61
|
+
import {
|
|
62
|
+
gfmTaskListItemFromMarkdown,
|
|
63
|
+
gfmTaskListItemToMarkdown,
|
|
64
|
+
} from 'mdast-util-gfm-task-list-item';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @returns {Array.<FromMarkdownExtension>}
|
|
68
|
+
*/
|
|
69
|
+
export function gfmFromMarkdown() {
|
|
70
|
+
return [
|
|
71
|
+
gfmFootnoteFromMarkdown(),
|
|
72
|
+
gfmStrikethroughFromMarkdown,
|
|
73
|
+
gfmTableFromMarkdown,
|
|
74
|
+
// gfmAutolinkLiteralFromMarkdown,
|
|
75
|
+
gfmTaskListItemFromMarkdown,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {Options} [options]
|
|
81
|
+
* @returns {ToMarkdownExtension}
|
|
82
|
+
*/
|
|
83
|
+
export function gfmToMarkdown(options) {
|
|
84
|
+
return {
|
|
85
|
+
extensions: [
|
|
86
|
+
// gfmAutolinkLiteralToMarkdown,
|
|
87
|
+
gfmFootnoteToMarkdown(),
|
|
88
|
+
gfmStrikethroughToMarkdown,
|
|
89
|
+
gfmTableToMarkdown(options),
|
|
90
|
+
gfmTaskListItemToMarkdown,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|