@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,128 @@
1
+ /*
2
+ * Copyright 2021 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /* eslint-disable no-await-in-loop,no-console */
14
+ import {
15
+ readdir, readFile, stat, writeFile,
16
+ } from 'fs/promises';
17
+ import path from 'path';
18
+ import { decode } from 'entities';
19
+ import { md2jcr } from '../index.js';
20
+
21
+ /**
22
+ * Read a JSON file. If the file does not exist, return an empty object.
23
+ * @param {string} filePath The path to the JSON file.
24
+ * @returns {Promise<{}|any>} A promise that resolves to the JSON object in the file,
25
+ */
26
+ async function readJsonFile(filePath) {
27
+ try {
28
+ if ((await stat(filePath)).isFile()) {
29
+ const models = await readFile(filePath, 'utf-8');
30
+ return JSON.parse(models);
31
+ }
32
+ } catch (error) {
33
+ // ignore the fact that the file doesn't exist
34
+ // console.error(`Error due to: ${error} for file ${filePath}`);
35
+ }
36
+ return {};
37
+ }
38
+
39
+ /**
40
+ * Convert a markdown file to JCR XML.
41
+ * @param mdFile {string} The path to the markdown file.
42
+ * @param decodeXML - if true a file with the decoded XML will be created.
43
+ * @returns {Promise<void>} A promise that resolves when the conversion is
44
+ * complete, or rejects if the file is not a markdown file.
45
+ */
46
+ async function convert(mdFile, decodeXML = false) {
47
+ if (!mdFile.endsWith('.md')) {
48
+ return Promise.reject(new Error('File must be a markdown file'));
49
+ }
50
+
51
+ const dir = path.dirname(mdFile);
52
+ const base = path.basename(mdFile, '.md');
53
+ const fileJcrXML = path.resolve(dir, `${base}.xml`);
54
+ const decodedXML = path.resolve(dir, `${base}-decoded.xml`);
55
+
56
+ let modelJson;
57
+ let definitionJson;
58
+ let filtersJson;
59
+
60
+ const blockDefinitionsPath = path.resolve(dir, `_${base}.json`);
61
+
62
+ // check to see if the blockFiles exists
63
+ try {
64
+ await stat(blockDefinitionsPath);
65
+ const blockDefinitionFile = await readJsonFile(path.resolve(dir, `_${base}.json`));
66
+ modelJson = blockDefinitionFile.models;
67
+ definitionJson = {
68
+ groups: [
69
+ {
70
+ title: 'Blocks',
71
+ id: 'blocks',
72
+ components: [
73
+ ...blockDefinitionFile.definitions,
74
+ ],
75
+ },
76
+ ],
77
+ };
78
+ filtersJson = blockDefinitionFile.filters;
79
+ } catch (err) {
80
+ const modelFile = path.resolve(dir, `${base}-models.json`);
81
+ const definitionFile = path.resolve(dir, `${base}-definitions.json`);
82
+ const filtersFile = path.resolve(dir, `${base}-filters.json`);
83
+ modelJson = await readJsonFile(modelFile);
84
+ definitionJson = await readJsonFile(definitionFile);
85
+ filtersJson = await readJsonFile(filtersFile);
86
+ }
87
+
88
+ const md = await readFile(mdFile, 'utf-8');
89
+
90
+ /** @type {Mdast2JCROptions} */
91
+ const opts = {
92
+ models: modelJson,
93
+ definition: definitionJson,
94
+ filters: filtersJson,
95
+ };
96
+
97
+ const xml = await md2jcr(md, opts);
98
+
99
+ if (decodeXML) {
100
+ writeFile(decodedXML, decode(xml));
101
+ }
102
+
103
+ return writeFile(fileJcrXML, xml);
104
+ }
105
+
106
+ /**
107
+ * Entry point to convert a markdown file to JCR XML.
108
+ * @param inPath {string} The path to the markdown file or directory containing
109
+ * markdown files.
110
+ * @returns {Promise<void>} A promise that resolves when the conversion is
111
+ * complete.
112
+ */
113
+ async function run(inPath) {
114
+ const dirOrFilePath = path.resolve(process.cwd(), inPath);
115
+
116
+ const files = [];
117
+ if ((await stat(dirOrFilePath)).isDirectory()) {
118
+ files.push(...await readdir(dirOrFilePath));
119
+ } else {
120
+ files.push(dirOrFilePath);
121
+ }
122
+
123
+ for (const file of files) {
124
+ await convert(file, process.argv.includes('-d'));
125
+ }
126
+ }
127
+
128
+ run(process.argv[2] || process.cwd()).catch(console.error);
package/src/index.d.ts ADDED
@@ -0,0 +1,16 @@
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 mdast2jcr from './mdast2jcr/index.js';
14
+ import md2jcr from './md2jcr/index.js';
15
+
16
+ export { mdast2jcr, md2jcr };
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import mdast2jcr from './mdast2jcr/index.js';
13
+ import md2jcr from './md2jcr/index.js';
14
+
15
+ export { mdast2jcr, md2jcr };
@@ -0,0 +1,20 @@
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 { Mdast2JCROptions } from '../mdast2jcr'
13
+
14
+ /**
15
+ * Convert a Markdown document to a JCR XML document.
16
+ * @param {string} md The Markdown document
17
+ * @param {Mdast2JCROptions} options options
18
+ * @returns {Promise<Buffer>} the converted JCR XML document
19
+ */
20
+ export default function md2jcr(md:string, options?: Mdast2JCROptions): Promise<Buffer>;
@@ -0,0 +1,36 @@
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 { unified } from 'unified';
13
+ import remark from 'remark-parse';
14
+ import gfm from 'remark-gfm';
15
+ import { dereference } from '@adobe/helix-markdown-support';
16
+ import { remarkMatter } from '@adobe/helix-markdown-support/matter';
17
+ import remarkGridTable from '@adobe/remark-gridtables';
18
+ import mdast2jcr from '../mdast2jcr/index.js';
19
+
20
+ /**
21
+ * Convert markdown to JCR XML.
22
+ * @param md {string} The markdown content.
23
+ * @param opts {Mdast2JCROptions} Options.
24
+ * @return {Promise<String>} The JCR XML.
25
+ */
26
+ export default async function md2jcr(md, opts) {
27
+ const mdast = unified()
28
+ .use(remark, { position: false })
29
+ .use(gfm)
30
+ .use(remarkMatter)
31
+ .use(remarkGridTable)
32
+ .parse(md);
33
+
34
+ dereference(mdast);
35
+ return mdast2jcr(mdast, opts);
36
+ }
@@ -0,0 +1,66 @@
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
+ * The Component class represents a single component located in the
15
+ * component-definitions.json file.
16
+ */
17
+ class Component {
18
+ /**
19
+ * Create a new Component.
20
+ * @param {string} name
21
+ * @param {string} id
22
+ * @param {object} template
23
+ */
24
+ constructor(name, id, template) {
25
+ this._template = template;
26
+ this._filterId = template?.filter;
27
+ this._modelId = template?.model;
28
+ this._keyValue = template?.['key-value'];
29
+ this._name = name;
30
+ this._id = id;
31
+ }
32
+
33
+ get id() {
34
+ return this._id;
35
+ }
36
+
37
+ get filterId() {
38
+ return this._filterId;
39
+ }
40
+
41
+ get modelId() {
42
+ return this._modelId;
43
+ }
44
+
45
+ get keyValue() {
46
+ return this._keyValue;
47
+ }
48
+
49
+ get name() {
50
+ return this._name;
51
+ }
52
+
53
+ /* eslint-disable no-unused-vars */
54
+ get defaultFields() {
55
+ // remove the fields that are listed below, but keep the rest as default fields
56
+ // to be added to the block
57
+ const {
58
+ 'key-value': keyValue,
59
+ ...rest
60
+ } = this._template;
61
+
62
+ return rest;
63
+ }
64
+ }
65
+
66
+ export default Component;
@@ -0,0 +1,78 @@
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
+ * @typedef {import('../mdast2jcr/index.d.ts').ComponentDef} ComponentDef
15
+ */
16
+
17
+ import Component from './Component.js';
18
+
19
+ /**
20
+ * Get the components from the definitions object, (component-definitions.json).
21
+ * @param {DefinitionDef} definitions The Definitions object.
22
+ * @returns {Component[]} An array of Component objects.
23
+ */
24
+ function getAllComponents(definitions) {
25
+ return definitions.groups.map((group) => group.components)
26
+ .flat()
27
+ .map((/** @type {ComponentDef} */ component) => new Component(
28
+ component.title,
29
+ component.id,
30
+ component.plugins?.xwalk?.page?.template,
31
+ ));
32
+ }
33
+
34
+ /**
35
+ * Get a component by locating it against the template name.
36
+ * @param {DefinitionDef} definitions The Definitions object.
37
+ * @param {string} title The title of the component.
38
+ * @returns {Component} The component.
39
+ */
40
+ function getComponentByTitle(definitions, title) {
41
+ return getAllComponents(definitions).find(
42
+ (component) => component.name === title,
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Get a component by its id.
48
+ * @param {DefinitionDef} definitions The Definitions object.
49
+ * @param {string} id The id of the component.
50
+ * @return {Component}
51
+ */
52
+ function getComponentById(definitions, id) {
53
+ return getAllComponents(definitions).find(
54
+ (component) => component.id === id,
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Get the model id given a component name, or null if not found.
60
+ * @param {DefinitionDef} definitions The Definitions object.
61
+ * @param {string} title of the component.
62
+ * @returns {string | null} The model id associated with the component or null
63
+ * if not found.
64
+ */
65
+ function getModelId(definitions, title) {
66
+ const component = getComponentByTitle(definitions, title);
67
+ if (component) {
68
+ return component.modelId || null;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ export {
74
+ getAllComponents,
75
+ getComponentByTitle,
76
+ getModelId,
77
+ getComponentById,
78
+ };
@@ -0,0 +1,85 @@
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
+ class FieldGroup {
13
+ /**
14
+ * Constructor.
15
+ * @param {Model} model the model to use for the field group.
16
+ */
17
+ constructor(model) {
18
+ this.model = model;
19
+
20
+ // protect against missing fields
21
+ if (!this.model.fields) {
22
+ this.model.fields = [];
23
+ }
24
+
25
+ this.fields = [];
26
+ this._groupFields();
27
+ }
28
+
29
+ /**
30
+ * Group the fields in the model by their group name.
31
+ * @return {*[]}
32
+ */
33
+ _groupFields() {
34
+ const suffixes = ['Alt', 'MimeType', 'Type', 'Text', 'Title'];
35
+
36
+ this.model.fields
37
+ .filter((field) => field.name !== 'classes')
38
+ .forEach((field) => {
39
+ if (field.name.includes('_')) {
40
+ const groupName = field.name.split('_')[0];
41
+ const groupObj = this.fields.find((item) => item.name === groupName) || {
42
+ name: groupName,
43
+ fields: [],
44
+ };
45
+
46
+ if (!this.fields.includes(groupObj)) {
47
+ this.fields.push(groupObj);
48
+ }
49
+
50
+ const suffix = suffixes.find((s) => field.name.endsWith(s));
51
+ const collapsedName = field.name.substring(0, field.name.lastIndexOf(suffix));
52
+ const collapsedField = groupObj.fields.find((item) => item.name === collapsedName);
53
+
54
+ if (collapsedField) {
55
+ collapsedField.collapsed = collapsedField.collapsed || [];
56
+ collapsedField.collapsed.push(field);
57
+ } else {
58
+ groupObj.fields.push(field);
59
+ }
60
+ } else {
61
+ const suffix = suffixes.find((s) => field.name.endsWith(s));
62
+ const groupName = field.name.substring(0, field.name.indexOf(suffix));
63
+ let groupObj = this.fields.find((item) => item.name === groupName);
64
+
65
+ if (!groupObj) {
66
+ groupObj = {
67
+ name: field.name,
68
+ fields: [field],
69
+ };
70
+ this.fields.push(groupObj);
71
+ } else {
72
+ // find the field in the group that has a name that starts with the field.name
73
+ const collapsedField = groupObj.fields.find((item) => field.name.startsWith(item.name));
74
+ if (!collapsedField) {
75
+ throw new Error(`Unable to find the collapsed field for field: ${field.name}`);
76
+ }
77
+ collapsedField.collapsed = collapsedField.collapsed || [];
78
+ collapsedField.collapsed.push(field);
79
+ }
80
+ }
81
+ });
82
+ }
83
+ }
84
+
85
+ export default FieldGroup;
@@ -0,0 +1,96 @@
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
+ * The FieldResolver class attempts to resolve a node to a model's field.
15
+ */
16
+ class FieldGroupFieldResolver {
17
+ /**
18
+ * Constructor.
19
+ * @param {Component} component - the component associated with the model.
20
+ */
21
+ constructor(component) {
22
+ this.component = component;
23
+ }
24
+
25
+ /**
26
+ * Return the collapsed field name, that can support the node's heading depth.
27
+ * If the field does not have a collapsed field that matches the heading
28
+ * depth, then return undefined.
29
+ * @param node - the node
30
+ * @param field - the field
31
+ * @return {Field} a field that matches the node or undefined
32
+ */
33
+ getFieldSupportingHeading(node, field) {
34
+ const fieldsFromTemplate = Object.entries(this.component.defaultFields)
35
+ .filter(([name]) => name.endsWith('Type'))
36
+ .filter(([, value]) => value === `h${node.depth}`);
37
+
38
+ // go through all the collapsed fields of the field and see if we can find
39
+ // a field that matches the name of our best match from the template
40
+ return field.collapsed
41
+ .find((cf) => fieldsFromTemplate.find(([name]) => cf.name === name));
42
+ }
43
+
44
+ /**
45
+ * Resolve the field.
46
+ * @param {Node} node - the node
47
+ * @param {{isGrouped: boolean, name: string, fields: [],
48
+ * collapsed: [{isGrouped: boolean, name: string, fields: []}]}} fieldGroup - the field group
49
+ */
50
+ resolve(node, fieldGroup) {
51
+ const { fields } = fieldGroup;
52
+ if (fields === undefined) {
53
+ // throw an Error indicating the issue
54
+ throw new Error(`Field group ${fieldGroup.name} does not have any fields defined.`);
55
+ }
56
+
57
+ let foundField = fields.find((field) => {
58
+ if (node.type === 'heading' && field.collapsed?.find((c) => c.name.endsWith('Type'))) {
59
+ // try to find a field that matches the heading depth, if we have one
60
+ // then the field supports it so we can return true
61
+ if (!this.getFieldSupportingHeading(node, field)) {
62
+ // if the field does not match the heading type then we return false
63
+ return false;
64
+ }
65
+ // if we have a match then we remove all fields up to and including the match
66
+ fields.splice(0, fields.indexOf(field) + 1);
67
+ return true;
68
+ } else if (node.type === 'paragraph') {
69
+ // is the first child an image?
70
+ if (node.children[0]?.type === 'image') {
71
+ // remove the field from fields
72
+ fields.splice(fields.indexOf(field), 1);
73
+ return true;
74
+ }
75
+
76
+ // do we have a link
77
+ if (node.children[0]?.type === 'link') {
78
+ fields.splice(fields.indexOf(field), 1);
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ return false;
85
+ });
86
+
87
+ if (!foundField) {
88
+ [foundField] = fields.splice(0, 1);
89
+ }
90
+
91
+ // if foundField is undefined then return the next field from the fields.
92
+ return foundField;
93
+ }
94
+ }
95
+
96
+ export default FieldGroupFieldResolver;
@@ -0,0 +1,123 @@
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 { getComponentById, getComponentByTitle } from './Definitions.js';
14
+ import FieldGroup from './FieldGroup.js';
15
+
16
+ /**
17
+ * @typedef {import('../index.d.ts').Filter} Filter
18
+ */
19
+
20
+ class ModelHelper {
21
+ /**
22
+ * Constructor.
23
+ * @param {string} blockName The block name.
24
+ * @param {Array<Model>} models The models.
25
+ * @param {Definition} definition The definition object.
26
+ * @param {Array<Filter>} filters The filters to apply to the fields.
27
+ */
28
+ constructor(blockName, models, definition, filters) {
29
+ this.blockName = blockName;
30
+ this._checkFieldOrder(models);
31
+ this.models = models;
32
+ this.definition = definition;
33
+ this.filters = filters;
34
+ this.groups = [];
35
+ this._groupModelFields();
36
+ }
37
+
38
+ // eslint-disable-next-line class-methods-use-this
39
+ _checkFieldOrder(models) {
40
+ const suffixes = ['Alt', 'MimeType', 'Type', 'Text', 'Title'];
41
+
42
+ models.forEach((model) => {
43
+ const seenBaseFields = new Set();
44
+ let warningGenerated = false;
45
+ const warnings = [];
46
+
47
+ model.fields.forEach((field) => {
48
+ const suffixMatch = suffixes.find((suffix) => field.name.endsWith(suffix));
49
+ const baseName = suffixMatch ? field.name.slice(0, -suffixMatch.length) : field.name;
50
+
51
+ if (suffixMatch) {
52
+ // This is a suffix field, we need to check if its base field exists
53
+ if (!seenBaseFields.has(baseName)) {
54
+ if (!warningGenerated) {
55
+ warnings.push(`Warning in model '${model.id}': Fields with suffixes should follow their base field.`);
56
+ warningGenerated = true;
57
+ }
58
+ warnings.push(` - Field '${field.name}' appears before its base field '${baseName}'.`);
59
+ }
60
+ } else {
61
+ // This is a base field, add it to seenBaseFields
62
+ seenBaseFields.add(baseName);
63
+ }
64
+ });
65
+
66
+ if (warningGenerated) {
67
+ throw new Error(warnings.join('\n'));
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Group the fields in the models.
74
+ *
75
+ * Given the name of the block, get the component and then locate the model. If the component
76
+ * has a filter, then get the filter and locate the associated component and its model.
77
+ *
78
+ * Each model will have a FieldGroup associated with it.
79
+ * Each FieldGroup will contain the fields that belong to the model and any fields that
80
+ * are part of a collapsed field will be grouped together.
81
+ */
82
+ _groupModelFields() {
83
+ const component = getComponentByTitle(this.definition, this.blockName);
84
+ const model = this.models.find((m) => m.id === component.modelId);
85
+
86
+ // this FieldGroup is for the main model
87
+ if (model) {
88
+ this.groups.push({ modelId: model?.id || undefined, fieldGroup: new FieldGroup(model) });
89
+ }
90
+
91
+ // if the component has a filter, then get the associated model and create a FieldGroup for it
92
+ if (component.filterId) {
93
+ const filter = this.filters.find((f) => f.id === component.filterId);
94
+
95
+ // now for each filter, get the model and create a FieldGroup for it
96
+ filter.components.forEach((componentName) => {
97
+ const c = getComponentById(this.definition, componentName);
98
+ const filtersModel = this.models.find((m) => m.id === c.modelId);
99
+
100
+ // could not locate the model for the filter throw
101
+ if (!filtersModel) {
102
+ throw new Error(`Unable to locate model for filter ${componentName}`);
103
+ }
104
+
105
+ this.groups.push({ modelId: filtersModel.id, fieldGroup: new FieldGroup(filtersModel) });
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Return the field group associated with the model id.
112
+ * @param modelId
113
+ * @return {FieldGroup|*|null}
114
+ */
115
+ getModelFieldGroup(modelId) {
116
+ const result = this.groups.find((group) => group.modelId === modelId);
117
+
118
+ // no need to pass the fieldGroup just return the fields in the group
119
+ return (result) ? result.fieldGroup : null;
120
+ }
121
+ }
122
+
123
+ export default ModelHelper;