@manuscripts/transform 4.5.2 → 4.5.3

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 (60) hide show
  1. package/dist/cjs/jats/__tests__/data/section-categories.js +5 -5
  2. package/dist/cjs/jats/__tests__/jats-importer.test.js +19 -5
  3. package/dist/cjs/jats/exporter/jats-exporter.js +725 -910
  4. package/dist/cjs/jats/exporter/jats-versions.js +9 -9
  5. package/dist/cjs/jats/importer/jats-dom-parser.js +25 -24
  6. package/dist/cjs/jats/importer/jats-transformations.js +14 -30
  7. package/dist/cjs/lib/section-categories.js +31 -1
  8. package/dist/cjs/schema/index.js +3 -0
  9. package/dist/cjs/schema/migration/migration-scripts/4.5.3.js +37 -0
  10. package/dist/cjs/schema/migration/migration-scripts/index.js +2 -0
  11. package/dist/cjs/schema/nodes/abstract.js +51 -0
  12. package/dist/cjs/schema/nodes/abstracts.js +1 -1
  13. package/dist/cjs/transformer/node-names.js +1 -0
  14. package/dist/cjs/transformer/node-title.js +2 -0
  15. package/dist/cjs/version.js +1 -1
  16. package/dist/es/jats/__tests__/data/section-categories.js +5 -5
  17. package/dist/es/jats/__tests__/jats-importer.test.js +19 -5
  18. package/dist/es/jats/exporter/jats-exporter.js +728 -913
  19. package/dist/es/jats/exporter/jats-versions.js +7 -7
  20. package/dist/es/jats/importer/jats-dom-parser.js +25 -24
  21. package/dist/es/jats/importer/jats-transformations.js +14 -30
  22. package/dist/es/lib/section-categories.js +28 -0
  23. package/dist/es/schema/index.js +3 -0
  24. package/dist/es/schema/migration/migration-scripts/4.5.3.js +35 -0
  25. package/dist/es/schema/migration/migration-scripts/index.js +2 -0
  26. package/dist/es/schema/nodes/abstract.js +47 -0
  27. package/dist/es/schema/nodes/abstracts.js +1 -1
  28. package/dist/es/transformer/node-names.js +1 -0
  29. package/dist/es/transformer/node-title.js +2 -0
  30. package/dist/es/version.js +1 -1
  31. package/dist/types/jats/exporter/jats-exporter.d.ts +31 -27
  32. package/dist/types/jats/exporter/jats-versions.d.ts +4 -4
  33. package/dist/types/jats/importer/jats-dom-parser.d.ts +0 -1
  34. package/dist/types/lib/section-categories.d.ts +3 -0
  35. package/dist/types/schema/index.d.ts +1 -0
  36. package/dist/types/schema/migration/migration-scripts/4.5.3.d.ts +8 -0
  37. package/dist/types/schema/migration/migration-scripts/index.d.ts +2 -1
  38. package/dist/types/schema/nodes/abstract.d.ts +26 -0
  39. package/dist/types/schema/types.d.ts +1 -1
  40. package/dist/types/version.d.ts +1 -1
  41. package/package.json +1 -1
  42. package/src/jats/__tests__/__snapshots__/jats-exporter.test.ts.snap +3 -3
  43. package/src/jats/__tests__/__snapshots__/jats-importer.test.ts.snap +86 -132
  44. package/src/jats/__tests__/__snapshots__/jats-roundtrip.test.ts.snap +3 -3
  45. package/src/jats/__tests__/data/section-categories.ts +5 -5
  46. package/src/jats/__tests__/jats-importer.test.ts +22 -6
  47. package/src/jats/exporter/jats-exporter.ts +855 -1126
  48. package/src/jats/exporter/jats-versions.ts +11 -11
  49. package/src/jats/importer/jats-dom-parser.ts +34 -34
  50. package/src/jats/importer/jats-transformations.ts +21 -37
  51. package/src/lib/section-categories.ts +36 -0
  52. package/src/schema/index.ts +3 -0
  53. package/src/schema/migration/migration-scripts/4.5.3.ts +57 -0
  54. package/src/schema/migration/migration-scripts/index.ts +2 -0
  55. package/src/schema/nodes/abstract.ts +66 -0
  56. package/src/schema/nodes/abstracts.ts +2 -1
  57. package/src/schema/types.ts +1 -0
  58. package/src/transformer/node-names.ts +1 -0
  59. package/src/transformer/node-title.ts +2 -0
  60. package/src/version.ts +1 -1
@@ -14,34 +14,20 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import * as Citeproc from 'citeproc';
17
- import { DOMParser as ProsemirrorDOMParser, DOMSerializer, } from 'prosemirror-model';
17
+ import { DOMSerializer } from 'prosemirror-model';
18
18
  import { findChildrenByAttr, findChildrenByType } from 'prosemirror-utils';
19
19
  import serializeToXML from 'w3c-xmlserializer';
20
20
  import { buildCiteprocCitation } from '../../lib/citeproc';
21
21
  import { CreditRoleUrls } from '../../lib/credit-roles';
22
22
  import { generateFootnoteLabels } from '../../lib/footnotes';
23
- import { nodeFromHTML } from '../../lib/html';
23
+ import { FOOTNOTE_SECTION_CATEGORY_IDS } from '../../lib/section-categories';
24
24
  import { sanitizeXmlString, XLINK_NAMESPACE, XML_NAMESPACE, } from '../../lib/xml';
25
25
  import { isBibliographyItemNode, isCitationNode, isNodeOfType, schema, } from '../../schema';
26
- import { isExecutableNodeType, isNodeType } from '../../transformer';
26
+ import { isExecutableNodeType } from '../../transformer';
27
27
  import { initJats, jatsVariableWrapper } from './citeproc';
28
- import { selectVersionIds } from './jats-versions';
28
+ import { selectVersionIDs } from './jats-versions';
29
29
  import { buildTargets } from './labels';
30
30
  const normalizeID = (id) => id.replace(/:/g, '_');
31
- const parser = ProsemirrorDOMParser.fromSchema(schema);
32
- const insertAbstractNode = (front, abstractNode) => {
33
- const articleMeta = front.querySelector(':scope > article-meta');
34
- if (!articleMeta) {
35
- return;
36
- }
37
- const insertBeforeElement = articleMeta.querySelector('kwd-group, funding-group, support-group, conference, counts, custom-meta-group');
38
- if (insertBeforeElement) {
39
- articleMeta.insertBefore(abstractNode, insertBeforeElement);
40
- }
41
- else {
42
- articleMeta.appendChild(abstractNode);
43
- }
44
- };
45
31
  export const createCounter = () => {
46
32
  const counts = new Map();
47
33
  return {
@@ -53,7 +39,7 @@ export const createCounter = () => {
53
39
  },
54
40
  };
55
41
  };
