@adobe/helix-md2jcr 1.1.24 → 1.1.25

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 CHANGED
@@ -1,3 +1,10 @@
1
+ ## [1.1.25](https://github.com/adobe/helix-md2jcr/compare/v1.1.24...v1.1.25) (2025-06-27)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **108:** support columns block ([#111](https://github.com/adobe/helix-md2jcr/issues/111)) ([7792a12](https://github.com/adobe/helix-md2jcr/commit/7792a12f5178c16f13e15e92b0970f6c826e23ad))
7
+
1
8
  ## [1.1.24](https://github.com/adobe/helix-md2jcr/compare/v1.1.23...v1.1.24) (2025-06-18)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-md2jcr",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "description": "Helix Service that converts markdown to JCR content structure",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@adobe/eslint-config-helix": "2.0.9",
58
- "@adobe/helix-mediahandler": "2.7.11",
58
+ "@adobe/helix-mediahandler": "2.7.13",
59
59
  "@commitlint/cli": "^19.6.1",
60
60
  "@commitlint/config-conventional": "^19.6.0",
61
61
  "@semantic-release/changelog": "6.0.3",
@@ -67,7 +67,7 @@
67
67
  "eslint": "8.57.1",
68
68
  "eslint-import-resolver-exports": "1.0.0-beta.5",
69
69
  "eslint-plugin-header": "3.1.1",
70
- "eslint-plugin-import": "2.31.0",
70
+ "eslint-plugin-import": "2.32.0",
71
71
  "husky": "^9.1.7",
72
72
  "junit-report-builder": "5.1.1",
73
73
  "lint-staged": "15.5.2",
@@ -32,20 +32,22 @@ const nameCounter = [];
32
32
  * or an empty string if the node is unique.
33
33
  */
