@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,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
|
+
}
|