@adobe/helix-md2jcr 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 (43) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/CODE_OF_CONDUCT.md +74 -0
  3. package/CONTRIBUTING.md +74 -0
  4. package/LICENSE.txt +264 -0
  5. package/README.md +27 -0
  6. package/baseline-tests.sh +45 -0
  7. package/package.json +81 -0
  8. package/src/cli/convert2jcr.js +128 -0
  9. package/src/index.d.ts +16 -0
  10. package/src/index.js +15 -0
  11. package/src/md2jcr/index.d.ts +20 -0
  12. package/src/md2jcr/index.js +36 -0
  13. package/src/mdast2jcr/domain/Component.js +66 -0
  14. package/src/mdast2jcr/domain/Definitions.js +78 -0
  15. package/src/mdast2jcr/domain/FieldGroup.js +85 -0
  16. package/src/mdast2jcr/domain/FieldGroupFieldResolver.js +96 -0
  17. package/src/mdast2jcr/domain/ModelHelper.js +123 -0
  18. package/src/mdast2jcr/domain/Models.js +46 -0
  19. package/src/mdast2jcr/hast-table-cell-handler.js +48 -0
  20. package/src/mdast2jcr/hast-table-handler.js +126 -0
  21. package/src/mdast2jcr/hb/helpers/encode-helper.js +18 -0
  22. package/src/mdast2jcr/hb/helpers/name-helper.js +61 -0
  23. package/src/mdast2jcr/hb/helpers/page-helper.js +109 -0
  24. package/src/mdast2jcr/hb/helpers/section-helper.js +53 -0
  25. package/src/mdast2jcr/hb/helpers/which-partial-helper.js +22 -0
  26. package/src/mdast2jcr/hb/partials/blockquote.js +16 -0
  27. package/src/mdast2jcr/hb/partials/emphasis.js +34 -0
  28. package/src/mdast2jcr/hb/partials/grid-table.js +495 -0
  29. package/src/mdast2jcr/hb/partials/heading.js +53 -0
  30. package/src/mdast2jcr/hb/partials/image.js +24 -0
  31. package/src/mdast2jcr/hb/partials/link.js +33 -0
  32. package/src/mdast2jcr/hb/partials/paragraph.js +30 -0
  33. package/src/mdast2jcr/hb/partials/strong.js +33 -0
  34. package/src/mdast2jcr/hb/partials/supports/image.js +27 -0
  35. package/src/mdast2jcr/hb/partials/supports/link.js +46 -0
  36. package/src/mdast2jcr/hb/partials/table.js +16 -0
  37. package/src/mdast2jcr/index.d.ts +122 -0
  38. package/src/mdast2jcr/index.js +94 -0
  39. package/src/mdast2jcr/mdast-download-images.js +165 -0
  40. package/src/mdast2jcr/mdast-sanitize-html.js +233 -0
  41. package/src/mdast2jcr/templates/page-template.js +27 -0
  42. package/src/mdast2jcr/utils/mdast.js +47 -0
  43. package/src/mdast2jcr/utils.js +233 -0
