@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.
Files changed (60) hide show
  1. package/.eslintrc.cjs +33 -0
  2. package/.husky/pre-commit +4 -0
  3. package/.mocha-multi.json +6 -0
  4. package/.nycrc.json +10 -0
  5. package/.releaserc.cjs +16 -0
  6. package/CHANGELOG.md +6 -0
  7. package/CODE_OF_CONDUCT.md +74 -0
  8. package/CONTRIBUTING.md +74 -0
  9. package/LICENSE.txt +264 -0
  10. package/README.md +45 -0
  11. package/docs/API.md +12 -0
  12. package/package.json +101 -0
  13. package/src/PipelineContent.d.ts +69 -0
  14. package/src/PipelineContent.js +26 -0
  15. package/src/PipelineRequest.d.ts +26 -0
  16. package/src/PipelineRequest.js +36 -0
  17. package/src/PipelineResponse.d.ts +32 -0
  18. package/src/PipelineResponse.js +44 -0
  19. package/src/PipelineState.d.ts +72 -0
  20. package/src/PipelineState.js +42 -0
  21. package/src/PipelineStatusError.d.ts +14 -0
  22. package/src/PipelineStatusError.js +17 -0
  23. package/src/html-pipe.js +100 -0
  24. package/src/index.d.ts +98 -0
  25. package/src/index.js +18 -0
  26. package/src/json-pipe.js +87 -0
  27. package/src/steps/add-heading-ids.js +32 -0
  28. package/src/steps/create-page-blocks.js +78 -0
  29. package/src/steps/create-pictures.js +35 -0
  30. package/src/steps/extract-metadata.js +257 -0
  31. package/src/steps/fetch-config.js +42 -0
  32. package/src/steps/fetch-content.js +83 -0
  33. package/src/steps/fetch-metadata.js +53 -0
  34. package/src/steps/fix-sections.js +36 -0
  35. package/src/steps/folder-mapping.js +61 -0
  36. package/src/steps/get-metadata.js +170 -0
  37. package/src/steps/make-html.js +34 -0
  38. package/src/steps/parse-markdown.js +42 -0
  39. package/src/steps/removeHlxProps.js +34 -0
  40. package/src/steps/render-code.js +25 -0
  41. package/src/steps/render.js +158 -0
  42. package/src/steps/rewrite-blob-images.js +44 -0
  43. package/src/steps/rewrite-icons.js +93 -0
  44. package/src/steps/set-custom-response-headers.js +41 -0
  45. package/src/steps/set-x-surrogate-key-header.js +35 -0
  46. package/src/steps/split-sections.js +57 -0
  47. package/src/steps/stringify-response.js +39 -0
  48. package/src/steps/utils.js +107 -0
  49. package/src/utils/hast-util-to-dom.js +190 -0
  50. package/src/utils/heading-handler.js +42 -0
  51. package/src/utils/icon-handler.js +40 -0
  52. package/src/utils/json-filter.js +143 -0
  53. package/src/utils/last-modified.js +48 -0
  54. package/src/utils/link-handler.js +25 -0
  55. package/src/utils/mdast-to-vdom.js +323 -0
  56. package/src/utils/mdast-util-gfm-nolink.js +93 -0
  57. package/src/utils/path.js +103 -0
  58. package/src/utils/remark-gfm-nolink.js +128 -0
  59. package/src/utils/section-handler.js +69 -0
  60. package/src/utils/table-handler.js +27 -0