34
34
  function nameHelper(name) {
35
+ // Support for per-context nameCounter
36
+ const context = this || {};
37
+ const counter = context.nameCounter || nameCounter;
38
+
35
39
  // reset the name counter if the node is a section or if the name counter is empty
36
- // we could reset the name counter and empty all the counters, but it's cool
37
- // to see the counts for debugging
38
- if (name === 'section' || !nameCounter.length) {
39
- nameCounter.push({});
40
+ if (name === 'section' || !counter.length) {
41
+ counter.push({});
40
42
  }
41
43
 
42
44
  // treat sections as a top level node and return the length of the name counter
43
45
  if (name === 'section') {
44
- return nameCounter.length > 1 ? `_${nameCounter.length - 1}` : '';
46
+ return counter.length > 1 ? `_${counter.length - 1}` : '';
45
47
  }
46
48
 
47
49
  // get the current counter
48
- const currentCounter = nameCounter[nameCounter.length - 1];
50
+ const currentCounter = counter[counter.length - 1];
49
51
 
50
52
  // increment the counter for the node name
51
53
  currentCounter[name] = (currentCounter[name] || 0) + 1;
@@ -0,0 +1,106 @@
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
+ import { toString } from 'mdast-util-to-string';
15
+ import { find } from 'unist-util-find';
16
+ import { findAllDeep } from '../../utils/mdast.js';
17
+ import whichPartialHelper from '../helpers/which-partial-helper.js';
18
+ import { getModelId } from '../../domain/Definitions.js';
19
+ import {
20
+ findModelById,
21
+ getField,
22
+ } from '../../domain/Models.js';
23
+
24
+ // eslint-disable-next-line no-unused-vars
25
+ function columnsPartial(context) {
26
+ const {
27
+ children: [tHeader, tBody],
28
+ models,
29
+ definition,
30
+ } = context;
31
+
32
+ const uniqueName = Handlebars.helpers.nameHelper.call(context, 'columns');
33
+
34
+ const rows = findAllDeep(tBody, (node) => node.type === 'gtRow');
35
+ const columns = findAllDeep(rows[0], (node) => node.type === 'gtCell');
36
+
37
+ const textNode = find(tHeader, { type: 'text' });
38
+ const regex = /^(?<blockName>[^(]+)\s*(\((?<classes>[^)]+)\))?$/;
39
+ const match = toString(textNode).match(regex);
40
+
41
+ let headerProps = {};
42
+
43
+ if (match) {
44
+ const blockName = match.groups.blockName.trim();
45
+ const modelId = getModelId(definition, blockName);
46
+ const blockClasses = match.groups.classes ? match.groups.classes.split(',').map((c) => c.trim()) : [];
47
+
48
+ const model = findModelById(models, modelId);
49
+ const classesField = getField(model, 'classes');
50
+ let classes = [];
51
+
52
+ if (blockClasses.length > 0 && classesField) {
53
+ classes = (classesField.multi === true) ? `[${blockClasses.join(',')}]` : blockClasses.join(',');
54
+ }
55
+
56
+ headerProps = {
57
+ // name: match.groups.blockName.trim(),
58
+ // modelFields: `[${getModelFieldNames(model).join(',')}]`,
59
+ classes,
60
+ rows: rows.length,
61
+ columns: columns.length,
62
+ };
63
+
64
+ // remove properties that are empty
65
+ headerProps = Object.fromEntries(Object.entries(headerProps).filter(([_, v]) => v !== '' && (Array.isArray(v) ? v.length > 0 : v !== null)));
66
+ }
67
+
68
+ const attributes = {
69
+ 'sling:resourceType': 'core/franklin/components/columns/v1/columns',
70
+ 'jcr:primaryType': 'nt:unstructured',
71
+ ...headerProps,
72
+ };
73
+
74
+ const rowEls = rows.map((row, rowIndex) => {
75
+ const cells = findAllDeep(row, (node) => node.type === 'gtCell');
76
+ const columnEls = cells.map((cell, cellIndex) => {
77
+ // custom naming counter for columns
78
+ const nameCounter = [{}];
79
+
80
+ // const columnFilter = filters.find((f) => f.id === 'column');
81
+ // const components = columnFilter?.components || [];
82
+
83
+ // render the children of the cell by calling the associated default content partial function
84
+ // eg. text, image, title, link
85
+ const childrenEls = cell.children.map((child) => {
86
+ const partial = whichPartialHelper(child.type);
87
+ // we shouldn't get a unsupported partial as the mdast should have been cleaned up earlier
88
+ // see mdast-columns-block.js how it cleans up the mdast tree
89
+ if (partial === Handlebars.partials.unsupported) {
90
+ throw new Error(`Unsupported node type in columns: ${child.type}`);
91
+ }
92
+
93
+ // call the partial to render the xml for this node, pass the name counter
94
+ // to keep track of the unique names
95
+ return partial({ ...child, nameCounter });
96
+ });
97
+ return `<col${cellIndex + 1} jcr:primaryType="nt:unstructured">${childrenEls.join('\n')}</col${cellIndex + 1}>`;
98
+ });
99
+ return `<row${rowIndex + 1} jcr:primaryType="nt:unstructured">${columnEls.join('\n')}</row${rowIndex + 1}>`;
100
+ });
101
+
102
+ const attributesStr = Object.entries(attributes).map(([k, v]) => `${k}="${v}"`).join(' ');
103
+ return `<columns${uniqueName} ${attributesStr}>${rowEls.join('\n')}</columns${uniqueName}>`;
104
+ }
105
+
106
+ export default columnsPartial;
@@ -13,11 +13,16 @@
13
13
  /* eslint-disable no-param-reassign */
14
14
  import Handlebars from 'handlebars';
15
15
  import xmlFormatter from 'xml-formatter';
16
- import { splitSection, unwrapImages as unwrapElements, wrapParagraphs } from './utils.js';
16
+ import {
17
+ splitSection,
18
+ unwrapImages as unwrapElements,
19
+ wrapParagraphs,
20
+ } from './utils.js';
17
21
  import sanitizeHtml from './mdast-sanitize-html.js';
18
22
  import headingPartial from './hb/partials/heading.js';
19
23
  import stringPartial from './hb/partials/strong.js';
20
24
  import emphasisPartial from './hb/partials/emphasis.js';
25
+ import columnsPartial from './hb/partials/columns.js';
21
26
  import linkPartial from './hb/partials/link.js';
22
27
  import paragraphWrapperPartial from './hb/partials/paragraph.js';
23
28
  import nameHelper, { nameReset } from './hb/helpers/name-helper.js';
@@ -30,6 +35,7 @@ import pageHelper from './hb/helpers/page-helper.js';
30
35
  import pageTemplate from './templates/page-template.js';
31
36
  import unsupportedPartial from './hb/partials/unsupported.js';
32
37
  import sanitizeEds from './mdast-sanitize-eds.js';
38
+ import { splitColumns } from './mdast-columns-block.js';
33
39
 
34
40
  /**
35
41
  * Converts a markdown AST to JCR XML. This function is the main entry point
@@ -46,6 +52,7 @@ export default async function mdast2jcr(mdast, options = {}) {
46
52
  mdast = splitSection(mdast);
47
53
  mdast = unwrapElements(mdast);
48
54
  mdast = wrapParagraphs(mdast);
55
+ mdast = splitColumns(mdast);
49
56
 
50
57
  Handlebars.registerPartial('heading', headingPartial);
51
58
  Handlebars.registerPartial('image', imagePartial);
@@ -53,6 +60,7 @@ export default async function mdast2jcr(mdast, options = {}) {
53
60
  Handlebars.registerPartial('strong', stringPartial);
54
61
  Handlebars.registerPartial('emphasis', emphasisPartial);
55
62
  Handlebars.registerPartial('paragraphWrapper', paragraphWrapperPartial);
63
+ Handlebars.registerPartial('columns', columnsPartial);
56
64
  Handlebars.registerPartial('gridTable', gridTablePartial);
57
65
  Handlebars.registerPartial('unsupported', unsupportedPartial);
58
66
 
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Copyright 2025 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 { toString } from 'mdast-util-to-string';
13
+ import { findAllDeep } from './utils/mdast.js';
14
+ import link from './hb/partials/supports/link.js';
15
+ import image from './hb/partials/supports/image.js';
16
+
17
+ export function splitColumns(mdast) {
18
+ const tables = findAllDeep(mdast, (node) => node.type === 'gridTable');
19
+
20
+ tables.map((table) => {
21
+ const header = findAllDeep(table, (node) => node.type === 'gtHeader');
22
+ const value = toString(header);
23
+ if (value.toLowerCase().startsWith('columns')) {
24
+ // eslint-disable-next-line no-param-reassign
25
+ table.type = 'columns';
26
+ }
27
+ return table;
28
+ });
29
+
30
+ // now that we have columns, we need to merge their children
31
+ findAllDeep(mdast, (node) => node.type === 'columns').forEach((column) => {
32
+ // find all rows of the table body
33
+ const rows = findAllDeep(column.children[1], (node) => node.type === 'gtRow');
34
+
35
+ rows.forEach((row) => {
36
+ const cells = findAllDeep(row, (node) => node.type === 'gtCell');
37
+
38
+ cells.forEach((cell) => {
39
+ const newChildren = [];
40
+ let paragraphGroup = [];
41
+
42
+ const { children } = cell;
43
+
44
+ // group elements that are not links or images into a paragraphWrapper
45
+ // until the next link or image
46
+ for (let i = 0; i < children.length; i += 1) {
47
+ const node = children[i];
48
+ // if we are a liink image, or heading we need to push the node to the new children
49
+ // otherwise we need to group the node and its children into a paragraphWrapper
50
+ if (node?.children?.length === 1
51
+ && (node.children[0].type === 'link'
52
+ || node.children[0].type === 'image'
53
+ || node.type === 'heading')
54
+ ) {
55
+ // if we had previously grouped nodes we need to close them off and reset
56
+ if (paragraphGroup.length > 0) {
57
+ newChildren.push({
58
+ type: 'paragraphWrapper',
59
+ children: paragraphGroup,
60
+ });
61
+ // reset the paragraphGroup
62
+ paragraphGroup = [];
63
+ }
64
+
65
+ // push the node as a new child
66
+ if (image.supports(node) || link.supports(node)) {
67
+ newChildren.push(node.children[0]);
68
+ } else {
69
+ newChildren.push(node);
70
+ }
71
+ } else {
72
+ // we have something that needs to be grouped into a paragraphWrapper
73
+ paragraphGroup.push(node);
74
+
75
+ // if it's the last node, we need to add the paragraphWrapper to the newChildren
76
+ if (i === children.length - 1) {
77
+ newChildren.push({
78
+ type: 'paragraphWrapper',
79
+ children: paragraphGroup,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ // eslint-disable-next-line no-param-reassign
85
+ cell.children = newChildren;
86
+ });
87
+ });
88
+ });
89
+
90
+ return mdast;
91
+ }
@@ -10,6 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ import { visit } from 'unist-util-visit';
14
+
13
15
  /**
14
16
  * findAll goes through the children of a node searching for nodes that match a
15
17
  * test function.
@@ -42,6 +44,21 @@ function findAll(node, test, flatten = false) {
42
44
  }
43
45
  }
44
46
 
47
+ /**
48
+ * Find all nodes that match the test condition
49
+ * @param {Node} tree - Node(s) to search
50
+ * @param {Function|string} test - Test function or node type
51
+ * @returns {Array} Array of matching nodes
52
+ */
53
+ function findAllDeep(tree, test) {
54
+ const results = [];
55
+ visit(tree, test, (node) => {
56
+ results.push(node);
57
+ });
58
+ return results;
59
+ }
60
+
45
61
  export {
46
62
  findAll,
63
+ findAllDeep,
47
64
  };