@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,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
|
+
/* eslint-disable no-param-reassign */
|
|
14
|
+
|
|
15
|
+
const REGEXP_ICON = /:(#?[a-zA-Z_-]+[a-zA-Z0-9]*):/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a <img> or <svg> icon dom element eg:
|
|
19
|
+
* `<img class="icon icon-smile" src="/icons/smile.svg"/>` or
|
|
20
|
+
* `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-smile"><use href="/icons.svg#smile"></use></svg>`
|
|
21
|
+
* @param {Document} document the dom document
|
|
22
|
+
* @param {string} value the identifier of the icon
|
|
23
|
+
*/
|
|
24
|
+
function createIcon(document, value) {
|
|
25
|
+
value = encodeURIComponent(value);
|
|
26
|
+
|
|
27
|
+
// icon starts with #
|
|
28
|
+
if (value.startsWith('%23')) {
|
|
29
|
+
value = value.substring(3);
|
|
30
|
+
const $el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
31
|
+
$el.classList.add('icon', `icon-${value}`);
|
|
32
|
+
const $use = document.createElement('use');
|
|
33
|
+
$use.setAttribute('href', `/icons.svg#${value}`);
|
|
34
|
+
$el.appendChild($use);
|
|
35
|
+
return $el;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// create normal image
|
|
39
|
+
const $el = document.createElement('img');
|
|
40
|
+
$el.classList.add('icon', `icon-${value}`);
|
|
41
|
+
$el.setAttribute('src', `/icons/${value}.svg`);
|
|
42
|
+
$el.setAttribute('alt', `${value} icon`);
|
|
43
|
+
return $el;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Rewrite :icons:
|
|
48
|
+
*
|
|
49
|
+
* @param {Document} document The (vdom) document
|
|
50
|
+
*/
|
|
51
|
+
function rewriteIcons(document) {
|
|
52
|
+
const { NodeFilter } = document.window;
|
|
53
|
+
const nodeIterator = document.createNodeIterator(
|
|
54
|
+
document.body,
|
|
55
|
+
NodeFilter.SHOW_TEXT,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let textNode;
|
|
59
|
+
// eslint-disable-next-line no-cond-assign
|
|
60
|
+
while (textNode = nodeIterator.nextNode()) {
|
|
61
|
+
const text = textNode.data;
|
|
62
|
+
let lastIdx = 0;
|
|
63
|
+
for (const match of text.matchAll(REGEXP_ICON)) {
|
|
64
|
+
const [matched, icon] = match;
|
|
65
|
+
const before = text.substring(lastIdx, match.index);
|
|
66
|
+
if (before) {
|
|
67
|
+
textNode.parentNode.insertBefore(document.createTextNode(before), textNode);
|
|
68
|
+
}
|
|
69
|
+
textNode.parentNode.insertBefore(createIcon(document, icon), textNode);
|
|
70
|
+
lastIdx = match.index + matched.length;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (lastIdx && lastIdx <= text.length) {
|
|
74
|
+
// there is still some text left
|
|
75
|
+
const after = text.substring(lastIdx);
|
|
76
|
+
if (after) {
|
|
77
|
+
textNode.data = after;
|
|
78
|
+
} else {
|
|
79
|
+
textNode.remove();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @type PipelineStep
|
|
87
|
+
* @param content
|
|
88
|
+
*/
|
|
89
|
+
export default function rewrite({ content }) {
|
|
90
|
+
if (content.document) {
|
|
91
|
+
rewriteIcons(content.document);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
|
|
13
|
+
import { filterGlobalMetadata } from './extract-metadata.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Array of headers allowed in the metadata.json file.
|
|
17
|
+
*/
|
|
18
|
+
const allowList = [
|
|
19
|
+
'content-security-policy',
|
|
20
|
+
'content-security-policy-report-only',
|
|
21
|
+
'access-control-allow-origin',
|
|
22
|
+
'access-control-allow-methods',
|
|
23
|
+
'link',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decorates the pipeline response object with the headers defined in metadata.json.
|
|
28
|
+
* @type PipelineStep
|
|
29
|
+
* @param {PipelineState} state
|
|
30
|
+
* @param {PipelineRequest} req
|
|
31
|
+
* @param {PipelineResponse} res
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
export default function setCustomResponseHeaders(state, req, res) {
|
|
35
|
+
const meta = filterGlobalMetadata(state.metadata, state.info.path);
|
|
36
|
+
Object.entries(meta).forEach(([name, value]) => {
|
|
37
|
+
if (allowList.includes(name)) {
|
|
38
|
+
res.headers.set(name, cleanupHeaderValue(value));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 { computeSurrogateKey } from '@adobe/helix-shared-utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @type PipelineStep
|
|
16
|
+
* @param {PipelineState} state
|
|
17
|
+
* @param {PipelineRequest} req
|
|
18
|
+
* @param {PipelineResponse} res
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
export default function setXSurrogateKeyHeader(state, req, res) {
|
|
22
|
+
const {
|
|
23
|
+
content, contentBusId, info, owner, repo, ref,
|
|
24
|
+
} = state;
|
|
25
|
+
|
|
26
|
+
const keys = [];
|
|
27
|
+
if (content.sourceLocation) {
|
|
28
|
+
keys.push(computeSurrogateKey(content.sourceLocation));
|
|
29
|
+
}
|
|
30
|
+
if (info.selector !== 'plain') {
|
|
31
|
+
keys.push(`${contentBusId}_metadata`);
|
|
32
|
+
keys.push(`${ref}--${repo}--${owner}_head`);
|
|
33
|
+
}
|
|
34
|
+
res.headers.set('x-surrogate-key', keys.join(' '));
|
|
35
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { selectAll } from 'unist-util-select';
|
|
13
|
+
|
|
14
|
+
// Compute the meta information for the section
|
|
15
|
+
function computeMeta(section) {
|
|
16
|
+
return selectAll('yaml', section).reduce((prev, { payload }) => Object.assign(prev, payload), {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Splits the sections in the mdast tree
|
|
21
|
+
* @type PipelineStep
|
|
22
|
+
* @param {PipelineState} state
|
|
23
|
+
*/
|
|
24
|
+
export default function split(state) {
|
|
25
|
+
const { content: { mdast } } = state;
|
|
26
|
+
|
|
27
|
+
// filter all children that are either yaml or break blocks
|
|
28
|
+
const dividers = mdast.children.filter((node) => node.type === 'yaml' || node.type === 'thematicBreak')
|
|
29
|
+
// then get their index in the list of children
|
|
30
|
+
.map((node) => mdast.children.indexOf(node));
|
|
31
|
+
|
|
32
|
+
// find pairwise permutations of spaces between blocks
|
|
33
|
+
// include the very start and end of the document
|
|
34
|
+
const starts = [0, ...dividers];
|
|
35
|
+
const ends = [...dividers, mdast.children.length];
|
|
36
|
+
|
|
37
|
+
// content.mdast.children = _.zip(starts, ends)
|
|
38
|
+
mdast.children = starts.map((k, i) => [k, ends[i]])
|
|
39
|
+
// but filter out empty section
|
|
40
|
+
.filter(([start, end]) => start !== end)
|
|
41
|
+
// then return all nodes that are in between
|
|
42
|
+
.map(([start, end]) => {
|
|
43
|
+
// skip 'thematicBreak' nodes
|
|
44
|
+
const index = mdast.children[start].type === 'thematicBreak' ? start + 1 : start;
|
|
45
|
+
const section = {
|
|
46
|
+
type: 'section',
|
|
47
|
+
children: mdast.children.slice(index, end),
|
|
48
|
+
};
|
|
49
|
+
section.meta = computeMeta(section);
|
|
50
|
+
return section;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// unwrap sole section directly on the root
|
|
54
|
+
if (mdast.children.length === 1 && mdast.children[0].type === 'section') {
|
|
55
|
+
mdast.children = mdast.children[0].children;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2019 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
|
+
* Serializes the response document to HTML
|
|
14
|
+
* @param {PipelineState} state
|
|
15
|
+
* @param {PipelineRequest} req
|
|
16
|
+
* @param {PipelineResponse} res
|
|
17
|
+
*/
|
|
18
|
+
export default function stringify(state, req, res) {
|
|
19
|
+
const { log } = state;
|
|
20
|
+
if (res.body) {
|
|
21
|
+
log.debug('stringify: ignoring already defined context.response.body');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const doc = res.document;
|
|
25
|
+
if (!doc) {
|
|
26
|
+
throw Error('no response document');
|
|
27
|
+
}
|
|
28
|
+
if (doc.serialize) {
|
|
29
|
+
res.body = doc.serialize();
|
|
30
|
+
} else if (doc.doctype) {
|
|
31
|
+
res.body = `<!DOCTYPE ${doc.doctype.name}>${doc.documentElement.outerHTML}`;
|
|
32
|
+
} else if (doc.documentElement) {
|
|
33
|
+
res.body = doc.documentElement.outerHTML;
|
|
34
|
+
} else if (doc.innerHTML) {
|
|
35
|
+
res.body = doc.innerHTML;
|
|
36
|
+
} else {
|
|
37
|
+
throw Error(`unexpected context.response.document: ${doc}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2020 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 { parse, resolve } from 'url';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the original host name from the request to the outer CDN.
|
|
16
|
+
* @param {object} headers The request headers
|
|
17
|
+
* @returns {string} The original host
|
|
18
|
+
*/
|
|
19
|
+
export function getOriginalHost(headers) {
|
|
20
|
+
const xfh = headers.get('x-forwarded-host');
|
|
21
|
+
if (xfh) {
|
|
22
|
+
return xfh.split(',')[0].trim();
|
|
23
|
+
}
|
|
24
|
+
return headers.get('host');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Turns a relative into an absolute URL.
|
|
29
|
+
* @param {object} headers The request headers
|
|
30
|
+
* @param {string} url The relative or absolute URL
|
|
31
|
+
* @returns {string} The absolute URL or <code>null</code>
|
|
32
|
+
* if <code>url</code> is not a string
|
|
33
|
+
*/
|
|
34
|
+
export function getAbsoluteUrl(headers, url) {
|
|
35
|
+
if (typeof url !== 'string') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return resolve(`https://${getOriginalHost(headers)}/`, url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the canonical HTML url for the give one by
|
|
43
|
+
*
|
|
44
|
+
* - removing .html extension
|
|
45
|
+
* - removing index
|
|
46
|
+
*
|
|
47
|
+
* @param {string} url
|
|
48
|
+
* @return {string} canonical url
|
|
49
|
+
*/
|
|
50
|
+
export function makeCanonicalHtmlUrl(url) {
|
|
51
|
+
if (typeof url !== 'string') {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const queryIdx = url.indexOf('?');
|
|
55
|
+
const query = queryIdx > 0 ? url.substring(queryIdx) : '';
|
|
56
|
+
let base = queryIdx > 0 ? url.substring(0, queryIdx) : url;
|
|
57
|
+
if (base.endsWith('.html')) {
|
|
58
|
+
base = base.substring(0, base.length - 5);
|
|
59
|
+
}
|
|
60
|
+
if (base.endsWith('index')) {
|
|
61
|
+
base = base.substring(0, base.length - 5);
|
|
62
|
+
}
|
|
63
|
+
return `${base}${query}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Wraps the content of $node with a new $parent node and then appends the new parent to the node.
|
|
68
|
+
*
|
|
69
|
+
* @param {DOMNode} $node The content of the node to wrap
|
|
70
|
+
* @param {DOMNode} $parent The new parent node
|
|
71
|
+
*/
|
|
72
|
+
export function wrapContent($parent, $node) {
|
|
73
|
+
$parent.append(...$node.childNodes);
|
|
74
|
+
$node.append($parent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Converts all non-valid-css-classname characters to `-`.
|
|
79
|
+
* @param {string} text input text
|
|
80
|
+
* @returns {string} the css class name
|
|
81
|
+
*/
|
|
82
|
+
export function toClassName(text) {
|
|
83
|
+
return text
|
|
84
|
+
.trim()
|
|
85
|
+
.toLowerCase()
|
|
86
|
+
.replace(/[^0-9a-z]/gi, '-');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adds the fastly-image-optimization url params to the given image src.
|
|
91
|
+
* @param {string} src The image source.
|
|
92
|
+
* @param {number} [width = 0] optional 'width' parameter
|
|
93
|
+
* @param {string} [format = 'webply'] image format.
|
|
94
|
+
* @param {string} [optimize = 'medium'] optimization.
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
export function optimizeImageURL(src, width, format = 'webply', optimize = 'medium') {
|
|
98
|
+
// use deprecated api to avoid complexity with non absolute paths
|
|
99
|
+
const url = parse(src, true);
|
|
100
|
+
delete url.search;
|
|
101
|
+
if (width) {
|
|
102
|
+
url.query.width = String(width);
|
|
103
|
+
}
|
|
104
|
+
url.query.format = format;
|
|
105
|
+
url.query.optimize = optimize;
|
|
106
|
+
return url.format();
|
|
107
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* (ISC License)
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2018 Keith McKnight <keith@mcknig.ht>
|
|
5
|
+
*
|
|
6
|
+
* Permission to use, copy, modify, and/or distribute this software for any purpose
|
|
7
|
+
* with or without fee is hereby granted, provided that the above copyright notice
|
|
8
|
+
* and this permission notice appear in all copies.
|
|
9
|
+
*
|
|
10
|
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
+
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
12
|
+
* FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
+
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
14
|
+
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
15
|
+
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
|
16
|
+
* THIS SOFTWARE.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// This was copied from https://github.com/syntax-tree/hast-util-to-dom/blob/master/src/index.js
|
|
20
|
+
// and adapted so that it can be used with JSDOM
|
|
21
|
+
// TODO: contribute back to original
|
|
22
|
+
|
|
23
|
+
/* eslint-disable header/header */
|
|
24
|
+
import { find as infoFind, html as infoHtml } from 'property-information';
|
|
25
|
+
|
|
26
|
+
const ns = {
|
|
27
|
+
html: 'http://www.w3.org/1999/xhtml',
|
|
28
|
+
};
|
|
29
|
+
/* istanbul ignore next */
|
|
30
|
+
const wrap = (document) => {
|
|
31
|
+
// Add all children.
|
|
32
|
+
function appendAll(node, children, options) {
|
|
33
|
+
const childrenLength = children.length;
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < childrenLength; i += 1) {
|
|
36
|
+
// eslint-disable-next-line no-use-before-define
|
|
37
|
+
node.appendChild(transform(children[i], options));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create a document.
|
|
44
|
+
function root(node, options) {
|
|
45
|
+
const { fragment, namespace: optionsNamespace } = options;
|
|
46
|
+
const { children = [] } = node;
|
|
47
|
+
const { length: childrenLength } = children;
|
|
48
|
+
|
|
49
|
+
let namespace = optionsNamespace;
|
|
50
|
+
let rootIsDocument = childrenLength === 0;
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < childrenLength; i += 1) {
|
|
53
|
+
const { tagName, properties = {} } = children[i];
|
|
54
|
+
|
|
55
|
+
/* c8 ignore start */
|
|
56
|
+
if (tagName === 'html') {
|
|
57
|
+
// If we have a root HTML node, we don’t need to render as a fragment.
|
|
58
|
+
rootIsDocument = true;
|
|
59
|
+
|
|
60
|
+
// Take namespace of the first child.
|
|
61
|
+
if (typeof optionsNamespace === 'undefined') {
|
|
62
|
+
namespace = properties.xmlns || ns.html;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/* c8 ignore end */
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The root node will be a Document, DocumentFragment, or HTMLElement.
|
|
69
|
+
let el;
|
|
70
|
+
|
|
71
|
+
if (rootIsDocument) {
|
|
72
|
+
el = document.implementation.createDocument(namespace, '', null);
|
|
73
|
+
} else if (fragment) {
|
|
74
|
+
el = document.createDocumentFragment();
|
|
75
|
+
} else {
|
|
76
|
+
/* c8 ignore next */
|
|
77
|
+
el = document.createElement('html');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return appendAll(el, children, { fragment, namespace, ...options });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create a `doctype`.
|
|
84
|
+
function doctype(node) {
|
|
85
|
+
return document.implementation.createDocumentType(
|
|
86
|
+
node.name || 'html',
|
|
87
|
+
node.public || '',
|
|
88
|
+
node.system || '',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create a `text`.
|
|
93
|
+
function text(node) {
|
|
94
|
+
return document.createTextNode(node.value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create a `comment`.
|
|
98
|
+
function comment(node) {
|
|
99
|
+
return document.createComment(node.value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create an `element`.
|
|
103
|
+
function element(node, options) {
|
|
104
|
+
const { namespace } = options;
|
|
105
|
+
// TODO: use `g` in SVG space.
|
|
106
|
+
const { tagName = 'div', properties = {}, children = [] } = node;
|
|
107
|
+
const el = typeof namespace !== 'undefined'
|
|
108
|
+
? document.createElementNS(namespace, tagName)
|
|
109
|
+
: document.createElement(tagName);
|
|
110
|
+
|
|
111
|
+
// Add HTML attributes.
|
|
112
|
+
const props = Object.keys(properties);
|
|
113
|
+
const { length } = props;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < length; i += 1) {
|
|
116
|
+
const key = props[i];
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
attribute,
|
|
120
|
+
property,
|
|
121
|
+
// `mustUseAttribute`,
|
|
122
|
+
mustUseProperty,
|
|
123
|
+
boolean,
|
|
124
|
+
booleanish,
|
|
125
|
+
overloadedBoolean,
|
|
126
|
+
// `number`,
|
|
127
|
+
// `defined`,
|
|
128
|
+
commaSeparated,
|
|
129
|
+
// `spaceSeparated`,
|
|
130
|
+
// `commaOrSpaceSeparated`,
|
|
131
|
+
} = infoFind(infoHtml, key);
|
|
132
|
+
|
|
133
|
+
let value = properties[key];
|
|
134
|
+
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
value = value.join(commaSeparated ? ', ' : ' ');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (mustUseProperty) {
|
|
140
|
+
el[property] = value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (boolean || (overloadedBoolean && typeof value === 'boolean')) {
|
|
144
|
+
if (value) {
|
|
145
|
+
el.setAttribute(attribute, '');
|
|
146
|
+
} else {
|
|
147
|
+
el.removeAttribute(attribute);
|
|
148
|
+
}
|
|
149
|
+
} else if (booleanish) {
|
|
150
|
+
el.setAttribute(attribute, value);
|
|
151
|
+
} else if (value === true) {
|
|
152
|
+
el.setAttribute(attribute, '');
|
|
153
|
+
} else if (value || value === 0 || value === '') {
|
|
154
|
+
el.setAttribute(attribute, value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return appendAll(el, children, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// the raw node is stored in the value directly
|
|
162
|
+
function raw(node) {
|
|
163
|
+
return node.frag;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function transform(node, options) {
|
|
167
|
+
switch (node.type) {
|
|
168
|
+
case 'root':
|
|
169
|
+
return root(node, options);
|
|
170
|
+
case 'text':
|
|
171
|
+
return text(node);
|
|
172
|
+
case 'element':
|
|
173
|
+
return element(node, options);
|
|
174
|
+
case 'doctype':
|
|
175
|
+
return doctype(node);
|
|
176
|
+
case 'comment':
|
|
177
|
+
return comment(node);
|
|
178
|
+
case 'raw':
|
|
179
|
+
return raw(node);
|
|
180
|
+
default:
|
|
181
|
+
return element(node, options);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return transform;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default function toDOM(document, hast, options = {}) {
|
|
189
|
+
return wrap(document)(hast, options);
|
|
190
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2019 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 { heading as fallback } from 'mdast-util-to-hast/lib/handlers/heading.js';
|
|
13
|
+
import { toString } from 'mdast-util-to-string';
|
|
14
|
+
import strip from 'strip-markdown';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Utility class injects heading identifiers during the MDAST to VDOM transformation.
|
|
18
|
+
*/
|
|
19
|
+
export default class HeadingHandler {
|
|
20
|
+
/**
|
|
21
|
+
* Initializes the handler
|
|
22
|
+
*/
|
|
23
|
+
constructor(slugger) {
|
|
24
|
+
this.slugger = slugger;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns the handler function
|
|
29
|
+
*/
|
|
30
|
+
handler() {
|
|
31
|
+
return (h, node) => {
|
|
32
|
+
// Prepare the heading id
|
|
33
|
+
const headingIdentifier = this.slugger.slug(toString(strip()(node)));
|
|
34
|
+
|
|
35
|
+
// Inject the id after transformation
|
|
36
|
+
const n = { ...node };
|
|
37
|
+
const el = fallback(h, n);
|
|
38
|
+
el.properties.id = el.properties.id || headingIdentifier;
|
|
39
|
+
return el;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2019 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
|
+
* Handles `icon` MDAST nodes by converting them into `img` or `<svg>` tags, e.g.
|
|
15
|
+
* `<img class="icon icon-smile" src="/icons/smile.svg"/>` or
|
|
16
|
+
* `<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-smile"><use href="/icons.svg#smile"></use></svg>`
|
|
17
|
+
* @param {string} id the identifier of the icon
|
|
18
|
+
*/
|
|
19
|
+
export default function icon() {
|
|
20
|
+
return function handler(h, node) {
|
|
21
|
+
let { value } = node;
|
|
22
|
+
value = encodeURIComponent(value);
|
|
23
|
+
if (value.startsWith('%23')) {
|
|
24
|
+
// icon starts with #
|
|
25
|
+
value = value.substring(3);
|
|
26
|
+
return [h(node, 'svg', {
|
|
27
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
28
|
+
className: `icon icon-${value}`,
|
|
29
|
+
}, [h(node, 'use', {
|
|
30
|
+
href: `/icons.svg#${value}`,
|
|
31
|
+
})])];
|
|
32
|
+
} else {
|
|
33
|
+
return [h(node, 'img', {
|
|
34
|
+
className: `icon icon-${value}`,
|
|
35
|
+
src: `/icons/${value}.svg`,
|
|
36
|
+
alt: `${value} icon`,
|
|
37
|
+
})];
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|