@@ -0,0 +1,36 @@
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 { wrapContent } from './utils.js';
13
+
14
+ /**
15
+ * fixes the sections of a document.
16
+ * @type PipelineStep
17
+ * @param {PipelineContent} content
18
+ */
19
+ export default async function fixSections({ content }) {
20
+ const { document } = content;
21
+ const $sections = document.querySelectorAll('body > div');
22
+
23
+ // if there are no sections wrap everything in a div with appropriate class names from meta
24
+ if ($sections.length === 0) {
25
+ const $outerDiv = document.createElement('div');
26
+ if (content.meta && content.meta.class) {
27
+ content.meta.class.split(/[ ,]/)
28
+ .map((c) => c.trim())
29
+ .filter((c) => !!c)
30
+ .forEach((c) => {
31
+ $outerDiv.classList.add(c);
32
+ });
33
+ }
34
+ wrapContent($outerDiv, document.body);
35
+ }
36
+ }
@@ -0,0 +1,61 @@
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 { extname } from 'path';
13
+ import { getPathInfo } from '../utils/path.js';
14
+
15
+ /**
16
+ * Finds the mapping from path to folders in fstab
17
+ * @param {object} folders folder/path mapping
18
+ * @param {string} path given path
19
+ * @returns {null|string} returns the mapped path or null
20
+ */
21
+ export function mapPath(folders, path) {
22
+ for (const [folder, mapping] of Object.entries(folders)) {
23
+ if (path === folder) {
24
+ return mapping;
25
+ }
26
+ if (folder.endsWith('/') && path.startsWith(folder)) {
27
+ return mapping;
28
+ }
29
+ if (path.startsWith(`${folder}/`)) {
30
+ return mapping;
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /**
37
+ * Checks the fstab for folder mapping entries and then re-adjusts the path infos if needed.
38
+ * if the remapped resource is *not* extensionless, it will be declared as code-bus resource.
39
+ *
40
+ * @type PipelineStep
41
+ * @param {PipelineState} state
42
+ */
43
+ export default function folderMapping(state) {
44
+ const folders = state.helixConfig?.fstab?.folders;
45
+ if (!folders) {
46
+ return;
47
+ }
48
+ const { path } = state.info;
49
+ const mapped = mapPath(folders, path);
50
+ if (mapped) {
51
+ state.info = getPathInfo(mapped);
52
+ if (extname(mapped)) {
53
+ // special case: use code-bus
54
+ state.content.sourceBus = 'code';
55
+ state.info.resourcePath = mapped;
56
+ state.log.info(`mapped ${path} to ${state.info.resourcePath} (${state.content.sourceBus}-bus)`);
57
+ } else {
58
+ state.log.info(`mapped ${path} to ${state.info.path} (${state.content.sourceBus}-bus)`);
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,170 @@
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 { select, selectAll } from 'unist-util-select';
13
+ import { toString as plain } from 'mdast-util-to-string';
14
+
15
+ function yaml(section) {
16
+ section.meta = selectAll('yaml', section)
17
+ .reduce((prev, { payload }) => Object.assign(prev, payload), {});
18
+ return section;
19
+ }
20
+
21
+ function title(section) {
22
+ const header = select('heading', section);
23
+ section.title = header ? plain(header) : '';
24
+ }
25
+
26
+ function intro(section) {
27
+ const para = selectAll('paragraph', section).filter((p) => {
28
+ if ((!p.children || p.children.length === 0)
29
+ || (p.children.length === 1 && p.children[0].type === 'image')) {
30
+ return false;
31
+ }
32
+ return true;
33
+ })[0];
34
+ section.intro = para ? plain(para) : '';
35
+ }
36
+
37
+ function image(section) {
38
+ // selects the most prominent image of the section
39
+ // TODO: get a better measure of prominence than "first"
40
+ const img = select('image', section);
41
+ if (img) {
42
+ section.image = img.url;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Construct the strings corresponding to the number of occurences per type.
48
+ * @param {Object} typecounter Type as a key, number of occurences as value
49
+ */
50
+ function constructTypes(typecounter) {
51
+ const types = Object.keys(typecounter).map((type) => `has-${type}`); // has-{type}
52
+ types.push(...Object.keys(typecounter).map((type) => `nb-${type}-${typecounter[type]}`)); // nb-{type}-{nb-occurences}
53
+ if (Object.keys(typecounter).length === 1) {
54
+ types.push(`has-only-${Object.keys(typecounter)[0]}`);
55
+ } else {
56
+ types.push(...Object.entries(typecounter) // get pairs of type, count
57
+ // sort first descending by count, then alphabetical by key if count is the same
58
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
59
+ // take the top three
60
+ .slice(0, 3)
61
+ // keep only the type
62
+ .map(([name]) => name)
63
+ // generate names
64
+ .reduce((names, name) => [`${names[0] || 'is'}-${name}`, ...names], []));
65
+ }
66
+ return types;
67
+ }
68
+
69
+ /**
70
+ * Sets the `types` attribute of the section, using following patterns:
71
+ * 1. has-<type> for every type of content found in the section
72
+ * 2. is-<type>-only for sections that have only content of type
73
+ * 3. is-<type1>-<type2>-<type3> ranks the top three most common types of content
74
+ * 4. nb-<type>-<nb_occurences> is the number of occurences per type
75
+ * @param {*} section
76
+ */
77
+ function sectiontype(section) {
78
+ const children = section.children || [];
79
+
80
+ function reducer(counter, node) {
81
+ const { type, children: pChildren } = node;
82
+
83
+ node.meta = { types: [], ...node.meta };
84
+
85
+ if (type === 'yaml') {
86
+ return counter;
87
+ }
88
+
89
+ const mycounter = {};
90
+
91
+ if (type === 'paragraph' && pChildren && pChildren.length > 0) {
92
+ // if child is a paragraph, check its children, it might contain an image or a list
93
+ // which are always wrapped by default.
94
+ pChildren.forEach((p) => {
95
+ let prefix = 'has';
96
+ if (p.type === 'text') {
97
+ // do not count "empty" paragraphs
98
+ if (p.value === '\n' || p.value === '') return;
99
+
100
+ // paragraph with type text "is" a text
101
+ prefix = 'is';
102
+ }
103
+ if (!node.meta.types.includes(`${prefix}-${p.type}`)) {
104
+ node.meta.types.push(`${prefix}-${p.type}`);
105
+ }
106
+ const mycount = mycounter[p.type] || 0;
107
+ mycounter[p.type] = mycount + 1;
108
+ });
109
+ }
110
+
111
+ if (type === 'list' && pChildren && pChildren.length > 0) {
112
+ // if list, analyze the children of its children (listitems)
113
+ let listtypecounter = {};
114
+ pChildren.forEach((listitem) => {
115
+ listtypecounter = listitem.children.reduce(reducer, listtypecounter);
116
+ });
117
+ constructTypes(listtypecounter).forEach((item) => node.meta.types.push(item));
118
+ }
119
+
120
+ if (Object.keys(mycounter).length === 0) {
121
+ // was really a paragraph, only text inside
122
+ const mycount = mycounter[type] || 0;
123
+ mycounter[type] = mycount + 1;
124
+ node.meta.types.push(`is-${type}`);
125
+ }
126
+
127
+ Object.keys(counter).forEach((key) => {
128
+ mycounter[key] = counter[key] + (mycounter[key] || 0);
129
+ });
130
+ return mycounter;
131
+ }
132
+
133
+ const typecounter = children.reduce(reducer, {});
134
+ section.meta.types = constructTypes(typecounter);
135
+ }
136
+
137
+ function fallback(section) {
138
+ if (section.intro && !section.title) {
139
+ section.title = section.intro;
140
+ } else if (section.title && !section.intro) {
141
+ section.intro = section.title;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Extract the metadata from the mdast
147
+ * @type PipelineStep
148
+ * @param {PipelineState} state
149
+ */
150
+ export default function getMetadata(state) {
151
+ const { content } = state;
152
+ const { mdast: { children = [] } } = content;
153
+ let sections = children.filter((node) => node.type === 'section');
154
+ if (!sections.length) {
155
+ sections = [content.mdast];
156
+ }
157
+
158
+ [yaml, title, intro, image, sectiontype, fallback].forEach((fn) => {
159
+ sections.forEach(fn);
160
+ });
161
+
162
+ const img = sections.filter((section) => section.image)[0];
163
+ const titl = sections.filter((section) => section.title)[0];
164
+
165
+ // todo: cleanup meta data confusion
166
+ content.meta = sections[0].meta;
167
+ content.title = titl?.title ?? '';
168
+ content.intro = sections[0].intro;
169
+ content.image = img?.image ?? undefined;
170
+ }
@@ -0,0 +1,34 @@
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 GithubSlugger from 'github-slugger';
14
+ import VDOMTransformer from '../utils/mdast-to-vdom.js';
15
+
16
+ /**
17
+ * Converts the markdown to a jsdom dom and stores it in `content.document`
18
+ * @type PipelineStep
19
+ * @param {PipelineState} state
20
+ */
21
+ export default function html(state) {
22
+ const { log, content } = state;
23
+ const { mdast } = content;
24
+ log.debug(`Turning Markdown into HTML from ${typeof mdast}`);
25
+ // initialize transformer
26
+ content.slugger = new GithubSlugger();
27
+ const transformer = new VDOMTransformer()
28
+ .withOptions({
29
+ slugger: content.slugger,
30
+ });
31
+ content.document = transformer
32
+ .withMdast(mdast)
33
+ .getDocument();
34
+ }
@@ -0,0 +1,42 @@
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 { unified } from 'unified';
13
+ import remarkParse from 'remark-parse';
14
+ import { removePosition } from 'unist-util-remove-position';
15
+ import { remarkMatter } from '@adobe/helix-markdown-support';
16
+ import remarkGfm from '../utils/remark-gfm-nolink.js';
17
+
18
+ export class FrontmatterParsingError extends Error {
19
+ }
20
+
21
+ /**
22
+ * Parses the markdown body
23
+ * @type PipelineStep
24
+ * @param {PipelineState} state
25
+ */
26
+ export default function parseMarkdown(state) {
27
+ const { log, content } = state;
28
+
29
+ // convert linebreaks
30
+ const converted = content.data.replace(/(\r\n|\n|\r)/gm, '\n');
31
+ content.mdast = unified()
32
+ .use(remarkParse)
33
+ .use(remarkGfm)
34
+ .use(remarkMatter, {
35
+ errorHandler: (e) => {
36
+ log.warn(new FrontmatterParsingError(e));
37
+ },
38
+ })
39
+ .parse(converted);
40
+
41
+ removePosition(content.mdast, true);
42
+ }
@@ -0,0 +1,34 @@
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
+ * Cleans the response document by removing `hlx-` stuff
14
+ * @param {PipelineState} state
15
+ * @param {PipelineRequest} req
16
+ * @param {PipelineResponse} res
17
+ */
18
+ export default function clean(state, req, res) {
19
+ const { document } = res;
20
+ document.querySelectorAll('[class]').forEach((el) => {
21
+ // Remove all `hlx-*` classes on the elements
22
+ el.classList.value.split(' ')
23
+ .filter((cls) => cls.indexOf('hlx-') === 0)
24
+ .forEach((cls) => el.classList.remove(cls));
25
+ if (!el.classList.length) {
26
+ el.removeAttribute('class');
27
+ }
28
+
29
+ // Remove all `data-hlx-*` attributes on these elements
30
+ Object.keys(el.dataset)
31
+ .filter((key) => key.match(/^hlx[A-Z]/))
32
+ .forEach((key) => delete el.dataset[key]);
33
+ });
34
+ }
@@ -0,0 +1,25 @@
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 mime from 'mime';
13
+
14
+ /**
15
+ * "Renders" the content from the code-bus as-is
16
+ * @type PipelineStep
17
+ * @param {PipelineState} state
18
+ * @param {PipelineRequest} req
19
+ * @param {PipelineResponse} res
20
+ * @returns {Promise<void>}
21
+ */
22
+ export default async function renderCode(state, req, res) {
23
+ res.body = state.content.data;
24
+ res.headers.set('content-type', mime.getType(state.info.resourcePath));
25
+ }
@@ -0,0 +1,158 @@
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 max-len */
14
+
15
+ import { JSDOM } from 'jsdom';
16
+
17
+ /*
18
+ <!DOCTYPE html>
19
+ <html data-sly-attribute="${content.document.documentElement.attributesMap}">
20
+ <head>
21
+ <title>${content.meta.title}</title>
22
+ <link data-sly-test="${content.meta.url}" rel="canonical" href="${content.meta.url}"/>
23
+ <meta data-sly-test="${content.meta.description}" name="description" content="${content.meta.description}"/>
24
+ <meta data-sly-test="${content.meta.keywords}" name="keywords" content="${content.meta.keywords}"/>
25
+ <meta data-sly-test="${content.meta.title}" property="og:title" content="${content.meta.title}"/>
26
+ <meta data-sly-test="${content.meta.description}" property="og:description" content="${content.meta.description}"/>
27
+ <meta data-sly-test="${content.meta.url}" property="og:url" content="${content.meta.url}"/>
28
+ <meta data-sly-test="${content.meta.image}" property="og:image" content="${content.meta.image}"/>
29
+ <meta data-sly-test="${content.meta.image}" property="og:image:secure_url" content="${content.meta.image}"/>
30
+ <sly data-sly-test="${content.meta.imageAlt}">
31
+ <meta data-sly-test="${content.meta.imageAlt}" property="og:image:alt" content="${content.meta.imageAlt}"/>
32
+ </sly>
33
+ <meta data-sly-test="${content.meta.modifiedTime}" property="og:updated_time" content="${content.meta.modified_time}"/>
34
+ <sly data-sly-test="${content.meta.tags}" data-sly-list.tag="${content.meta.tags}">
35
+ <meta property="article:tag" content="${tag}"/>
36
+ </sly>
37
+ <meta data-sly-test="${content.meta.section}" property="article:section" content="${section}"/>
38
+ <meta data-sly-test="${content.meta.published_time}" property="article:published_time" content="${content.meta.published_time}"/>
39
+ <meta data-sly-test="${content.meta.modified_time}" property="article:modified_time" content="${content.meta.modified_time}"/>
40
+ <meta data-sly-test="${content.meta.title}" name="twitter:title" content="${content.meta.title}"/>
41
+ <meta data-sly-test="${content.meta.description}" name="twitter:description" content="${content.meta.description}"/>
42
+ <meta data-sly-test="${content.meta.image}" name="twitter:image" content="${content.meta.image}"/>
43
+ <sly data-sly-test="${content.meta.custom}" data-sly-list="${content.meta.custom}">
44
+ <meta data-sly-test="${item.property}" property="${item.name}" content="${item.value}">
45
+ <meta data-sly-test="${!item.property}" name="${item.name}" content="${item.value}">
46
+ </sly>
47
+ <esi:include src="/head.html" onerror="continue"/>
48
+ </head>
49
+ <body data-sly-attribute="${content.document.body.attributesMap}">
50
+ <!-- header -->
51
+ <header><esi:include src="/header.plain.html" onerror="continue"/></header>
52
+ <!-- main content -->
53
+ <main>${content.document.body}</main>
54
+ <!-- footer -->
55
+ <footer><esi:include src="/footer.plain.html" onerror="continue"/></footer>
56
+ </body>
57
+ </html>
58
+ */
59
+
60
+ function appendElement($parent, $el) {
61
+ if ($el) {
62
+ $parent.append($el);
63
+ }
64
+ }
65
+
66
+ function createElement(doc, name, ...attrs) {
67
+ // check for empty values
68
+ for (let i = 0; i < attrs.length; i += 2) {
69
+ if (!attrs[i + 1]) {
70
+ return null;
71
+ }
72
+ }
73
+ const $el = doc.createElement(name);
74
+ for (let i = 0; i < attrs.length; i += 2) {
75
+ $el.setAttribute(attrs[i], attrs[i + 1]);
76
+ }
77
+ return $el;
78
+ }
79
+
80
+ /**
81
+ * @type PipelineStep
82
+ * @param {PipelineState} state
83
+ * @param {PipelineRequest} req
84
+ * @param {PipelineResponse} res
85
+ * @returns {Promise<void>}
86
+ */
87
+ export default async function render(state, req, res) {
88
+ const { content } = state;
89
+ const srcDoc = content.document;
90
+ if (state.info.selector === 'plain') {
91
+ // just return body
92
+ res.document = srcDoc.body;
93
+ } else {
94
+ // create document like HTL used to do
95
+ const dom = new JSDOM('<!DOCTYPE html>'
96
+ + '<html>'
97
+ + '<head></head>'
98
+ + '<body>'
99
+ + '<header></header>' // todo: are those still required ?
100
+ + '<main></main>'
101
+ + '<footer></footer>' // todo: are those still required ?
102
+ + '</body>'
103
+ + '</html>');
104
+ const doc = dom.window.document;
105
+
106
+ // add title
107
+ const $head = doc.head;
108
+ const { meta } = content;
109
+ const $title = doc.createElement('title');
110
+ $title.innerHTML = meta.title;
111
+ $head.append($title);
112
+
113
+ // add meta
114
+ appendElement($head, createElement(doc, 'link', 'rel', 'canonical', 'href', content.meta.canonical));
115
+
116
+ appendElement($head, createElement(doc, 'meta', 'name', 'description', 'content', content.meta.description));
117
+ appendElement($head, createElement(doc, 'meta', 'name', 'keywords', 'content', content.meta.keywords));
118
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:title', 'content', content.meta.title));
119
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:description', 'content', content.meta.description));
120
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:url', 'content', content.meta.url));
121
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:image', 'content', content.meta.image));
122
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:image:secure_url', 'content', content.meta.image));
123
+ if (content.meta.imageAlt) {
124
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:image:alt', 'content', content.meta.imageAlt));
125
+ }
126
+ appendElement($head, createElement(doc, 'meta', 'property', 'og:updated_time', 'content', content.meta.modified_time));
127
+ for (const tag of (meta.tags || [])) {
128
+ appendElement($head, createElement(doc, 'meta', 'property', 'article:tag', 'content', tag));
129
+ }
130
+ appendElement($head, createElement(doc, 'meta', 'property', 'article:section', 'content', content.meta.section));
131
+ appendElement($head, createElement(doc, 'meta', 'property', 'article:published_time', 'content', content.meta.published_time));
132
+ appendElement($head, createElement(doc, 'meta', 'property', 'article:modified_time', 'content', content.meta.modified_time));
133
+
134
+ appendElement($head, createElement(doc, 'meta', 'name', 'twitter:title', 'content', content.meta.title));
135
+ appendElement($head, createElement(doc, 'meta', 'name', 'twitter:description', 'content', content.meta.description));
136
+ appendElement($head, createElement(doc, 'meta', 'name', 'twitter:image', 'content', content.meta.image));
137
+
138
+ for (const custom of (meta.custom || [])) {
139
+ appendElement($head, createElement(doc, 'meta', custom.property ? 'property' : 'name', custom.name, 'content', custom.value));
140
+ }
141
+ if (meta.feed) {
142
+ appendElement($head, createElement(doc, 'link', 'rel', 'alternate', 'type', 'application/xml+atom', 'href', meta.feed, 'title', `${meta.title} feed`));
143
+ }
144
+ // inject head.html
145
+ const $headHtml = doc.createElement('template');
146
+ $headHtml.innerHTML = state.helixConfig?.head?.html ?? `
147
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
148
+ <script src="/scripts.js" type="module" crossorigin="use-credentials"></script>
149
+ <link rel="stylesheet" href="/styles.css"/>`;
150
+ $head.appendChild($headHtml.content);
151
+
152
+ // add body to main
153
+ const $main = doc.querySelector('main');
154
+
155
+ $main.append(...srcDoc.body.childNodes);
156
+ res.document = doc;
157
+ }
158
+ }
@@ -0,0 +1,44 @@
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
+
13
+ const AZURE_BLOB_REGEXP = /^https:\/\/hlx\.blob\.core\.windows\.net\/external\//;
14
+
15
+ const MEDIA_BLOB_REGEXP = /^https:\/\/.*\.hlx3?\.(live|page)\/media_.*/;
16
+
17
+ /**
18
+ * Rewrite blob store image URLs to /hlx_* URLs
19
+ *
20
+ * @param {Document} document The (vdom) document
21
+ */
22
+ function images(document) {
23
+ document.querySelectorAll('img').forEach((img) => {
24
+ if (AZURE_BLOB_REGEXP.test(img.src)) {
25
+ const { pathname, hash } = new URL(img.src);
26
+ const filename = pathname.split('/').pop();
27
+ const extension = hash.split('?').shift().split('.').pop() || 'jpg';
28
+ img.src = `./media_${filename}.${extension}`;
29
+ } else if (MEDIA_BLOB_REGEXP.test(img.src)) {
30
+ const { pathname } = new URL(img.src);
31
+ img.src = `.${pathname}`; // don't append fragment until picture tag supports width/height
32
+ }
33
+ });
34
+ }
35
+
36
+ /**
37
+ * @type PipelineStep
38
+ * @param content
39
+ */
40
+ export default function rewrite({ content }) {
41
+ if (content.document) {
42
+ images(content.document);
43
+ }
44
+ }