@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,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;