@@ -0,0 +1,233 @@
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 { visit } from 'unist-util-visit';
13
+ import { unified } from 'unified';
14
+ import parse from 'rehype-parse';
15
+ import { defaultHandlers, toMdast } from 'hast-util-to-mdast';
16
+ import tableHandler from './hast-table-handler.js';
17
+ import tableCellHandler from './hast-table-cell-handler.js';
18
+
19
+ /**
20
+ * Creates simple format handler
21
+ * @param type
22
+ */
23
+ function formatHandler(type) {
24
+ return (state, node) => {
25
+ const result = { type, children: state.all(node) };
26
+ state.patch(node, result);
27
+ return result;
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @param {State} state
33
+ * State.
34
+ * @param {Readonly<Element>} node
35
+ * hast element to transform.
36
+ * @returns {Link}
37
+ * mdast node.
38
+ */
39
+ export function linkHandler(state, node) {
40
+ const properties = node.properties || {};
41
+ // Allow potentially “invalid” nodes, they might be unknown.
42
+ // We also support straddling later.
43
+ const children = /** @type {Array<PhrasingContent>} */ (state.all(node));
44
+
45
+ /** @type {Link} */
46
+ const result = {
47
+ type: 'link',
48
+ url: state.resolve(String(properties.href || '') || null),
49
+ title: properties.title ? String(properties.title) : null,
50
+ anchor: properties.name ?? properties.id,
51
+
52
+ // if the link has a child of type link, then we need to unwrap it
53
+ children: children.length === 1 && children[0].type === 'link' ? children[0].children : children,
54
+ };
55
+ state.patch(node, result);
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * removes paragraphs from the child nodes recursively.
61
+ * @param node
62
+ */
63
+ function unwrapParagraphs(node) {
64
+ if (!node.children) {
65
+ return node;
66
+ }
67
+ for (let idx = 0; idx < node.children.length; idx += 1) {
68
+ const child = node.children[idx];
69
+ if (child.type === 'paragraph') {
70
+ node.children.splice(idx, 1, ...child.children);
71
+ idx += child.children.length - 1;
72
+ } else {
73
+ // eslint-disable-next-line no-param-reassign
74
+ node.children[idx] = unwrapParagraphs(child);
75
+ }
76
+ }
77
+ return node;
78
+ }
79
+
80
+ /**
81
+ * Handler for `<markdown>` elements.
82
+ * @param {[]} mdasts array of mdast sub trees
83
+ */
84
+ function mdHandler(mdasts) {
85
+ return (state, node) => {
86
+ // given the index property from the <markdown idx> element, return the
87
+ // corresponding mdast sub tree from the mdasts array.
88
+ const { idx } = node.properties;
89
+ return mdasts[+idx];
90
+ };
91
+ }
92
+
93
+ function isPhrasingParent(node) {
94
+ return [
95
+ 'paragraph',
96
+ 'underline',
97
+ 'subscript',
98
+ 'superscript',
99
+ 'heading',
100
+ 'emphasis',
101
+ 'strong',
102
+ 'link',
103
+ 'linkReference',
104
+ 'tableCell',
105
+ 'delete',
106
+ 'footnote',
107
+ ].includes(node.type);
108
+ }
109
+
110
+ /**
111
+ * Find the index of the last HTML node in the list of siblings, if no HTML node
112
+ * is found, return an index of 0;
113
+ * @param siblings list of siblings to search through to find the last HTML node
114
+ * @returns {number}
115
+ */
116
+ export function locateIndexOfLastHtmlNode(siblings) {
117
+ const index = [...siblings]
118
+ .reverse()
119
+ .findIndex((sibling) => sibling.type === 'html');
120
+
121
+ // restore the order of the siblings array before returning the index
122
+ return index === -1 ? 0 : siblings.length - index - 1;
123
+ }
124
+
125
+ /**
126
+ * Sanitizes html:
127
+ * - collapses consecutive html content (simply concat all nodes until the last html sibling)
128
+ * - parses and converts them to mdast again
129
+ *
130
+ * @param {object} tree
131
+ * @returns {object} The modified (original) tree.
132
+ */
133
+ export default function sanitizeHtml(tree) {
134
+ const mdInserts = [];
135
+
136
+ visit(tree, (node, index, parent) => {
137
+ const { children: siblings = [] } = parent || {};
138
+
139
+ // collapse html blocks
140
+ if (node.type === 'html') {
141
+ // find the index of the last html node in the list of siblings
142
+ const lastHtmlIndex = locateIndexOfLastHtmlNode(siblings, index);
143
+
144
+ // get the node's value and store it in html variable
145
+ let html = node.value;
146
+
147
+ // if there are more html nodes after this one, remove them from the siblings array
148
+ if (lastHtmlIndex > index) {
149
+ // splice the siblings array to remove the nodes that are between the
150
+ // current node and the last html node, leaving the siblings array with
151
+ // only the nodes that come before the first HTML node, and include the
152
+ // first HTML node
153
+ const removed = siblings.splice(index + 1, lastHtmlIndex - index);
154
+
155
+ // append to html as special markdown element marker which is then handled in the
156
+ // mdHandler for the `<markdown>` elements.
157
+ removed.forEach((n) => {
158
+ if (n.type === 'html' || n.type === 'text') {
159
+ html += n.value;
160
+ } else {
161
+ // eventually toMdast will be called, and when we encounter a markdown element
162
+ // we need to replace it with the actual markdown content, so store the index
163
+ // such that we can look it up in the mdInserts array later.
164
+ html += `<markdown idx="${mdInserts.length}"></markdown>`;
165
+ mdInserts.push(n);
166
+ }
167
+ });
168
+ }
169
+
170
+ if (isPhrasingParent(parent)) {
171
+ html = `<p>${html}</p>`;
172
+ }
173
+
174
+ // try parse html
175
+ const hast = unified()
176
+ .use(parse, { fragment: true })
177
+ .parse(html);
178
+
179
+ // convert to mdast with extra handlers
180
+ const mdast = toMdast(hast, {
181
+ document: false,
182
+ handlers: {
183
+ ...defaultHandlers,
184
+ a: linkHandler,
185
+ u: formatHandler('underline'),
186
+ sub: formatHandler('subscript'),
187
+ sup: formatHandler('superscript'),
188
+ table: tableHandler,
189
+ markdown: mdHandler(mdInserts),
190
+ th: tableCellHandler,
191
+ td: tableCellHandler,
192
+ },
193
+ });
194
+ // clear inserts
195
+ mdInserts.length = 0;
196
+
197
+ // ensure that flow nodes are in phrasing context
198
+ if (!isPhrasingParent(parent)) {
199
+ let lastParagraph;
200
+ for (let idx = 0; idx < mdast.children.length; idx += 1) {
201
+ const child = mdast.children[idx];
202
+ if (child.type === 'underline' || child.type === 'subscript' || child.type === 'superscript') {
203
+ unwrapParagraphs(child);
204
+ if (!lastParagraph) {
205
+ lastParagraph = {
206
+ type: 'paragraph',
207
+ children: [child],
208
+ };
209
+ mdast.children.splice(idx, 1, lastParagraph);
210
+ } else {
211
+ lastParagraph.children.push(child);
212
+ mdast.children.splice(idx, 1);
213
+ idx -= 1;
214
+ }
215
+ } else {
216
+ lastParagraph = null;
217
+ }
218
+ }
219
+ } else {
220
+ unwrapParagraphs(mdast);
221
+ }
222
+
223
+ // inject children of parsed tree
224
+ siblings.splice(index, 1, ...mdast.children);
225
+
226
+ // continue after
227
+ return index + mdast.children.length;
228
+ }
229
+
230
+ return visit.CONTINUE;
231
+ });
232
+ return tree;
233
+ }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright 2024 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
+ export default function pageTemplate() {
13
+ return `<?xml version="1.0" encoding="UTF-8"?>
14
+ <jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" jcr:primaryType="cq:Page">
15
+ {{#page}}
16
+ <root jcr:primaryType="nt:unstructured" sling:resourceType="core/franklin/components/root/v1/root">
17
+ {{#each children }}
18
+ {{#section @index children }}
19
+ {{#each children }}
20
+ {{> (whichPartial this.type) models=../../models definition=../../definition filters=../../filters}}
21
+ {{/each}}
22
+ {{/section}}
23
+ {{/each}}
24
+ </root>
25
+ {{/page}}
26
+ </jcr:root>`;
27
+ }
@@ -0,0 +1,47 @@
1
+ /*
2
+ * Copyright 2024 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
+ * findAll goes through the children of a node searching for nodes that match a
15
+ * test function.
16
+ *
17
+ * @param node the node to search through.
18
+ * @param test the test function
19
+ * @param flatten if true, the function will return the children of the matching nodes, without
20
+ * their parent.
21
+ * @return {*[]} an array of nodes that match the test function.
22
+ */
23
+ function findAll(node, test, flatten = false) {
24
+ // given the root of the mdast tree, find all the nodes that are identified by the test function
25
+ const results = [];
26
+
27
+ if (test(node)) {
28
+ results.push(node);
29
+ return results;
30
+ }
31
+
32
+ if (node.children) {
33
+ node.children.forEach((child) => {
34
+ results.push(...findAll(child, test));
35
+ });
36
+ }
37
+
38
+ if (flatten) {
39
+ return results.map((c) => c.children).flat();
40
+ } else {
41
+ return results;
42
+ }
43
+ }
44
+
45
+ export {
46
+ findAll,
47
+ };
@@ -0,0 +1,233 @@
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 Handlebars from 'handlebars';
13
+ import { toString } from 'mdast-util-to-string';
14
+
15
+ /* eslint-disable no-param-reassign */
16
+
17
+ /**
18
+ * This method modifies the mdast tree in place. Given a flat list of nodes,
19
+ * gather the children of each section and wrap them in a section node.
20
+ * For example given a mdast tree of:
21
+ * [Heading, Text, Image, ThematicBreak, Text, ThematicBreak, Text] would
22
+ * result in [Section[Heading, Text, Image], Section[Text], Section[Text]].
23
+ */
24
+ export function splitSection(mdast) {
25
+ // obtain the index of all thematicBreak nodes from the list of children
26
+ const thematicBreakIndexes = mdast.children
27
+ .filter((node) => node.type === 'thematicBreak')
28
+ // then get their index in the list of children
29
+ .map((node) => mdast.children.indexOf(node));
30
+
31
+ // find pairwise permutations of spaces between blocks
32
+ // include the very start and end of the document
33
+ const starts = [0, ...thematicBreakIndexes];
34
+ const ends = [...thematicBreakIndexes, mdast.children.length];
35
+
36
+ // content.mdast.children = _.zip(starts, ends)
37
+ const nodes = starts
38
+ .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
+
43
+ mdast.children = nodes.map(([start, end]) => {
44
+ // skip 'thematicBreak' nodes
45
+ const index = mdast.children[start].type === 'thematicBreak' ? start + 1 : start;
46
+ return {
47
+ type: 'section',
48
+ children: mdast.children.slice(index, end),
49
+ };
50
+ });
51
+
52
+ // unwrap sole section directly on the root
53
+ // if (mdast.children.length === 1 && mdast.children[0].type === 'section') {
54
+ // mdast.children = mdast.children[0].children;
55
+ // }
56
+
57
+ return mdast;
58
+ }
59
+
60
+ export function wrapParagraphs(mdast) {
61
+ const sections = mdast.children.filter((node) => node.type === 'section');
62
+
63
+ sections.forEach((section) => {
64
+ const { children } = section;
65
+ const newChildren = [];
66
+ let paragraphGroup = [];
67
+
68
+ // process.stdout.write('==================================================\n');
69
+ // process.stdout.write(inspect(section));
70
+ // process.stdout.write('\n');
71
+ // process.stdout.write('==================================================\n');
72
+
73
+ for (let i = 0; i < children.length; i += 1) {
74
+ const node = children[i];
75
+
76
+ // Group paragraph and heading (depth >= 3) into the same paragraphWrapper
77
+ // group strong and emphasis into the same paragraphWrapper if they are not a button
78
+ if (node.type === 'paragraph'
79
+ || node.type === 'list'
80
+ || node.type === 'code'
81
+ || (node.type === 'heading' && node.depth >= 3)
82
+ || (node.type === 'strong' && (node.children.length !== 1 || (node.children.length === 1 && node.children[0].type !== 'link')))
83
+ || (node.type === 'emphasis' && (node.children.length !== 1 || (node.children.length === 1 && node.children[0].type !== 'link')))
84
+ ) {
85
+ paragraphGroup.push(node);
86
+
87
+ if (i === children.length - 1) {
88
+ newChildren.push({
89
+ type: 'paragraphWrapper',
90
+ children: paragraphGroup,
91
+ });
92
+ }
93
+ } else {
94
+ if (paragraphGroup.length > 0) {
95
+ newChildren.push({
96
+ type: 'paragraphWrapper',
97
+ children: paragraphGroup,
98
+ });
99
+ paragraphGroup = [];
100
+ }
101
+ newChildren.push(node);
102
+ }
103
+ }
104
+ section.children = newChildren;
105
+
106
+ // process.stdout.write('==================================================\n');
107
+ // process.stdout.write(inspect(section));
108
+ // process.stdout.write('\n');
109
+ // process.stdout.write('==================================================\n');
110
+ });
111
+ return mdast;
112
+ }
113
+
114
+ export function unwrapImages(mdast) {
115
+ const sections = mdast.children.filter((node) => node.type === 'section');
116
+
117
+ sections.forEach((section) => {
118
+ const newChildren = [];
119
+
120
+ for (const node of section.children) {
121
+ if (node.type === 'paragraph') {
122
+ let currentText = [];
123
+ const images = [];
124
+
125
+ // Handle single child unwrapping for `link`, `strong`, and `em`
126
+ if (node.children.length === 1) {
127
+ const onlyChild = node.children[0];
128
+ if (['link', 'strong', 'emphasis'].includes(onlyChild.type)) {
129
+ // Move the single child outside the paragraph
130
+ newChildren.push(onlyChild);
131
+ // eslint-disable-next-line no-continue
132
+ continue; // Skip further processing of this paragraph node
133
+ }
134
+ }
135
+
136
+ // process.stdout.write('==================================================\n');
137
+ // process.stdout.write(inspect(node));
138
+ // process.stdout.write('\n');
139
+ // process.stdout.write('==================================================\n');
140
+
141
+ // Traverse paragraph children to separate images and text nodes
142
+ for (let i = 0; i < node.children.length; i += 1) {
143
+ const child = node.children[i];
144
+
145
+ if (child.type === 'image') {
146
+ // If we have accumulated text, push it as a paragraph before the image
147
+ if (currentText.length > 0) {
148
+ newChildren.push({
149
+ type: 'paragraph',
150
+ children: currentText,
151
+ position: node.position,
152
+ });
153
+ currentText = [];
154
+ }
155
+ // Push the image outside the paragraph
156
+ images.push(child);
157
+ } else {
158
+ // Collect non-image children as part of the paragraph
159
+ currentText.push(child);
160
+ }
161
+ }
162
+
163
+ // If there are remaining text nodes, add them as a paragraph
164
+ if (currentText.length > 0) {
165
+ newChildren.push({
166
+ type: 'paragraph',
167
+ children: currentText,
168
+ position: node.position,
169
+ });
170
+ }
171
+
172
+ // Move all images to their proper position in the parent tree
173
+ newChildren.push(...images);
174
+ } else {
175
+ // Non-paragraph nodes stay as they are
176
+ newChildren.push(node);
177
+ }
178
+ }
179
+
180
+ section.children = newChildren;
181
+ });
182
+ return mdast;
183
+ }
184
+
185
+ export function encodeHTMLEntities(str) {
186
+ return str ? str.replace(/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;)/g, '&amp;')
187
+ .replace(/</g, '&lt;')
188
+ .replace(/"/g, '&quot;') : '';
189
+ }
190
+
191
+ export function encodeHtml(str) {
192
+ return Handlebars.escapeExpression(str);
193
+ }
194
+
195
+ /**
196
+ * Strip the new line characters from the given value. If the value is of Node, toString
197
+ * is then performed and then a replacement occurs. If the value is a string, a simple replace
198
+ * is performed.
199
+ * @param value the string or Node to strip newlines from.
200
+ * @return {string} the string with newlines replaced with spaces.
201
+ */
202
+ export function stripNewlines(value) {
203
+ if (!value) {
204
+ return '';
205
+ }
206
+
207
+ if (typeof value === 'string') {
208
+ return value.replace(/\n/g, ' ').trim();
209
+ }
210
+ return toString(value).replace(/\n/g, ' ').trim();
211
+ }
212
+
213
+ /**
214
+ * Sort properties by the following order: sling, jcr, other.
215
+ * @param {string} p1 - property 1
216
+ * @param {string} p2 - property 2
217
+ * @return {number} -1 if p1 is before p2, 1 if p2 is before p1, 0 if they are equal.
218
+ */
219
+ export const sortJcrProperties = ([p1], [p2]) => {
220
+ if (p1.startsWith('sling:')) {
221
+ return -1;
222
+ }
223
+ if (p2.startsWith('sling:')) {
224
+ return 1;
225
+ }
226
+ if (p1.startsWith('jcr:')) {
227
+ return -1;
228
+ }
229
+ if (p2.startsWith('jcr:')) {
230
+ return 1;
231
+ }
232
+ return p1.localeCompare(p2);
233
+ };