@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.
- package/CHANGELOG.md +15 -0
- package/CODE_OF_CONDUCT.md +74 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE.txt +264 -0
- package/README.md +27 -0
- package/baseline-tests.sh +45 -0
- package/package.json +81 -0
- package/src/cli/convert2jcr.js +128 -0
- package/src/index.d.ts +16 -0
- package/src/index.js +15 -0
- package/src/md2jcr/index.d.ts +20 -0
- package/src/md2jcr/index.js +36 -0
- package/src/mdast2jcr/domain/Component.js +66 -0
- package/src/mdast2jcr/domain/Definitions.js +78 -0
- package/src/mdast2jcr/domain/FieldGroup.js +85 -0
- package/src/mdast2jcr/domain/FieldGroupFieldResolver.js +96 -0
- package/src/mdast2jcr/domain/ModelHelper.js +123 -0
- package/src/mdast2jcr/domain/Models.js +46 -0
- package/src/mdast2jcr/hast-table-cell-handler.js +48 -0
- package/src/mdast2jcr/hast-table-handler.js +126 -0
- package/src/mdast2jcr/hb/helpers/encode-helper.js +18 -0
- package/src/mdast2jcr/hb/helpers/name-helper.js +61 -0
- package/src/mdast2jcr/hb/helpers/page-helper.js +109 -0
- package/src/mdast2jcr/hb/helpers/section-helper.js +53 -0
- package/src/mdast2jcr/hb/helpers/which-partial-helper.js +22 -0
- package/src/mdast2jcr/hb/partials/blockquote.js +16 -0
- package/src/mdast2jcr/hb/partials/emphasis.js +34 -0
- package/src/mdast2jcr/hb/partials/grid-table.js +495 -0
- package/src/mdast2jcr/hb/partials/heading.js +53 -0
- package/src/mdast2jcr/hb/partials/image.js +24 -0
- package/src/mdast2jcr/hb/partials/link.js +33 -0
- package/src/mdast2jcr/hb/partials/paragraph.js +30 -0
- package/src/mdast2jcr/hb/partials/strong.js +33 -0
- package/src/mdast2jcr/hb/partials/supports/image.js +27 -0
- package/src/mdast2jcr/hb/partials/supports/link.js +46 -0
- package/src/mdast2jcr/hb/partials/table.js +16 -0
- package/src/mdast2jcr/index.d.ts +122 -0
- package/src/mdast2jcr/index.js +94 -0
- package/src/mdast2jcr/mdast-download-images.js +165 -0
- package/src/mdast2jcr/mdast-sanitize-html.js +233 -0
- package/src/mdast2jcr/templates/page-template.js +27 -0
- package/src/mdast2jcr/utils/mdast.js +47 -0
- package/src/mdast2jcr/utils.js +233 -0
|
@@ -0,0 +1,495 @@
|
|
|
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
|
+
import { find } from 'unist-util-find';
|
|
13
|
+
import { remove } from 'unist-util-remove';
|
|
14
|
+
import { is } from 'unist-util-is';
|
|
15
|
+
import { toString } from 'mdast-util-to-string';
|
|
16
|
+
import Handlebars from 'handlebars';
|
|
17
|
+
import { toHast } from 'mdast-util-to-hast';
|
|
18
|
+
import { toHtml } from 'hast-util-to-html';
|
|
19
|
+
import { getComponentById, getComponentByTitle, getModelId } from '../../domain/Definitions.js';
|
|
20
|
+
import {
|
|
21
|
+
findModelById, getField,
|
|
22
|
+
} from '../../domain/Models.js';
|
|
23
|
+
import { findAll } from '../../utils/mdast.js';
|
|
24
|
+
import link from './supports/link.js';
|
|
25
|
+
import {
|
|
26
|
+
encodeHtml, encodeHTMLEntities, sortJcrProperties, stripNewlines,
|
|
27
|
+
} from '../../utils.js';
|
|
28
|
+
import image from './supports/image.js';
|
|
29
|
+
import FieldGroupFieldResolver from '../../domain/FieldGroupFieldResolver.js';
|
|
30
|
+
import ModelHelper from '../../domain/ModelHelper.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {import('../../index.d.ts').FieldDef} Field
|
|
34
|
+
* @typedef {import('../../index.d.ts').DefinitionDef} Definition
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Locate the name of the Block and any classes that are associated with it.
|
|
39
|
+
* Return null if the block name can not be found in the models.
|
|
40
|
+
* @param mdast {object} - the mdast tree
|
|
41
|
+
* @param {Definition} definition - the definitions object.
|
|
42
|
+
* @returns {null|{name: string, classes: string[]}, modelId: string} - the name of the
|
|
43
|
+
* block, any classes, and an associated model id.
|
|
44
|
+
*/
|
|
45
|
+
function getBlockDetails(mdast, definition) {
|
|
46
|
+
const header = find(mdast, { type: 'gtHeader' });
|
|
47
|
+
if (header) {
|
|
48
|
+
const textNode = find(header, { type: 'text' });
|
|
49
|
+
// if the textNode.value looks like "block (foo, bar)", return an object that looks like
|
|
50
|
+
// { name: "block", classes: ["foo", "bar"] }
|
|
51
|
+
const regex = /^(?<blockName>[^(]+)\s*(\((?<classes>[^)]+)\))?$/;
|
|
52
|
+
const match = toString(textNode).match(regex);
|
|
53
|
+
|
|
54
|
+
if (match) {
|
|
55
|
+
const blockName = match.groups.blockName.trim();
|
|
56
|
+
// try to locate the model name by inspecting the definition file
|
|
57
|
+
const modelId = getModelId(definition, blockName);
|
|
58
|
+
|
|
59
|
+
const block = {
|
|
60
|
+
name: match.groups.blockName.trim(),
|
|
61
|
+
classes: match.groups.classes ? match.groups.classes.split(',')
|
|
62
|
+
.map((c) => c.trim()) : [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (modelId) {
|
|
66
|
+
block.modelId = modelId;
|
|
67
|
+
} else if (block.name.toLowerCase() === 'metadata') {
|
|
68
|
+
block.modelId = 'page-metadata';
|
|
69
|
+
} else if (block.name.toLowerCase() === 'section metadata') {
|
|
70
|
+
block.modelId = 'section-metadata';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return block;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Process the field and collapse the field into the properties object.
|
|
81
|
+
* @param id {string} - the id of the field
|
|
82
|
+
* @param fields {Array<Field>} - the fields array
|
|
83
|
+
* @param node {Node} - the node to process
|
|
84
|
+
* @param parentNode {Node} - the parent node if necessary to inspect the child's parent for details
|
|
85
|
+
* @param properties {object} - the properties object
|
|
86
|
+
*/
|
|
87
|
+
function collapseField(id, fields, node, parentNode, properties) {
|
|
88
|
+
/* eslint-disable no-param-reassign */
|
|
89
|
+
if (!fields) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const suffixes = ['Alt', 'Type', 'MimeType', 'Text', 'Title'];
|
|
94
|
+
suffixes.forEach((suffix) => {
|
|
95
|
+
const field = fields.find((f) => f.name === `${id}${suffix}`);
|
|
96
|
+
if (field) {
|
|
97
|
+
if (suffix === 'Type') {
|
|
98
|
+
// a heading can have a type like h1, h2
|
|
99
|
+
if (node.type === 'heading') {
|
|
100
|
+
properties[field.name] = `h${node.depth}`;
|
|
101
|
+
} else if (link.supports(node)) {
|
|
102
|
+
// determine the type of the link by inspecting the parent node
|
|
103
|
+
// links can be wrapped in strong or em tags, or have no wrapping
|
|
104
|
+
properties[field.name] = link.getType(parentNode);
|
|
105
|
+
}
|
|
106
|
+
} else if (link.supports(node)) {
|
|
107
|
+
if (suffix === 'Text') {
|
|
108
|
+
properties[field.name] = encodeHTMLEntities(link.getProperties(node).text);
|
|
109
|
+
} else {
|
|
110
|
+
properties[field.name] = encodeHTMLEntities(node[suffix.toLowerCase()]);
|
|
111
|
+
}
|
|
112
|
+
} else if (suffix === 'MimeType') {
|
|
113
|
+
// TODO: can we guess the mime type from the src?
|
|
114
|
+
properties[field.name] = 'image/unknown';
|
|
115
|
+
} else {
|
|
116
|
+
// take the suffix and read the property from the node
|
|
117
|
+
properties[field.name] = encodeHTMLEntities(node[suffix.toLowerCase()]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// clean out any empty properties so that we don't pollute the output
|
|
121
|
+
if (!properties[field.name]) {
|
|
122
|
+
delete properties[field.name];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function extractPropertiesForNode(field, currentNode, properties) {
|
|
129
|
+
const fields = field.collapsed;
|
|
130
|
+
|
|
131
|
+
if (field.component === 'richtext') {
|
|
132
|
+
let value;
|
|
133
|
+
if (currentNode.type === 'wrapper') {
|
|
134
|
+
// combine all the children into a single string, but wrap them in a paragraph
|
|
135
|
+
value = currentNode.children.reduce((acc, node) => {
|
|
136
|
+
let str = toHtml(toHast(node));
|
|
137
|
+
|
|
138
|
+
// don't wrap nodes that are already paragraphs or code blocks
|
|
139
|
+
if (node.type !== 'paragraph' && node.type !== 'code') {
|
|
140
|
+
str = `<p>${str}</p>`;
|
|
141
|
+
}
|
|
142
|
+
return acc + encodeHtml(str);
|
|
143
|
+
}, '');
|
|
144
|
+
} else {
|
|
145
|
+
value = encodeHtml(toHtml(toHast(currentNode)));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// if the node is a code block then don't strip out the newlines
|
|
149
|
+
properties[field.name] = find(currentNode, { type: 'code' }) ? value : stripNewlines(value);
|
|
150
|
+
} else if (find(currentNode, { type: 'image' })) {
|
|
151
|
+
const imageNode = find(currentNode, { type: 'image' });
|
|
152
|
+
const { url } = image.getProperties(imageNode);
|
|
153
|
+
properties[field.name] = encodeHTMLEntities(url);
|
|
154
|
+
collapseField(field.name, fields, imageNode, null, properties);
|
|
155
|
+
} else {
|
|
156
|
+
const linkNode = find(currentNode, { type: 'link' });
|
|
157
|
+
const headlineNode = find(currentNode, { type: 'heading' });
|
|
158
|
+
if (linkNode) {
|
|
159
|
+
properties[field.name] = linkNode.url;
|
|
160
|
+
collapseField(field.name, fields, linkNode, currentNode, properties);
|
|
161
|
+
} else if (headlineNode) {
|
|
162
|
+
properties[field.name] = encodeHTMLEntities(toString(headlineNode));
|
|
163
|
+
collapseField(field.name, fields, headlineNode, null, properties);
|
|
164
|
+
} else {
|
|
165
|
+
let value = encodeHTMLEntities(toString(currentNode));
|
|
166
|
+
if (field.component === 'multiselect' || field.component === 'aem-tag') {
|
|
167
|
+
value = `[${value.split(',')
|
|
168
|
+
.map((v) => v.trim())
|
|
169
|
+
.join(',')}]`;
|
|
170
|
+
}
|
|
171
|
+
if (value) {
|
|
172
|
+
properties[field.name] = stripNewlines(value);
|
|
173
|
+
collapseField(field.name, fields, currentNode, null, properties);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractKeyValueProperties(row, model, fieldResolver, fieldGroup, properties) {
|
|
180
|
+
const [, ...nodes] = findAll(row, (node) => node.type === 'gtCell', true);
|
|
181
|
+
|
|
182
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
183
|
+
const node = nodes[i];
|
|
184
|
+
const field = fieldResolver.resolve(node, fieldGroup);
|
|
185
|
+
extractPropertiesForNode(field, node, properties);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Given a row, a component, and a model, extract the properties from the row
|
|
191
|
+
* @param {object} mdast - the mdast tree.
|
|
192
|
+
* @param {Model} model - the model.
|
|
193
|
+
* @param {string} mode - the mode either 'keyValue' or 'simple'.
|
|
194
|
+
* @param {Component} component - the component.
|
|
195
|
+
* @param fields - the field grouping object.
|
|
196
|
+
* @param properties
|
|
197
|
+
* @return {{}} - the properties
|
|
198
|
+
*/
|
|
199
|
+
function extractProperties(mdast, model, mode, component, fields, properties) {
|
|
200
|
+
const fieldsCloned = structuredClone(fields);
|
|
201
|
+
|
|
202
|
+
// the first cells is the header row, so we skip it
|
|
203
|
+
// const nodes = findAll(mdast, (node) => node.type === 'gtCell', true);
|
|
204
|
+
const rows = findAll(mdast, (node) => node.type === 'gtRow', false);
|
|
205
|
+
let classesField;
|
|
206
|
+
if (mode === 'blockItem') {
|
|
207
|
+
classesField = getField(model, 'classes');
|
|
208
|
+
// if our model defines a classes field then dig out the classes from the first cell
|
|
209
|
+
if (classesField) {
|
|
210
|
+
// if we are a block item we need to look at the first cell to see if it has any class
|
|
211
|
+
// properties by inspecting the text value for any commas
|
|
212
|
+
// if there is a comma that then becomes the classes property for the block item
|
|
213
|
+
const firstCell = rows[0].children[0];
|
|
214
|
+
const textValue = toString(firstCell);
|
|
215
|
+
const classes = textValue.split(',').map((c) => c.trim());
|
|
216
|
+
|
|
217
|
+
// discard the component name leaving only the block option names (class names)
|
|
218
|
+
classes.shift();
|
|
219
|
+
|
|
220
|
+
// if we are left with any classes to add to the block item, then add them
|
|
221
|
+
if (classes.length > 0) {
|
|
222
|
+
properties.classes = classesField?.multi === true
|
|
223
|
+
? `[${classes.join(', ')}]` : classes.join(', ');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
// get rid of the header row, no need for that
|
|
228
|
+
rows.shift();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const modelFields = fieldsCloned.map((group) => group.fields).flat();
|
|
232
|
+
const fieldResolver = new FieldGroupFieldResolver(component);
|
|
233
|
+
|
|
234
|
+
for (const [index, row] of rows.entries()) {
|
|
235
|
+
if (modelFields.length === index || fieldsCloned.length === index) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let fieldGroup = fieldsCloned[index];
|
|
240
|
+
|
|
241
|
+
// gather all the cells from the row
|
|
242
|
+
const cells = findAll(row, (node) => node.type === 'gtCell', false);
|
|
243
|
+
|
|
244
|
+
// if we are block and our first cell is a class field then skip it
|
|
245
|
+
if (mode === 'blockItem' && classesField) {
|
|
246
|
+
cells.shift();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (mode === 'keyValue') {
|
|
250
|
+
extractKeyValueProperties(row, model, fieldResolver, fieldGroup, properties);
|
|
251
|
+
} else {
|
|
252
|
+
// for each cell in the row, process the nodes that exist in the cell
|
|
253
|
+
let processsing = false;
|
|
254
|
+
for (const cell of cells) {
|
|
255
|
+
if (mode === 'blockItem' && !processsing) {
|
|
256
|
+
fieldGroup = fieldsCloned.shift();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const nodes = cell.children;
|
|
260
|
+
if (nodes.length === 0) {
|
|
261
|
+
// throw away the field that was associated with the cell
|
|
262
|
+
fieldGroup.fields.shift();
|
|
263
|
+
processsing = true;
|
|
264
|
+
// if there are no nodes in the cell, then we need to skip it
|
|
265
|
+
} else {
|
|
266
|
+
while (nodes.length > 0) {
|
|
267
|
+
const node = nodes.shift();
|
|
268
|
+
|
|
269
|
+
// give the field group (all fields to the resolver)
|
|
270
|
+
if (fieldGroup === undefined) {
|
|
271
|
+
console.warn(`No field group for ${model.id}`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const field = fieldResolver.resolve(node, fieldGroup);
|
|
276
|
+
let pWrapper;
|
|
277
|
+
if (field.component === 'richtext') {
|
|
278
|
+
pWrapper = {
|
|
279
|
+
type: 'wrapper',
|
|
280
|
+
children: [node],
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
let searching = true;
|
|
284
|
+
|
|
285
|
+
while (searching) {
|
|
286
|
+
const n = nodes.shift();
|
|
287
|
+
if (!n) break;
|
|
288
|
+
|
|
289
|
+
if (find(n, { type: 'image' })) {
|
|
290
|
+
nodes.unshift(n);
|
|
291
|
+
searching = false;
|
|
292
|
+
}
|
|
293
|
+
if (searching) {
|
|
294
|
+
pWrapper.children.push(n);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
pWrapper = node;
|
|
299
|
+
}
|
|
300
|
+
extractPropertiesForNode(field, pWrapper, properties);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Extract the properties that are belong to the block header. Properties like
|
|
310
|
+
* name, model id, and classes.
|
|
311
|
+
* @param {Array<Model>} models - the models object
|
|
312
|
+
* @param {Definition} definition - the definitions object
|
|
313
|
+
* @param {object} mdast - the mdast tree
|
|
314
|
+
* @return {{
|
|
315
|
+
* name: string,
|
|
316
|
+
* model: string,
|
|
317
|
+
* classes?: string
|
|
318
|
+
* }}
|
|
319
|
+
*/
|
|
320
|
+
function extractBlockHeaderProperties(models, definition, mdast) {
|
|
321
|
+
const blockDetails = getBlockDetails(mdast, definition);
|
|
322
|
+
const props = {};
|
|
323
|
+
|
|
324
|
+
props.name = blockDetails.name;
|
|
325
|
+
if (blockDetails.modelId) {
|
|
326
|
+
props.model = blockDetails.modelId;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const model = findModelById(models, blockDetails.modelId);
|
|
330
|
+
|
|
331
|
+
// section metadata may not have a model
|
|
332
|
+
if (model) {
|
|
333
|
+
const classesField = getField(model, 'classes');
|
|
334
|
+
|
|
335
|
+
if (blockDetails.classes.length > 0 && classesField) {
|
|
336
|
+
props.classes = (classesField.multi === true)
|
|
337
|
+
? `[${blockDetails.classes.join(', ')}]` : blockDetails.classes.join(', ');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return props;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function getBlockItems(mdast, modelHelper, definitions, allowedComponents) {
|
|
345
|
+
// if there are no allowed components then we can't do anything
|
|
346
|
+
if (!allowedComponents.length) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const items = [];
|
|
351
|
+
// get all rows after the header that are more than one cell wide
|
|
352
|
+
const rows = findAll(mdast, (node) => node.type === 'gtRow', false);
|
|
353
|
+
|
|
354
|
+
rows.forEach((row) => {
|
|
355
|
+
const cellText = toString(row.children[0]);
|
|
356
|
+
|
|
357
|
+
// the first cell may be defined as the component id (with optional classes)
|
|
358
|
+
let componentId = cellText.split(',').shift().trim();
|
|
359
|
+
|
|
360
|
+
// if we can't find the component id in the allowed components it means
|
|
361
|
+
// the user has not specified what component to use therefore we fall back
|
|
362
|
+
// to the first allowed component.
|
|
363
|
+
if (allowedComponents.indexOf(componentId) === -1) {
|
|
364
|
+
[componentId] = allowedComponents;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// check to see if we can use this component
|
|
368
|
+
if (allowedComponents.includes(componentId)) {
|
|
369
|
+
const fieldGroup = modelHelper.getModelFieldGroup(componentId);
|
|
370
|
+
if (fieldGroup) {
|
|
371
|
+
const component = getComponentById(definitions, componentId);
|
|
372
|
+
const properties = { ...component.defaultFields };
|
|
373
|
+
extractProperties(row, fieldGroup.model, 'blockItem', component, fieldGroup.fields, properties);
|
|
374
|
+
items.push(`<item_${items.length} sling:resourceType="core/franklin/components/block/v1/block/item" name="${fieldGroup.model.id}" ${Object.entries(properties).map(([k, v]) => `${k}="${v}"`).join(' ')}></item_${items.length}>`);
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
// const msg = `The component '${componentId}' is not allowed in the block
|
|
378
|
+
// '${modelHelper.blockName}'. Modify the ${modelHelper.blockName} filters
|
|
379
|
+
// file to include the '${componentId}' component in the list of
|
|
380
|
+
// components.`;
|
|
381
|
+
// console.error(msg);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return items;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The gridTablePartial function is a Handlebars partial that generates a block element.
|
|
390
|
+
* @param {{models: Array<Model>,
|
|
391
|
+
* definition: Definition,
|
|
392
|
+
* filters: Filters,
|
|
393
|
+
* mdast: object}} context - The context
|
|
394
|
+
* object that contains the models, definition, and mdast.
|
|
395
|
+
* @return {string} - The generated block element.
|
|
396
|
+
*/
|
|
397
|
+
function gridTablePartial(context) {
|
|
398
|
+
const {
|
|
399
|
+
models,
|
|
400
|
+
definition,
|
|
401
|
+
filters,
|
|
402
|
+
...mdast
|
|
403
|
+
} = context;
|
|
404
|
+
|
|
405
|
+
// if models is not an array throw an error
|
|
406
|
+
if (!Array.isArray(models)) {
|
|
407
|
+
throw new Error('Do you have a `*-models.json` file?');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'block');
|
|
411
|
+
|
|
412
|
+
// assign the header properties to the block properties
|
|
413
|
+
const blockHeaderProperties = extractBlockHeaderProperties(models, definition, mdast);
|
|
414
|
+
|
|
415
|
+
// now that we have the name of the block, we can find the associated model
|
|
416
|
+
const model = findModelById(models, blockHeaderProperties.model);
|
|
417
|
+
|
|
418
|
+
let component;
|
|
419
|
+
let mode = 'simple';
|
|
420
|
+
|
|
421
|
+
// both pageHelper metadata and section metadata are tables, but we don't want to process them
|
|
422
|
+
// here they have been processed by the page helper partial and section helper.
|
|
423
|
+
if (blockHeaderProperties.model === 'section-metadata' || blockHeaderProperties.model === 'page-metadata') {
|
|
424
|
+
// we already processed pageHelper metadata in the pageHelper helper
|
|
425
|
+
return '';
|
|
426
|
+
} else {
|
|
427
|
+
component = getComponentByTitle(definition, blockHeaderProperties.name);
|
|
428
|
+
if (component === undefined) {
|
|
429
|
+
// we could possibly do a case-insensitive lookup?
|
|
430
|
+
throw new Error(`The component '${blockHeaderProperties.name}' does not exist. Check the spelling of the component name.`);
|
|
431
|
+
}
|
|
432
|
+
mode = component.keyValue ? 'keyValue' : 'simple';
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Assign the template properties to the block properties
|
|
436
|
+
const properties = {
|
|
437
|
+
'sling:resourceType': 'core/franklin/components/block/v1/block',
|
|
438
|
+
'jcr:primaryType': 'nt:unstructured',
|
|
439
|
+
...component.defaultFields,
|
|
440
|
+
...blockHeaderProperties,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
let blockProperties = '';
|
|
444
|
+
let fieldGroup;
|
|
445
|
+
|
|
446
|
+
const modelHelper = new ModelHelper(
|
|
447
|
+
blockHeaderProperties.name,
|
|
448
|
+
models,
|
|
449
|
+
definition,
|
|
450
|
+
filters,
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
// it is possible that a block (Accordion) does not have a model, but the
|
|
454
|
+
// child component will, which will be handled in the Component Block Processing
|
|
455
|
+
// section
|
|
456
|
+
if (model) {
|
|
457
|
+
fieldGroup = modelHelper.getModelFieldGroup(model.id);
|
|
458
|
+
extractProperties(mdast, model, mode, component, fieldGroup.fields, properties);
|
|
459
|
+
|
|
460
|
+
// sort all the properties so that they are in a consistent order
|
|
461
|
+
// helpful for debugging and xml readability
|
|
462
|
+
const sorted = Object.entries(properties).sort(sortJcrProperties);
|
|
463
|
+
blockProperties = sorted.map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
464
|
+
} else {
|
|
465
|
+
// because we have no model we expect the block to have a filter with a component that does
|
|
466
|
+
// so that means we can remove the header row from the mdast tree
|
|
467
|
+
remove(mdast, (n) => is(n, { type: 'gtHeader' }));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// *****************************************************
|
|
471
|
+
// Component Block Processing
|
|
472
|
+
// *****************************************************
|
|
473
|
+
// 1. In this section attempt to locate the associated model for the block.
|
|
474
|
+
// 2. Trim the mdast nodes to only be relevant for the child block.
|
|
475
|
+
// 3. Then getBlockitems will process the mdast nodes and return the block items.
|
|
476
|
+
const allowedComponents = filters.find((f) => f.id === component.filterId)?.components || [];
|
|
477
|
+
// collect all rows
|
|
478
|
+
const blockRows = findAll(mdast, (node) => node.type === 'gtRow', true);
|
|
479
|
+
// the fieldGroup (parent model) determines the expected number of rows in the table
|
|
480
|
+
// so we can remove the rows that belong to the parent and leave only the
|
|
481
|
+
// relevant rows for the child
|
|
482
|
+
if (model) {
|
|
483
|
+
const removed = blockRows.splice(0, fieldGroup.fields.length + 1);
|
|
484
|
+
// remove the elements from the mdast tree that match the items in the removed array
|
|
485
|
+
removed.forEach((r) => {
|
|
486
|
+
remove(mdast, (n) => is(n, r));
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const blockItems = getBlockItems(mdast, modelHelper, definition, allowedComponents) || [];
|
|
491
|
+
|
|
492
|
+
return `<block${uniqueName} ${blockProperties}>${blockItems.length > 0 ? blockItems.join('\n') : ''}</block${uniqueName}>`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export default gridTablePartial;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
13
|
+
|
|
14
|
+
function heading(context) {
|
|
15
|
+
// You can access the context here and return a string based on it
|
|
16
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'title');
|
|
17
|
+
const { depth } = context;
|
|
18
|
+
|
|
19
|
+
const parseInlineElements = (node) => {
|
|
20
|
+
if (node.type === 'text') {
|
|
21
|
+
return node.value;
|
|
22
|
+
}
|
|
23
|
+
if (node.type === 'emphasis') {
|
|
24
|
+
return `<em>${node.children.map(parseInlineElements).join('')}</em>`;
|
|
25
|
+
}
|
|
26
|
+
if (node.type === 'strong') {
|
|
27
|
+
return `<strong>${node.children.map(parseInlineElements).join('')}</strong>`;
|
|
28
|
+
}
|
|
29
|
+
if (node.type === 'link') {
|
|
30
|
+
return `<a href="${node.url}">${node.children.map(parseInlineElements).join('')}</a>`;
|
|
31
|
+
}
|
|
32
|
+
if (node.type === 'inlineCode') {
|
|
33
|
+
return `<code>${node.value}</code>`;
|
|
34
|
+
}
|
|
35
|
+
if (node.type === 'image') {
|
|
36
|
+
return `<img src="${node.url}" alt="${node.alt}"/>`;
|
|
37
|
+
}
|
|
38
|
+
if (node.type === 'html') {
|
|
39
|
+
return node.value; // HTML tags are included directly
|
|
40
|
+
}
|
|
41
|
+
return '';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let title = '';
|
|
45
|
+
// Traverse the children of the heading to build the title, there can be
|
|
46
|
+
// multiple children that contribute to the title like text, link, strong, etc.
|
|
47
|
+
|
|
48
|
+
title = Handlebars.Utils.escapeExpression(context.children.map(parseInlineElements).join(''));
|
|
49
|
+
|
|
50
|
+
return `<title${uniqueName} sling:resourceType="core/franklin/components/title/v1/title" jcr:primaryType="nt:unstructured" jcr:title="${title}" type="h${depth}"/>\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default heading;
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
13
|
+
import { encodeHtml } from '../../utils.js';
|
|
14
|
+
|
|
15
|
+
function image(context) {
|
|
16
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'image');
|
|
17
|
+
const { alt, url } = context;
|
|
18
|
+
|
|
19
|
+
const encoded = encodeHtml(url);
|
|
20
|
+
return `<image${uniqueName} sling:resourceType="core/franklin/components/image/v1/image"
|
|
21
|
+
jcr:primaryType="nt:unstructured" fileReference="${encoded}" alt="${alt}"/>\n`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default image;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line no-unused-vars
|
|
16
|
+
function link(context) {
|
|
17
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'button');
|
|
18
|
+
const { url, title, children: [child] } = context;
|
|
19
|
+
const attributes = {
|
|
20
|
+
'sling:resourceType': 'core/franklin/components/button/v1/button',
|
|
21
|
+
'jcr:primaryType': 'nt:unstructured',
|
|
22
|
+
link: url,
|
|
23
|
+
linkText: child?.value || '',
|
|
24
|
+
};
|
|
25
|
+
if (title != null) {
|
|
26
|
+
attributes.linkTitle = title;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const attributesStr = Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
30
|
+
return `<button${uniqueName} ${attributesStr} />`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default link;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
13
|
+
|
|
14
|
+
import { toHast } from 'mdast-util-to-hast';
|
|
15
|
+
import { toHtml } from 'hast-util-to-html';
|
|
16
|
+
|
|
17
|
+
function paragraph(context) {
|
|
18
|
+
const html = context.children.map((child) => {
|
|
19
|
+
const hast = toHast(child);
|
|
20
|
+
return toHtml(hast);
|
|
21
|
+
}).join('');
|
|
22
|
+
|
|
23
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'text');
|
|
24
|
+
return `<text${uniqueName}
|
|
25
|
+
sling:resourceType="core/franklin/components/text/v1/text"
|
|
26
|
+
jcr:primaryType="nt:unstructured"
|
|
27
|
+
text="${Handlebars.Utils.escapeExpression(html)}"/>`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default paragraph;
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import Handlebars from 'handlebars';
|
|
13
|
+
|
|
14
|
+
function strong(context) {
|
|
15
|
+
const uniqueName = Handlebars.helpers.nameHelper.call(context, 'button');
|
|
16
|
+
const { children: [child] } = context;
|
|
17
|
+
const { title, url, children: [{ value }] } = child;
|
|
18
|
+
const attributes = {
|
|
19
|
+
'sling:resourceType': 'core/franklin/components/button/v1/button',
|
|
20
|
+
'jcr:primaryType': 'nt:unstructured',
|
|
21
|
+
link: url,
|
|
22
|
+
linkText: value,
|
|
23
|
+
linkType: 'primary',
|
|
24
|
+
};
|
|
25
|
+
if (title != null) {
|
|
26
|
+
attributes.linkTitle = title;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const attributesStr = Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
30
|
+
return `<button${uniqueName} ${attributesStr} />`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default strong;
|