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