56
- const createDefaultIdGenerator = () => {
42
+ const createDefaultIDGenerator = () => {
57
43
  const counter = createCounter();
58
44
  return async (element) => {
59
45
  const value = String(counter.increment(element.nodeName));
@@ -71,6 +57,7 @@ const chooseRefType = (type) => {
71
57
  case schema.nodes.table_element:
72
58
  return 'table';
73
59
  case schema.nodes.section:
60
+ case schema.nodes.abstract:
74
61
  return 'sec';
75
62
  case schema.nodes.equation:
76
63
  case schema.nodes.equation_element:
@@ -79,10 +66,10 @@ const chooseRefType = (type) => {
79
66
  return 'supplementary-material';
80
67
  }
81
68
  };
82
- const sortContributors = (a, b) => Number(a.attrs.priority) - Number(b.attrs.priority);
83
69
  export class JATSExporter {
84
70
  constructor() {
85
71
  this.nodesMap = new Map();
72
+ this.contributorLabels = new Map();
86
73
  this.populateNodesMap = () => {
87
74
  this.manuscriptNode.descendants((node) => {
88
75
  const type = node.type;
@@ -96,53 +83,53 @@ export class JATSExporter {
96
83
  this.populateNodesMap();
97
84
  this.initCiteprocEngine(options);
98
85
  this.createSerializer();
99
- const versionIds = selectVersionIds(options.version ?? '1.2');
100
- this.document = document.implementation.createDocument(null, 'article', document.implementation.createDocumentType('article', versionIds.publicId, versionIds.systemId));
101
- const article = this.document.documentElement;
102
- article.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', XLINK_NAMESPACE);
103
- const front = this.buildFront();
104
- article.appendChild(front);
105
- article.setAttribute('article-type', manuscriptNode.attrs.articleType || 'other');
106
- article.setAttributeNS(XML_NAMESPACE, 'lang', manuscriptNode.attrs.primaryLanguageCode || 'en');
86
+ const versionIDs = selectVersionIDs(options.version ?? '1.2');
87
+ this.document = document.implementation.createDocument(null, 'article', document.implementation.createDocumentType('article', versionIDs.publicID, versionIDs.systemID));
88
+ const $article = this.document.documentElement;
89
+ $article.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', XLINK_NAMESPACE);
107
90
  this.labelTargets = buildTargets(manuscriptNode.descendants.bind(manuscriptNode));
108
91
  this.footnoteLabels = generateFootnoteLabels(manuscriptNode);
109
- const body = this.buildBody();
110
- article.appendChild(body);
111
- this.addParagraphsToSections(article);
112
- const back = this.buildBack(body);
113
- this.moveCoiStatementToAuthorNotes(back, front);
114
- article.appendChild(back);
115
- this.unwrapBody(body);
116
- this.moveAbstracts(front, body);
117
- this.removeBackContainer(body);
118
- this.updateFootnoteTypes(front, back);
119
- this.fillEmptyTableFooters(article);
120
- this.fillEmptyFootnotes(article);
121
- this.fillEmptyListItem(article);
122
- this.moveAwards(front, body);
123
- this.moveFloatsGroup(article);
92
+ const $body = this.buildBody();
93
+ const $back = this.buildBack();
94
+ this.routeBackmatterSections($body, $back);
95
+ const $coiStatement = this.extractCoiStatement($back);
96
+ $back.appendChild(this.buildRefList());
97
+ const $front = this.buildFront($coiStatement);
98
+ $article.appendChild($front);
99
+ $article.setAttribute('article-type', manuscriptNode.attrs.articleType || 'other');
100
+ $article.setAttributeNS(XML_NAMESPACE, 'lang', manuscriptNode.attrs.primaryLanguageCode || 'en');
101
+ $article.appendChild($body);
102
+ $article.appendChild($back);
103
+ this.addParagraphsToSections($article);
104
+ this.fillEmptyTableFooters($article);
105
+ this.fillEmptyFootnotes($article);
106
+ this.fillEmptyListItem($article);
107
+ const $floatsGroup = this.buildFloatsGroup();
108
+ if ($floatsGroup) {
109
+ $article.appendChild($floatsGroup);
110
+ }
124
111
  await this.rewriteIDs();
125
112
  return serializeToXML(this.document);
126
113
  };
127
- this.appendChildNodeOfType = (element, node, type) => {
114
+ this.appendChildNodeOfType = ($element, node, type) => {
128
115
  const childNode = this.getFirstChildOfType(type, node);
129
116
  if (childNode) {
130
- element.appendChild(this.serializeNode(childNode));
117
+ $element.appendChild(this.serializeNode(childNode));
131
118
  }
132
119
  };
133
- this.appendChildrenNodeOfType = (element, node, type) => {
120
+ this.appendChildrenNodeOfType = ($element, node, type) => {
134
121
  const children = this.getChildrenOfType(type, node);
135
- children.map((childNode) => element.appendChild(this.serializeNode(childNode)));
122
+ children.map((childNode) => $element.appendChild(this.serializeNode(childNode)));
136
123
  };
137
- this.appendCaption = (element, node) => {
138
- const caption = this.createElement('caption');
139
- this.appendChildNodeOfType(caption, node, schema.nodes.caption_title);
124
+ this.appendCaption = ($element, node) => {
125
+ const $caption = this.createElement('caption');
126
+ this.appendChildNodeOfType($caption, node, schema.nodes.caption_title);
140
127
  const captionNode = this.getChildrenOfType(schema.nodes.caption, node)[0];
141
128
  if (captionNode && captionNode.content.size > 2) {
142
- this.appendChildrenNodeOfType(caption, captionNode, schema.nodes.text_block);
129
+ this.appendChildrenNodeOfType($caption, captionNode, schema.nodes.text_block);
143
130
  }
144
- if (caption.children.length > 0) {
145
- element.appendChild(caption);
131
+ if ($caption.children.length > 0) {
132
+ $element.appendChild($caption);
146
133
  }
147
134
  };
148
135
  this.initCiteprocEngine = (options) => {
@@ -177,33 +164,32 @@ export class JATSExporter {
177
164
  this.engine = engine;
178
165
  this.renderedCitations = new Map(output.map((i) => [i[0], i[2]]));
179
166
  };
180
- this.nodeFromJATS = (JATSFragment) => {
181
- JATSFragment = JATSFragment.trim();
182
- JATSFragment = JATSFragment.replace(' ', ' ');
183
- if (!JATSFragment.length) {
167
+ this.nodeFromJATS = (fragment) => {
168
+ fragment = fragment.trim().replace(' ', ' ');
169
+ if (!fragment.length) {
184
170
  return null;
185
171
  }
186
- const template = this.createElement('template');
187
- template.innerHTML = JATSFragment;
188
- return template.firstChild;
172
+ const $template = this.createElement('template');
173
+ $template.innerHTML = fragment;
174
+ return $template.firstChild;
189
175
  };
190
- this.rewriteIDs = async (generator = createDefaultIdGenerator()) => {
176
+ this.rewriteIDs = async (generator = createDefaultIDGenerator()) => {
191
177
  const ids = new Map();
192
- for (const element of this.document.querySelectorAll('[id]')) {
193
- const oldID = element.getAttribute('id');
194
- const newID = await generator(element);
178
+ for (const $element of this.document.querySelectorAll('[id]')) {
179
+ const oldID = $element.getAttribute('id');
180
+ const newID = await generator($element);
195
181
  if (newID) {
196
- element.setAttribute('id', newID);
182
+ $element.setAttribute('id', newID);
197
183
  }
198
184
  else {
199
- element.removeAttribute('id');
185
+ $element.removeAttribute('id');
200
186
  }
201
187
  if (oldID) {
202
188
  ids.set(oldID, newID);
203
189
  }
204
190
  }
205
- for (const node of this.document.querySelectorAll('[rid]')) {
206
- const rids = node.getAttribute('rid');
191
+ for (const $node of this.document.querySelectorAll('[rid]')) {
192
+ const rids = $node.getAttribute('rid');
207
193
  if (rids) {
208
194
  const newRids = rids
209
195
  .split(/\s+/)
@@ -211,126 +197,113 @@ export class JATSExporter {
211
197
  .map((rid) => ids.get(rid))
212
198
  .filter(Boolean);
213
199
  if (newRids.length) {
214
- node.setAttribute('rid', newRids.join(' '));
200
+ $node.setAttribute('rid', newRids.join(' '));
215
201
  }
216
202
  }
217
203
  }
218
204
  };
219
- this.setTitleContent = (element, title) => {
220
- const htmlTitleNode = nodeFromHTML(`<h1>${title}</h1>`);
221
- if (htmlTitleNode) {
222
- const titleNode = parser.parse(htmlTitleNode, {
223
- topNode: schema.nodes.section_title.create(),
224
- });
225
- const jatsTitleNode = this.serializeNode(titleNode);
226
- while (jatsTitleNode.firstChild) {
227
- element.appendChild(jatsTitleNode.firstChild);
228
- }
205
+ this.buildFront = ($coiStatement) => {
206
+ const $front = this.createElement('front');
207
+ const $articleMeta = this.createElement('article-meta');
208
+ $front.appendChild($articleMeta);
209
+ const contributors = this.getChildrenOfType(schema.nodes.contributor).sort((a, b) => Number(a.attrs.priority) - Number(b.attrs.priority));
210
+ this.contributorLabels = this.computeContributorLabels(contributors);
211
+ const $frontNodes = [
212
+ this.buildArticleID(),
213
+ this.buildTitleGroup(),
214
+ this.buildContributorGroup(contributors),
215
+ this.buildAuthorNotes($coiStatement),
216
+ this.buildSupplements(),
217
+ this.buildHistory(),
218
+ this.buildSelfUris(),
219
+ this.buildAbstracts(),
220
+ this.buildKeywords(),
221
+ this.buildAwards(),
222
+ this.buildCounts(),
223
+ ];
224
+ $frontNodes.forEach(($nodes) => this.appendChildren($articleMeta, $nodes));
225
+ return $front;
226
+ };
227
+ this.appendChildren = ($parent, $elements) => {
228
+ if (!$elements) {
229
+ return;
229
230
  }
231
+ const list = Array.isArray($elements) ? $elements : [$elements];
232
+ list.forEach(($element) => $parent.appendChild($element));
230
233
  };
231
- this.buildFront = () => {
232
- const front = this.createElement('front');
233
- const articleMeta = this.createElement('article-meta');
234
- front.appendChild(articleMeta);
234
+ this.buildArticleID = () => {
235
235
  if (this.manuscriptNode.attrs.doi) {
236
- const articleID = this.createElement('article-id');
237
- articleID.setAttribute('pub-id-type', 'doi');
238
- articleID.textContent = this.manuscriptNode.attrs.doi;
239
- articleMeta.appendChild(articleID);
236
+ const $articleID = this.createElement('article-id');
237
+ $articleID.setAttribute('pub-id-type', 'doi');
238
+ $articleID.textContent = this.manuscriptNode.attrs.doi;
239
+ return $articleID;
240
240
  }
241
- const titleGroup = this.createElement('title-group');
241
+ };
242
+ this.buildTitleGroup = () => {
243
+ const $titleGroup = this.createElement('title-group');
242
244
  const titleNode = this.getFirstChildOfType(schema.nodes.title);
243
245
  if (titleNode) {
244
- const element = this.createElement('article-title');
245
- this.setTitleContent(element, titleNode.textContent);
246
- titleGroup.appendChild(element);
246
+ $titleGroup.appendChild(this.serializeNode(titleNode));
247
247
  }
248
- const subtitlesNodes = this.getChildrenOfType(schema.nodes.subtitle);
249
- subtitlesNodes.forEach((subtitleNode) => {
250
- const element = this.createElement('subtitle');
251
- this.setTitleContent(element, subtitleNode.textContent);
252
- titleGroup.appendChild(element);
253
- });
254
- const altTitlesNodes = this.getChildrenOfType(schema.nodes.alt_title);
255
- altTitlesNodes.forEach((titleNode) => {
256
- const element = this.createElement('alt-title');
257
- element.setAttribute('alt-title-type', titleNode.attrs.type);
258
- this.setTitleContent(element, titleNode.textContent);
259
- titleGroup.appendChild(element);
260
- });
261
- articleMeta.appendChild(titleGroup);
262
- this.buildContributors(articleMeta);
263
- const supplementsNodes = this.getChildrenOfType(schema.nodes.supplement);
264
- supplementsNodes.forEach((node) => {
265
- const supplementaryMaterial = this.createElement('supplementary-material');
266
- supplementaryMaterial.setAttribute('id', normalizeID(node.attrs.id));
267
- supplementaryMaterial.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href ?? '');
268
- supplementaryMaterial.setAttribute('mimetype', node.attrs.mimeType ?? '');
269
- supplementaryMaterial.setAttribute('mime-subtype', node.attrs.mimeSubType ?? '');
270
- this.appendCaption(supplementaryMaterial, node);
271
- articleMeta.append(supplementaryMaterial);
248
+ this.getChildrenOfType(schema.nodes.subtitle).forEach((node) => $titleGroup.appendChild(this.serializeNode(node)));
249
+ this.getChildrenOfType(schema.nodes.alt_title).forEach((node) => $titleGroup.appendChild(this.serializeNode(node)));
250
+ return $titleGroup;
251
+ };
252
+ this.buildSupplements = () => this.getChildrenOfType(schema.nodes.supplement).map((node) => this.serializeNode(node));
253
+ this.buildHistory = () => {
254
+ const $history = this.createElement('history');
255
+ const dates = new Map([
256
+ ['accepted', this.manuscriptNode.attrs.acceptanceDate],
257
+ ['corrected', this.manuscriptNode.attrs.correctionDate],
258
+ ['retracted', this.manuscriptNode.attrs.retractionDate],
259
+ ['received', this.manuscriptNode.attrs.receiveDate],
260
+ ['rev-recd', this.manuscriptNode.attrs.revisionReceiveDate],
261
+ ['rev-request', this.manuscriptNode.attrs.revisionRequestDate],
262
+ ]);
263
+ dates.forEach((val, key) => {
264
+ if (val) {
265
+ $history.appendChild(this.buildDateElement(val, key));
266
+ }
272
267
  });
273
- const history = articleMeta.querySelector('history') || this.createElement('history');
274
- if (this.manuscriptNode.attrs.acceptanceDate) {
275
- const date = this.buildDateElement(this.manuscriptNode.attrs.acceptanceDate, 'accepted');
276
- history.appendChild(date);
277
- }
278
- if (this.manuscriptNode.attrs.correctionDate) {
279
- const date = this.buildDateElement(this.manuscriptNode.attrs.correctionDate, 'corrected');
280
- history.appendChild(date);
268
+ if ($history.childElementCount) {
269
+ return $history;
281
270
  }
282
- if (this.manuscriptNode.attrs.retractionDate) {
283
- const date = this.buildDateElement(this.manuscriptNode.attrs.retractionDate, 'retracted');
284
- history.appendChild(date);
285
- }
286
- if (this.manuscriptNode.attrs.receiveDate) {
287
- const date = this.buildDateElement(this.manuscriptNode.attrs.receiveDate, 'received');
288
- history.appendChild(date);
289
- }
290
- if (this.manuscriptNode.attrs.revisionReceiveDate) {
291
- const date = this.buildDateElement(this.manuscriptNode.attrs.revisionReceiveDate, 'rev-recd');
292
- history.appendChild(date);
293
- }
294
- if (this.manuscriptNode.attrs.revisionRequestDate) {
295
- const date = this.buildDateElement(this.manuscriptNode.attrs.revisionRequestDate, 'rev-request');
296
- history.appendChild(date);
297
- }
298
- if (history.childElementCount) {
299
- articleMeta.appendChild(history);
271
+ };
272
+ this.buildCounts = () => {
273
+ const $counts = this.createElement('counts');
274
+ const counts = new Map([
275
+ ['fig-count', this.getChildrenOfType(schema.nodes.figure).length],
276
+ ['table-count', this.getChildrenOfType(schema.nodes.table).length],
277
+ [
278
+ 'equation-count',
279
+ this.getChildrenOfType(schema.nodes.equation_element).length,
280
+ ],
281
+ [
282
+ 'ref-count',
283
+ this.getChildrenOfType(schema.nodes.bibliography_item).length,
284
+ ],
285
+ ['word-count', this.manuscriptNode.textContent.split(/\s+/).length],
286
+ ]);
287
+ counts.forEach((val, key) => {
288
+ if (val) {
289
+ $counts.appendChild(this.createElement(key, undefined, { count: String(val) }));
290
+ }
291
+ });
292
+ if ($counts.childElementCount) {
293
+ return $counts;
300
294
  }
301
- this.buildKeywords(articleMeta);
302
- let countingElements = [];
303
- const figureCount = this.getChildrenOfType(schema.nodes.figure).length;
304
- countingElements.push(this.buildCountingElement('fig-count', figureCount));
305
- const tableCount = this.getChildrenOfType(schema.nodes.table).length;
306
- countingElements.push(this.buildCountingElement('table-count', tableCount));
307
- const equationCount = this.getChildrenOfType(schema.nodes.equation_element).length;
308
- countingElements.push(this.buildCountingElement('equation-count', equationCount));
309
- const referencesCount = this.getChildrenOfType(schema.nodes.bibliography_item).length;
310
- countingElements.push(this.buildCountingElement('ref-count', referencesCount));
311
- const wordCount = this.manuscriptNode.textContent.split(/\s+/).length;
312
- countingElements.push(this.buildCountingElement('word-count', wordCount));
313
- countingElements = countingElements.filter((el) => el);
314
- if (countingElements.length > 0) {
315
- const counts = this.createElement('counts');
316
- counts.append(...countingElements);
317
- articleMeta.append(counts);
295
+ };
296
+ this.buildAwards = () => {
297
+ const awards = this.getFirstChildOfType(schema.nodes.awards);
298
+ if (!awards || !awards.childCount) {
299
+ return;
318
300
  }
319
- const attachments = this.getChildrenOfType(schema.nodes.attachment);
320
- attachments.forEach((attachment) => {
321
- const $selfUri = this.createElement('self-uri');
322
- $selfUri.setAttribute('content-type', attachment.attrs.type);
323
- $selfUri.setAttributeNS(XLINK_NAMESPACE, 'href', attachment.attrs.href);
324
- const insertBeforeElements = articleMeta.querySelector('related-article, related-object, abstract, trans-abstract, kwd-group, funding-group, support-group, conference, counts, custom-meta-group');
325
- insertBeforeElements
326
- ? articleMeta.insertBefore($selfUri, insertBeforeElements)
327
- : articleMeta.appendChild($selfUri);
328
- });
329
- return front;
301
+ return this.serializeNode(awards);
330
302
  };
303
+ this.buildSelfUris = () => this.getChildrenOfType(schema.nodes.attachment).map((attachment) => this.serializeNode(attachment));
331
304
  this.buildDateElement = (timestamp, type) => {
332
- const dateElement = this.createElement('date');
333
- dateElement.setAttribute('date-type', type);
305
+ const $date = this.createElement('date');
306
+ $date.setAttribute('date-type', type);
334
307
  const date = new Date(timestamp * 1000);
335
308
  const lookup = {
336
309
  year: date.getUTCFullYear().toString(),
@@ -338,127 +311,154 @@ export class JATSExporter {
338
311
  day: date.getUTCDate().toString().padStart(2, '0'),
339
312
  };
340
313
  for (const [key, value] of Object.entries(lookup).reverse()) {
341
- const el = this.createElement(key);
342
- el.textContent = value;
343
- dateElement.appendChild(el);
344
- }
345
- return dateElement;
346
- };
347
- this.buildCountingElement = (tagName, count) => {
348
- if (count) {
349
- const wordCount = this.createElement(tagName);
350
- wordCount.setAttribute('count', String(count));
351
- return wordCount;
314
+ const $datePart = this.createElement(key, value);
315
+ $date.appendChild($datePart);
352
316
  }
317
+ return $date;
353
318
  };
354
319
  this.buildBody = () => {
355
- const body = this.createElement('body');
356
- this.manuscriptNode.forEach((cFragment) => {
357
- const serializedNode = this.serializeNode(cFragment);
358
- body.append(...serializedNode.childNodes);
320
+ const body = this.getFirstChildOfType(schema.nodes.body);
321
+ const $body = this.createElement('body');
322
+ body?.forEach((node) => {
323
+ $body.appendChild(this.serializeNode(node));
359
324
  });
360
- this.fixBody(body);
361
- return body;
325
+ return $body;
362
326
  };
363
- this.buildBack = (body) => {
364
- const back = this.createElement('back');
365
- this.moveSectionsToBack(back, body);
366
- const footnotesElements = this.document.querySelectorAll('sec > fn-group');
367
- for (const footnotesElement of footnotesElements) {
368
- const previousParent = footnotesElement.parentElement;
369
- back.appendChild(footnotesElement);
370
- if (previousParent) {
371
- const title = previousParent.querySelector('title');
372
- if (title) {
373
- footnotesElement.insertBefore(title, footnotesElement.firstElementChild);
374
- }
375
- if (!previousParent.childElementCount) {
376
- previousParent.remove();
377
- }
327
+ this.buildBack = () => {
328
+ const backmatter = this.getFirstChildOfType(schema.nodes.backmatter);
329
+ const $back = this.createElement('back');
330
+ backmatter?.forEach((node) => {
331
+ $back.appendChild(this.serializeNode(node));
332
+ });
333
+ return $back;
334
+ };
335
+ this.findSection = (category, ...$roots) => {
336
+ for (const $root of $roots) {
337
+ const $match = $root.querySelector(`sec[sec-type="${category}"]`);
338
+ if ($match) {
339
+ return $match;
378
340
  }
379
341
  }
380
- let refList = this.document.querySelector('ref-list');
381
- if (!refList) {
382
- refList = this.createElement('ref-list');
342
+ };
343
+ this.findSections = (category, ...$roots) => $roots.flatMap(($root) => Array.from($root.querySelectorAll(`sec[sec-type="${category}"]`)));
344
+ this.routeBackmatterSections = ($body, $back) => {
345
+ const $footnoteSections = FOOTNOTE_SECTION_CATEGORY_IDS.flatMap((category) => this.findSections(category, $body, $back).map(($section) => ({
346
+ $section,
347
+ category,
348
+ })));
349
+ let $fnGroup;
350
+ if ($footnoteSections.length) {
351
+ $fnGroup = this.createElement('fn-group');
352
+ for (const { $section, category } of $footnoteSections) {
353
+ $fnGroup.appendChild(this.sectionToFootnote($section, category));
354
+ }
383
355
  }
384
- back.appendChild(refList);
385
- const parser = new DOMParser();
386
- const [_, bibliography] = this.engine.makeBibliography();
387
- for (let i = 0; i < bibliography.length; i++) {
388
- const item = `<template xmlns:xlink="${XLINK_NAMESPACE}">${sanitizeXmlString(bibliography[i])}</template>`;
389
- const ref = parser.parseFromString(item, 'text/xml').querySelector('ref');
390
- if (ref) {
391
- refList.appendChild(ref);
356
+ const $appendices = this.findSections('appendices', $body, $back);
357
+ let $appGroup;
358
+ if ($appendices.length) {
359
+ $appGroup = this.createElement('app-group');
360
+ for (const $section of $appendices) {
361
+ const $app = this.createElement('app');
362
+ $app.appendChild($section);
363
+ $appGroup.appendChild($app);
392
364
  }
393
365
  }
394
- return back;
366
+ const $ackSection = this.findSection('acknowledgements', $body, $back);
367
+ let $ack;
368
+ if ($ackSection) {
369
+ $ack = this.createElement('ack');
370
+ $ack.append(...$ackSection.childNodes);
371
+ $ackSection.remove();
372
+ }
373
+ const $availabilitySection = this.findSection('availability', $body, $back);
374
+ const $ethicsSection = this.findSection('ethics-statement', $body, $back);
375
+ const $prepend = [
376
+ $appGroup,
377
+ $ack,
378
+ $availabilitySection,
379
+ $ethicsSection,
380
+ $fnGroup,
381
+ ].filter((el) => el != null);
382
+ if ($prepend.length) {
383
+ $back.prepend(...$prepend);
384
+ }
385
+ };
386
+ this.extractCoiStatement = ($back) => {
387
+ const $coiStatement = $back.querySelector('fn[fn-type="coi-statement"]');
388
+ if (!$coiStatement) {
389
+ return undefined;
390
+ }
391
+ const $fnGroup = $coiStatement.parentElement;
392
+ $coiStatement.remove();
393
+ if ($fnGroup && !$fnGroup.hasChildNodes()) {
394
+ $fnGroup.remove();
395
+ }
396
+ return $coiStatement;
397
+ };
398
+ this.buildRefList = () => {
399
+ const $refList = this.createElement('ref-list');
400
+ const [, bibliography] = this.engine.makeBibliography();
401
+ const parser = new DOMParser();
402
+ bibliography.forEach((item) => {
403
+ const fragment = `<template xmlns:xlink="${XLINK_NAMESPACE}">${sanitizeXmlString(item)}</template>`;
404
+ const $ref = parser
405
+ .parseFromString(fragment, 'text/xml')
406
+ .querySelector('ref');
407
+ if ($ref) {
408
+ $refList.appendChild($ref);
409
+ }
410
+ });
411
+ return $refList;
395
412
  };
396
413
  this.createElement = (tag, content, attrs) => {
397
- const el = this.document.createElement(tag);
414
+ const $element = this.document.createElement(tag);
398
415
  if (content) {
399
- el.textContent = content;
416
+ $element.textContent = content;
400
417
  }
401
418
  if (attrs) {
402
419
  Object.entries(attrs).forEach(([k, v]) => {
403
420
  if (v) {
404
- el.setAttribute(k, v);
421
+ $element.setAttribute(k, v);
405
422
  }
406
423
  });
407
424
  }
408
- return el;
425
+ return $element;
409
426
  };
410
- this.appendElement = (parent, tag, content, attrs) => {
411
- const el = this.createElement(tag, content, attrs);
412
- parent.appendChild(el);
413
- return el;
427
+ this.appendElement = ($parent, tag, content, attrs) => {
428
+ const $element = this.createElement(tag, content, attrs);
429
+ $parent.appendChild($element);
430
+ return $element;
414
431
  };
415
432
  this.createSerializer = () => {
416
433
  const nodes = {
417
- trans_abstract: (node) => {
418
- const attrs = {
419
- id: normalizeID(node.attrs.id),
420
- };
421
- if (node.attrs.lang) {
422
- attrs[`${XML_NAMESPACE} lang`] = node.attrs.lang;
423
- }
424
- if (node.attrs.category) {
425
- attrs['sec-type'] = node.attrs.category;
426
- }
427
- return ['trans-abstract', attrs, 0];
428
- },
429
- trans_graphical_abstract: (node) => {
430
- const attrs = {
431
- id: normalizeID(node.attrs.id),
432
- };
433
- if (node.attrs.lang) {
434
- attrs[`${XML_NAMESPACE} lang`] = node.attrs.lang;
435
- }
436
- if (node.attrs.category) {
437
- attrs['sec-type'] = node.attrs.category;
438
- }
439
- return ['trans-abstract', attrs, 0];
440
- },
434
+ trans_abstract: (node) => createTransAbstract(node),
435
+ trans_graphical_abstract: (node) => createTransAbstract(node),
441
436
  hero_image: () => '',
442
437
  headshot_grid: () => ['p', { 'content-type': 'headshots' }, 0],
443
438
  headshot_element: (node) => createImage(node),
444
439
  headshot_image: () => '',
445
440
  alt_text: (node) => {
446
441
  if (node.textContent) {
447
- const altText = this.createElement('alt-text');
448
- altText.textContent = node.textContent;
449
- return altText;
442
+ const $altText = this.createElement('alt-text');
443
+ $altText.textContent = node.textContent;
444
+ return $altText;
450
445
  }
451
446
  return '';
452
447
  },
453
448
  long_desc: (node) => {
454
449
  if (node.textContent) {
455
- const longDesc = this.createElement('long-desc');
456
- longDesc.textContent = node.textContent;
457
- return longDesc;
450
+ const $longDesc = this.createElement('long-desc');
451
+ $longDesc.textContent = node.textContent;
452
+ return $longDesc;
458
453
  }
459
454
  return '';
460
455
  },
461
- attachment: () => '',
456
+ attachment: (node) => {
457
+ const $selfUri = this.createElement('self-uri');
458
+ $selfUri.setAttribute('content-type', node.attrs.type);
459
+ $selfUri.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href);
460
+ return $selfUri;
461
+ },
462
462
  attachments: () => '',
463
463
  image_element: (node) => createImage(node),
464
464
  embed: (node) => {
@@ -466,41 +466,45 @@ export class JATSExporter {
466
466
  if (!href) {
467
467
  return '';
468
468
  }
469
- const mediaElement = this.createElement('media');
470
- mediaElement.setAttribute('id', normalizeID(id));
471
- mediaElement.setAttributeNS(XLINK_NAMESPACE, 'show', 'embed');
472
- mediaElement.setAttributeNS(XLINK_NAMESPACE, 'href', href);
469
+ const $media = this.createElement('media');
470
+ $media.setAttribute('id', normalizeID(id));
471
+ $media.setAttributeNS(XLINK_NAMESPACE, 'show', 'embed');
472
+ $media.setAttributeNS(XLINK_NAMESPACE, 'href', href);
473
473
  if (mimetype) {
474
- mediaElement.setAttribute('mimetype', node.attrs.mimetype);
474
+ $media.setAttribute('mimetype', node.attrs.mimetype);
475
475
  }
476
476
  if (mimeSubtype) {
477
- mediaElement.setAttribute('mime-subtype', node.attrs.mimeSubtype);
477
+ $media.setAttribute('mime-subtype', node.attrs.mimeSubtype);
478
478
  }
479
- appendLabels(mediaElement, node);
480
- this.appendChildNodeOfType(mediaElement, node, schema.nodes.alt_text);
481
- this.appendChildNodeOfType(mediaElement, node, schema.nodes.long_desc);
482
- this.appendCaption(mediaElement, node);
483
- return mediaElement;
479
+ appendLabels($media, node);
480
+ this.appendChildNodeOfType($media, node, schema.nodes.alt_text);
481
+ this.appendChildNodeOfType($media, node, schema.nodes.long_desc);
482
+ this.appendCaption($media, node);
483
+ return $media;
484
484
  },
485
485
  awards: () => ['funding-group', 0],
486
486
  award: (node) => {
487
487
  const awardGroup = node;
488
- const awardGroupElement = this.createElement('award-group');
489
- awardGroupElement.setAttribute('id', normalizeID(awardGroup.attrs.id));
490
- appendChildIfPresent(awardGroupElement, 'funding-source', awardGroup.attrs.source);
488
+ const $awardGroup = this.createElement('award-group');
489
+ $awardGroup.setAttribute('id', normalizeID(awardGroup.attrs.id));
490
+ appendChildIfPresent($awardGroup, 'funding-source', awardGroup.attrs.source);
491
491
  awardGroup.attrs.code
492
492
  ?.split(';')
493
- .forEach((code) => appendChildIfPresent(awardGroupElement, 'award-id', code));
494
- appendChildIfPresent(awardGroupElement, 'principal-award-recipient', awardGroup.attrs.recipient);
495
- return awardGroupElement;
493
+ .forEach((code) => appendChildIfPresent($awardGroup, 'award-id', code));
494
+ appendChildIfPresent($awardGroup, 'principal-award-recipient', awardGroup.attrs.recipient);
495
+ return $awardGroup;
496
496
  },
497
497
  box_element: (node) => createBoxElement(node),
498
498
  author_notes: () => '',
499
499
  corresp: () => '',
500
- title: () => '',
501
- alt_title: () => '',
500
+ title: () => ['article-title', 0],
501
+ alt_title: (node) => [
502
+ 'alt-title',
503
+ { 'alt-title-type': node.attrs.type },
504
+ 0,
505
+ ],
502
506
  alt_titles: () => '',
503
- subtitle: () => '',
507
+ subtitle: () => ['subtitle', 0],
504
508
  subtitles: () => '',
505
509
  text_block: (node) => nodes.paragraph(node),
506
510
  affiliations: () => '',
@@ -508,17 +512,31 @@ export class JATSExporter {
508
512
  table_element_footer: (node) => node.childCount == 0
509
513
  ? ['table-wrap-foot', ['fn-group', ['fn', ['p']]]]
510
514
  : ['table-wrap-foot', 0],
511
- contributor: () => '',
512
- affiliation: () => '',
515
+ contributor: (node) => this.buildContributorElement(node),
516
+ affiliation: (node) => this.buildAffiliationElement(node),
513
517
  attribution: () => ['attrib', 0],
514
518
  bibliography_element: () => '',
515
519
  bibliography_item: () => '',
516
520
  comments: () => '',
517
- keyword_group: () => '',
521
+ keyword_group: (node) => {
522
+ const attrs = {};
523
+ if (node.attrs.type) {
524
+ attrs['kwd-group-type'] = node.attrs.type;
525
+ }
526
+ return ['kwd-group', attrs, 0];
527
+ },
518
528
  body: () => ['body', 0],
519
529
  abstracts: () => ['abstract', 0],
520
530
  backmatter: () => ['backmatter', 0],
521
- supplement: () => '',
531
+ supplement: (node) => {
532
+ const $supplementaryMaterial = this.createElement('supplementary-material');
533
+ $supplementaryMaterial.setAttribute('id', normalizeID(node.attrs.id));
534
+ $supplementaryMaterial.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href ?? '');
535
+ $supplementaryMaterial.setAttribute('mimetype', node.attrs.mimeType ?? '');
536
+ $supplementaryMaterial.setAttribute('mime-subtype', node.attrs.mimeSubType ?? '');
537
+ this.appendCaption($supplementaryMaterial, node);
538
+ return $supplementaryMaterial;
539
+ },
522
540
  supplements: () => '',
523
541
  bibliography_section: () => '',
524
542
  blockquote_element: () => ['disp-quote', { 'content-type': 'quote' }, 0],
@@ -542,14 +560,14 @@ export class JATSExporter {
542
560
  if (!rids.length) {
543
561
  return '';
544
562
  }
545
- const xref = this.createElement('xref');
546
- xref.setAttribute('ref-type', 'bibr');
547
- xref.setAttribute('rid', normalizeID(rids.join(' ')));
563
+ const $xref = this.createElement('xref');
564
+ $xref.setAttribute('ref-type', 'bibr');
565
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')));
548
566
  const fragment = this.renderedCitations.get(node.attrs.id);
549
567
  if (fragment) {
550
- xref.innerHTML = fragment;
568
+ $xref.innerHTML = fragment;
551
569
  }
552
- return xref;
570
+ return $xref;
553
571
  },
554
572
  cross_reference: (node) => {
555
573
  const cross = node;
@@ -563,40 +581,41 @@ export class JATSExporter {
563
581
  if (!target) {
564
582
  return text ?? '';
565
583
  }
566
- const xref = this.createElement('xref');
584
+ const $xref = this.createElement('xref');
567
585
  const type = chooseRefType(target.type);
568
586
  if (type) {
569
- xref.setAttribute('ref-type', type);
587
+ $xref.setAttribute('ref-type', type);
570
588
  }
571
- xref.setAttribute('rid', normalizeID(rids.join(' ')));
572
- xref.textContent = text ?? '';
573
- return xref;
589
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')));
590
+ $xref.textContent = text ?? '';
591
+ return $xref;
574
592
  },
575
593
  doc: () => '',
576
594
  equation: (node) => {
577
595
  return node.attrs.contents ? this.createEquation(node) : '';
578
596
  },
579
597
  general_table_footnote: (node) => {
580
- const el = this.createElement('general-table-footnote');
581
- el.setAttribute('id', normalizeID(node.attrs.id));
582
- processChildNodes(el, node, schema.nodes.general_table_footnote);
583
- return el;
598
+ const $fragment = this.document.createDocumentFragment();
599
+ node.forEach((child) => {
600
+ $fragment.appendChild(this.serializeNode(child));
601
+ });
602
+ return $fragment;
584
603
  },
585
604
  inline_equation: (node) => {
586
605
  if (!node.attrs.contents) {
587
606
  return '';
588
607
  }
589
- const eqElement = this.createElement('inline-formula');
608
+ const $inlineFormula = this.createElement('inline-formula');
590
609
  const equation = this.createEquation(node, true);
591
- eqElement.append(equation);
592
- return eqElement;
610
+ $inlineFormula.append(equation);
611
+ return $inlineFormula;
593
612
  },
594
613
  equation_element: (node) => {
595
- const eqElement = this.createElement('disp-formula');
596
- eqElement.setAttribute('id', normalizeID(node.attrs.id));
597
- appendLabels(eqElement, node);
598
- processChildNodes(eqElement, node, schema.nodes.equation);
599
- return eqElement;
614
+ const $dispFormula = this.createElement('disp-formula');
615
+ $dispFormula.setAttribute('id', normalizeID(node.attrs.id));
616
+ appendLabels($dispFormula, node);
617
+ processChildNodes($dispFormula, node, schema.nodes.equation);
618
+ return $dispFormula;
600
619
  },
601
620
  figure: (node) => createGraphic(node),
602
621
  figure_element: (node) => createFigureElement(node, node.type.schema.nodes.figure),
@@ -614,25 +633,39 @@ export class JATSExporter {
614
633
  ? ['fn-group', { id: normalizeID(node.attrs.id) }, ['fn', ['p']]]
615
634
  : ['fn-group', { id: normalizeID(node.attrs.id) }, 0],
616
635
  footnotes_section: (node) => {
617
- const attrs = {
636
+ const $fnGroup = this.createElement('fn-group', undefined, {
618
637
  id: normalizeID(node.attrs.id),
619
- 'sec-type': 'endnotes',
620
- };
621
- return ['sec', attrs, 0];
638
+ });
639
+ const titleNode = this.getFirstChildOfType(schema.nodes.section_title, node);
640
+ if (titleNode) {
641
+ $fnGroup.appendChild(this.serializeNode(titleNode));
642
+ }
643
+ const footnotesNode = this.getFirstChildOfType(schema.nodes.footnotes_element, node);
644
+ if (footnotesNode?.childCount) {
645
+ footnotesNode.forEach((footnote) => {
646
+ $fnGroup.appendChild(this.serializeNode(footnote));
647
+ });
648
+ }
649
+ else {
650
+ const $fn = this.createElement('fn');
651
+ $fn.appendChild(this.createElement('p'));
652
+ $fnGroup.appendChild($fn);
653
+ }
654
+ return $fnGroup;
622
655
  },
623
656
  hard_break: () => '',
624
657
  highlight_marker: () => '',
625
658
  inline_footnote: (node) => {
626
659
  const rids = node.attrs.rids;
627
- const xref = this.createElement('xref');
628
- xref.setAttribute('ref-type', 'fn');
629
- xref.setAttribute('rid', normalizeID(rids.join(' ')));
630
- xref.textContent = rids
660
+ const $xref = this.createElement('xref');
661
+ $xref.setAttribute('ref-type', 'fn');
662
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')));
663
+ $xref.textContent = rids
631
664
  .map((rid) => this.footnoteLabels.get(rid))
632
665
  .join(', ');
633
- return xref;
666
+ return $xref;
634
667
  },
635
- keyword: () => '',
668
+ keyword: () => ['kwd', 0],
636
669
  keywords_element: () => '',
637
670
  keywords: () => '',
638
671
  link: (node) => {
@@ -643,30 +676,30 @@ export class JATSExporter {
643
676
  if (!node.attrs.href) {
644
677
  return text;
645
678
  }
646
- const linkNode = this.createElement('ext-link');
647
- linkNode.setAttribute('ext-link-type', 'uri');
648
- linkNode.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href);
649
- linkNode.textContent = text;
679
+ const $extLink = this.createElement('ext-link');
680
+ $extLink.setAttribute('ext-link-type', 'uri');
681
+ $extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href);
682
+ $extLink.textContent = text;
650
683
  if (node.attrs.title) {
651
- linkNode.setAttributeNS(XLINK_NAMESPACE, 'xlink:title', node.attrs.title);
684
+ $extLink.setAttributeNS(XLINK_NAMESPACE, 'xlink:title', node.attrs.title);
652
685
  }
653
- return linkNode;
686
+ return $extLink;
654
687
  },
655
688
  list_item: () => ['list-item', 0],
656
689
  listing: (node) => {
657
- const code = this.createElement('code');
658
- code.setAttribute('id', normalizeID(node.attrs.id));
659
- code.setAttribute('language', node.attrs.languageKey);
660
- code.textContent = node.attrs.contents;
661
- return code;
690
+ const $code = this.createElement('code');
691
+ $code.setAttribute('id', normalizeID(node.attrs.id));
692
+ $code.setAttribute('language', node.attrs.languageKey);
693
+ $code.textContent = node.attrs.contents;
694
+ return $code;
662
695
  },
663
696
  listing_element: (node) => createFigureElement(node, node.type.schema.nodes.listing),
664
697
  manuscript: (node) => ['article', { id: normalizeID(node.attrs.id) }, 0],
665
698
  missing_figure: () => {
666
- const graphic = this.createElement('graphic');
667
- graphic.setAttribute('specific-use', 'MISSING');
668
- graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', '');
669
- return graphic;
699
+ const $graphic = this.createElement('graphic');
700
+ $graphic.setAttribute('specific-use', 'MISSING');
701
+ $graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', '');
702
+ return $graphic;
670
703
  },
671
704
  paragraph: (node) => {
672
705
  if (!node.childCount) {
@@ -701,15 +734,7 @@ export class JATSExporter {
701
734
  }
702
735
  return '';
703
736
  },
704
- graphical_abstract_section: (node) => {
705
- const attrs = {
706
- id: normalizeID(node.attrs.id),
707
- };
708
- if (node.attrs.category) {
709
- attrs['sec-type'] = node.attrs.category;
710
- }
711
- return ['sec', attrs, 0];
712
- },
737
+ graphical_abstract_section: (node) => createAbstract(node),
713
738
  section: (node) => {
714
739
  const attrs = {
715
740
  id: normalizeID(node.attrs.id),
@@ -719,17 +744,18 @@ export class JATSExporter {
719
744
  }
720
745
  return ['sec', attrs, 0];
721
746
  },
747
+ abstract: (node) => createAbstract(node),
722
748
  section_label: () => ['label', 0],
723
749
  section_title: () => ['title', 0],
724
750
  section_title_plain: () => ['title', 0],
725
751
  table: (node) => ['table', { id: normalizeID(node.attrs.id) }, 0],
726
752
  table_element: (node) => {
727
- const element = createTableElement(node);
728
- element.setAttribute('position', 'anchor');
753
+ const $tableWrap = createTableElement(node);
754
+ $tableWrap.setAttribute('position', 'anchor');
729
755
  if (node.attrs.type) {
730
- element.setAttribute('content-type', node.attrs.type);
756
+ $tableWrap.setAttribute('content-type', node.attrs.type);
731
757
  }
732
- return element;
758
+ return $tableWrap;
733
759
  },
734
760
  table_cell: (node) => [
735
761
  'td',
@@ -775,84 +801,99 @@ export class JATSExporter {
775
801
  tracked_delete: () => ['del'],
776
802
  };
777
803
  this.serializer = new DOMSerializer(nodes, marks);
778
- const appendChildIfPresent = (parent, tagName, textContent) => {
804
+ const appendChildIfPresent = ($parent, tagName, textContent) => {
779
805
  if (!textContent) {
780
806
  return;
781
807
  }
782
- const element = this.createElement(tagName);
783
- element.textContent = textContent;
784
- parent.appendChild(element);
808
+ const $element = this.createElement(tagName);
809
+ $element.textContent = textContent;
810
+ $parent.appendChild($element);
785
811
  };
786
- const processChildNodes = (element, node, contentNodeType) => {
812
+ const processChildNodes = ($element, node, contentNodeType) => {
787
813
  node.forEach((childNode) => {
788
814
  if (childNode.type === contentNodeType) {
789
815
  if (childNode.attrs.id) {
790
- element.appendChild(this.serializeNode(childNode));
816
+ $element.appendChild(this.serializeNode(childNode));
791
817
  }
792
818
  }
793
819
  else if (childNode.type === node.type.schema.nodes.paragraph) {
794
- element.appendChild(this.serializeNode(childNode));
820
+ $element.appendChild(this.serializeNode(childNode));
795
821
  }
796
822
  else if (childNode.type === node.type.schema.nodes.missing_figure) {
797
- element.appendChild(this.serializeNode(childNode));
823
+ $element.appendChild(this.serializeNode(childNode));
798
824
  }
799
825
  });
800
826
  };
801
827
  const createElement = (node, nodeName) => {
802
- const element = this.createElement(nodeName);
803
- element.setAttribute('id', normalizeID(node.attrs.id));
804
- return element;
828
+ const $element = this.createElement(nodeName);
829
+ $element.setAttribute('id', normalizeID(node.attrs.id));
830
+ return $element;
805
831
  };
806
- const appendLabels = (element, node) => {
832
+ const appendLabels = ($element, node) => {
807
833
  if (this.labelTargets) {
808
834
  const target = this.labelTargets.get(node.attrs.id);
809
835
  if (target) {
810
- const label = this.createElement('label');
811
- label.textContent = target.label;
812
- element.appendChild(label);
836
+ const $label = this.createElement('label');
837
+ $label.textContent = target.label;
838
+ $element.appendChild($label);
813
839
  }
814
840
  }
815
841
  };
816
- const appendAttributions = (element, node) => {
842
+ const appendAttributions = ($element, node) => {
817
843
  if (node.attrs.attribution) {
818
- const attribution = this.createElement('attrib');
819
- attribution.textContent = node.attrs.attribution.literal;
820
- element.appendChild(attribution);
844
+ const $attrib = this.createElement('attrib');
845
+ $attrib.textContent = node.attrs.attribution.literal;
846
+ $element.appendChild($attrib);
821
847
  }
822
848
  };
823
- const appendTable = (element, node) => {
849
+ const appendTable = ($element, node) => {
824
850
  const tableNode = this.getFirstChildOfType(schema.nodes.table, node);
825
851
  const colGroupNode = this.getFirstChildOfType(schema.nodes.table_colgroup, node);
826
852
  if (!tableNode) {
827
853
  return;
828
854
  }
829
- const table = this.serializeNode(tableNode);
830
- const tbodyElement = this.createElement('tbody');
831
- while (table.firstChild) {
832
- const child = table.firstChild;
833
- table.removeChild(child);
834
- tbodyElement.appendChild(child);
855
+ const $table = this.serializeNode(tableNode);
856
+ const $tbody = this.createElement('tbody');
857
+ while ($table.firstChild) {
858
+ const $child = $table.firstChild;
859
+ $table.removeChild($child);
860
+ $tbody.appendChild($child);
835
861
  }
836
- table.appendChild(tbodyElement);
862
+ $table.appendChild($tbody);
863
+ this.normalizeTable($table);
837
864
  if (colGroupNode) {
838
- const colGroup = this.serializeNode(colGroupNode);
839
- table.insertBefore(colGroup, table.firstChild);
865
+ const $colGroup = this.serializeNode(colGroupNode);
866
+ $table.insertBefore($colGroup, $table.firstChild);
840
867
  }
841
- element.appendChild(table);
868
+ $element.appendChild($table);
842
869
  };
843
870
  const createBoxElement = (node) => {
844
- const element = createElement(node, 'boxed-text');
871
+ const $boxedText = createElement(node, 'boxed-text');
845
872
  if (node.attrs.type) {
846
- element.setAttribute('content-type', node.attrs.type);
873
+ $boxedText.setAttribute('content-type', node.attrs.type);
847
874
  }
848
- appendLabels(element, node);
875
+ appendLabels($boxedText, node);
849
876
  const child = node.firstChild;
850
877
  if (child?.type === schema.nodes.caption_title) {
851
- this.appendCaption(element, node);
878
+ this.appendCaption($boxedText, node);
852
879
  }
853
- processChildNodes(element, node, node.type.schema.nodes.section);
854
- return element;
880
+ processChildNodes($boxedText, node, node.type.schema.nodes.section);
881
+ return $boxedText;
855
882
  };
883
+ const abstractTypeAttrs = (category) => category && category !== 'abstract' ? { 'abstract-type': category } : {};
884
+ const createAbstract = (node) => [
885
+ 'abstract',
886
+ abstractTypeAttrs(node.attrs.category),
887
+ 0,
888
+ ];
889
+ const createTransAbstract = (node) => [
890
+ 'trans-abstract',
891
+ {
892
+ [`${XML_NAMESPACE} lang`]: node.attrs.lang ?? '',
893
+ ...abstractTypeAttrs(node.attrs.category),
894
+ },
895
+ 0,
896
+ ];
856
897
  const isChildOfNodeType = (targetID, type, descend = false) => {
857
898
  const nodes = this.getChildrenOfType(type);
858
899
  return nodes.some((node) => {
@@ -869,94 +910,94 @@ export class JATSExporter {
869
910
  if (!graphicNode) {
870
911
  return '';
871
912
  }
872
- const graphicElement = createGraphic(graphicNode);
913
+ const $graphic = createGraphic(graphicNode);
873
914
  if (node.attrs.extLink) {
874
- const extLink = this.appendElement(graphicElement, 'ext-link');
875
- extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.extLink);
915
+ const $extLink = this.appendElement($graphic, 'ext-link');
916
+ $extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.extLink);
876
917
  }
877
- this.appendCaption(graphicElement, node);
878
- this.appendChildNodeOfType(graphicElement, node, schema.nodes.alt_text);
879
- this.appendChildNodeOfType(graphicElement, node, schema.nodes.long_desc);
880
- return graphicElement;
918
+ this.appendCaption($graphic, node);
919
+ this.appendChildNodeOfType($graphic, node, schema.nodes.alt_text);
920
+ this.appendChildNodeOfType($graphic, node, schema.nodes.long_desc);
921
+ return $graphic;
881
922
  };
882
923
  const createGraphic = (node) => {
883
- const graphic = this.createElement('graphic');
884
- graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', node.attrs.src);
924
+ const $graphic = this.createElement('graphic');
925
+ $graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', node.attrs.src);
885
926
  const hero = findParentHeroImage(node.attrs.id);
886
927
  if (hero) {
887
- graphic.setAttribute('content-type', hero.attrs.type || 'leading');
928
+ $graphic.setAttribute('content-type', hero.attrs.type || 'leading');
888
929
  }
889
930
  else if (!isChildOfNodeType(node.attrs.id, schema.nodes.figure_element) &&
890
931
  node.attrs.type) {
891
- graphic.setAttribute('content-type', node.attrs.type);
932
+ $graphic.setAttribute('content-type', node.attrs.type);
892
933
  }
893
- return graphic;
934
+ return $graphic;
894
935
  };
895
936
  const createFigureElement = (node, contentNodeType) => {
896
- const element = createElement(node, 'fig');
937
+ const $fig = createElement(node, 'fig');
897
938
  const figNode = this.getFirstChildOfType(schema.nodes.figure, node);
898
939
  const figType = figNode?.attrs.type;
899
940
  if (figType) {
900
- element.setAttribute('fig-type', figType);
941
+ $fig.setAttribute('fig-type', figType);
901
942
  }
902
- appendLabels(element, node);
903
- this.appendCaption(element, node);
904
- this.appendChildNodeOfType(element, node, schema.nodes.alt_text);
905
- this.appendChildNodeOfType(element, node, schema.nodes.long_desc);
906
- this.appendChildNodeOfType(element, node, node.type.schema.nodes.footnotes_element);
907
- processChildNodes(element, node, contentNodeType);
908
- appendAttributions(element, node);
943
+ appendLabels($fig, node);
944
+ this.appendCaption($fig, node);
945
+ this.appendChildNodeOfType($fig, node, schema.nodes.alt_text);
946
+ this.appendChildNodeOfType($fig, node, schema.nodes.long_desc);
947
+ this.appendChildNodeOfType($fig, node, node.type.schema.nodes.footnotes_element);
948
+ processChildNodes($fig, node, contentNodeType);
949
+ appendAttributions($fig, node);
909
950
  if (isExecutableNodeType(node.type)) {
910
- processExecutableNode(node, element);
951
+ processExecutableNode(node, $fig);
911
952
  }
912
- moveAltTextAndLongDescToGraphics(element);
913
- return element;
953
+ moveAltTextAndLongDescToGraphics($fig);
954
+ return $fig;
914
955
  };
915
- const moveAltTextAndLongDescToGraphics = (element) => {
916
- const altText = element.querySelector('alt-text');
917
- const longDesc = element.querySelector('long-desc');
918
- const graphics = element.querySelectorAll('graphic');
919
- if (graphics.length === 0) {
956
+ const moveAltTextAndLongDescToGraphics = ($element) => {
957
+ const $altText = $element.querySelector('alt-text');
958
+ const $longDesc = $element.querySelector('long-desc');
959
+ const $graphics = $element.querySelectorAll('graphic');
960
+ if ($graphics.length === 0) {
920
961
  return;
921
962
  }
922
- graphics.forEach((graphic) => {
923
- if (longDesc) {
924
- graphic.prepend(longDesc.cloneNode(true));
963
+ $graphics.forEach(($graphic) => {
964
+ if ($longDesc) {
965
+ $graphic.prepend($longDesc.cloneNode(true));
925
966
  }
926
- if (altText) {
927
- graphic.prepend(altText.cloneNode(true));
967
+ if ($altText) {
968
+ $graphic.prepend($altText.cloneNode(true));
928
969
  }
929
970
  });
930
- altText?.remove();
931
- longDesc?.remove();
971
+ $altText?.remove();
972
+ $longDesc?.remove();
932
973
  };
933
974
  const createTableElement = (node) => {
934
975
  const nodeName = 'table-wrap';
935
- const element = createElement(node, nodeName);
936
- appendLabels(element, node);
937
- this.appendCaption(element, node);
938
- this.appendChildNodeOfType(element, node, schema.nodes.alt_text);
939
- this.appendChildNodeOfType(element, node, schema.nodes.long_desc);
940
- appendTable(element, node);
941
- this.appendChildNodeOfType(element, node, node.type.schema.nodes.table_element_footer);
976
+ const $tableWrap = createElement(node, nodeName);
977
+ appendLabels($tableWrap, node);
978
+ this.appendCaption($tableWrap, node);
979
+ this.appendChildNodeOfType($tableWrap, node, schema.nodes.alt_text);
980
+ this.appendChildNodeOfType($tableWrap, node, schema.nodes.long_desc);
981
+ appendTable($tableWrap, node);
982
+ this.appendChildNodeOfType($tableWrap, node, node.type.schema.nodes.table_element_footer);
942
983
  if (isExecutableNodeType(node.type)) {
943
- processExecutableNode(node, element);
984
+ processExecutableNode(node, $tableWrap);
944
985
  }
945
- return element;
986
+ return $tableWrap;
946
987
  };
947
- const processExecutableNode = (node, element) => {
988
+ const processExecutableNode = (node, $element) => {
948
989
  const listingNode = this.getFirstChildOfType(schema.nodes.listing, node);
949
990
  if (listingNode) {
950
991
  const { contents, languageKey } = listingNode.attrs;
951
992
  if (contents && languageKey) {
952
- const listing = this.createElement('fig');
953
- listing.setAttribute('specific-use', 'source');
954
- element.appendChild(listing);
955
- const code = this.createElement('code');
956
- code.setAttribute('executable', 'true');
957
- code.setAttribute('language', languageKey);
958
- code.textContent = contents;
959
- listing.appendChild(code);
993
+ const $fig = this.createElement('fig');
994
+ $fig.setAttribute('specific-use', 'source');
995
+ $element.appendChild($fig);
996
+ const $code = this.createElement('code');
997
+ $code.setAttribute('executable', 'true');
998
+ $code.setAttribute('language', languageKey);
999
+ $code.textContent = contents;
1000
+ $fig.appendChild($code);
960
1001
  }
961
1002
  }
962
1003
  };
@@ -964,201 +1005,187 @@ export class JATSExporter {
964
1005
  this.serializeNode = (node) => this.serializer.serializeNode(node, {
965
1006
  document: this.document,
966
1007
  });
967
- this.buildContributors = (articleMeta) => {
968
- const contributors = this.getChildrenOfType(schema.nodes.contributor).sort(sortContributors);
1008
+ this.buildContributorGroup = (contributors) => {
969
1009
  if (!contributors.length) {
970
1010
  return;
971
1011
  }
972
- const labels = new Map();
973
- const createLabel = (id) => {
974
- let label = labels.get(id);
975
- if (!label) {
976
- label = labels.size + 1;
977
- labels.set(id, label);
1012
+ const affiliationIDs = contributors.flatMap((n) => n.attrs.affiliationIDs);
1013
+ const affiliationOrder = new Map();
1014
+ affiliationIDs.forEach((id, index) => {
1015
+ if (!affiliationOrder.has(id)) {
1016
+ affiliationOrder.set(id, index);
978
1017
  }
979
- const sup = this.createElement('sup');
980
- sup.textContent = String(label);
981
- return sup;
982
- };
1018
+ });
1019
+ const affiliations = this.getChildrenOfType(schema.nodes.affiliation)
1020
+ .filter((a) => affiliationIDs.includes(a.attrs.id))
1021
+ .sort((a, b) => (affiliationOrder.get(a.attrs.id) ?? Infinity) -
1022
+ (affiliationOrder.get(b.attrs.id) ?? Infinity));
983
1023
  const $contribGroup = this.createElement('contrib-group');
984
1024
  $contribGroup.setAttribute('content-type', 'authors');
985
- articleMeta.appendChild($contribGroup);
986
1025
  contributors.forEach((contributor) => {
987
- const $contrib = this.createElement('contrib');
988
- $contrib.setAttribute('contrib-type', 'author');
989
- $contrib.setAttribute('id', normalizeID(contributor.attrs.id));
990
- if (contributor.attrs.isCorresponding) {
991
- $contrib.setAttribute('corresp', 'yes');
992
- }
993
- if (contributor.attrs.role) {
994
- this.appendElement($contrib, 'role', contributor.attrs.role);
995
- }
996
- if (contributor.attrs.ORCID) {
997
- const contribIdAttrs = {
998
- 'contrib-id-type': 'orcid',
999
- };
1000
- if (contributor.attrs.isAuthenticated) {
1001
- contribIdAttrs['authenticated'] = 'true';
1002
- }
1003
- this.appendElement($contrib, 'contrib-id', contributor.attrs.ORCID, contribIdAttrs);
1004
- }
1005
- const $name = this.buildContributorName(contributor);
1006
- $contrib.appendChild($name);
1007
- if (contributor.attrs.suffix) {
1008
- this.appendElement($name, 'suffix', contributor.attrs.suffix);
1026
+ $contribGroup.appendChild(this.serializeNode(contributor));
1027
+ });
1028
+ affiliations.forEach((affiliation) => {
1029
+ $contribGroup.appendChild(this.serializeNode(affiliation));
1030
+ });
1031
+ return $contribGroup;
1032
+ };
1033
+ this.buildAuthorNotes = ($coiStatement) => {
1034
+ const authorNotes = this.getFirstChildOfType(schema.nodes.author_notes);
1035
+ const $authorNotes = this.createElement('author-notes');
1036
+ if (authorNotes) {
1037
+ this.appendAuthorNotes($authorNotes, authorNotes);
1038
+ }
1039
+ if ($coiStatement) {
1040
+ $authorNotes.appendChild($coiStatement);
1041
+ }
1042
+ return $authorNotes.hasChildNodes() ? $authorNotes : undefined;
1043
+ };
1044
+ this.computeContributorLabels = (contributors) => {
1045
+ const labels = new Map();
1046
+ const assign = (id) => {
1047
+ if (!labels.has(id)) {
1048
+ labels.set(id, labels.size + 1);
1009
1049
  }
1010
- contributor.attrs.degrees?.forEach((degree) => {
1011
- this.appendElement($contrib, 'degrees', degree);
1012
- });
1013
- if (contributor.attrs.email) {
1014
- this.appendElement($contrib, 'email', contributor.attrs.email);
1050
+ };
1051
+ contributors.forEach((contributor) => {
1052
+ contributor.attrs.affiliationIDs?.forEach(assign);
1053
+ contributor.attrs.footnoteIDs?.forEach(assign);
1054
+ contributor.attrs.correspIDs?.forEach(assign);
1055
+ });
1056
+ return labels;
1057
+ };
1058
+ this.createLabelSup = (id) => {
1059
+ const $sup = this.createElement('sup');
1060
+ $sup.textContent = String(this.contributorLabels.get(id));
1061
+ return $sup;
1062
+ };
1063
+ this.buildContributorElement = (contributor) => {
1064
+ const $contrib = this.createElement('contrib');
1065
+ $contrib.setAttribute('contrib-type', 'author');
1066
+ $contrib.setAttribute('id', normalizeID(contributor.attrs.id));
1067
+ if (contributor.attrs.isCorresponding) {
1068
+ $contrib.setAttribute('corresp', 'yes');
1069
+ }
1070
+ if (contributor.attrs.role) {
1071
+ this.appendElement($contrib, 'role', contributor.attrs.role);
1072
+ }
1073
+ if (contributor.attrs.ORCID) {
1074
+ const contribIDAttrs = {
1075
+ 'contrib-id-type': 'orcid',
1076
+ };
1077
+ if (contributor.attrs.isAuthenticated) {
1078
+ contribIDAttrs['authenticated'] = 'true';
1015
1079
  }
1016
- contributor.attrs.affiliationIDs?.forEach((rid) => {
1017
- const $xref = this.appendElement($contrib, 'xref', '', {
1018
- 'ref-type': 'aff',
1019
- rid: normalizeID(rid),
1020
- });
1021
- $xref.appendChild(createLabel(rid));
1022
- });
1023
- contributor.attrs.footnoteIDs?.forEach((rid) => {
1024
- const $xref = this.appendElement($contrib, 'xref', '', {
1025
- 'ref-type': 'fn',
1026
- rid: normalizeID(rid),
1027
- });
1028
- $xref.appendChild(createLabel(rid));
1080
+ this.appendElement($contrib, 'contrib-id', contributor.attrs.ORCID, contribIDAttrs);
1081
+ }
1082
+ const $name = this.buildContributorName(contributor);
1083
+ $contrib.appendChild($name);
1084
+ if (contributor.attrs.suffix) {
1085
+ this.appendElement($name, 'suffix', contributor.attrs.suffix);
1086
+ }
1087
+ contributor.attrs.degrees?.forEach((degree) => {
1088
+ this.appendElement($contrib, 'degrees', degree);
1089
+ });
1090
+ if (contributor.attrs.email) {
1091
+ this.appendElement($contrib, 'email', contributor.attrs.email);
1092
+ }
1093
+ contributor.attrs.affiliationIDs?.forEach((rid) => {
1094
+ const $xref = this.appendElement($contrib, 'xref', '', {
1095
+ 'ref-type': 'aff',
1096
+ rid: normalizeID(rid),
1029
1097
  });
1030
- contributor.attrs.correspIDs?.forEach((rid) => {
1031
- const $xref = this.appendElement($contrib, 'xref', '', {
1032
- 'ref-type': 'corresp',
1033
- rid: normalizeID(rid),
1034
- });
1035
- $xref.appendChild(createLabel(rid));
1098
+ $xref.appendChild(this.createLabelSup(rid));
1099
+ });
1100
+ contributor.attrs.footnoteIDs?.forEach((rid) => {
1101
+ const $xref = this.appendElement($contrib, 'xref', '', {
1102
+ 'ref-type': 'fn',
1103
+ rid: normalizeID(rid),
1036
1104
  });
1037
- contributor.attrs.creditRoles?.forEach((credit) => {
1038
- const url = CreditRoleUrls.get(credit.vocabTerm);
1039
- if (!url) {
1040
- return;
1041
- }
1042
- this.appendElement($contrib, 'role', credit.vocabTerm, {
1043
- 'vocab-identifier': 'http://credit.niso.org/',
1044
- vocab: 'CRediT',
1045
- 'vocab-term': credit.vocabTerm,
1046
- 'vocab-term-identifier': url,
1047
- });
1105
+ $xref.appendChild(this.createLabelSup(rid));
1106
+ });
1107
+ contributor.attrs.correspIDs?.forEach((rid) => {
1108
+ const $xref = this.appendElement($contrib, 'xref', '', {
1109
+ 'ref-type': 'corresp',
1110
+ rid: normalizeID(rid),
1048
1111
  });
1049
- $contribGroup.appendChild($contrib);
1112
+ $xref.appendChild(this.createLabelSup(rid));
1050
1113
  });
1051
- const affiliationIDs = contributors.flatMap((n) => n.attrs.affiliationIDs);
1052
- const sortAffiliations = (a, b) => affiliationIDs.indexOf(a.attrs.id) - affiliationIDs.indexOf(b.attrs.id);
1053
- this.getChildrenOfType(schema.nodes.affiliation)
1054
- .filter((a) => affiliationIDs.includes(a.attrs.id))
1055
- .sort(sortAffiliations)
1056
- .forEach((affiliation) => {
1057
- const $content = [];
1058
- if (affiliation.attrs.department) {
1059
- const $institution = this.createElement('institution', affiliation.attrs.department, {
1060
- 'content-type': 'dept',
1061
- });
1062
- $content.push($institution);
1063
- }
1064
- if (affiliation.attrs.institution) {
1065
- const $institution = this.createElement('institution', affiliation.attrs.institution);
1066
- $content.push($institution);
1067
- }
1068
- if (affiliation.attrs.addressLine1) {
1069
- const $addrLine = this.createElement('addr-line', affiliation.attrs.addressLine1);
1070
- $content.push($addrLine);
1071
- }
1072
- if (affiliation.attrs.addressLine2) {
1073
- const $addrLine = this.createElement('addr-line', affiliation.attrs.addressLine2);
1074
- $content.push($addrLine);
1075
- }
1076
- if (affiliation.attrs.addressLine3) {
1077
- const $addrLine = this.createElement('addr-line', affiliation.attrs.addressLine3);
1078
- $content.push($addrLine);
1079
- }
1080
- if (affiliation.attrs.city) {
1081
- const $city = this.createElement('city', affiliation.attrs.city);
1082
- $content.push($city);
1083
- }
1084
- if (affiliation.attrs.county) {
1085
- const $state = this.createElement('state', affiliation.attrs.county);
1086
- $content.push($state);
1087
- }
1088
- if (affiliation.attrs.country) {
1089
- const $country = this.createElement('country', affiliation.attrs.country);
1090
- $content.push($country);
1091
- }
1092
- if (affiliation.attrs.postCode) {
1093
- const $postalCode = this.createElement('postal-code', affiliation.attrs.postCode);
1094
- $content.push($postalCode);
1095
- }
1096
- const $aff = this.createElement('aff');
1097
- $aff.setAttribute('id', normalizeID(affiliation.attrs.id));
1098
- $contribGroup.appendChild($aff);
1099
- const label = labels.get(affiliation.attrs.id);
1100
- if (label) {
1101
- this.appendElement($aff, 'label', String(label));
1114
+ contributor.attrs.creditRoles?.forEach((credit) => {
1115
+ const url = CreditRoleUrls.get(credit.vocabTerm);
1116
+ if (!url) {
1117
+ return;
1102
1118
  }
1103
- $content.forEach((node, i) => {
1104
- if (i > 0) {
1105
- $aff.appendChild(this.document.createTextNode(', '));
1106
- }
1107
- $aff.appendChild(node);
1119
+ this.appendElement($contrib, 'role', credit.vocabTerm, {
1120
+ 'vocab-identifier': 'http://credit.niso.org/',
1121
+ vocab: 'CRediT',
1122
+ 'vocab-term': credit.vocabTerm,
1123
+ 'vocab-term-identifier': url,
1108
1124
  });
1109
1125
  });
1110
- const $authorNotes = this.createAuthorNotesElement(contributors);
1111
- if ($authorNotes) {
1112
- articleMeta.insertBefore($authorNotes, $contribGroup.nextSibling);
1126
+ return $contrib;
1127
+ };
1128
+ this.buildAffiliationElement = (affiliation) => {
1129
+ const $content = [];
1130
+ if (affiliation.attrs.department) {
1131
+ $content.push(this.createElement('institution', affiliation.attrs.department, {
1132
+ 'content-type': 'dept',
1133
+ }));
1134
+ }
1135
+ if (affiliation.attrs.institution) {
1136
+ $content.push(this.createElement('institution', affiliation.attrs.institution));
1137
+ }
1138
+ if (affiliation.attrs.addressLine1) {
1139
+ $content.push(this.createElement('addr-line', affiliation.attrs.addressLine1));
1140
+ }
1141
+ if (affiliation.attrs.addressLine2) {
1142
+ $content.push(this.createElement('addr-line', affiliation.attrs.addressLine2));
1143
+ }
1144
+ if (affiliation.attrs.addressLine3) {
1145
+ $content.push(this.createElement('addr-line', affiliation.attrs.addressLine3));
1146
+ }
1147
+ if (affiliation.attrs.city) {
1148
+ $content.push(this.createElement('city', affiliation.attrs.city));
1149
+ }
1150
+ if (affiliation.attrs.county) {
1151
+ $content.push(this.createElement('state', affiliation.attrs.county));
1152
+ }
1153
+ if (affiliation.attrs.country) {
1154
+ $content.push(this.createElement('country', affiliation.attrs.country));
1155
+ }
1156
+ if (affiliation.attrs.postCode) {
1157
+ $content.push(this.createElement('postal-code', affiliation.attrs.postCode));
1113
1158
  }
1159
+ const $aff = this.createElement('aff');
1160
+ $aff.setAttribute('id', normalizeID(affiliation.attrs.id));
1161
+ const label = this.contributorLabels.get(affiliation.attrs.id);
1162
+ if (label) {
1163
+ this.appendElement($aff, 'label', String(label));
1164
+ }
1165
+ $content.forEach((node, i) => {
1166
+ if (i > 0) {
1167
+ $aff.appendChild(this.document.createTextNode(', '));
1168
+ }
1169
+ $aff.appendChild(node);
1170
+ });
1171
+ return $aff;
1114
1172
  };
1115
1173
  this.buildContributorName = (contributor) => {
1116
1174
  const { given, family } = contributor.attrs;
1117
1175
  if (Boolean(given) !== Boolean(family)) {
1118
1176
  return this.createElement('string-name', given || family);
1119
1177
  }
1120
- const name = this.createElement('name');
1178
+ const $name = this.createElement('name');
1121
1179
  if (contributor.attrs.family) {
1122
- this.appendElement(name, 'surname', contributor.attrs.family);
1180
+ this.appendElement($name, 'surname', contributor.attrs.family);
1123
1181
  }
1124
1182
  if (contributor.attrs.given) {
1125
- this.appendElement(name, 'given-names', contributor.attrs.given);
1183
+ this.appendElement($name, 'given-names', contributor.attrs.given);
1126
1184
  }
1127
1185
  if (contributor.attrs.prefix) {
1128
- this.appendElement(name, 'prefix', contributor.attrs.prefix);
1129
- }
1130
- return name;
1131
- };
1132
- this.createAuthorNotesElement = (contributors) => {
1133
- const authorNotes = this.getFirstChildOfType(schema.nodes.author_notes);
1134
- if (!authorNotes || !authorNotes.childCount) {
1135
- return;
1186
+ this.appendElement($name, 'prefix', contributor.attrs.prefix);
1136
1187
  }
1137
- const $authorNotes = this.createElement('author-notes');
1138
- const used = contributors.flatMap((c) => c.attrs.correspIDs);
1139
- authorNotes.descendants((node) => {
1140
- switch (node.type) {
1141
- case schema.nodes.paragraph: {
1142
- const $p = this.writeParagraph(node);
1143
- $authorNotes.append($p);
1144
- break;
1145
- }
1146
- case schema.nodes.footnote: {
1147
- const $fn = this.writeFootnote(node);
1148
- $authorNotes.append($fn);
1149
- break;
1150
- }
1151
- case schema.nodes.corresp: {
1152
- if (used.includes(node.attrs.id)) {
1153
- const $corresp = this.writeCorresp(node);
1154
- $authorNotes.append($corresp);
1155
- }
1156
- break;
1157
- }
1158
- }
1159
- return false;
1160
- });
1161
- return $authorNotes.hasChildNodes() ? $authorNotes : undefined;
1188
+ return $name;
1162
1189
  };
1163
1190
  this.writeCorresp = (corresp) => {
1164
1191
  const $corresp = this.createElement('corresp');
@@ -1188,222 +1215,120 @@ export class JATSExporter {
1188
1215
  $fn.innerHTML = content;
1189
1216
  return $fn;
1190
1217
  };
1191
- this.fixBody = (body) => {
1192
- this.manuscriptNode.descendants((node) => {
1193
- if (node.attrs.id) {
1194
- if (isNodeType(node, 'general_table_footnote')) {
1195
- const generalTableFootnote = body.querySelector(`#${normalizeID(node.attrs.id)}`);
1196
- if (generalTableFootnote) {
1197
- Array.from(generalTableFootnote.childNodes).forEach((cn) => {
1198
- generalTableFootnote.before(cn);
1199
- });
1200
- generalTableFootnote.remove();
1201
- }
1218
+ this.appendAuthorNotes = ($authorNotes, authorNotes) => {
1219
+ const correspIDs = new Set(this.getChildrenOfType(schema.nodes.contributor).flatMap((contributor) => contributor.attrs.correspIDs));
1220
+ authorNotes.forEach((node) => {
1221
+ switch (node.type) {
1222
+ case schema.nodes.paragraph: {
1223
+ $authorNotes.append(this.writeParagraph(node));
1224
+ break;
1202
1225
  }
1203
- if (isNodeType(node, 'table_element')) {
1204
- const tableElement = body.querySelector(`#${normalizeID(node.attrs.id)}`);
1205
- if (tableElement) {
1206
- for (const childNode of tableElement.childNodes) {
1207
- switch (childNode.nodeName) {
1208
- case 'caption': {
1209
- const label = tableElement.querySelector('label');
1210
- tableElement.insertBefore(childNode, label ? label.nextSibling : tableElement.firstChild);
1211
- break;
1212
- }
1213
- case 'table': {
1214
- this.fixTable(childNode);
1215
- break;
1216
- }
1217
- }
1218
- }
1226
+ case schema.nodes.footnote: {
1227
+ $authorNotes.append(this.writeFootnote(node));
1228
+ break;
1229
+ }
1230
+ case schema.nodes.corresp: {
1231
+ if (correspIDs.has(node.attrs.id)) {
1232
+ $authorNotes.append(this.writeCorresp(node));
1219
1233
  }
1234
+ break;
1220
1235
  }
1221
1236
  }
1222
1237
  });
1223
1238
  };
1224
- this.changeTag = (node, tag) => {
1225
- const clone = this.createElement(tag);
1226
- for (const attr of node.attributes) {
1227
- clone.setAttributeNS(null, attr.name, attr.value);
1239
+ this.buildKeywords = () => this.getChildrenOfType(schema.nodes.keyword_group).map((group) => this.serializeNode(group));
1240
+ this.changeTag = ($node, tag) => {
1241
+ const $clone = this.createElement(tag);
1242
+ for (const attr of $node.attributes) {
1243
+ $clone.setAttributeNS(null, attr.name, attr.value);
1228
1244
  }
1229
- while (node.firstChild) {
1230
- clone.appendChild(node.firstChild);
1245
+ while ($node.firstChild) {
1246
+ $clone.appendChild($node.firstChild);
1231
1247
  }
1232
- node.replaceWith(clone);
1233
- return clone;
1248
+ $node.replaceWith($clone);
1249
+ return $clone;
1234
1250
  };
1235
- this.fixTable = (table) => {
1236
- let tbody;
1237
- Array.from(table.childNodes).forEach((child) => {
1238
- if (child instanceof Element && child.tagName.toLowerCase() === 'tbody') {
1239
- tbody = child;
1251
+ this.normalizeTable = ($table) => {
1252
+ let $tbody;
1253
+ Array.from($table.childNodes).forEach(($child) => {
1254
+ if ($child instanceof Element &&
1255
+ $child.tagName.toLowerCase() === 'tbody') {
1256
+ $tbody = $child;
1240
1257
  }
1241
1258
  });
1242
- if (!tbody) {
1259
+ if (!$tbody) {
1243
1260
  return;
1244
1261
  }
1245
- const tbodyRows = Array.from(tbody.childNodes);
1246
- const thead = this.createElement('thead');
1247
- tbodyRows.forEach((row, i) => {
1248
- const isRow = row instanceof Element && row.tagName.toLowerCase() === 'tr';
1262
+ const tbodyRows = Array.from($tbody.childNodes);
1263
+ const $thead = this.createElement('thead');
1264
+ tbodyRows.forEach(($row, i) => {
1265
+ const isRow = $row instanceof Element && $row.tagName.toLowerCase() === 'tr';
1249
1266
  if (isRow) {
1250
- const headerCell = row.querySelector('th[scope="col"], th[scope="colgroup"]');
1251
- if (i === 0 || headerCell) {
1252
- tbody?.removeChild(row);
1253
- const tableCells = row.querySelectorAll('td');
1254
- for (const td of tableCells) {
1255
- this.changeTag(td, 'th');
1267
+ const $headerCell = $row.querySelector('th[scope="col"], th[scope="colgroup"]');
1268
+ if (i === 0 || $headerCell) {
1269
+ $tbody?.removeChild($row);
1270
+ const $tableCells = $row.querySelectorAll('td');
1271
+ for (const $td of $tableCells) {
1272
+ this.changeTag($td, 'th');
1256
1273
  }
1257
- thead.appendChild(row);
1274
+ $thead.appendChild($row);
1258
1275
  }
1259
1276
  }
1260
1277
  });
1261
- if (thead.hasChildNodes()) {
1262
- table.insertBefore(thead, tbody);
1263
- }
1264
- };
1265
- this.unwrapBody = (body) => {
1266
- const container = body.querySelector(':scope > sec[sec-type="body"]');
1267
- if (!container) {
1268
- return;
1269
- }
1270
- const sections = container.querySelectorAll(':scope > sec');
1271
- sections.forEach((section) => {
1272
- body.appendChild(section.cloneNode(true));
1273
- });
1274
- body.removeChild(container);
1275
- };
1276
- this.removeBackContainer = (body) => {
1277
- const container = body.querySelector(':scope > sec[sec-type="backmatter"]');
1278
- if (!container) {
1279
- return;
1280
- }
1281
- body.removeChild(container);
1282
- };
1283
- this.moveAwards = (front, body) => {
1284
- const awardGroups = body.querySelectorAll(':scope > award-group');
1285
- if (!awardGroups.length) {
1286
- return;
1287
- }
1288
- const fundingGroup = this.createElement('funding-group');
1289
- awardGroups.forEach((award) => {
1290
- fundingGroup.appendChild(award);
1291
- });
1292
- const articleMeta = front.querySelector(':scope > article-meta');
1293
- if (articleMeta) {
1294
- const insertBeforeElement = articleMeta.querySelector(':scope > support-group, :scope > conference, :scope > counts, :scope > custom-meta-group');
1295
- insertBeforeElement
1296
- ? articleMeta.insertBefore(fundingGroup, insertBeforeElement)
1297
- : articleMeta.appendChild(fundingGroup);
1278
+ if ($thead.hasChildNodes()) {
1279
+ $table.insertBefore($thead, $tbody);
1298
1280
  }
1299
1281
  };
1300
- this.moveAbstracts = (front, body) => {
1301
- const abstractSections = this.getAbstractSections(body);
1302
- for (const abstractSection of abstractSections) {
1303
- const node = abstractSection.nodeName === 'trans-abstract'
1304
- ? this.createTransAbstractNode(abstractSection)
1305
- : this.createAbstractNode(abstractSection);
1306
- abstractSection.remove();
1307
- insertAbstractNode(front, node);
1308
- }
1309
- };
1310
- this.moveSectionsToBack = (back, body) => {
1311
- const availabilitySection = body.querySelector('sec[sec-type="availability"]');
1312
- if (availabilitySection) {
1313
- back.insertBefore(availabilitySection, back.firstChild);
1314
- }
1315
- const ethicsSection = body.querySelector('sec[sec-type="ethics-statement"]');
1316
- if (ethicsSection) {
1317
- back.appendChild(ethicsSection);
1318
- }
1319
- const section = body.querySelector('sec[sec-type="acknowledgements"]');
1320
- if (section) {
1321
- const ack = this.createElement('ack');
1322
- while (section.firstChild) {
1323
- ack.appendChild(section.firstChild);
1324
- }
1325
- if (section.parentNode) {
1326
- section.parentNode.removeChild(section);
1327
- }
1328
- back.insertBefore(ack, back.firstChild);
1329
- }
1330
- const appendicesSections = body.querySelectorAll('sec[sec-type="appendices"]');
1331
- if (appendicesSections) {
1332
- const appGroup = this.createElement('app-group');
1333
- appendicesSections.forEach((section) => {
1334
- if (section.parentNode) {
1335
- section.parentNode.removeChild(section);
1336
- }
1337
- const app = this.createElement('app');
1338
- app.appendChild(section);
1339
- appGroup.appendChild(app);
1340
- });
1341
- back.insertBefore(appGroup, back.firstChild);
1342
- }
1343
- const footNotes = [];
1344
- const footnoteCategories = [
1345
- 'con',
1346
- 'conflict',
1347
- 'deceased',
1348
- 'equal',
1349
- 'present-address',
1350
- 'presented-at',
1351
- 'previously-at',
1352
- 'supplementary-material',
1353
- 'supported-by',
1354
- 'financial-disclosure',
1355
- 'coi-statement',
1356
- ];
1357
- const sections = body.querySelectorAll('sec');
1358
- for (const currentSection of sections) {
1359
- const currentSectionType = currentSection.getAttribute('sec-type');
1360
- if (currentSectionType &&
1361
- footnoteCategories.indexOf(currentSectionType) >= 0) {
1362
- footNotes.push(this.sectionToFootnote(currentSection, currentSectionType));
1282
+ this.buildAbstracts = () => {
1283
+ const $abstracts = [];
1284
+ const abstractsNode = this.getFirstChildOfType(schema.nodes.abstracts);
1285
+ abstractsNode?.forEach((child) => {
1286
+ const $abstract = this.serializeNode(child);
1287
+ if ($abstract.nodeName === 'abstract') {
1288
+ $abstract
1289
+ .querySelectorAll(':scope > title')
1290
+ .forEach((title) => title.remove());
1363
1291
  }
1364
- }
1365
- if (footNotes.length > 0) {
1366
- const fnGroup = this.createElement('fn-group');
1367
- fnGroup.append(...footNotes);
1368
- back.append(fnGroup);
1369
- }
1292
+ $abstracts.push($abstract);
1293
+ });
1294
+ return $abstracts;
1370
1295
  };
1371
- this.sectionToFootnote = (section, fnType) => {
1372
- const footNote = this.createElement('fn');
1373
- footNote.setAttribute('fn-type', fnType);
1374
- const title = section.querySelector('title');
1375
- if (title) {
1376
- const footNoteTitle = this.createElement('label');
1377
- footNoteTitle.textContent = title.textContent;
1378
- section.removeChild(title);
1379
- footNote.append(footNoteTitle);
1296
+ this.sectionToFootnote = ($section, fnType) => {
1297
+ const $fn = this.createElement('fn');
1298
+ $fn.setAttribute('fn-type', fnType);
1299
+ const $title = $section.querySelector(':scope > title');
1300
+ if ($title) {
1301
+ const $label = this.createElement('label');
1302
+ $label.textContent = $title.textContent;
1303
+ $section.removeChild($title);
1304
+ $fn.append($label);
1380
1305
  }
1381
- footNote.append(...section.children);
1382
- if (section.parentNode) {
1383
- section.parentNode.removeChild(section);
1306
+ $fn.append(...$section.children);
1307
+ if ($section.parentNode) {
1308
+ $section.parentNode.removeChild($section);
1384
1309
  }
1385
- return footNote;
1310
+ return $fn;
1386
1311
  };
1387
- this.moveFloatsGroup = (article) => {
1312
+ this.buildFloatsGroup = () => {
1388
1313
  const heroImage = this.getFirstChildOfType(schema.nodes.hero_image);
1389
1314
  if (!heroImage) {
1390
1315
  return;
1391
1316
  }
1392
- const floatsGroup = this.createElement('floats-group');
1393
- let figure = null;
1317
+ const $floatsGroup = this.createElement('floats-group');
1318
+ let $graphic = null;
1394
1319
  heroImage.descendants((node) => {
1395
1320
  if (node.type === schema.nodes.figure) {
1396
- figure = this.serializeNode(node);
1397
- floatsGroup.appendChild(figure);
1321
+ $graphic = this.serializeNode(node);
1322
+ $floatsGroup.appendChild($graphic);
1398
1323
  }
1399
1324
  else {
1400
- const serializedNode = this.serializeNode(node);
1401
- figure?.appendChild(serializedNode);
1325
+ const $serializedNode = this.serializeNode(node);
1326
+ $graphic?.appendChild($serializedNode);
1402
1327
  }
1403
1328
  return false;
1404
1329
  });
1405
- if (floatsGroup.children.length > 0) {
1406
- article.appendChild(floatsGroup);
1330
+ if ($floatsGroup.children.length > 0) {
1331
+ return $floatsGroup;
1407
1332
  }
1408
1333
  };
1409
1334
  }
@@ -1418,167 +1343,57 @@ export class JATSExporter {
1418
1343
  }
1419
1344
  createEquation(node, isInline = false) {
1420
1345
  if (node.attrs.format === 'tex') {
1421
- const texMath = this.createElement('tex-math');
1422
- texMath.setAttribute('notation', 'LaTeX');
1423
- texMath.setAttribute('version', 'MathJax');
1346
+ const $texMath = this.createElement('tex-math');
1347
+ $texMath.setAttribute('notation', 'LaTeX');
1348
+ $texMath.setAttribute('version', 'MathJax');
1424
1349
  if (node.attrs.contents.includes('<![CDATA[')) {
1425
- texMath.innerHTML = node.attrs.contents;
1350
+ $texMath.innerHTML = node.attrs.contents;
1426
1351
  }
1427
1352
  else {
1428
- texMath.innerHTML = `<![CDATA[ ${node.attrs.contents} ]]>`;
1353
+ $texMath.innerHTML = `<![CDATA[ ${node.attrs.contents} ]]>`;
1429
1354
  }
1430
- return texMath;
1355
+ return $texMath;
1431
1356
  }
1432
1357
  else {
1433
1358
  const math = this.nodeFromJATS(node.attrs.contents);
1434
- const mathml = math;
1359
+ const $mathml = math;
1435
1360
  if (!isInline) {
1436
- mathml.setAttribute('id', normalizeID(node.attrs.id));
1437
- }
1438
- return mathml;
1439
- }
1440
- }
1441
- buildKeywords(articleMeta) {
1442
- const keywordGroups = this.getChildrenOfType(schema.nodes.keyword_group);
1443
- keywordGroups.forEach((group) => {
1444
- const kwdGroup = this.createElement('kwd-group');
1445
- if (group.attrs.type) {
1446
- kwdGroup.setAttribute('kwd-group-type', group.attrs.type);
1447
- }
1448
- articleMeta.appendChild(kwdGroup);
1449
- group.content.forEach((keyword) => {
1450
- const kwd = this.createElement('kwd');
1451
- kwd.textContent = keyword.textContent;
1452
- kwdGroup.appendChild(kwd);
1453
- });
1454
- articleMeta.appendChild(kwdGroup);
1455
- });
1456
- }
1457
- getAbstractCategories(abstractsNode) {
1458
- const categories = [];
1459
- abstractsNode?.content.descendants((node) => {
1460
- categories.push(node.attrs.category);
1461
- return false;
1462
- });
1463
- return categories;
1464
- }
1465
- getAbstractSections(body) {
1466
- {
1467
- const abstractsNode = this.getFirstChildOfType(schema.nodes.abstracts);
1468
- const abstractCategories = this.getAbstractCategories(abstractsNode);
1469
- const sections = Array.from(body.querySelectorAll(':scope > sec, :scope > trans-abstract'));
1470
- return sections.filter((section) => this.isAbstractSection(section, abstractCategories));
1471
- }
1472
- }
1473
- isAbstractSection(section, abstractCategories) {
1474
- const sectionType = section.getAttribute('sec-type');
1475
- return sectionType ? abstractCategories.includes(sectionType) : false;
1476
- }
1477
- createTransAbstractNode(transAbstract) {
1478
- const transAbstractNode = this.createElement('trans-abstract');
1479
- transAbstractNode.setAttributeNS(XML_NAMESPACE, 'lang', transAbstract.getAttributeNS(XML_NAMESPACE, 'lang') ?? '');
1480
- this.setAbstractType(transAbstractNode, transAbstract);
1481
- transAbstractNode.append(...transAbstract.childNodes);
1482
- return transAbstractNode;
1483
- }
1484
- createAbstractNode(abstractSection) {
1485
- const abstractNode = this.createElement('abstract');
1486
- for (const node of abstractSection.childNodes) {
1487
- if (node.nodeName !== 'title') {
1488
- abstractNode.appendChild(node.cloneNode(true));
1361
+ $mathml.setAttribute('id', normalizeID(node.attrs.id));
1489
1362
  }
1363
+ return $mathml;
1490
1364
  }
1491
- this.setAbstractType(abstractNode, abstractSection);
1492
- return abstractNode;
1493
1365
  }
1494
- setAbstractType(abstractNode, abstractSection) {
1495
- const sectionType = abstractSection.getAttribute('sec-type');
1496
- if (sectionType && sectionType !== 'abstract') {
1497
- const abstractType = sectionType.replace('abstract-', '');
1498
- abstractNode.setAttribute('abstract-type', abstractType);
1499
- }
1366
+ fillEmptyElements($article, selector, tagName = 'p') {
1367
+ const $empty = Array.from($article.querySelectorAll(selector)).filter(($el) => !$el.innerHTML);
1368
+ $empty.forEach(($element) => $element.appendChild(this.createElement(tagName)));
1500
1369
  }
1501
- moveCoiStatementToAuthorNotes(back, front) {
1502
- const fnGroups = back.querySelectorAll('fn-group');
1503
- fnGroups.forEach((fnGroup) => {
1504
- if (fnGroup) {
1505
- const coiStatement = fnGroup.querySelector('fn[fn-type="coi-statement"]');
1506
- if (coiStatement) {
1507
- const authorNotes = this.createElement('author-notes');
1508
- authorNotes.append(coiStatement);
1509
- const articleMeta = front.querySelector('article-meta');
1510
- if (articleMeta) {
1511
- const authorNoteEl = articleMeta.querySelector('author-notes');
1512
- if (authorNoteEl) {
1513
- authorNoteEl.append(...authorNotes.childNodes);
1514
- }
1515
- else {
1516
- const appendableSelectors = [
1517
- 'contrib-group',
1518
- 'title-group',
1519
- 'article-id',
1520
- ];
1521
- const appendable = [
1522
- ...articleMeta.querySelectorAll(appendableSelectors.join(', ')),
1523
- ];
1524
- for (let i = 0; i < appendableSelectors.length; i++) {
1525
- const sel = appendableSelectors[i];
1526
- const match = appendable.find((el) => el.matches(sel));
1527
- if (match) {
1528
- articleMeta.insertBefore(authorNotes, match.nextSibling);
1529
- break;
1530
- }
1531
- }
1532
- }
1533
- }
1534
- if (!fnGroup.hasChildNodes()) {
1535
- fnGroup.remove();
1536
- }
1537
- }
1538
- }
1539
- });
1540
- }
1541
- updateFootnoteTypes(front, body) {
1542
- const footnotes = [...front.querySelectorAll('fn').values()];
1543
- footnotes.push(...body.querySelectorAll('fn'));
1544
- footnotes.forEach((fn) => {
1545
- const fnType = fn.getAttribute('fn-type');
1546
- if (fnType) {
1547
- fn.setAttribute('fn-type', fnType);
1548
- }
1549
- });
1550
- }
1551
- fillEmptyElements(articleElement, selector, tagName = 'p') {
1552
- const emptyElements = Array.from(articleElement.querySelectorAll(selector)).filter((element) => !element.innerHTML);
1553
- emptyElements.forEach((element) => element.appendChild(this.createElement(tagName)));
1554
- }
1555
- addParagraphsToSections(articleElement) {
1556
- const sections = articleElement.querySelectorAll('sec');
1370
+ addParagraphsToSections($article) {
1371
+ const $sections = $article.querySelectorAll('sec, abstract');
1557
1372
  const TITLE_TAGS = new Set(['title', 'label', 'sec-meta']);
1558
- for (const section of sections) {
1559
- const hasContent = Array.from(section.children).some((child) => !TITLE_TAGS.has(child.tagName));
1373
+ for (const $section of $sections) {
1374
+ const hasContent = Array.from($section.children).some(($child) => !TITLE_TAGS.has($child.tagName));
1560
1375
  if (hasContent) {
1561
1376
  continue;
1562
1377
  }
1563
- const p = this.createElement('p');
1564
- const insertAfterElement = section.querySelector(':scope > title') ??
1565
- section.querySelector(':scope > label') ??
1566
- section.querySelector(':scope > sec-meta');
1567
- if (insertAfterElement) {
1568
- insertAfterElement.insertAdjacentElement('afterend', p);
1378
+ const $p = this.createElement('p');
1379
+ const $insertAfter = $section.querySelector(':scope > title') ??
1380
+ $section.querySelector(':scope > label') ??
1381
+ $section.querySelector(':scope > sec-meta');
1382
+ if ($insertAfter) {
1383
+ $insertAfter.insertAdjacentElement('afterend', $p);
1569
1384
  }
1570
1385
  else {
1571
- section.prepend(p);
1386
+ $section.prepend($p);
1572
1387
  }
1573
1388
  }
1574
1389
  }
1575
- fillEmptyFootnotes(articleElement) {
1576
- this.fillEmptyElements(articleElement, 'fn');
1390
+ fillEmptyFootnotes($article) {
1391
+ this.fillEmptyElements($article, 'fn');
1577
1392
  }
1578
- fillEmptyTableFooters(articleElement) {
1579
- this.fillEmptyElements(articleElement, 'table-wrap-foot');
1393
+ fillEmptyTableFooters($article) {
1394
+ this.fillEmptyElements($article, 'table-wrap-foot');
1580
1395
  }
1581
- fillEmptyListItem(articleElement) {
1582
- this.fillEmptyElements(articleElement, 'list-item');
1396
+ fillEmptyListItem($article) {
1397
+ this.fillEmptyElements($article, 'list-item');
1583
1398
  }
1584
1399
  }