@manuscripts/transform 4.5.7 → 4.6.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 (63) hide show
  1. package/dist/cjs/jats/__tests__/jats-exporter.test.js +50 -20
  2. package/dist/cjs/jats/__tests__/jats-importer.test.js +26 -0
  3. package/dist/cjs/jats/__tests__/jats-roundtrip.test.js +6 -1
  4. package/dist/cjs/jats/exporter/JATSExporterBase.js +118 -0
  5. package/dist/cjs/jats/exporter/jats-exporter.js +227 -349
  6. package/dist/cjs/jats/exporter/lib.js +62 -0
  7. package/dist/cjs/jats/importer/jats-dom-parser.js +11 -3
  8. package/dist/cjs/schema/index.js +4 -1
  9. package/dist/cjs/schema/migration/migration-scripts/4.5.9.js +30 -0
  10. package/dist/cjs/schema/migration/migration-scripts/index.js +3 -1
  11. package/dist/cjs/schema/nodes/bibliography_element.js +0 -1
  12. package/dist/cjs/schema/nodes/bio.js +24 -0
  13. package/dist/cjs/schema/nodes/contributor.js +1 -2
  14. package/dist/cjs/schema/nodes/keywords_element.js +0 -1
  15. package/dist/cjs/types.js +1 -1
  16. package/dist/cjs/version.js +1 -1
  17. package/dist/es/jats/__tests__/jats-exporter.test.js +47 -20
  18. package/dist/es/jats/__tests__/jats-importer.test.js +26 -0
  19. package/dist/es/jats/__tests__/jats-roundtrip.test.js +3 -1
  20. package/dist/es/jats/exporter/JATSExporterBase.js +114 -0
  21. package/dist/es/jats/exporter/jats-exporter.js +201 -319
  22. package/dist/es/jats/exporter/lib.js +55 -0
  23. package/dist/es/jats/importer/jats-dom-parser.js +11 -3
  24. package/dist/es/schema/index.js +4 -1
  25. package/dist/es/schema/migration/migration-scripts/4.5.9.js +28 -0
  26. package/dist/es/schema/migration/migration-scripts/index.js +3 -1
  27. package/dist/es/schema/nodes/bibliography_element.js +0 -1
  28. package/dist/es/schema/nodes/bio.js +20 -0
  29. package/dist/es/schema/nodes/contributor.js +1 -2
  30. package/dist/es/schema/nodes/keywords_element.js +0 -1
  31. package/dist/es/types.js +1 -1
  32. package/dist/es/version.js +1 -1
  33. package/dist/types/jats/exporter/JATSExporterBase.d.ts +35 -0
  34. package/dist/types/jats/exporter/jats-exporter.d.ts +23 -19
  35. package/dist/types/jats/exporter/lib.d.ts +23 -0
  36. package/dist/types/schema/index.d.ts +2 -1
  37. package/dist/types/schema/migration/migration-scripts/4.5.9.d.ts +8 -0
  38. package/dist/types/schema/migration/migration-scripts/index.d.ts +3 -2
  39. package/dist/types/schema/nodes/bio.d.ts +10 -0
  40. package/dist/types/schema/types.d.ts +1 -1
  41. package/dist/types/types.d.ts +1 -1
  42. package/dist/types/version.d.ts +1 -1
  43. package/package.json +4 -4
  44. package/src/jats/__tests__/__fixtures__/jats-import.xml +7 -0
  45. package/src/jats/__tests__/__snapshots__/jats-importer.test.ts.snap +272 -151
  46. package/src/jats/__tests__/__snapshots__/jats-roundtrip.test.ts.snap +2 -2
  47. package/src/jats/__tests__/jats-exporter.test.ts +60 -20
  48. package/src/jats/__tests__/jats-importer.test.ts +37 -0
  49. package/src/jats/__tests__/jats-roundtrip.test.ts +3 -1
  50. package/src/jats/exporter/JATSExporterBase.ts +164 -0
  51. package/src/jats/exporter/jats-exporter.ts +217 -386
  52. package/src/jats/exporter/lib.ts +69 -0
  53. package/src/jats/importer/jats-dom-parser.ts +15 -5
  54. package/src/schema/index.ts +4 -1
  55. package/src/schema/migration/migration-scripts/4.5.9.ts +62 -0
  56. package/src/schema/migration/migration-scripts/index.ts +3 -1
  57. package/src/schema/nodes/bibliography_element.ts +0 -1
  58. package/src/schema/nodes/bio.ts +48 -0
  59. package/src/schema/nodes/contributor.ts +1 -2
  60. package/src/schema/nodes/keywords_element.ts +0 -1
  61. package/src/schema/types.ts +1 -0
  62. package/src/types.ts +1 -1
  63. package/src/version.ts +1 -1
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ /*!
3
+ * © 2026 Atypon Systems LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.chooseRefType = exports.createDefaultIDGenerator = exports.createCounter = exports.normalizeID = void 0;
19
+ const schema_1 = require("../../schema");
20
+ const normalizeID = (id) => id.replace(/:/g, '_');
21
+ exports.normalizeID = normalizeID;
22
+ const createCounter = () => {
23
+ const counts = new Map();
24
+ return {
25
+ increment: (field) => {
26
+ const value = counts.get(field);
27
+ const newValue = value === undefined ? 1 : value + 1;
28
+ counts.set(field, newValue);
29
+ return newValue;
30
+ },
31
+ };
32
+ };
33
+ exports.createCounter = createCounter;
34
+ const createDefaultIDGenerator = () => {
35
+ const counter = (0, exports.createCounter)();
36
+ return async (element) => {
37
+ const value = String(counter.increment(element.nodeName));
38
+ return `${element.localName}-${value}`;
39
+ };
40
+ };
41
+ exports.createDefaultIDGenerator = createDefaultIDGenerator;
42
+ const chooseRefType = (type) => {
43
+ switch (type) {
44
+ case schema_1.schema.nodes.figure:
45
+ case schema_1.schema.nodes.figure_element:
46
+ return 'fig';
47
+ case schema_1.schema.nodes.footnote:
48
+ return 'fn';
49
+ case schema_1.schema.nodes.table:
50
+ case schema_1.schema.nodes.table_element:
51
+ return 'table';
52
+ case schema_1.schema.nodes.section:
53
+ case schema_1.schema.nodes.abstract:
54
+ return 'sec';
55
+ case schema_1.schema.nodes.equation:
56
+ case schema_1.schema.nodes.equation_element:
57
+ return 'disp-formula';
58
+ case schema_1.schema.nodes.supplement:
59
+ return 'supplementary-material';
60
+ }
61
+ };
62
+ exports.chooseRefType = chooseRefType;
@@ -378,7 +378,7 @@ class JATSDOMParser {
378
378
  const footnoteIDs = [];
379
379
  const affiliationIDs = [];
380
380
  const correspIDs = [];
381
- const xrefs = element.querySelectorAll('xref');
381
+ const xrefs = element.querySelectorAll(':scope > xref');
382
382
  for (const xref of xrefs) {
383
383
  const rid = xref.getAttribute('rid');
384
384
  const type = xref.getAttribute('ref-type');
@@ -416,10 +416,18 @@ class JATSDOMParser {
416
416
  email: (0, utils_1.getTrimmedTextContent)(element, 'email') || '',
417
417
  };
418
418
  },
419
- getContent: () => {
420
- return prosemirror_model_1.Fragment.from(this.schema.text('_'));
419
+ getContent: (node) => {
420
+ const bio = node.querySelector('bio');
421
+ if (!bio) {
422
+ return prosemirror_model_1.Fragment.empty;
423
+ }
424
+ return prosemirror_model_1.Fragment.from(this.parse(bio, { topNode: this.schema.nodes.bio.create() }));
421
425
  },
422
426
  },
427
+ {
428
+ tag: 'bio',
429
+ node: 'bio',
430
+ },
423
431
  {
424
432
  tag: 'affiliations',
425
433
  node: 'affiliations',
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /*!
3
- * © 2019 Atypon Systems LLC
3
+ * © 2026 Atypon Systems LLC
4
4
  *
5
5
  * Licensed under the Apache License, Version 2.0 (the "License");
6
6
  * you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@ const backmatter_1 = require("./nodes/backmatter");
50
50
  const bibliography_element_1 = require("./nodes/bibliography_element");
51
51
  const bibliography_item_1 = require("./nodes/bibliography_item");
52
52
  const bibliography_section_1 = require("./nodes/bibliography_section");
53
+ const bio_1 = require("./nodes/bio");
53
54
  const blockquote_element_1 = require("./nodes/blockquote_element");
54
55
  const body_1 = require("./nodes/body");
55
56
  const box_element_1 = require("./nodes/box_element");
@@ -129,6 +130,7 @@ __exportStar(require("./nodes/awards"), exports);
129
130
  __exportStar(require("./nodes/bibliography_element"), exports);
130
131
  __exportStar(require("./nodes/bibliography_item"), exports);
131
132
  __exportStar(require("./nodes/bibliography_section"), exports);
133
+ __exportStar(require("./nodes/bio"), exports);
132
134
  __exportStar(require("./nodes/blockquote_element"), exports);
133
135
  __exportStar(require("./nodes/box_element"), exports);
134
136
  __exportStar(require("./nodes/caption"), exports);
@@ -214,6 +216,7 @@ exports.schema = new prosemirror_model_1.Schema({
214
216
  bibliography_element: bibliography_element_1.bibliographyElement,
215
217
  bibliography_section: bibliography_section_1.bibliographySection,
216
218
  blockquote_element: blockquote_element_1.blockquoteElement,
219
+ bio: bio_1.bio,
217
220
  list: list_1.list,
218
221
  caption: caption_1.caption,
219
222
  caption_title: caption_title_1.captionTitle,
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const LEGACY_CONTENTS_NODES = new Set([
4
+ 'contributor',
5
+ 'bibliography_element',
6
+ 'keywords_element',
7
+ ]);
8
+ class Migration459 {
9
+ constructor() {
10
+ this.fromVersion = '4.5.8';
11
+ this.toVersion = '4.5.9';
12
+ }
13
+ migrateNode(node) {
14
+ if (LEGACY_CONTENTS_NODES.has(node.type) &&
15
+ node.attrs &&
16
+ 'contents' in node.attrs) {
17
+ const attrs = { ...node.attrs };
18
+ delete attrs.contents;
19
+ node = { ...node, attrs };
20
+ }
21
+ if (node.type === 'contributor') {
22
+ const bio = node.content?.filter((child) => child.type === 'bio');
23
+ const migrated = { ...node };
24
+ delete migrated.content;
25
+ return bio?.length ? { ...migrated, content: bio } : migrated;
26
+ }
27
+ return node;
28
+ }
29
+ }
30
+ exports.default = Migration459;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /*!
3
- * © 2024 Atypon Systems LLC
3
+ * © 2026 Atypon Systems LLC
4
4
  *
5
5
  * Licensed under the Apache License, Version 2.0 (the "License");
6
6
  * you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ const _4_3_35_1 = __importDefault(require("./4.3.35"));
35
35
  const _4_4_2_1 = __importDefault(require("./4.4.2"));
36
36
  const _4_4_7_1 = __importDefault(require("./4.4.7"));
37
37
  const _4_5_3_1 = __importDefault(require("./4.5.3"));
38
+ const _4_5_9_1 = __importDefault(require("./4.5.9"));
38
39
  const migrations = [
39
40
  new _1_2_5_1.default(),
40
41
  new _2_3_22_1.default(),
@@ -53,5 +54,6 @@ const migrations = [
53
54
  new _4_4_2_1.default(),
54
55
  new _4_4_7_1.default(),
55
56
  new _4_5_3_1.default(),
57
+ new _4_5_9_1.default(),
56
58
  ];
57
59
  exports.default = migrations;
@@ -20,7 +20,6 @@ exports.bibliographyElement = {
20
20
  content: 'bibliography_item*',
21
21
  attrs: {
22
22
  id: { default: '' },
23
- contents: { default: '' },
24
23
  dataTracked: { default: null },
25
24
  },
26
25
  selectable: false,
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isBioNode = exports.bio = void 0;
4
+ exports.bio = {
5
+ content: 'image_element? paragraph',
6
+ attrs: {
7
+ id: { default: '' },
8
+ dataTracked: { default: null },
9
+ },
10
+ group: 'block',
11
+ toDOM: (node) => {
12
+ const bioNode = node;
13
+ return [
14
+ 'div',
15
+ {
16
+ class: 'bio',
17
+ id: bioNode.attrs.id,
18
+ },
19
+ 0,
20
+ ];
21
+ },
22
+ };
23
+ const isBioNode = (node) => node.type === node.type.schema.nodes.bio;
24
+ exports.isBioNode = isBioNode;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isContributorNode = exports.contributor = void 0;
4
4
  exports.contributor = {
5
- content: 'inline*',
5
+ content: 'bio?',
6
6
  atom: true,
7
7
  attrs: {
8
8
  id: { default: '' },
@@ -23,7 +23,6 @@ exports.contributor = {
23
23
  priority: { default: undefined },
24
24
  creditRoles: { default: [] },
25
25
  dataTracked: { default: null },
26
- contents: { default: '' },
27
26
  },
28
27
  group: 'block',
29
28
  toDOM: (node) => {
@@ -21,7 +21,6 @@ exports.keywordsElement = {
21
21
  content: 'keyword_group*',
22
22
  attrs: {
23
23
  id: { default: '' },
24
- contents: { default: '' },
25
24
  dataTracked: { default: null },
26
25
  },
27
26
  group: 'block element',
package/dist/cjs/types.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /*!
3
- * © 2019 Atypon Systems LLC
3
+ * © 2026 Atypon Systems LLC
4
4
  *
5
5
  * Licensed under the Apache License, Version 2.0 (the "License");
6
6
  * you may not use this file except in compliance with the License.
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
- exports.VERSION = "4.5.7";
4
+ exports.VERSION = "4.6.0";
@@ -14,6 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { parseXml } from 'libxmljs2';
17
+ import serializeToXML from 'w3c-xmlserializer';
17
18
  import { JATSExporter } from '../exporter/jats-exporter';
18
19
  import { parseJATSArticle } from '../importer/parse-jats-article';
19
20
  import { DEFAULT_CSL_OPTIONS } from './citations';
@@ -30,10 +31,10 @@ describe('JATS exporter', () => {
30
31
  const transformer = new JATSExporter();
31
32
  const input = await readAndParseFixture('jats-example-full.xml');
32
33
  const node = parseJATSArticle(input, sectionCategories);
33
- const result = await transformer.serializeToJATS(node, {
34
+ const document = await transformer.export(node, {
34
35
  csl: DEFAULT_CSL_OPTIONS,
35
36
  });
36
- expect(result).toMatchSnapshot('jats-export');
37
+ expect(serializeToXML(document)).toMatchSnapshot('jats-export');
37
38
  });
38
39
  test('export with & and < in bibliography metadata', async () => {
39
40
  const transformer = new JATSExporter();
@@ -46,27 +47,27 @@ describe('JATS exporter', () => {
46
47
  }
47
48
  });
48
49
  biblio.attrs.title += ' & Sons 55 < 135';
49
- const result = await transformer.serializeToJATS(node, {
50
+ const document = await transformer.export(node, {
50
51
  csl: DEFAULT_CSL_OPTIONS,
51
52
  });
52
- expect(result).toMatchSnapshot('jats-export-with-xml-unsafe-in-biblios');
53
+ expect(serializeToXML(document)).toMatchSnapshot('jats-export-with-xml-unsafe-in-biblios');
53
54
  });
54
55
  test('export v1.1', async () => {
55
56
  const transformer = new JATSExporter();
56
57
  const input = await readAndParseFixture('jats-example-full.xml');
57
58
  const node = parseJATSArticle(input, sectionCategories);
58
- const result = await transformer.serializeToJATS(node, {
59
+ const document = await transformer.export(node, {
59
60
  csl: DEFAULT_CSL_OPTIONS,
60
61
  version: '1.1',
61
62
  });
62
- expect(result).toMatchSnapshot('jats-export-1.1');
63
+ expect(serializeToXML(document)).toMatchSnapshot('jats-export-1.1');
63
64
  });
64
65
  test('export unknown version', async () => {
65
66
  const transformer = new JATSExporter();
66
67
  const input = await readAndParseFixture('jats-example-full.xml');
67
68
  const node = parseJATSArticle(input, sectionCategories);
68
69
  await expect(async () => {
69
- await transformer.serializeToJATS(node, {
70
+ await transformer.export(node, {
70
71
  csl: DEFAULT_CSL_OPTIONS,
71
72
  version: '1.0',
72
73
  });
@@ -76,9 +77,10 @@ describe('JATS exporter', () => {
76
77
  const transformer = new JATSExporter();
77
78
  const input = await readAndParseFixture('jats-tables-example.xml');
78
79
  const node = parseJATSArticle(input, sectionCategories);
79
- const xml = await transformer.serializeToJATS(node, {
80
+ const document = await transformer.export(node, {
80
81
  csl: DEFAULT_CSL_OPTIONS,
81
82
  });
83
+ const xml = serializeToXML(document);
82
84
  const resultDoc = parseXMLWithDTD(xml);
83
85
  const tableWrapFoot = resultDoc.get('//table-wrap/table-wrap-foot');
84
86
  const paragraph = resultDoc.get('//table-wrap/table-wrap-foot/p');
@@ -89,30 +91,30 @@ describe('JATS exporter', () => {
89
91
  const transformer = new JATSExporter();
90
92
  const input = await readAndParseFixture('jats-import.xml');
91
93
  const node = parseJATSArticle(input, sectionCategories);
92
- const xml = await transformer.serializeToJATS(node, {
94
+ const document = await transformer.export(node, {
93
95
  csl: DEFAULT_CSL_OPTIONS,
94
96
  });
95
- const { errors } = parseXMLWithDTD(xml);
97
+ const { errors } = parseXMLWithDTD(serializeToXML(document));
96
98
  expect(errors).toHaveLength(0);
97
99
  });
98
100
  test('DTD validation: article with title markup and citations', async () => {
99
101
  const transformer = new JATSExporter();
100
102
  const input = await readAndParseFixture('jats-document.xml');
101
103
  const node = parseJATSArticle(input, sectionCategories);
102
- const xml = await transformer.serializeToJATS(node, {
104
+ const document = await transformer.export(node, {
103
105
  csl: DEFAULT_CSL_OPTIONS,
104
106
  });
105
- const { errors } = parseXMLWithDTD(xml);
107
+ const { errors } = parseXMLWithDTD(serializeToXML(document));
106
108
  expect(errors).toHaveLength(0);
107
109
  });
108
110
  test('Export link', async () => {
109
111
  const transformer = new JATSExporter();
110
112
  const input = await readAndParseFixture('jats-import.xml');
111
113
  const node = parseJATSArticle(input, sectionCategories);
112
- const xml = await transformer.serializeToJATS(node, {
114
+ const document = await transformer.export(node, {
113
115
  csl: DEFAULT_CSL_OPTIONS,
114
116
  });
115
- const output = parseXMLWithDTD(xml);
117
+ const output = parseXMLWithDTD(serializeToXML(document));
116
118
  const link = output.get('//ext-link[@ext-link-type="uri"]');
117
119
  expect(link).not.toBeNull();
118
120
  expect(link.text()).toBe('https//orcid.org/0000-0003-2217-5904');
@@ -127,20 +129,20 @@ describe('JATS exporter', () => {
127
129
  const transformer = new JATSExporter();
128
130
  const input = await readAndParseFixture('jats-example-doc.xml');
129
131
  const node = parseJATSArticle(input, sectionCategories);
130
- const xml = await transformer.serializeToJATS(node, {
132
+ const document = await transformer.export(node, {
131
133
  csl: DEFAULT_CSL_OPTIONS,
132
134
  });
133
- const { errors } = parseXMLWithDTD(xml);
135
+ const { errors } = parseXMLWithDTD(serializeToXML(document));
134
136
  expect(errors).toHaveLength(0);
135
137
  });
136
138
  test('export with supplement', async () => {
137
139
  const transformer = new JATSExporter();
138
140
  const input = await readAndParseFixture('jats-import.xml');
139
141
  const node = parseJATSArticle(input, sectionCategories);
140
- const xml = await transformer.serializeToJATS(node, {
142
+ const document = await transformer.export(node, {
141
143
  csl: DEFAULT_CSL_OPTIONS,
142
144
  });
143
- const resultDoc = parseXMLWithDTD(xml);
145
+ const resultDoc = parseXMLWithDTD(serializeToXML(document));
144
146
  const supplementaryMaterial = resultDoc.get('//supplementary-material');
145
147
  if (!supplementaryMaterial) {
146
148
  throw new Error('No supplementary material found');
@@ -167,9 +169,10 @@ describe('JATS exporter', () => {
167
169
  n.attrs.rids.forEach((rid) => citedIDs.add(rid));
168
170
  }
169
171
  });
170
- const xml = await new JATSExporter().serializeToJATS(node, {
172
+ const document = await new JATSExporter().export(node, {
171
173
  csl: DEFAULT_CSL_OPTIONS,
172
174
  });
175
+ const xml = serializeToXML(document);
173
176
  const refCount = parseXMLWithDTD(xml).find('//ref-list/ref').length;
174
177
  expect(refCount).toBe(bibliographyItemIDs.size);
175
178
  const { errors } = parseXMLWithDTD(xml);
@@ -179,9 +182,10 @@ describe('JATS exporter', () => {
179
182
  const transformer = new JATSExporter();
180
183
  const input = await readAndParseFixture('jats-fn-group.xml');
181
184
  const node = parseJATSArticle(input, sectionCategories);
182
- const xml = await transformer.serializeToJATS(node, {
185
+ const document = await transformer.export(node, {
183
186
  csl: DEFAULT_CSL_OPTIONS,
184
187
  });
188
+ const xml = serializeToXML(document);
185
189
  const footnoteCategories = [
186
190
  'con',
187
191
  'deceased',
@@ -194,4 +198,27 @@ describe('JATS exporter', () => {
194
198
  expect(fn).not.toBeUndefined();
195
199
  }
196
200
  });
201
+ test('export contributor bio', async () => {
202
+ const transformer = new JATSExporter();
203
+ const input = await readAndParseFixture('jats-import.xml');
204
+ const node = parseJATSArticle(input, sectionCategories);
205
+ const xml = await transformer.export(node, {
206
+ csl: DEFAULT_CSL_OPTIONS,
207
+ });
208
+ const serialized = serializeToXML(xml);
209
+ const resultDoc = parseXMLWithDTD(serialized);
210
+ expect(resultDoc.errors).toHaveLength(0);
211
+ const contribs = resultDoc.find('//contrib-group/contrib');
212
+ expect(contribs).toHaveLength(2);
213
+ expect(contribs[0].find('bio')).toHaveLength(1);
214
+ expect(contribs[1].find('bio')).toHaveLength(0);
215
+ const graphic = resultDoc.get('//contrib/bio/graphic');
216
+ expect(graphic).not.toBeNull();
217
+ expect(graphic.attr('href')?.value()).toBe('agnete-hornnes.png');
218
+ expect(graphic.find('caption')).toHaveLength(0);
219
+ expect(resultDoc.get('//contrib/bio/graphic/alt-text').text()).toBe('Headshot of Agnete Hviid Hornnes');
220
+ const paragraph = resultDoc.get('//contrib/bio/p');
221
+ expect(paragraph.text()).toBe('Agnete is a Professor of Neurology at Herlev og Gentofte Hospital, specializing in stroke research.');
222
+ expect(resultDoc.get('//contrib/bio/p/italic')?.text()).toBe('Professor of Neurology');
223
+ });
197
224
  });
@@ -58,6 +58,32 @@ describe('JATS importer', () => {
58
58
  expect(contributorsNode).toHaveLength(0);
59
59
  });
60
60
  });
61
+ describe('bio', () => {
62
+ it('should parse bio with graphic and paragraph into the contributor node', async () => {
63
+ const jats = await readAndParseFixture('jats-import.xml');
64
+ const node = parseJATSArticle(jats, sectionCategories);
65
+ changeIDs(node);
66
+ const [contributor] = findNodesByType(node, schema.nodes.contributor);
67
+ expect(contributor.childCount).toBe(1);
68
+ const bio = contributor.firstChild;
69
+ expect(bio.type).toBe(schema.nodes.bio);
70
+ expect(bio.childCount).toBe(2);
71
+ const image = findNodeByType(bio, schema.nodes.image_element);
72
+ expect(findNodeByType(image, schema.nodes.figure).attrs.src).toBe('agnete-hornnes.png');
73
+ expect(findNodeByType(image, schema.nodes.alt_text).textContent).toBe('Headshot of Agnete Hviid Hornnes');
74
+ const paragraph = bio.lastChild;
75
+ expect(paragraph.type).toBe(schema.nodes.paragraph);
76
+ expect(paragraph.textContent).toBe('Agnete is a Professor of Neurology at Herlev og Gentofte Hospital, specializing in stroke research.');
77
+ expect(bio).toMatchSnapshot();
78
+ });
79
+ it('should not add bio content to contributors without a bio element', async () => {
80
+ const jats = await readAndParseFixture('jats-import.xml');
81
+ const node = parseJATSArticle(jats, sectionCategories);
82
+ const [, contributor] = findNodesByType(node, schema.nodes.contributor);
83
+ expect(contributor.childCount).toBe(0);
84
+ expect(findNodesByType(node, schema.nodes.bio)).toHaveLength(1);
85
+ });
86
+ });
61
87
  describe('affiliations', () => {
62
88
  it('should correctly parse affiliation nodes', async () => {
63
89
  const jats = await readAndParseFixture('jats-import.xml');
@@ -14,6 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { parseXml } from 'libxmljs2';
17
+ import serializeToXML from 'w3c-xmlserializer';
17
18
  import { JATSExporter } from '../exporter/jats-exporter';
18
19
  import { parseJATSArticle } from '../importer/parse-jats-article';
19
20
  import { DEFAULT_CSL_OPTIONS } from './citations';
@@ -29,9 +30,10 @@ const roundtrip = async (filename) => {
29
30
  const doc = new DOMParser().parseFromString(input, 'application/xml');
30
31
  const node = parseJATSArticle(doc, sectionCategories);
31
32
  const exporter = new JATSExporter();
32
- return await exporter.serializeToJATS(node, {
33
+ const document = await exporter.export(node, {
33
34
  csl: DEFAULT_CSL_OPTIONS,
34
35
  });
36
+ return serializeToXML(document);
35
37
  };
36
38
  describe('JATS roundtrip', () => {
37
39
  test('jats-import.xml roundtrip', async () => {
@@ -0,0 +1,114 @@
1
+ /*!
2
+ * © 2026 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { findChildrenByAttr, findChildrenByType } from 'prosemirror-utils';
17
+ import { isNodeOfType } from '../../schema';
18
+ import { normalizeID } from './lib';
19
+ export class JATSExporterBase {
20
+ constructor() {
21
+ this.nodesMap = new Map();
22
+ this.populateNodesMap = () => {
23
+ this.manuscriptNode.descendants((node) => {
24
+ const type = node.type;
25
+ const nodes = this.nodesMap.get(type) ?? [];
26
+ nodes.push(node);
27
+ this.nodesMap.set(type, nodes);
28
+ });
29
+ };
30
+ this.createElement = (tag, content, attrs) => {
31
+ const $element = this.document.createElement(tag);
32
+ if (content) {
33
+ $element.textContent = content;
34
+ }
35
+ if (attrs) {
36
+ Object.entries(attrs).forEach(([k, v]) => {
37
+ if (v) {
38
+ $element.setAttribute(k, v);
39
+ }
40
+ });
41
+ }
42
+ return $element;
43
+ };
44
+ this.appendElement = ($parent, tag, content, attrs) => {
45
+ const $element = this.createElement(tag, content, attrs);
46
+ $parent.appendChild($element);
47
+ return $element;
48
+ };
49
+ this.changeTag = ($node, tag) => {
50
+ const $clone = this.createElement(tag);
51
+ for (const attr of $node.attributes) {
52
+ $clone.setAttributeNS(null, attr.name, attr.value);
53
+ }
54
+ while ($node.firstChild) {
55
+ $clone.appendChild($node.firstChild);
56
+ }
57
+ $node.replaceWith($clone);
58
+ return $clone;
59
+ };
60
+ this.processChildNodes = ($element, node, contentNodeType) => {
61
+ node.forEach((childNode) => {
62
+ if (childNode.type === contentNodeType) {
63
+ if (childNode.attrs.id) {
64
+ $element.appendChild(this.serializeNode(childNode));
65
+ }
66
+ }
67
+ else if (childNode.type === node.type.schema.nodes.paragraph) {
68
+ $element.appendChild(this.serializeNode(childNode));
69
+ }
70
+ else if (childNode.type === node.type.schema.nodes.missing_figure) {
71
+ $element.appendChild(this.serializeNode(childNode));
72
+ }
73
+ });
74
+ };
75
+ }
76
+ isChildOfNodeType(targetID, type, descend = false) {
77
+ const nodes = this.getChildrenOfType(type);
78
+ return nodes.some((node) => {
79
+ const result = findChildrenByAttr(node, (attrs) => attrs.id === targetID, descend)[0];
80
+ return !!result;
81
+ });
82
+ }
83
+ getChildrenOfType(type, node) {
84
+ const nodes = node
85
+ ? findChildrenByType(node, type).map(({ node }) => node)
86
+ : this.nodesMap.get(type);
87
+ return (nodes ?? []).filter((n) => isNodeOfType(n, type));
88
+ }
89
+ getFirstChildOfType(type, node) {
90
+ return this.getChildrenOfType(type, node)[0];
91
+ }
92
+ fillEmptyElements($article, selector, tagName = 'p') {
93
+ const $empty = Array.from($article.querySelectorAll(selector)).filter(($el) => !$el.innerHTML);
94
+ $empty.forEach(($element) => $element.appendChild(this.createElement(tagName)));
95
+ }
96
+ createElementWithID(node, nodeName) {
97
+ const $element = this.createElement(nodeName);
98
+ $element.setAttribute('id', normalizeID(node.attrs.id));
99
+ return $element;
100
+ }
101
+ serializeNode(node) {
102
+ return this.serializer.serializeNode(node, {
103
+ document: this.document,
104
+ });
105
+ }
106
+ appendChildIfPresent($parent, tagName, textContent) {
107
+ if (!textContent) {
108
+ return;
109
+ }
110
+ const $element = this.createElement(tagName);
111
+ $element.textContent = textContent;
112
+ $parent.appendChild($element);
113
+ }
114
+ }