@manuscripts/transform 4.5.1 → 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 (66) 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/schema/nodes/footnote.js +1 -0
  14. package/dist/cjs/schema/nodes/general_table_footnote.js +1 -0
  15. package/dist/cjs/transformer/node-names.js +1 -0
  16. package/dist/cjs/transformer/node-title.js +2 -0
  17. package/dist/cjs/version.js +1 -1
  18. package/dist/es/jats/__tests__/data/section-categories.js +5 -5
  19. package/dist/es/jats/__tests__/jats-importer.test.js +19 -5
  20. package/dist/es/jats/exporter/jats-exporter.js +728 -913
  21. package/dist/es/jats/exporter/jats-versions.js +7 -7
  22. package/dist/es/jats/importer/jats-dom-parser.js +25 -24
  23. package/dist/es/jats/importer/jats-transformations.js +14 -30
  24. package/dist/es/lib/section-categories.js +28 -0
  25. package/dist/es/schema/index.js +3 -0
  26. package/dist/es/schema/migration/migration-scripts/4.5.3.js +35 -0
  27. package/dist/es/schema/migration/migration-scripts/index.js +2 -0
  28. package/dist/es/schema/nodes/abstract.js +47 -0
  29. package/dist/es/schema/nodes/abstracts.js +1 -1
  30. package/dist/es/schema/nodes/footnote.js +1 -0
  31. package/dist/es/schema/nodes/general_table_footnote.js +1 -0
  32. package/dist/es/transformer/node-names.js +1 -0
  33. package/dist/es/transformer/node-title.js +2 -0
  34. package/dist/es/version.js +1 -1
  35. package/dist/types/jats/exporter/jats-exporter.d.ts +31 -27
  36. package/dist/types/jats/exporter/jats-versions.d.ts +4 -4
  37. package/dist/types/jats/importer/jats-dom-parser.d.ts +0 -1
  38. package/dist/types/lib/section-categories.d.ts +3 -0
  39. package/dist/types/schema/index.d.ts +1 -0
  40. package/dist/types/schema/migration/migration-scripts/4.5.3.d.ts +8 -0
  41. package/dist/types/schema/migration/migration-scripts/index.d.ts +2 -1
  42. package/dist/types/schema/nodes/abstract.d.ts +26 -0
  43. package/dist/types/schema/types.d.ts +1 -1
  44. package/dist/types/version.d.ts +1 -1
  45. package/package.json +1 -1
  46. package/src/jats/__tests__/__snapshots__/jats-exporter.test.ts.snap +3 -3
  47. package/src/jats/__tests__/__snapshots__/jats-importer.test.ts.snap +86 -132
  48. package/src/jats/__tests__/__snapshots__/jats-roundtrip.test.ts.snap +3 -3
  49. package/src/jats/__tests__/data/section-categories.ts +5 -5
  50. package/src/jats/__tests__/jats-importer.test.ts +22 -6
  51. package/src/jats/exporter/jats-exporter.ts +855 -1126
  52. package/src/jats/exporter/jats-versions.ts +11 -11
  53. package/src/jats/importer/jats-dom-parser.ts +34 -34
  54. package/src/jats/importer/jats-transformations.ts +21 -37
  55. package/src/lib/section-categories.ts +36 -0
  56. package/src/schema/index.ts +3 -0
  57. package/src/schema/migration/migration-scripts/4.5.3.ts +57 -0
  58. package/src/schema/migration/migration-scripts/index.ts +2 -0
  59. package/src/schema/nodes/abstract.ts +66 -0
  60. package/src/schema/nodes/abstracts.ts +2 -1
  61. package/src/schema/nodes/footnote.ts +1 -0
  62. package/src/schema/nodes/general_table_footnote.ts +1 -0
  63. package/src/schema/types.ts +1 -0
  64. package/src/transformer/node-names.ts +1 -0
  65. package/src/transformer/node-title.ts +2 -0
  66. package/src/version.ts +1 -1
@@ -15,25 +15,21 @@
15
15
  */
16
16
 
17
17
  import * as Citeproc from 'citeproc'
18
- import {
19
- DOMOutputSpec,
20
- DOMParser as ProsemirrorDOMParser,
21
- DOMSerializer,
22
- type NodeType,
23
- } from 'prosemirror-model'
18
+ import { DOMOutputSpec, DOMSerializer, type NodeType } from 'prosemirror-model'
24
19
  import { findChildrenByAttr, findChildrenByType } from 'prosemirror-utils'
25
20
  import serializeToXML from 'w3c-xmlserializer'
26
21
 
27
22
  import { buildCiteprocCitation } from '../../lib/citeproc'
28
23
  import { CreditRoleUrls } from '../../lib/credit-roles'
29
24
  import { generateFootnoteLabels } from '../../lib/footnotes'
30
- import { nodeFromHTML } from '../../lib/html'
25
+ import { FOOTNOTE_SECTION_CATEGORY_IDS } from '../../lib/section-categories'
31
26
  import {
32
27
  sanitizeXmlString,
33
28
  XLINK_NAMESPACE,
34
29
  XML_NAMESPACE,
35
30
  } from '../../lib/xml'
36
31
  import {
32
+ ActualManuscriptNode,
37
33
  AffiliationNode,
38
34
  AuthorNotesNode,
39
35
  AwardNode,
@@ -53,13 +49,11 @@ import {
53
49
  ParagraphNode,
54
50
  QuoteImageNode,
55
51
  schema,
56
- TableElementFooterNode,
57
- TableElementNode,
58
52
  } from '../../schema'
59
- import { isExecutableNodeType, isNodeType } from '../../transformer'
53
+ import { isExecutableNodeType } from '../../transformer'
60
54
  import { IDGenerator } from '../types'
61
55
  import { initJats, jatsVariableWrapper } from './citeproc'
62
- import { selectVersionIds, Version } from './jats-versions'
56
+ import { selectVersionIDs, Version } from './jats-versions'
63
57
  import { buildTargets, Target } from './labels'
64
58
 
65
59
  interface Attrs {
@@ -74,24 +68,6 @@ type MarkSpecs = {
74
68
 
75
69
  const normalizeID = (id: string) => id.replace(/:/g, '_')
76
70
 
77
- const parser = ProsemirrorDOMParser.fromSchema(schema)
78
- // siblings from https://jats.nlm.nih.gov/archiving/tag-library/1.2/element/article-meta.html
79
- const insertAbstractNode = (front: Element, abstractNode: Element) => {
80
- const articleMeta = front.querySelector(':scope > article-meta')
81
- if (!articleMeta) {
82
- return
83
- }
84
- const insertBeforeElement = articleMeta.querySelector(
85
- 'kwd-group, funding-group, support-group, conference, counts, custom-meta-group'
86
- )
87
-
88
- if (insertBeforeElement) {
89
- articleMeta.insertBefore(abstractNode, insertBeforeElement)
90
- } else {
91
- articleMeta.appendChild(abstractNode)
92
- }
93
- }
94
-
95
71
  export const createCounter = () => {
96
72
  const counts = new Map<string, number>()
97
73
 
@@ -105,7 +81,7 @@ export const createCounter = () => {
105
81
  }
106
82
  }
107
83
 
108
- const createDefaultIdGenerator = (): IDGenerator => {
84
+ const createDefaultIDGenerator = (): IDGenerator => {
109
85
  const counter = createCounter()
110
86
 
111
87
  return async (element: Element) => {
@@ -129,6 +105,7 @@ const chooseRefType = (type: ManuscriptNodeType): string | undefined => {
129
105
  return 'table'
130
106
 
131
107
  case schema.nodes.section:
108
+ case schema.nodes.abstract:
132
109
  return 'sec'
133
110
 
134
111
  case schema.nodes.equation:
@@ -139,10 +116,6 @@ const chooseRefType = (type: ManuscriptNodeType): string | undefined => {
139
116
  return 'supplementary-material'
140
117
  }
141
118
  }
142
-
143
- const sortContributors = (a: ContributorNode, b: ContributorNode) =>
144
- Number(a.attrs.priority) - Number(b.attrs.priority)
145
-
146
119
  export type CSLOptions = {
147
120
  style: string
148
121
  locale: string
@@ -157,10 +130,11 @@ export class JATSExporter {
157
130
  protected serializer: DOMSerializer
158
131
  protected labelTargets: Map<string, Target>
159
132
  protected footnoteLabels: Map<string, string>
160
- protected manuscriptNode: ManuscriptNode
133
+ protected manuscriptNode: ActualManuscriptNode
161
134
  private engine: Citeproc.Engine
162
135
  private renderedCitations: Map<string, string>
163
136
  private nodesMap: Map<NodeType, ManuscriptNode[]> = new Map()
137
+ private contributorLabels: Map<string, number> = new Map()
164
138
 
165
139
  private populateNodesMap = () => {
166
140
  this.manuscriptNode.descendants((node) => {
@@ -189,101 +163,103 @@ export class JATSExporter {
189
163
  }
190
164
 
191
165
  public serializeToJATS = async (
192
- manuscriptNode: ManuscriptNode,
166
+ manuscriptNode: ActualManuscriptNode,
193
167
  options: ExportOptions
194
168
  ): Promise<string> => {
195
169
  this.manuscriptNode = manuscriptNode
196
170
  this.populateNodesMap()
197
171
  this.initCiteprocEngine(options)
198
172
  this.createSerializer()
199
- const versionIds = selectVersionIds(options.version ?? '1.2')
173
+ const versionIDs = selectVersionIDs(options.version ?? '1.2')
200
174
 
201
175
  this.document = document.implementation.createDocument(
202
176
  null,
203
177
  'article',
204
178
  document.implementation.createDocumentType(
205
179
  'article',
206
- versionIds.publicId,
207
- versionIds.systemId
180
+ versionIDs.publicID,
181
+ versionIDs.systemID
208
182
  )
209
183
  )
210
184
 
211
- const article = this.document.documentElement
185
+ const $article = this.document.documentElement
212
186
 
213
- article.setAttributeNS(
187
+ $article.setAttributeNS(
214
188
  'http://www.w3.org/2000/xmlns/',
215
189
  'xmlns:xlink',
216
190
  XLINK_NAMESPACE
217
191
  )
218
- const front = this.buildFront()
219
- article.appendChild(front)
220
- article.setAttribute(
192
+ this.labelTargets = buildTargets(
193
+ manuscriptNode.descendants.bind(manuscriptNode)
194
+ )
195
+ this.footnoteLabels = generateFootnoteLabels(manuscriptNode)
196
+
197
+ const $body = this.buildBody()
198
+ const $back = this.buildBack()
199
+ this.routeBackmatterSections($body, $back)
200
+ const $coiStatement = this.extractCoiStatement($back)
201
+ $back.appendChild(this.buildRefList())
202
+
203
+ const $front = this.buildFront($coiStatement)
204
+ $article.appendChild($front)
205
+ $article.setAttribute(
221
206
  'article-type',
222
207
  manuscriptNode.attrs.articleType || 'other'
223
208
  )
224
- article.setAttributeNS(
209
+ $article.setAttributeNS(
225
210
  XML_NAMESPACE,
226
211
  'lang',
227
212
  manuscriptNode.attrs.primaryLanguageCode || 'en'
228
213
  )
229
- this.labelTargets = buildTargets(
230
- manuscriptNode.descendants.bind(manuscriptNode)
231
- )
232
- this.footnoteLabels = generateFootnoteLabels(manuscriptNode)
233
- const body = this.buildBody()
234
- article.appendChild(body)
235
- this.addParagraphsToSections(article)
236
- const back = this.buildBack(body)
237
- this.moveCoiStatementToAuthorNotes(back, front)
238
- article.appendChild(back)
239
- this.unwrapBody(body)
240
- this.moveAbstracts(front, body)
241
- this.removeBackContainer(body)
242
- this.updateFootnoteTypes(front, back)
243
- this.fillEmptyTableFooters(article)
244
- this.fillEmptyFootnotes(article)
245
- this.fillEmptyListItem(article)
246
- this.moveAwards(front, body)
247
- this.moveFloatsGroup(article)
214
+ $article.appendChild($body)
215
+ $article.appendChild($back)
216
+ this.addParagraphsToSections($article)
217
+ this.fillEmptyTableFooters($article)
218
+ this.fillEmptyFootnotes($article)
219
+ this.fillEmptyListItem($article)
220
+ const $floatsGroup = this.buildFloatsGroup()
221
+ if ($floatsGroup) {
222
+ $article.appendChild($floatsGroup)
223
+ }
248
224
  await this.rewriteIDs()
249
225
  return serializeToXML(this.document)
250
226
  }
251
227
 
252
228
  private appendChildNodeOfType = (
253
- element: HTMLElement,
229
+ $element: Element,
254
230
  node: ManuscriptNode,
255
231
  type: ManuscriptNodeType
256
232
  ) => {
257
233
  const childNode = this.getFirstChildOfType(type, node)
258
234
  if (childNode) {
259
- element.appendChild(this.serializeNode(childNode))
235
+ $element.appendChild(this.serializeNode(childNode))
260
236
  }
261
237
  }
262
238
 
263
239
  private appendChildrenNodeOfType = (
264
- element: HTMLElement,
240
+ $element: Element,
265
241
  node: ManuscriptNode,
266
242
  type: ManuscriptNodeType
267
243
  ) => {
268
244
  const children = this.getChildrenOfType(type, node)
269
245
  children.map((childNode) =>
270
- element.appendChild(this.serializeNode(childNode))
246
+ $element.appendChild(this.serializeNode(childNode))
271
247
  )
272
248
  }
273
249
 
274
- protected appendCaption = (element: HTMLElement, node: ManuscriptNode) => {
275
- const caption = this.createElement('caption')
276
- this.appendChildNodeOfType(caption, node, schema.nodes.caption_title)
250
+ protected appendCaption = ($element: Element, node: ManuscriptNode) => {
251
+ const $caption = this.createElement('caption')
252
+ this.appendChildNodeOfType($caption, node, schema.nodes.caption_title)
277
253
  const captionNode = this.getChildrenOfType(schema.nodes.caption, node)[0]
278
254
  if (captionNode && captionNode.content.size > 2) {
279
255
  this.appendChildrenNodeOfType(
280
- caption,
256
+ $caption,
281
257
  captionNode,
282
258
  schema.nodes.text_block
283
259
  )
284
260
  }
285
- if (caption.children.length > 0) {
286
- element.appendChild(caption)
261
+ if ($caption.children.length > 0) {
262
+ $element.appendChild($caption)
287
263
  }
288
264
  }
289
265
  private initCiteprocEngine = (options: ExportOptions) => {
@@ -333,43 +309,36 @@ export class JATSExporter {
333
309
  this.renderedCitations = new Map(output.map((i) => [i[0], i[2]]))
334
310
  }
335
311
 
336
- private nodeFromJATS = (JATSFragment: string) => {
337
- JATSFragment = JATSFragment.trim()
338
- JATSFragment = JATSFragment.replace('&nbsp;', ' ')
339
-
340
- if (!JATSFragment.length) {
312
+ private nodeFromJATS = (fragment: string) => {
313
+ fragment = fragment.trim().replace('&nbsp;', ' ')
314
+ if (!fragment.length) {
341
315
  return null
342
316
  }
343
-
344
- const template = this.createElement('template')
345
-
346
- template.innerHTML = JATSFragment
347
-
348
- return template.firstChild
317
+ const $template = this.createElement('template')
318
+ $template.innerHTML = fragment
319
+ return $template.firstChild
349
320
  }
350
321
 
351
322
  protected rewriteIDs = async (
352
- generator: IDGenerator = createDefaultIdGenerator()
323
+ generator: IDGenerator = createDefaultIDGenerator()
353
324
  ) => {
354
325
  const ids = new Map<string, string | null>()
355
326
 
356
- for (const element of this.document.querySelectorAll('[id]')) {
357
- const oldID = element.getAttribute('id')
358
- const newID = await generator(element)
359
-
327
+ for (const $element of this.document.querySelectorAll('[id]')) {
328
+ const oldID = $element.getAttribute('id')
329
+ const newID = await generator($element)
360
330
  if (newID) {
361
- element.setAttribute('id', newID)
331
+ $element.setAttribute('id', newID)
362
332
  } else {
363
- element.removeAttribute('id')
333
+ $element.removeAttribute('id')
364
334
  }
365
-
366
335
  if (oldID) {
367
336
  ids.set(oldID, newID)
368
337
  }
369
338
  }
370
339
 
371
- for (const node of this.document.querySelectorAll('[rid]')) {
372
- const rids = node.getAttribute('rid')
340
+ for (const $node of this.document.querySelectorAll('[rid]')) {
341
+ const rids = $node.getAttribute('rid')
373
342
 
374
343
  if (rids) {
375
344
  const newRids = rids
@@ -379,198 +348,151 @@ export class JATSExporter {
379
348
  .filter(Boolean)
380
349
 
381
350
  if (newRids.length) {
382
- node.setAttribute('rid', newRids.join(' '))
351
+ $node.setAttribute('rid', newRids.join(' '))
383
352
  }
384
353
  }
385
354
  }
386
355
  }
387
356
 
388
- protected setTitleContent = (element: HTMLElement, title: string) => {
389
- const htmlTitleNode = nodeFromHTML(`<h1>${title}</h1>`)
357
+ protected buildFront = ($coiStatement?: Element) => {
358
+ const $front = this.createElement('front')
359
+ const $articleMeta = this.createElement('article-meta')
360
+ $front.appendChild($articleMeta)
390
361
 
391
- if (htmlTitleNode) {
392
- // TODO: parse and serialize with title schema
393
- const titleNode = parser.parse(htmlTitleNode, {
394
- topNode: schema.nodes.section_title.create(),
395
- })
362
+ const contributors = this.getChildrenOfType<ContributorNode>(
363
+ schema.nodes.contributor
364
+ ).sort((a, b) => Number(a.attrs.priority) - Number(b.attrs.priority))
365
+ this.contributorLabels = this.computeContributorLabels(contributors)
366
+ const $frontNodes = [
367
+ this.buildArticleID(),
368
+ this.buildTitleGroup(),
369
+ this.buildContributorGroup(contributors),
370
+ this.buildAuthorNotes($coiStatement),
371
+ this.buildSupplements(),
372
+ this.buildHistory(),
373
+ this.buildSelfUris(),
374
+ this.buildAbstracts(),
375
+ this.buildKeywords(),
376
+ this.buildAwards(),
377
+ this.buildCounts(),
378
+ ]
396
379
 
397
- const jatsTitleNode = this.serializeNode(titleNode)
380
+ $frontNodes.forEach(($nodes) => this.appendChildren($articleMeta, $nodes))
381
+ return $front
382
+ }
398
383
 
399
- while (jatsTitleNode.firstChild) {
400
- element.appendChild(jatsTitleNode.firstChild)
401
- }
384
+ private appendChildren = (
385
+ $parent: Element,
386
+ $elements?: Node | Node[] | null
387
+ ) => {
388
+ if (!$elements) {
389
+ return
402
390
  }
391
+ const list = Array.isArray($elements) ? $elements : [$elements]
392
+ list.forEach(($element) => $parent.appendChild($element))
403
393
  }
404
394
 
405
- protected buildFront = () => {
406
- // https://jats.nlm.nih.gov/archiving/tag-library/1.2/element/front.html
407
- const front = this.createElement('front')
408
-
409
- // https://jats.nlm.nih.gov/archiving/tag-library/1.2/element/article-meta.html
410
- const articleMeta = this.createElement('article-meta')
411
- front.appendChild(articleMeta)
412
-
395
+ private buildArticleID = (): Element | undefined => {
413
396
  if (this.manuscriptNode.attrs.doi) {
414
- const articleID = this.createElement('article-id')
415
- articleID.setAttribute('pub-id-type', 'doi')
416
- // @ts-ignore
417
- articleID.textContent = this.manuscriptNode.attrs.doi
418
- articleMeta.appendChild(articleID)
397
+ const $articleID = this.createElement('article-id')
398
+ $articleID.setAttribute('pub-id-type', 'doi')
399
+ $articleID.textContent = this.manuscriptNode.attrs.doi
400
+ return $articleID
419
401
  }
402
+ }
420
403
 
421
- const titleGroup = this.createElement('title-group')
404
+ private buildTitleGroup = (): Element => {
405
+ const $titleGroup = this.createElement('title-group')
422
406
 
423
407
  const titleNode = this.getFirstChildOfType(schema.nodes.title)
424
-
425
408
  if (titleNode) {
426
- const element = this.createElement('article-title')
427
- this.setTitleContent(element, titleNode.textContent)
428
- titleGroup.appendChild(element)
409
+ $titleGroup.appendChild(this.serializeNode(titleNode))
429
410
  }
430
411
 
431
- const subtitlesNodes = this.getChildrenOfType(schema.nodes.subtitle)
432
- subtitlesNodes.forEach((subtitleNode) => {
433
- const element = this.createElement('subtitle')
434
- this.setTitleContent(element, subtitleNode.textContent)
435
- titleGroup.appendChild(element)
436
- })
412
+ this.getChildrenOfType(schema.nodes.subtitle).forEach((node) =>
413
+ $titleGroup.appendChild(this.serializeNode(node))
414
+ )
437
415
 
438
- const altTitlesNodes = this.getChildrenOfType(schema.nodes.alt_title)
416
+ this.getChildrenOfType(schema.nodes.alt_title).forEach((node) =>
417
+ $titleGroup.appendChild(this.serializeNode(node))
418
+ )
439
419
 
440
- altTitlesNodes.forEach((titleNode) => {
441
- const element = this.createElement('alt-title')
442
- element.setAttribute('alt-title-type', titleNode.attrs.type)
443
- this.setTitleContent(element, titleNode.textContent)
444
- titleGroup.appendChild(element)
445
- })
420
+ return $titleGroup
421
+ }
446
422
 
447
- articleMeta.appendChild(titleGroup)
448
- this.buildContributors(articleMeta)
449
-
450
- const supplementsNodes = this.getChildrenOfType(schema.nodes.supplement)
451
- supplementsNodes.forEach((node) => {
452
- const supplementaryMaterial = this.createElement('supplementary-material')
453
- supplementaryMaterial.setAttribute('id', normalizeID(node.attrs.id))
454
- supplementaryMaterial.setAttributeNS(
455
- XLINK_NAMESPACE,
456
- 'href',
457
- node.attrs.href ?? ''
458
- )
459
- supplementaryMaterial.setAttribute('mimetype', node.attrs.mimeType ?? '')
460
- supplementaryMaterial.setAttribute(
461
- 'mime-subtype',
462
- node.attrs.mimeSubType ?? ''
463
- )
464
- this.appendCaption(supplementaryMaterial, node)
423
+ private buildSupplements = () =>
424
+ this.getChildrenOfType(schema.nodes.supplement).map((node) =>
425
+ this.serializeNode(node)
426
+ )
465
427
 
466
- articleMeta.append(supplementaryMaterial)
428
+ private buildHistory = () => {
429
+ const $history = this.createElement('history')
430
+ const dates = new Map([
431
+ ['accepted', this.manuscriptNode.attrs.acceptanceDate],
432
+ ['corrected', this.manuscriptNode.attrs.correctionDate],
433
+ ['retracted', this.manuscriptNode.attrs.retractionDate],
434
+ ['received', this.manuscriptNode.attrs.receiveDate],
435
+ ['rev-recd', this.manuscriptNode.attrs.revisionReceiveDate],
436
+ ['rev-request', this.manuscriptNode.attrs.revisionRequestDate],
437
+ ])
438
+
439
+ dates.forEach((val, key) => {
440
+ if (val) {
441
+ $history.appendChild(this.buildDateElement(val, key))
442
+ }
467
443
  })
468
444
 
469
- const history =
470
- articleMeta.querySelector('history') || this.createElement('history')
471
-
472
- if (this.manuscriptNode.attrs.acceptanceDate) {
473
- const date = this.buildDateElement(
474
- this.manuscriptNode.attrs.acceptanceDate,
475
- 'accepted'
476
- )
477
- history.appendChild(date)
478
- }
479
- if (this.manuscriptNode.attrs.correctionDate) {
480
- const date = this.buildDateElement(
481
- this.manuscriptNode.attrs.correctionDate,
482
- 'corrected'
483
- )
484
- history.appendChild(date)
485
- }
486
- if (this.manuscriptNode.attrs.retractionDate) {
487
- const date = this.buildDateElement(
488
- this.manuscriptNode.attrs.retractionDate,
489
- 'retracted'
490
- )
491
- history.appendChild(date)
445
+ if ($history.childElementCount) {
446
+ return $history
492
447
  }
493
- if (this.manuscriptNode.attrs.receiveDate) {
494
- const date = this.buildDateElement(
495
- this.manuscriptNode.attrs.receiveDate,
496
- 'received'
497
- )
498
- history.appendChild(date)
499
- }
500
- if (this.manuscriptNode.attrs.revisionReceiveDate) {
501
- const date = this.buildDateElement(
502
- this.manuscriptNode.attrs.revisionReceiveDate,
503
- 'rev-recd'
504
- )
505
- history.appendChild(date)
506
- }
507
- if (this.manuscriptNode.attrs.revisionRequestDate) {
508
- const date = this.buildDateElement(
509
- this.manuscriptNode.attrs.revisionRequestDate,
510
- 'rev-request'
511
- )
512
- history.appendChild(date)
513
- }
514
-
515
- if (history.childElementCount) {
516
- articleMeta.appendChild(history)
517
- }
518
-
519
- this.buildKeywords(articleMeta)
520
-
521
- let countingElements = []
522
- const figureCount = this.getChildrenOfType(schema.nodes.figure).length
523
- countingElements.push(this.buildCountingElement('fig-count', figureCount))
524
-
525
- const tableCount = this.getChildrenOfType(schema.nodes.table).length
526
- countingElements.push(this.buildCountingElement('table-count', tableCount))
527
-
528
- const equationCount = this.getChildrenOfType(
529
- schema.nodes.equation_element
530
- ).length
531
- countingElements.push(
532
- this.buildCountingElement('equation-count', equationCount)
533
- )
448
+ }
534
449
 
535
- const referencesCount = this.getChildrenOfType(
536
- schema.nodes.bibliography_item
537
- ).length
538
- countingElements.push(
539
- this.buildCountingElement('ref-count', referencesCount)
540
- )
450
+ private buildCounts = () => {
451
+ const $counts = this.createElement('counts')
452
+ const counts = new Map([
453
+ ['fig-count', this.getChildrenOfType(schema.nodes.figure).length],
454
+ ['table-count', this.getChildrenOfType(schema.nodes.table).length],
455
+ [
456
+ 'equation-count',
457
+ this.getChildrenOfType(schema.nodes.equation_element).length,
458
+ ],
459
+ [
460
+ 'ref-count',
461
+ this.getChildrenOfType(schema.nodes.bibliography_item).length,
462
+ ],
463
+ ['word-count', this.manuscriptNode.textContent.split(/\s+/).length],
464
+ ])
541
465
 
542
- //todo: is this correct?
543
- const wordCount = this.manuscriptNode.textContent.split(/\s+/).length
544
- countingElements.push(this.buildCountingElement('word-count', wordCount))
466
+ counts.forEach((val, key) => {
467
+ if (val) {
468
+ $counts.appendChild(
469
+ this.createElement(key, undefined, { count: String(val) })
470
+ )
471
+ }
472
+ })
545
473
 
546
- countingElements = countingElements.filter((el) => el) as Array<HTMLElement>
547
- if (countingElements.length > 0) {
548
- const counts = this.createElement('counts')
549
- counts.append(...countingElements)
550
- articleMeta.append(counts)
474
+ if ($counts.childElementCount) {
475
+ return $counts
551
476
  }
477
+ }
552
478
 
553
- const attachments = this.getChildrenOfType(schema.nodes.attachment)
554
-
555
- attachments.forEach((attachment) => {
556
- const $selfUri = this.createElement('self-uri')
557
- $selfUri.setAttribute('content-type', attachment.attrs.type)
558
- $selfUri.setAttributeNS(XLINK_NAMESPACE, 'href', attachment.attrs.href)
559
- const insertBeforeElements = articleMeta.querySelector(
560
- 'related-article, related-object, abstract, trans-abstract, kwd-group, funding-group, support-group, conference, counts, custom-meta-group'
561
- )
562
- insertBeforeElements
563
- ? articleMeta.insertBefore($selfUri, insertBeforeElements)
564
- : articleMeta.appendChild($selfUri)
565
- })
566
-
567
- return front
479
+ private buildAwards = () => {
480
+ const awards = this.getFirstChildOfType(schema.nodes.awards)
481
+ if (!awards || !awards.childCount) {
482
+ return
483
+ }
484
+ return this.serializeNode(awards)
568
485
  }
569
486
 
487
+ private buildSelfUris = () =>
488
+ this.getChildrenOfType(schema.nodes.attachment).map((attachment) =>
489
+ this.serializeNode(attachment)
490
+ )
491
+
570
492
  protected buildDateElement = (timestamp: number, type: string) => {
571
- const dateElement = this.createElement('date')
493
+ const $date = this.createElement('date')
572
494
 
573
- dateElement.setAttribute('date-type', type)
495
+ $date.setAttribute('date-type', type)
574
496
 
575
497
  const date = new Date(timestamp * 1000) // s => ms
576
498
  const lookup = {
@@ -580,160 +502,192 @@ export class JATSExporter {
580
502
  }
581
503
 
582
504
  for (const [key, value] of Object.entries(lookup).reverse()) {
583
- const el = this.createElement(key)
584
- el.textContent = value
585
- dateElement.appendChild(el)
505
+ const $datePart = this.createElement(key, value)
506
+ $date.appendChild($datePart)
586
507
  }
587
508
 
588
- return dateElement
589
- }
590
- protected buildCountingElement = (
591
- tagName: string,
592
- count: number | undefined
593
- ) => {
594
- if (count) {
595
- const wordCount = this.createElement(tagName)
596
- wordCount.setAttribute('count', String(count))
597
- return wordCount
598
- }
509
+ return $date
599
510
  }
511
+
600
512
  protected buildBody = () => {
601
- const body = this.createElement('body')
602
- this.manuscriptNode.forEach((cFragment) => {
603
- const serializedNode = this.serializeNode(cFragment)
604
- body.append(...serializedNode.childNodes)
513
+ const body = this.getFirstChildOfType(schema.nodes.body)
514
+ const $body = this.createElement('body')
515
+ body?.forEach((node) => {
516
+ $body.appendChild(this.serializeNode(node))
605
517
  })
606
- this.fixBody(body)
518
+ return $body
519
+ }
607
520
 
608
- return body
521
+ protected buildBack = () => {
522
+ const backmatter = this.getFirstChildOfType(schema.nodes.backmatter)
523
+ const $back = this.createElement('back')
524
+ backmatter?.forEach((node) => {
525
+ $back.appendChild(this.serializeNode(node))
526
+ })
527
+ return $back
609
528
  }
610
529
 
611
- protected buildBack = (body: HTMLElement) => {
612
- const back = this.createElement('back')
613
- this.moveSectionsToBack(back, body)
530
+ private findSection = (category: string, ...$roots: Element[]) => {
531
+ for (const $root of $roots) {
532
+ const $match = $root.querySelector(`sec[sec-type="${category}"]`)
533
+ if ($match) {
534
+ return $match
535
+ }
536
+ }
537
+ }
614
538
 
615
- // footnotes elements in footnotes section (i.e. not in table footer)
616
- const footnotesElements = this.document.querySelectorAll('sec > fn-group')
539
+ private findSections = (category: string, ...$roots: Element[]) =>
540
+ $roots.flatMap(($root) =>
541
+ Array.from($root.querySelectorAll<Element>(`sec[sec-type="${category}"]`))
542
+ )
617
543
 
618
- for (const footnotesElement of footnotesElements) {
619
- // move fn-group from body to back
620
- const previousParent = footnotesElement.parentElement
621
- back.appendChild(footnotesElement)
544
+ private routeBackmatterSections = ($body: Element, $back: Element) => {
545
+ const $footnoteSections = FOOTNOTE_SECTION_CATEGORY_IDS.flatMap(
546
+ (category) =>
547
+ this.findSections(category, $body, $back).map(($section) => ({
548
+ $section,
549
+ category,
550
+ }))
551
+ )
552
+ let $fnGroup: Element | undefined
553
+ if ($footnoteSections.length) {
554
+ $fnGroup = this.createElement('fn-group')
555
+ for (const { $section, category } of $footnoteSections) {
556
+ $fnGroup.appendChild(this.sectionToFootnote($section, category))
557
+ }
558
+ }
622
559
 
623
- if (previousParent) {
624
- const title = previousParent.querySelector('title')
625
- if (title) {
626
- footnotesElement.insertBefore(
627
- title,
628
- footnotesElement.firstElementChild
629
- )
630
- }
631
- if (!previousParent.childElementCount) {
632
- previousParent.remove()
633
- }
560
+ const $appendices = this.findSections('appendices', $body, $back)
561
+ let $appGroup: Element | undefined
562
+ if ($appendices.length) {
563
+ $appGroup = this.createElement('app-group')
564
+ for (const $section of $appendices) {
565
+ const $app = this.createElement('app')
566
+ $app.appendChild($section)
567
+ $appGroup.appendChild($app)
634
568
  }
635
569
  }
636
- // bibliography element
637
- let refList = this.document.querySelector('ref-list')
638
- if (!refList) {
639
- refList = this.createElement('ref-list')
570
+
571
+ const $ackSection = this.findSection('acknowledgements', $body, $back)
572
+ let $ack: Element | undefined
573
+ if ($ackSection) {
574
+ $ack = this.createElement('ack')
575
+ $ack.append(...$ackSection.childNodes)
576
+ $ackSection.remove()
640
577
  }
641
578
 
642
- // move ref-list from body to back
643
- back.appendChild(refList)
644
- const parser = new DOMParser()
645
- //eslint-disable-next-line @typescript-eslint/no-unused-vars
646
- const [_, bibliography] = this.engine.makeBibliography()
647
-
648
- for (let i = 0; i < bibliography.length; i++) {
649
- const item = `<template xmlns:xlink="${XLINK_NAMESPACE}">${sanitizeXmlString(bibliography[i])}</template>`
650
- const ref = parser.parseFromString(item, 'text/xml').querySelector('ref')
651
- if (ref) {
652
- refList.appendChild(ref)
653
- }
579
+ const $availabilitySection = this.findSection('availability', $body, $back)
580
+ const $ethicsSection = this.findSection('ethics-statement', $body, $back)
581
+
582
+ const $prepend = [
583
+ $appGroup,
584
+ $ack,
585
+ $availabilitySection,
586
+ $ethicsSection,
587
+ $fnGroup,
588
+ ].filter((el): el is Element => el != null)
589
+
590
+ if ($prepend.length) {
591
+ $back.prepend(...$prepend)
592
+ }
593
+ }
594
+
595
+ private extractCoiStatement = ($back: Element) => {
596
+ const $coiStatement = $back.querySelector<Element>(
597
+ 'fn[fn-type="coi-statement"]'
598
+ )
599
+ if (!$coiStatement) {
600
+ return undefined
601
+ }
602
+
603
+ const $fnGroup = $coiStatement.parentElement
604
+ $coiStatement.remove()
605
+
606
+ if ($fnGroup && !$fnGroup.hasChildNodes()) {
607
+ $fnGroup.remove()
654
608
  }
655
609
 
656
- return back
610
+ return $coiStatement
611
+ }
612
+
613
+ private buildRefList = () => {
614
+ const $refList = this.createElement('ref-list')
615
+ const [, bibliography] = this.engine.makeBibliography()
616
+ const parser = new DOMParser()
617
+ bibliography.forEach((item) => {
618
+ const fragment = `<template xmlns:xlink="${XLINK_NAMESPACE}">${sanitizeXmlString(
619
+ item
620
+ )}</template>`
621
+ const $ref = parser
622
+ .parseFromString(fragment, 'text/xml')
623
+ .querySelector('ref')
624
+ if ($ref) {
625
+ $refList.appendChild($ref)
626
+ }
627
+ })
628
+ return $refList
657
629
  }
658
630
 
659
- //@TODO: part of the export cleanup: check if we can use this elsewhere, maybe we can use strategy pattern for each element to have its own creator.
660
631
  private createElement = (
661
632
  tag: string,
662
633
  content?: string,
663
634
  attrs?: Record<string, string | undefined>
664
635
  ) => {
665
- const el = this.document.createElement(tag)
636
+ const $element = this.document.createElement(tag)
666
637
  if (content) {
667
- el.textContent = content
638
+ $element.textContent = content
668
639
  }
669
640
  if (attrs) {
670
641
  Object.entries(attrs).forEach(([k, v]) => {
671
642
  if (v) {
672
- el.setAttribute(k, v)
643
+ $element.setAttribute(k, v)
673
644
  }
674
645
  })
675
646
  }
676
- return el
647
+ return $element
677
648
  }
678
649
 
679
650
  private appendElement = (
680
- parent: HTMLElement,
651
+ $parent: Element,
681
652
  tag: string,
682
653
  content?: string,
683
654
  attrs?: Record<string, string | undefined>
684
655
  ) => {
685
- const el = this.createElement(tag, content, attrs)
686
- parent.appendChild(el)
687
- return el
656
+ const $element = this.createElement(tag, content, attrs)
657
+ $parent.appendChild($element)
658
+ return $element
688
659
  }
689
660
 
690
661
  protected createSerializer = () => {
691
662
  const nodes: NodeSpecs = {
692
- trans_abstract: (node) => {
693
- const attrs: { [key: string]: string } = {
694
- id: normalizeID(node.attrs.id),
695
- }
696
- if (node.attrs.lang) {
697
- attrs[`${XML_NAMESPACE} lang`] = node.attrs.lang
698
- }
699
- if (node.attrs.category) {
700
- attrs['sec-type'] = node.attrs.category
701
- }
702
- return ['trans-abstract', attrs, 0]
703
- },
704
- trans_graphical_abstract: (node) => {
705
- const attrs: { [key: string]: string } = {
706
- id: normalizeID(node.attrs.id),
707
- }
708
- if (node.attrs.lang) {
709
- attrs[`${XML_NAMESPACE} lang`] = node.attrs.lang
710
- }
711
- if (node.attrs.category) {
712
- attrs['sec-type'] = node.attrs.category
713
- }
714
- return ['trans-abstract', attrs, 0]
715
- },
663
+ trans_abstract: (node) => createTransAbstract(node),
664
+ trans_graphical_abstract: (node) => createTransAbstract(node),
716
665
  hero_image: () => '',
717
666
  headshot_grid: () => ['p', { 'content-type': 'headshots' }, 0],
718
667
  headshot_element: (node) => createImage(node),
719
668
  headshot_image: () => '',
720
669
  alt_text: (node) => {
721
670
  if (node.textContent) {
722
- const altText = this.createElement('alt-text')
723
- altText.textContent = node.textContent
724
- return altText
671
+ const $altText = this.createElement('alt-text')
672
+ $altText.textContent = node.textContent
673
+ return $altText
725
674
  }
726
675
  return ''
727
676
  },
728
677
  long_desc: (node) => {
729
678
  if (node.textContent) {
730
- const longDesc = this.createElement('long-desc')
731
- longDesc.textContent = node.textContent
732
- return longDesc
679
+ const $longDesc = this.createElement('long-desc')
680
+ $longDesc.textContent = node.textContent
681
+ return $longDesc
733
682
  }
734
683
  return ''
735
684
  },
736
- attachment: () => '',
685
+ attachment: (node) => {
686
+ const $selfUri = this.createElement('self-uri')
687
+ $selfUri.setAttribute('content-type', node.attrs.type)
688
+ $selfUri.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href)
689
+ return $selfUri
690
+ },
737
691
  attachments: () => '',
738
692
  image_element: (node) => createImage(node),
739
693
  embed: (node) => {
@@ -741,52 +695,56 @@ export class JATSExporter {
741
695
  if (!href) {
742
696
  return ''
743
697
  }
744
- const mediaElement = this.createElement('media')
745
- mediaElement.setAttribute('id', normalizeID(id))
746
- mediaElement.setAttributeNS(XLINK_NAMESPACE, 'show', 'embed')
747
- mediaElement.setAttributeNS(XLINK_NAMESPACE, 'href', href)
698
+ const $media = this.createElement('media')
699
+ $media.setAttribute('id', normalizeID(id))
700
+ $media.setAttributeNS(XLINK_NAMESPACE, 'show', 'embed')
701
+ $media.setAttributeNS(XLINK_NAMESPACE, 'href', href)
748
702
  if (mimetype) {
749
- mediaElement.setAttribute('mimetype', node.attrs.mimetype)
703
+ $media.setAttribute('mimetype', node.attrs.mimetype)
750
704
  }
751
705
  if (mimeSubtype) {
752
- mediaElement.setAttribute('mime-subtype', node.attrs.mimeSubtype)
706
+ $media.setAttribute('mime-subtype', node.attrs.mimeSubtype)
753
707
  }
754
- appendLabels(mediaElement, node)
755
- this.appendChildNodeOfType(mediaElement, node, schema.nodes.alt_text)
756
- this.appendChildNodeOfType(mediaElement, node, schema.nodes.long_desc)
757
- this.appendCaption(mediaElement, node)
758
- return mediaElement
708
+ appendLabels($media, node)
709
+ this.appendChildNodeOfType($media, node, schema.nodes.alt_text)
710
+ this.appendChildNodeOfType($media, node, schema.nodes.long_desc)
711
+ this.appendCaption($media, node)
712
+ return $media
759
713
  },
760
714
  awards: () => ['funding-group', 0],
761
715
  award: (node) => {
762
716
  const awardGroup = node as AwardNode
763
- const awardGroupElement = this.createElement('award-group')
764
- awardGroupElement.setAttribute('id', normalizeID(awardGroup.attrs.id))
717
+ const $awardGroup = this.createElement('award-group')
718
+ $awardGroup.setAttribute('id', normalizeID(awardGroup.attrs.id))
765
719
  appendChildIfPresent(
766
- awardGroupElement,
720
+ $awardGroup,
767
721
  'funding-source',
768
722
  awardGroup.attrs.source
769
723
  )
770
724
  awardGroup.attrs.code
771
725
  ?.split(';')
772
726
  .forEach((code) =>
773
- appendChildIfPresent(awardGroupElement, 'award-id', code)
727
+ appendChildIfPresent($awardGroup, 'award-id', code)
774
728
  )
775
729
  appendChildIfPresent(
776
- awardGroupElement,
730
+ $awardGroup,
777
731
  'principal-award-recipient',
778
732
  awardGroup.attrs.recipient
779
733
  )
780
734
 
781
- return awardGroupElement
735
+ return $awardGroup
782
736
  },
783
737
  box_element: (node) => createBoxElement(node),
784
738
  author_notes: () => '',
785
739
  corresp: () => '',
786
- title: () => '',
787
- alt_title: () => '',
740
+ title: () => ['article-title', 0],
741
+ alt_title: (node) => [
742
+ 'alt-title',
743
+ { 'alt-title-type': node.attrs.type },
744
+ 0,
745
+ ],
788
746
  alt_titles: () => '',
789
- subtitle: () => '',
747
+ subtitle: () => ['subtitle', 0],
790
748
  subtitles: () => '',
791
749
  text_block: (node) => nodes.paragraph(node),
792
750
  affiliations: () => '',
@@ -795,17 +753,45 @@ export class JATSExporter {
795
753
  node.childCount == 0
796
754
  ? ['table-wrap-foot', ['fn-group', ['fn', ['p']]]]
797
755
  : ['table-wrap-foot', 0],
798
- contributor: () => '',
799
- affiliation: () => '',
756
+ contributor: (node) =>
757
+ this.buildContributorElement(node as ContributorNode),
758
+ affiliation: (node) =>
759
+ this.buildAffiliationElement(node as AffiliationNode),
800
760
  attribution: () => ['attrib', 0],
801
761
  bibliography_element: () => '',
802
762
  bibliography_item: () => '',
803
763
  comments: () => '',
804
- keyword_group: () => '',
764
+ keyword_group: (node) => {
765
+ const attrs: { [key: string]: string } = {}
766
+ if (node.attrs.type) {
767
+ attrs['kwd-group-type'] = node.attrs.type
768
+ }
769
+ return ['kwd-group', attrs, 0]
770
+ },
805
771
  body: () => ['body', 0],
806
772
  abstracts: () => ['abstract', 0],
807
773
  backmatter: () => ['backmatter', 0],
808
- supplement: () => '',
774
+ supplement: (node) => {
775
+ const $supplementaryMaterial = this.createElement(
776
+ 'supplementary-material'
777
+ )
778
+ $supplementaryMaterial.setAttribute('id', normalizeID(node.attrs.id))
779
+ $supplementaryMaterial.setAttributeNS(
780
+ XLINK_NAMESPACE,
781
+ 'href',
782
+ node.attrs.href ?? ''
783
+ )
784
+ $supplementaryMaterial.setAttribute(
785
+ 'mimetype',
786
+ node.attrs.mimeType ?? ''
787
+ )
788
+ $supplementaryMaterial.setAttribute(
789
+ 'mime-subtype',
790
+ node.attrs.mimeSubType ?? ''
791
+ )
792
+ this.appendCaption($supplementaryMaterial, node)
793
+ return $supplementaryMaterial
794
+ },
809
795
  supplements: () => '',
810
796
  bibliography_section: () => '',
811
797
  blockquote_element: () => ['disp-quote', { 'content-type': 'quote' }, 0],
@@ -830,14 +816,14 @@ export class JATSExporter {
830
816
  return ''
831
817
  }
832
818
 
833
- const xref = this.createElement('xref')
834
- xref.setAttribute('ref-type', 'bibr')
835
- xref.setAttribute('rid', normalizeID(rids.join(' ')))
819
+ const $xref = this.createElement('xref')
820
+ $xref.setAttribute('ref-type', 'bibr')
821
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')))
836
822
  const fragment = this.renderedCitations.get(node.attrs.id)
837
823
  if (fragment) {
838
- xref.innerHTML = fragment
824
+ $xref.innerHTML = fragment
839
825
  }
840
- return xref
826
+ return $xref
841
827
  },
842
828
  cross_reference: (node) => {
843
829
  const cross = node as CrossReferenceNode
@@ -857,43 +843,44 @@ export class JATSExporter {
857
843
  return text ?? ''
858
844
  }
859
845
 
860
- const xref = this.createElement('xref')
846
+ const $xref = this.createElement('xref')
861
847
 
862
848
  const type = chooseRefType(target.type)
863
849
  if (type) {
864
- xref.setAttribute('ref-type', type)
850
+ $xref.setAttribute('ref-type', type)
865
851
  }
866
852
 
867
- xref.setAttribute('rid', normalizeID(rids.join(' ')))
868
- xref.textContent = text ?? ''
853
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')))
854
+ $xref.textContent = text ?? ''
869
855
 
870
- return xref
856
+ return $xref
871
857
  },
872
858
  doc: () => '',
873
859
  equation: (node) => {
874
860
  return node.attrs.contents ? this.createEquation(node) : ''
875
861
  },
876
862
  general_table_footnote: (node) => {
877
- const el = this.createElement('general-table-footnote')
878
- el.setAttribute('id', normalizeID(node.attrs.id))
879
- processChildNodes(el, node, schema.nodes.general_table_footnote)
880
- return el
863
+ const $fragment = this.document.createDocumentFragment()
864
+ node.forEach((child) => {
865
+ $fragment.appendChild(this.serializeNode(child))
866
+ })
867
+ return $fragment
881
868
  },
882
869
  inline_equation: (node) => {
883
870
  if (!node.attrs.contents) {
884
871
  return ''
885
872
  }
886
- const eqElement = this.createElement('inline-formula')
873
+ const $inlineFormula = this.createElement('inline-formula')
887
874
  const equation = this.createEquation(node, true)
888
- eqElement.append(equation)
889
- return eqElement
875
+ $inlineFormula.append(equation)
876
+ return $inlineFormula
890
877
  },
891
878
  equation_element: (node) => {
892
- const eqElement = this.createElement('disp-formula')
893
- eqElement.setAttribute('id', normalizeID(node.attrs.id))
894
- appendLabels(eqElement, node)
895
- processChildNodes(eqElement, node, schema.nodes.equation)
896
- return eqElement
879
+ const $dispFormula = this.createElement('disp-formula')
880
+ $dispFormula.setAttribute('id', normalizeID(node.attrs.id))
881
+ appendLabels($dispFormula, node)
882
+ processChildNodes($dispFormula, node, schema.nodes.equation)
883
+ return $dispFormula
897
884
  },
898
885
  figure: (node) => createGraphic(node),
899
886
  figure_element: (node) =>
@@ -914,26 +901,44 @@ export class JATSExporter {
914
901
  ? ['fn-group', { id: normalizeID(node.attrs.id) }, ['fn', ['p']]]
915
902
  : ['fn-group', { id: normalizeID(node.attrs.id) }, 0],
916
903
  footnotes_section: (node) => {
917
- const attrs: { [key: string]: string } = {
904
+ const $fnGroup = this.createElement('fn-group', undefined, {
918
905
  id: normalizeID(node.attrs.id),
919
- 'sec-type': 'endnotes', // chooseSecType(node.attrs.category),
906
+ })
907
+ const titleNode = this.getFirstChildOfType(
908
+ schema.nodes.section_title,
909
+ node
910
+ )
911
+ if (titleNode) {
912
+ $fnGroup.appendChild(this.serializeNode(titleNode))
920
913
  }
921
-
922
- return ['sec', attrs, 0]
914
+ const footnotesNode = this.getFirstChildOfType(
915
+ schema.nodes.footnotes_element,
916
+ node
917
+ )
918
+ if (footnotesNode?.childCount) {
919
+ footnotesNode.forEach((footnote) => {
920
+ $fnGroup.appendChild(this.serializeNode(footnote))
921
+ })
922
+ } else {
923
+ const $fn = this.createElement('fn')
924
+ $fn.appendChild(this.createElement('p'))
925
+ $fnGroup.appendChild($fn)
926
+ }
927
+ return $fnGroup
923
928
  },
924
929
  hard_break: () => '',
925
930
  highlight_marker: () => '',
926
931
  inline_footnote: (node) => {
927
932
  const rids: string[] = node.attrs.rids
928
- const xref = this.createElement('xref')
929
- xref.setAttribute('ref-type', 'fn')
930
- xref.setAttribute('rid', normalizeID(rids.join(' ')))
931
- xref.textContent = rids
933
+ const $xref = this.createElement('xref')
934
+ $xref.setAttribute('ref-type', 'fn')
935
+ $xref.setAttribute('rid', normalizeID(rids.join(' ')))
936
+ $xref.textContent = rids
932
937
  .map((rid) => this.footnoteLabels.get(rid))
933
938
  .join(', ')
934
- return xref
939
+ return $xref
935
940
  },
936
- keyword: () => '',
941
+ keyword: () => ['kwd', 0],
937
942
  keywords_element: () => '',
938
943
  keywords: () => '',
939
944
  link: (node) => {
@@ -947,38 +952,38 @@ export class JATSExporter {
947
952
  return text
948
953
  }
949
954
 
950
- const linkNode = this.createElement('ext-link')
951
- linkNode.setAttribute('ext-link-type', 'uri')
952
- linkNode.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href)
953
- linkNode.textContent = text
955
+ const $extLink = this.createElement('ext-link')
956
+ $extLink.setAttribute('ext-link-type', 'uri')
957
+ $extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href)
958
+ $extLink.textContent = text
954
959
 
955
960
  if (node.attrs.title) {
956
- linkNode.setAttributeNS(
961
+ $extLink.setAttributeNS(
957
962
  XLINK_NAMESPACE,
958
963
  'xlink:title',
959
964
  node.attrs.title
960
965
  )
961
966
  }
962
967
 
963
- return linkNode
968
+ return $extLink
964
969
  },
965
970
  list_item: () => ['list-item', 0],
966
971
  listing: (node) => {
967
- const code = this.createElement('code')
968
- code.setAttribute('id', normalizeID(node.attrs.id))
969
- code.setAttribute('language', node.attrs.languageKey)
970
- code.textContent = node.attrs.contents
972
+ const $code = this.createElement('code')
973
+ $code.setAttribute('id', normalizeID(node.attrs.id))
974
+ $code.setAttribute('language', node.attrs.languageKey)
975
+ $code.textContent = node.attrs.contents
971
976
 
972
- return code
977
+ return $code
973
978
  },
974
979
  listing_element: (node) =>
975
980
  createFigureElement(node, node.type.schema.nodes.listing),
976
981
  manuscript: (node) => ['article', { id: normalizeID(node.attrs.id) }, 0],
977
982
  missing_figure: () => {
978
- const graphic = this.createElement('graphic')
979
- graphic.setAttribute('specific-use', 'MISSING')
980
- graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', '')
981
- return graphic
983
+ const $graphic = this.createElement('graphic')
984
+ $graphic.setAttribute('specific-use', 'MISSING')
985
+ $graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', '')
986
+ return $graphic
982
987
  },
983
988
  paragraph: (node) => {
984
989
  if (!node.childCount) {
@@ -1017,15 +1022,7 @@ export class JATSExporter {
1017
1022
  }
1018
1023
  return ''
1019
1024
  },
1020
- graphical_abstract_section: (node) => {
1021
- const attrs: { [key: string]: string } = {
1022
- id: normalizeID(node.attrs.id),
1023
- }
1024
- if (node.attrs.category) {
1025
- attrs['sec-type'] = node.attrs.category
1026
- }
1027
- return ['sec', attrs, 0]
1028
- },
1025
+ graphical_abstract_section: (node) => createAbstract(node),
1029
1026
  section: (node) => {
1030
1027
  const attrs: { [key: string]: string } = {
1031
1028
  id: normalizeID(node.attrs.id),
@@ -1037,17 +1034,18 @@ export class JATSExporter {
1037
1034
 
1038
1035
  return ['sec', attrs, 0]
1039
1036
  },
1037
+ abstract: (node) => createAbstract(node),
1040
1038
  section_label: () => ['label', 0],
1041
1039
  section_title: () => ['title', 0],
1042
1040
  section_title_plain: () => ['title', 0],
1043
1041
  table: (node) => ['table', { id: normalizeID(node.attrs.id) }, 0],
1044
1042
  table_element: (node) => {
1045
- const element = createTableElement(node)
1046
- element.setAttribute('position', 'anchor')
1043
+ const $tableWrap = createTableElement(node)
1044
+ $tableWrap.setAttribute('position', 'anchor')
1047
1045
  if (node.attrs.type) {
1048
- element.setAttribute('content-type', node.attrs.type)
1046
+ $tableWrap.setAttribute('content-type', node.attrs.type)
1049
1047
  }
1050
- return element
1048
+ return $tableWrap
1051
1049
  },
1052
1050
  table_cell: (node) => [
1053
1051
  'td',
@@ -1097,60 +1095,60 @@ export class JATSExporter {
1097
1095
 
1098
1096
  this.serializer = new DOMSerializer(nodes, marks)
1099
1097
  const appendChildIfPresent = (
1100
- parent: Element,
1098
+ $parent: Element,
1101
1099
  tagName: string,
1102
1100
  textContent: string
1103
1101
  ) => {
1104
1102
  if (!textContent) {
1105
1103
  return
1106
1104
  }
1107
- const element = this.createElement(tagName)
1108
- element.textContent = textContent
1109
- parent.appendChild(element)
1105
+ const $element = this.createElement(tagName)
1106
+ $element.textContent = textContent
1107
+ $parent.appendChild($element)
1110
1108
  }
1111
1109
  const processChildNodes = (
1112
- element: HTMLElement,
1110
+ $element: Element,
1113
1111
  node: ManuscriptNode,
1114
1112
  contentNodeType: ManuscriptNodeType
1115
1113
  ) => {
1116
1114
  node.forEach((childNode) => {
1117
1115
  if (childNode.type === contentNodeType) {
1118
1116
  if (childNode.attrs.id) {
1119
- element.appendChild(this.serializeNode(childNode))
1117
+ $element.appendChild(this.serializeNode(childNode))
1120
1118
  }
1121
1119
  } else if (childNode.type === node.type.schema.nodes.paragraph) {
1122
- element.appendChild(this.serializeNode(childNode))
1120
+ $element.appendChild(this.serializeNode(childNode))
1123
1121
  } else if (childNode.type === node.type.schema.nodes.missing_figure) {
1124
- element.appendChild(this.serializeNode(childNode))
1122
+ $element.appendChild(this.serializeNode(childNode))
1125
1123
  }
1126
1124
  })
1127
1125
  }
1128
1126
  const createElement = (node: ManuscriptNode, nodeName: string) => {
1129
- const element = this.createElement(nodeName)
1130
- element.setAttribute('id', normalizeID(node.attrs.id))
1131
- return element
1127
+ const $element = this.createElement(nodeName)
1128
+ $element.setAttribute('id', normalizeID(node.attrs.id))
1129
+ return $element
1132
1130
  }
1133
1131
 
1134
- const appendLabels = (element: HTMLElement, node: ManuscriptNode) => {
1132
+ const appendLabels = ($element: Element, node: ManuscriptNode) => {
1135
1133
  if (this.labelTargets) {
1136
1134
  const target = this.labelTargets.get(node.attrs.id)
1137
1135
 
1138
1136
  if (target) {
1139
- const label = this.createElement('label')
1140
- label.textContent = target.label
1141
- element.appendChild(label)
1137
+ const $label = this.createElement('label')
1138
+ $label.textContent = target.label
1139
+ $element.appendChild($label)
1142
1140
  }
1143
1141
  }
1144
1142
  }
1145
- const appendAttributions = (element: HTMLElement, node: ManuscriptNode) => {
1143
+ const appendAttributions = ($element: Element, node: ManuscriptNode) => {
1146
1144
  if (node.attrs.attribution) {
1147
- const attribution = this.createElement('attrib')
1148
- attribution.textContent = node.attrs.attribution.literal
1149
- element.appendChild(attribution)
1145
+ const $attrib = this.createElement('attrib')
1146
+ $attrib.textContent = node.attrs.attribution.literal
1147
+ $element.appendChild($attrib)
1150
1148
  }
1151
1149
  }
1152
1150
 
1153
- const appendTable = (element: HTMLElement, node: ManuscriptNode) => {
1151
+ const appendTable = ($element: Element, node: ManuscriptNode) => {
1154
1152
  const tableNode = this.getFirstChildOfType(schema.nodes.table, node)
1155
1153
  const colGroupNode = this.getFirstChildOfType(
1156
1154
  schema.nodes.table_colgroup,
@@ -1159,42 +1157,61 @@ export class JATSExporter {
1159
1157
  if (!tableNode) {
1160
1158
  return
1161
1159
  }
1162
- const table = this.serializeNode(tableNode)
1163
- const tbodyElement = this.createElement('tbody')
1160
+ const $table = this.serializeNode(tableNode)
1161
+ const $tbody = this.createElement('tbody')
1164
1162
 
1165
- while (table.firstChild) {
1166
- const child = table.firstChild
1167
- table.removeChild(child)
1168
- tbodyElement.appendChild(child)
1163
+ while ($table.firstChild) {
1164
+ const $child = $table.firstChild
1165
+ $table.removeChild($child)
1166
+ $tbody.appendChild($child)
1169
1167
  }
1170
- table.appendChild(tbodyElement)
1168
+ $table.appendChild($tbody)
1169
+ this.normalizeTable($table)
1171
1170
  if (colGroupNode) {
1172
- const colGroup = this.serializeNode(colGroupNode)
1173
- table.insertBefore(colGroup, table.firstChild)
1171
+ const $colGroup = this.serializeNode(colGroupNode)
1172
+ $table.insertBefore($colGroup, $table.firstChild)
1174
1173
  }
1175
1174
 
1176
- element.appendChild(table)
1175
+ $element.appendChild($table)
1177
1176
  }
1178
1177
  const createBoxElement = (node: ManuscriptNode) => {
1179
- const element = createElement(node, 'boxed-text')
1178
+ const $boxedText = createElement(node, 'boxed-text')
1180
1179
  if (node.attrs.type) {
1181
- element.setAttribute('content-type', node.attrs.type)
1180
+ $boxedText.setAttribute('content-type', node.attrs.type)
1182
1181
  }
1183
- appendLabels(element, node)
1182
+ appendLabels($boxedText, node)
1184
1183
  const child = node.firstChild
1185
1184
  if (child?.type === schema.nodes.caption_title) {
1186
- this.appendCaption(element, node)
1185
+ this.appendCaption($boxedText, node)
1187
1186
  }
1188
1187
 
1189
- processChildNodes(element, node, node.type.schema.nodes.section)
1190
- return element
1188
+ processChildNodes($boxedText, node, node.type.schema.nodes.section)
1189
+ return $boxedText
1191
1190
  }
1192
1191
 
1192
+ const abstractTypeAttrs = (category: string) =>
1193
+ category && category !== 'abstract' ? { 'abstract-type': category } : {}
1194
+
1195
+ const createAbstract = (node: ManuscriptNode): DOMOutputSpec => [
1196
+ 'abstract',
1197
+ abstractTypeAttrs(node.attrs.category),
1198
+ 0,
1199
+ ]
1200
+
1201
+ const createTransAbstract = (node: ManuscriptNode): DOMOutputSpec => [
1202
+ 'trans-abstract',
1203
+ {
1204
+ [`${XML_NAMESPACE} lang`]: node.attrs.lang ?? '',
1205
+ ...abstractTypeAttrs(node.attrs.category),
1206
+ },
1207
+ 0,
1208
+ ]
1209
+
1193
1210
  const isChildOfNodeType = (
1194
1211
  targetID: string,
1195
1212
  type: NodeType,
1196
1213
  descend = false
1197
- ): boolean => {
1214
+ ) => {
1198
1215
  const nodes = this.getChildrenOfType(type)
1199
1216
  return nodes.some((node) => {
1200
1217
  const result = findChildrenByAttr(
@@ -1219,116 +1236,116 @@ export class JATSExporter {
1219
1236
  if (!graphicNode) {
1220
1237
  return ''
1221
1238
  }
1222
- const graphicElement = createGraphic(graphicNode)
1239
+ const $graphic = createGraphic(graphicNode)
1223
1240
  if (node.attrs.extLink) {
1224
- const extLink = this.appendElement(graphicElement, 'ext-link')
1225
- extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.extLink)
1241
+ const $extLink = this.appendElement($graphic, 'ext-link')
1242
+ $extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.extLink)
1226
1243
  }
1227
- this.appendCaption(graphicElement, node)
1228
- this.appendChildNodeOfType(graphicElement, node, schema.nodes.alt_text)
1229
- this.appendChildNodeOfType(graphicElement, node, schema.nodes.long_desc)
1230
- return graphicElement
1244
+ this.appendCaption($graphic, node)
1245
+ this.appendChildNodeOfType($graphic, node, schema.nodes.alt_text)
1246
+ this.appendChildNodeOfType($graphic, node, schema.nodes.long_desc)
1247
+ return $graphic
1231
1248
  }
1232
1249
 
1233
1250
  const createGraphic = (node: ManuscriptNode) => {
1234
- const graphic = this.createElement('graphic')
1235
- graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', node.attrs.src)
1251
+ const $graphic = this.createElement('graphic')
1252
+ $graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', node.attrs.src)
1236
1253
 
1237
1254
  const hero = findParentHeroImage(node.attrs.id)
1238
1255
  if (hero) {
1239
- graphic.setAttribute('content-type', hero.attrs.type || 'leading')
1256
+ $graphic.setAttribute('content-type', hero.attrs.type || 'leading')
1240
1257
  } else if (
1241
1258
  !isChildOfNodeType(node.attrs.id, schema.nodes.figure_element) &&
1242
1259
  node.attrs.type
1243
1260
  ) {
1244
- graphic.setAttribute('content-type', node.attrs.type)
1261
+ $graphic.setAttribute('content-type', node.attrs.type)
1245
1262
  }
1246
- return graphic
1263
+ return $graphic
1247
1264
  }
1248
1265
  const createFigureElement = (
1249
1266
  node: ManuscriptNode,
1250
1267
  contentNodeType: ManuscriptNodeType
1251
1268
  ) => {
1252
- const element = createElement(node, 'fig')
1269
+ const $fig = createElement(node, 'fig')
1253
1270
  const figNode = this.getFirstChildOfType(schema.nodes.figure, node)
1254
1271
  const figType = figNode?.attrs.type
1255
1272
  if (figType) {
1256
- element.setAttribute('fig-type', figType)
1273
+ $fig.setAttribute('fig-type', figType)
1257
1274
  }
1258
- appendLabels(element, node)
1259
- this.appendCaption(element, node)
1260
- this.appendChildNodeOfType(element, node, schema.nodes.alt_text)
1261
- this.appendChildNodeOfType(element, node, schema.nodes.long_desc)
1275
+ appendLabels($fig, node)
1276
+ this.appendCaption($fig, node)
1277
+ this.appendChildNodeOfType($fig, node, schema.nodes.alt_text)
1278
+ this.appendChildNodeOfType($fig, node, schema.nodes.long_desc)
1262
1279
  this.appendChildNodeOfType(
1263
- element,
1280
+ $fig,
1264
1281
  node,
1265
1282
  node.type.schema.nodes.footnotes_element
1266
1283
  )
1267
- processChildNodes(element, node, contentNodeType)
1268
- appendAttributions(element, node)
1284
+ processChildNodes($fig, node, contentNodeType)
1285
+ appendAttributions($fig, node)
1269
1286
  if (isExecutableNodeType(node.type)) {
1270
- processExecutableNode(node, element)
1287
+ processExecutableNode(node, $fig)
1271
1288
  }
1272
- moveAltTextAndLongDescToGraphics(element)
1273
- return element
1289
+ moveAltTextAndLongDescToGraphics($fig)
1290
+ return $fig
1274
1291
  }
1275
1292
 
1276
- const moveAltTextAndLongDescToGraphics = (element: Element) => {
1277
- const altText = element.querySelector('alt-text')
1278
- const longDesc = element.querySelector('long-desc')
1279
- const graphics = element.querySelectorAll('graphic')
1293
+ const moveAltTextAndLongDescToGraphics = ($element: Element) => {
1294
+ const $altText = $element.querySelector('alt-text')
1295
+ const $longDesc = $element.querySelector('long-desc')
1296
+ const $graphics = $element.querySelectorAll('graphic')
1280
1297
 
1281
- if (graphics.length === 0) {
1298
+ if ($graphics.length === 0) {
1282
1299
  return
1283
1300
  }
1284
1301
 
1285
- graphics.forEach((graphic) => {
1286
- if (longDesc) {
1287
- graphic.prepend(longDesc.cloneNode(true))
1302
+ $graphics.forEach(($graphic) => {
1303
+ if ($longDesc) {
1304
+ $graphic.prepend($longDesc.cloneNode(true))
1288
1305
  }
1289
- if (altText) {
1290
- graphic.prepend(altText.cloneNode(true))
1306
+ if ($altText) {
1307
+ $graphic.prepend($altText.cloneNode(true))
1291
1308
  }
1292
1309
  })
1293
1310
 
1294
- altText?.remove()
1295
- longDesc?.remove()
1311
+ $altText?.remove()
1312
+ $longDesc?.remove()
1296
1313
  }
1297
1314
 
1298
1315
  const createTableElement = (node: ManuscriptNode) => {
1299
1316
  const nodeName = 'table-wrap'
1300
- const element = createElement(node, nodeName)
1301
- appendLabels(element, node)
1302
- this.appendCaption(element, node)
1303
- this.appendChildNodeOfType(element, node, schema.nodes.alt_text)
1304
- this.appendChildNodeOfType(element, node, schema.nodes.long_desc)
1305
- appendTable(element, node)
1317
+ const $tableWrap = createElement(node, nodeName)
1318
+ appendLabels($tableWrap, node)
1319
+ this.appendCaption($tableWrap, node)
1320
+ this.appendChildNodeOfType($tableWrap, node, schema.nodes.alt_text)
1321
+ this.appendChildNodeOfType($tableWrap, node, schema.nodes.long_desc)
1322
+ appendTable($tableWrap, node)
1306
1323
  this.appendChildNodeOfType(
1307
- element,
1324
+ $tableWrap,
1308
1325
  node,
1309
1326
  node.type.schema.nodes.table_element_footer
1310
1327
  )
1311
1328
  if (isExecutableNodeType(node.type)) {
1312
- processExecutableNode(node, element)
1329
+ processExecutableNode(node, $tableWrap)
1313
1330
  }
1314
- return element
1331
+ return $tableWrap
1315
1332
  }
1316
- const processExecutableNode = (node: ManuscriptNode, element: Element) => {
1333
+ const processExecutableNode = (node: ManuscriptNode, $element: Element) => {
1317
1334
  const listingNode = this.getFirstChildOfType(schema.nodes.listing, node)
1318
1335
 
1319
1336
  if (listingNode) {
1320
1337
  const { contents, languageKey } = listingNode.attrs
1321
1338
 
1322
1339
  if (contents && languageKey) {
1323
- const listing = this.createElement('fig')
1324
- listing.setAttribute('specific-use', 'source')
1325
- element.appendChild(listing)
1326
-
1327
- const code = this.createElement('code')
1328
- code.setAttribute('executable', 'true')
1329
- code.setAttribute('language', languageKey)
1330
- code.textContent = contents
1331
- listing.appendChild(code)
1340
+ const $fig = this.createElement('fig')
1341
+ $fig.setAttribute('specific-use', 'source')
1342
+ $element.appendChild($fig)
1343
+
1344
+ const $code = this.createElement('code')
1345
+ $code.setAttribute('executable', 'true')
1346
+ $code.setAttribute('language', languageKey)
1347
+ $code.textContent = contents
1348
+ $fig.appendChild($code)
1332
1349
  }
1333
1350
  }
1334
1351
  }
@@ -1336,22 +1353,22 @@ export class JATSExporter {
1336
1353
 
1337
1354
  private createEquation(node: ManuscriptNode, isInline = false) {
1338
1355
  if (node.attrs.format === 'tex') {
1339
- const texMath = this.createElement('tex-math')
1340
- texMath.setAttribute('notation', 'LaTeX')
1341
- texMath.setAttribute('version', 'MathJax')
1356
+ const $texMath = this.createElement('tex-math')
1357
+ $texMath.setAttribute('notation', 'LaTeX')
1358
+ $texMath.setAttribute('version', 'MathJax')
1342
1359
  if (node.attrs.contents.includes('<![CDATA[')) {
1343
- texMath.innerHTML = node.attrs.contents
1360
+ $texMath.innerHTML = node.attrs.contents
1344
1361
  } else {
1345
- texMath.innerHTML = `<![CDATA[ ${node.attrs.contents} ]]>`
1362
+ $texMath.innerHTML = `<![CDATA[ ${node.attrs.contents} ]]>`
1346
1363
  }
1347
- return texMath
1364
+ return $texMath
1348
1365
  } else {
1349
1366
  const math = this.nodeFromJATS(node.attrs.contents)
1350
- const mathml = math as Element
1367
+ const $mathml = math as Element
1351
1368
  if (!isInline) {
1352
- mathml.setAttribute('id', normalizeID(node.attrs.id))
1369
+ $mathml.setAttribute('id', normalizeID(node.attrs.id))
1353
1370
  }
1354
- return mathml
1371
+ return $mathml
1355
1372
  }
1356
1373
  }
1357
1374
 
@@ -1360,213 +1377,227 @@ export class JATSExporter {
1360
1377
  document: this.document,
1361
1378
  })
1362
1379
 
1363
- private buildContributors = (articleMeta: Node) => {
1364
- const contributors = this.getChildrenOfType<ContributorNode>(
1365
- schema.nodes.contributor
1366
- ).sort(sortContributors)
1367
-
1380
+ private buildContributorGroup = (contributors: ContributorNode[]) => {
1368
1381
  if (!contributors.length) {
1369
1382
  return
1370
1383
  }
1371
1384
 
1372
- const labels = new Map<string, number>()
1373
- const createLabel = (id: string) => {
1374
- let label = labels.get(id)
1375
- if (!label) {
1376
- label = labels.size + 1
1377
- labels.set(id, label)
1385
+ const affiliationIDs = contributors.flatMap((n) => n.attrs.affiliationIDs)
1386
+ const affiliationOrder = new Map<string, number>()
1387
+ affiliationIDs.forEach((id, index) => {
1388
+ if (!affiliationOrder.has(id)) {
1389
+ affiliationOrder.set(id, index)
1378
1390
  }
1379
- const sup = this.createElement('sup')
1380
- sup.textContent = String(label)
1381
- return sup
1382
- }
1391
+ })
1392
+
1393
+ const affiliations = this.getChildrenOfType<AffiliationNode>(
1394
+ schema.nodes.affiliation
1395
+ )
1396
+ .filter((a) => affiliationIDs.includes(a.attrs.id))
1397
+ .sort(
1398
+ (a, b) =>
1399
+ (affiliationOrder.get(a.attrs.id) ?? Infinity) -
1400
+ (affiliationOrder.get(b.attrs.id) ?? Infinity)
1401
+ )
1383
1402
 
1384
1403
  const $contribGroup = this.createElement('contrib-group')
1385
1404
  $contribGroup.setAttribute('content-type', 'authors')
1386
- articleMeta.appendChild($contribGroup)
1387
-
1388
1405
  contributors.forEach((contributor) => {
1389
- const $contrib = this.createElement('contrib')
1390
- $contrib.setAttribute('contrib-type', 'author')
1391
- $contrib.setAttribute('id', normalizeID(contributor.attrs.id))
1406
+ $contribGroup.appendChild(this.serializeNode(contributor))
1407
+ })
1408
+ affiliations.forEach((affiliation) => {
1409
+ $contribGroup.appendChild(this.serializeNode(affiliation))
1410
+ })
1411
+ return $contribGroup
1412
+ }
1392
1413
 
1393
- if (contributor.attrs.isCorresponding) {
1394
- $contrib.setAttribute('corresp', 'yes')
1395
- }
1414
+ private buildAuthorNotes = ($coiStatement?: Element) => {
1415
+ const authorNotes = this.getFirstChildOfType<AuthorNotesNode>(
1416
+ schema.nodes.author_notes
1417
+ )
1418
+ const $authorNotes = this.createElement('author-notes')
1419
+ if (authorNotes) {
1420
+ this.appendAuthorNotes($authorNotes, authorNotes)
1421
+ }
1422
+ if ($coiStatement) {
1423
+ $authorNotes.appendChild($coiStatement)
1424
+ }
1425
+ return $authorNotes.hasChildNodes() ? $authorNotes : undefined
1426
+ }
1396
1427
 
1397
- if (contributor.attrs.role) {
1398
- this.appendElement($contrib, 'role', contributor.attrs.role)
1428
+ private computeContributorLabels = (contributors: ContributorNode[]) => {
1429
+ const labels = new Map<string, number>()
1430
+ const assign = (id: string) => {
1431
+ if (!labels.has(id)) {
1432
+ labels.set(id, labels.size + 1)
1399
1433
  }
1434
+ }
1435
+ contributors.forEach((contributor) => {
1436
+ contributor.attrs.affiliationIDs?.forEach(assign)
1437
+ contributor.attrs.footnoteIDs?.forEach(assign)
1438
+ contributor.attrs.correspIDs?.forEach(assign)
1439
+ })
1440
+ return labels
1441
+ }
1400
1442
 
1401
- if (contributor.attrs.ORCID) {
1402
- const contribIdAttrs: Record<string, string> = {
1403
- 'contrib-id-type': 'orcid',
1404
- }
1405
- if (contributor.attrs.isAuthenticated) {
1406
- contribIdAttrs['authenticated'] = 'true'
1407
- }
1408
- this.appendElement(
1409
- $contrib,
1410
- 'contrib-id',
1411
- contributor.attrs.ORCID,
1412
- contribIdAttrs
1413
- )
1414
- }
1443
+ private createLabelSup = (id: string) => {
1444
+ const $sup = this.createElement('sup')
1445
+ $sup.textContent = String(this.contributorLabels.get(id))
1446
+ return $sup
1447
+ }
1415
1448
 
1416
- const $name = this.buildContributorName(contributor)
1417
- $contrib.appendChild($name)
1449
+ private buildContributorElement = (contributor: ContributorNode) => {
1450
+ const $contrib = this.createElement('contrib')
1451
+ $contrib.setAttribute('contrib-type', 'author')
1452
+ $contrib.setAttribute('id', normalizeID(contributor.attrs.id))
1418
1453
 
1419
- if (contributor.attrs.suffix) {
1420
- this.appendElement($name, 'suffix', contributor.attrs.suffix)
1421
- }
1454
+ if (contributor.attrs.isCorresponding) {
1455
+ $contrib.setAttribute('corresp', 'yes')
1456
+ }
1422
1457
 
1423
- contributor.attrs.degrees?.forEach((degree) => {
1424
- this.appendElement($contrib, 'degrees', degree)
1425
- })
1458
+ if (contributor.attrs.role) {
1459
+ this.appendElement($contrib, 'role', contributor.attrs.role)
1460
+ }
1426
1461
 
1427
- if (contributor.attrs.email) {
1428
- this.appendElement($contrib, 'email', contributor.attrs.email)
1462
+ if (contributor.attrs.ORCID) {
1463
+ const contribIDAttrs: Record<string, string> = {
1464
+ 'contrib-id-type': 'orcid',
1429
1465
  }
1466
+ if (contributor.attrs.isAuthenticated) {
1467
+ contribIDAttrs['authenticated'] = 'true'
1468
+ }
1469
+ this.appendElement(
1470
+ $contrib,
1471
+ 'contrib-id',
1472
+ contributor.attrs.ORCID,
1473
+ contribIDAttrs
1474
+ )
1475
+ }
1430
1476
 
1431
- contributor.attrs.affiliationIDs?.forEach((rid) => {
1432
- const $xref = this.appendElement($contrib, 'xref', '', {
1433
- 'ref-type': 'aff',
1434
- rid: normalizeID(rid),
1435
- })
1436
- $xref.appendChild(createLabel(rid))
1437
- })
1477
+ const $name = this.buildContributorName(contributor)
1478
+ $contrib.appendChild($name)
1438
1479
 
1439
- contributor.attrs.footnoteIDs?.forEach((rid) => {
1440
- const $xref = this.appendElement($contrib, 'xref', '', {
1441
- 'ref-type': 'fn',
1442
- rid: normalizeID(rid),
1443
- })
1444
- $xref.appendChild(createLabel(rid))
1480
+ if (contributor.attrs.suffix) {
1481
+ this.appendElement($name, 'suffix', contributor.attrs.suffix)
1482
+ }
1483
+
1484
+ contributor.attrs.degrees?.forEach((degree) => {
1485
+ this.appendElement($contrib, 'degrees', degree)
1486
+ })
1487
+
1488
+ if (contributor.attrs.email) {
1489
+ this.appendElement($contrib, 'email', contributor.attrs.email)
1490
+ }
1491
+
1492
+ contributor.attrs.affiliationIDs?.forEach((rid) => {
1493
+ const $xref = this.appendElement($contrib, 'xref', '', {
1494
+ 'ref-type': 'aff',
1495
+ rid: normalizeID(rid),
1445
1496
  })
1497
+ $xref.appendChild(this.createLabelSup(rid))
1498
+ })
1446
1499
 
1447
- contributor.attrs.correspIDs?.forEach((rid) => {
1448
- const $xref = this.appendElement($contrib, 'xref', '', {
1449
- 'ref-type': 'corresp',
1450
- rid: normalizeID(rid),
1451
- })
1452
- $xref.appendChild(createLabel(rid))
1500
+ contributor.attrs.footnoteIDs?.forEach((rid) => {
1501
+ const $xref = this.appendElement($contrib, 'xref', '', {
1502
+ 'ref-type': 'fn',
1503
+ rid: normalizeID(rid),
1453
1504
  })
1505
+ $xref.appendChild(this.createLabelSup(rid))
1506
+ })
1454
1507
 
1455
- contributor.attrs.creditRoles?.forEach((credit) => {
1456
- const url = CreditRoleUrls.get(credit.vocabTerm)
1457
- if (!url) {
1458
- return
1459
- }
1460
- this.appendElement($contrib, 'role', credit.vocabTerm, {
1461
- 'vocab-identifier': 'http://credit.niso.org/',
1462
- vocab: 'CRediT',
1463
- 'vocab-term': credit.vocabTerm,
1464
- 'vocab-term-identifier': url,
1465
- })
1508
+ contributor.attrs.correspIDs?.forEach((rid) => {
1509
+ const $xref = this.appendElement($contrib, 'xref', '', {
1510
+ 'ref-type': 'corresp',
1511
+ rid: normalizeID(rid),
1466
1512
  })
1467
- $contribGroup.appendChild($contrib)
1513
+ $xref.appendChild(this.createLabelSup(rid))
1468
1514
  })
1469
1515
 
1470
- const affiliationIDs = contributors.flatMap((n) => n.attrs.affiliationIDs)
1471
- const sortAffiliations = (a: AffiliationNode, b: AffiliationNode) =>
1472
- affiliationIDs.indexOf(a.attrs.id) - affiliationIDs.indexOf(b.attrs.id)
1516
+ contributor.attrs.creditRoles?.forEach((credit) => {
1517
+ const url = CreditRoleUrls.get(credit.vocabTerm)
1518
+ if (!url) {
1519
+ return
1520
+ }
1521
+ this.appendElement($contrib, 'role', credit.vocabTerm, {
1522
+ 'vocab-identifier': 'http://credit.niso.org/',
1523
+ vocab: 'CRediT',
1524
+ 'vocab-term': credit.vocabTerm,
1525
+ 'vocab-term-identifier': url,
1526
+ })
1527
+ })
1473
1528
 
1474
- this.getChildrenOfType<AffiliationNode>(schema.nodes.affiliation)
1475
- .filter((a) => affiliationIDs.includes(a.attrs.id))
1476
- .sort(sortAffiliations)
1477
- .forEach((affiliation) => {
1478
- const $content = []
1479
-
1480
- if (affiliation.attrs.department) {
1481
- const $institution = this.createElement(
1482
- 'institution',
1483
- affiliation.attrs.department,
1484
- {
1485
- 'content-type': 'dept',
1486
- }
1487
- )
1488
- $content.push($institution)
1489
- }
1529
+ return $contrib
1530
+ }
1490
1531
 
1491
- if (affiliation.attrs.institution) {
1492
- const $institution = this.createElement(
1493
- 'institution',
1494
- affiliation.attrs.institution
1495
- )
1496
- $content.push($institution)
1497
- }
1532
+ private buildAffiliationElement = (affiliation: AffiliationNode) => {
1533
+ const $content: Element[] = []
1498
1534
 
1499
- if (affiliation.attrs.addressLine1) {
1500
- const $addrLine = this.createElement(
1501
- 'addr-line',
1502
- affiliation.attrs.addressLine1
1503
- )
1504
- $content.push($addrLine)
1505
- }
1535
+ if (affiliation.attrs.department) {
1536
+ $content.push(
1537
+ this.createElement('institution', affiliation.attrs.department, {
1538
+ 'content-type': 'dept',
1539
+ })
1540
+ )
1541
+ }
1506
1542
 
1507
- if (affiliation.attrs.addressLine2) {
1508
- const $addrLine = this.createElement(
1509
- 'addr-line',
1510
- affiliation.attrs.addressLine2
1511
- )
1512
- $content.push($addrLine)
1513
- }
1543
+ if (affiliation.attrs.institution) {
1544
+ $content.push(
1545
+ this.createElement('institution', affiliation.attrs.institution)
1546
+ )
1547
+ }
1514
1548
 
1515
- if (affiliation.attrs.addressLine3) {
1516
- const $addrLine = this.createElement(
1517
- 'addr-line',
1518
- affiliation.attrs.addressLine3
1519
- )
1520
- $content.push($addrLine)
1521
- }
1549
+ if (affiliation.attrs.addressLine1) {
1550
+ $content.push(
1551
+ this.createElement('addr-line', affiliation.attrs.addressLine1)
1552
+ )
1553
+ }
1522
1554
 
1523
- if (affiliation.attrs.city) {
1524
- const $city = this.createElement('city', affiliation.attrs.city)
1525
- $content.push($city)
1526
- }
1555
+ if (affiliation.attrs.addressLine2) {
1556
+ $content.push(
1557
+ this.createElement('addr-line', affiliation.attrs.addressLine2)
1558
+ )
1559
+ }
1527
1560
 
1528
- if (affiliation.attrs.county) {
1529
- const $state = this.createElement('state', affiliation.attrs.county)
1530
- $content.push($state)
1531
- }
1561
+ if (affiliation.attrs.addressLine3) {
1562
+ $content.push(
1563
+ this.createElement('addr-line', affiliation.attrs.addressLine3)
1564
+ )
1565
+ }
1532
1566
 
1533
- if (affiliation.attrs.country) {
1534
- const $country = this.createElement(
1535
- 'country',
1536
- affiliation.attrs.country
1537
- )
1538
- $content.push($country)
1539
- }
1567
+ if (affiliation.attrs.city) {
1568
+ $content.push(this.createElement('city', affiliation.attrs.city))
1569
+ }
1540
1570
 
1541
- if (affiliation.attrs.postCode) {
1542
- const $postalCode = this.createElement(
1543
- 'postal-code',
1544
- affiliation.attrs.postCode
1545
- )
1546
- $content.push($postalCode)
1547
- }
1571
+ if (affiliation.attrs.county) {
1572
+ $content.push(this.createElement('state', affiliation.attrs.county))
1573
+ }
1548
1574
 
1549
- const $aff = this.createElement('aff')
1550
- $aff.setAttribute('id', normalizeID(affiliation.attrs.id))
1551
- $contribGroup.appendChild($aff)
1575
+ if (affiliation.attrs.country) {
1576
+ $content.push(this.createElement('country', affiliation.attrs.country))
1577
+ }
1552
1578
 
1553
- const label = labels.get(affiliation.attrs.id)
1554
- if (label) {
1555
- this.appendElement($aff, 'label', String(label))
1556
- }
1579
+ if (affiliation.attrs.postCode) {
1580
+ $content.push(
1581
+ this.createElement('postal-code', affiliation.attrs.postCode)
1582
+ )
1583
+ }
1557
1584
 
1558
- $content.forEach((node, i) => {
1559
- if (i > 0) {
1560
- $aff.appendChild(this.document.createTextNode(', '))
1561
- }
1562
- $aff.appendChild(node)
1563
- })
1564
- })
1585
+ const $aff = this.createElement('aff')
1586
+ $aff.setAttribute('id', normalizeID(affiliation.attrs.id))
1565
1587
 
1566
- const $authorNotes = this.createAuthorNotesElement(contributors)
1567
- if ($authorNotes) {
1568
- articleMeta.insertBefore($authorNotes, $contribGroup.nextSibling)
1588
+ const label = this.contributorLabels.get(affiliation.attrs.id)
1589
+ if (label) {
1590
+ this.appendElement($aff, 'label', String(label))
1569
1591
  }
1592
+
1593
+ $content.forEach((node, i) => {
1594
+ if (i > 0) {
1595
+ $aff.appendChild(this.document.createTextNode(', '))
1596
+ }
1597
+ $aff.appendChild(node)
1598
+ })
1599
+
1600
+ return $aff
1570
1601
  }
1571
1602
 
1572
1603
  private buildContributorName = (contributor: ContributorNode) => {
@@ -1575,58 +1606,21 @@ export class JATSExporter {
1575
1606
  return this.createElement('string-name', given || family)
1576
1607
  }
1577
1608
 
1578
- const name = this.createElement('name')
1609
+ const $name = this.createElement('name')
1579
1610
 
1580
1611
  if (contributor.attrs.family) {
1581
- this.appendElement(name, 'surname', contributor.attrs.family)
1612
+ this.appendElement($name, 'surname', contributor.attrs.family)
1582
1613
  }
1583
1614
 
1584
1615
  if (contributor.attrs.given) {
1585
- this.appendElement(name, 'given-names', contributor.attrs.given)
1616
+ this.appendElement($name, 'given-names', contributor.attrs.given)
1586
1617
  }
1587
1618
 
1588
1619
  if (contributor.attrs.prefix) {
1589
- this.appendElement(name, 'prefix', contributor.attrs.prefix)
1590
- }
1591
-
1592
- return name
1593
- }
1594
-
1595
- private createAuthorNotesElement = (contributors: ContributorNode[]) => {
1596
- const authorNotes = this.getFirstChildOfType<AuthorNotesNode>(
1597
- schema.nodes.author_notes
1598
- )
1599
- if (!authorNotes || !authorNotes.childCount) {
1600
- return
1620
+ this.appendElement($name, 'prefix', contributor.attrs.prefix)
1601
1621
  }
1602
1622
 
1603
- const $authorNotes = this.createElement('author-notes')
1604
-
1605
- const used = contributors.flatMap((c) => c.attrs.correspIDs)
1606
-
1607
- authorNotes.descendants((node) => {
1608
- switch (node.type) {
1609
- case schema.nodes.paragraph: {
1610
- const $p = this.writeParagraph(node as ParagraphNode)
1611
- $authorNotes.append($p)
1612
- break
1613
- }
1614
- case schema.nodes.footnote: {
1615
- const $fn = this.writeFootnote(node as FootnoteNode)
1616
- $authorNotes.append($fn)
1617
- break
1618
- }
1619
- case schema.nodes.corresp: {
1620
- if (used.includes(node.attrs.id)) {
1621
- const $corresp = this.writeCorresp(node as CorrespNode)
1622
- $authorNotes.append($corresp)
1623
- }
1624
- break
1625
- }
1626
- }
1627
- return false
1628
- })
1629
- return $authorNotes.hasChildNodes() ? $authorNotes : undefined
1623
+ return $name
1630
1624
  }
1631
1625
 
1632
1626
  private writeCorresp = (corresp: CorrespNode) => {
@@ -1663,460 +1657,195 @@ export class JATSExporter {
1663
1657
  return $fn
1664
1658
  }
1665
1659
 
1666
- private buildKeywords(articleMeta: Node) {
1667
- const keywordGroups = this.getChildrenOfType(schema.nodes.keyword_group)
1668
-
1669
- keywordGroups.forEach((group) => {
1670
- const kwdGroup = this.createElement('kwd-group')
1671
- if (group.attrs.type) {
1672
- kwdGroup.setAttribute('kwd-group-type', group.attrs.type)
1673
- }
1674
- articleMeta.appendChild(kwdGroup)
1675
- group.content.forEach((keyword) => {
1676
- const kwd = this.createElement('kwd')
1677
- kwd.textContent = keyword.textContent
1678
- kwdGroup.appendChild(kwd)
1679
- })
1680
- articleMeta.appendChild(kwdGroup)
1681
- })
1682
- }
1683
-
1684
- private fixBody = (body: Element) => {
1685
- this.manuscriptNode.descendants((node) => {
1686
- if (node.attrs.id) {
1687
- if (
1688
- isNodeType<TableElementFooterNode>(node, 'general_table_footnote')
1689
- ) {
1690
- const generalTableFootnote = body.querySelector(
1691
- `#${normalizeID(node.attrs.id)}`
1692
- )
1693
- if (generalTableFootnote) {
1694
- Array.from(generalTableFootnote.childNodes).forEach((cn) => {
1695
- generalTableFootnote.before(cn)
1696
- })
1697
- generalTableFootnote.remove()
1698
- }
1660
+ private appendAuthorNotes = (
1661
+ $authorNotes: Element,
1662
+ authorNotes: AuthorNotesNode
1663
+ ) => {
1664
+ const correspIDs = new Set(
1665
+ this.getChildrenOfType<ContributorNode>(schema.nodes.contributor).flatMap(
1666
+ (contributor) => contributor.attrs.correspIDs
1667
+ )
1668
+ )
1669
+ authorNotes.forEach((node) => {
1670
+ switch (node.type) {
1671
+ case schema.nodes.paragraph: {
1672
+ $authorNotes.append(this.writeParagraph(node as ParagraphNode))
1673
+ break
1699
1674
  }
1700
- // move captions to the top of tables
1701
- if (isNodeType<TableElementNode>(node, 'table_element')) {
1702
- const tableElement = body.querySelector(
1703
- `#${normalizeID(node.attrs.id)}`
1704
- )
1705
-
1706
- if (tableElement) {
1707
- for (const childNode of tableElement.childNodes) {
1708
- switch (childNode.nodeName) {
1709
- case 'caption': {
1710
- const label = tableElement.querySelector('label')
1711
-
1712
- tableElement.insertBefore(
1713
- childNode,
1714
- label ? label.nextSibling : tableElement.firstChild
1715
- )
1716
- break
1717
- }
1718
-
1719
- case 'table': {
1720
- this.fixTable(childNode)
1721
- break
1722
- }
1723
- }
1724
- }
1675
+ case schema.nodes.footnote: {
1676
+ $authorNotes.append(this.writeFootnote(node as FootnoteNode))
1677
+ break
1678
+ }
1679
+ case schema.nodes.corresp: {
1680
+ if (correspIDs.has(node.attrs.id)) {
1681
+ $authorNotes.append(this.writeCorresp(node as CorrespNode))
1725
1682
  }
1683
+ break
1726
1684
  }
1727
1685
  }
1728
1686
  })
1729
1687
  }
1730
1688
 
1731
- private changeTag = (node: Element, tag: string) => {
1732
- const clone = this.createElement(tag)
1733
- for (const attr of node.attributes) {
1734
- clone.setAttributeNS(null, attr.name, attr.value)
1689
+ private buildKeywords = (): Node[] =>
1690
+ this.getChildrenOfType(schema.nodes.keyword_group).map((group) =>
1691
+ this.serializeNode(group)
1692
+ )
1693
+
1694
+ private changeTag = ($node: Element, tag: string) => {
1695
+ const $clone = this.createElement(tag)
1696
+ for (const attr of $node.attributes) {
1697
+ $clone.setAttributeNS(null, attr.name, attr.value)
1735
1698
  }
1736
- while (node.firstChild) {
1737
- clone.appendChild(node.firstChild)
1699
+ while ($node.firstChild) {
1700
+ $clone.appendChild($node.firstChild)
1738
1701
  }
1739
- node.replaceWith(clone)
1740
- return clone
1702
+ $node.replaceWith($clone)
1703
+ return $clone
1741
1704
  }
1742
1705
 
1743
- private fixTable = (table: ChildNode) => {
1744
- let tbody: Element | undefined
1706
+ private normalizeTable = ($table: Node) => {
1707
+ let $tbody: Element | undefined
1745
1708
 
1746
- Array.from(table.childNodes).forEach((child) => {
1747
- if (child instanceof Element && child.tagName.toLowerCase() === 'tbody') {
1748
- tbody = child
1709
+ Array.from($table.childNodes).forEach(($child) => {
1710
+ if (
1711
+ $child instanceof Element &&
1712
+ $child.tagName.toLowerCase() === 'tbody'
1713
+ ) {
1714
+ $tbody = $child
1749
1715
  }
1750
1716
  })
1751
1717
 
1752
- if (!tbody) {
1718
+ if (!$tbody) {
1753
1719
  return
1754
1720
  }
1755
1721
 
1756
- const tbodyRows = Array.from(tbody.childNodes)
1757
- const thead = this.createElement('thead')
1722
+ const tbodyRows = Array.from($tbody.childNodes)
1723
+ const $thead = this.createElement('thead')
1758
1724
 
1759
- tbodyRows.forEach((row, i) => {
1760
- const isRow = row instanceof Element && row.tagName.toLowerCase() === 'tr'
1725
+ tbodyRows.forEach(($row, i) => {
1726
+ const isRow =
1727
+ $row instanceof Element && $row.tagName.toLowerCase() === 'tr'
1761
1728
  if (isRow) {
1762
1729
  // we assume that <th scope="col | colgroup"> always belongs to <thead>
1763
- const headerCell = (row as Element).querySelector(
1730
+ const $headerCell = ($row as Element).querySelector(
1764
1731
  'th[scope="col"], th[scope="colgroup"]'
1765
1732
  )
1766
- if (i === 0 || headerCell) {
1767
- tbody?.removeChild(row)
1768
- const tableCells = (row as Element).querySelectorAll('td')
1769
- for (const td of tableCells) {
1733
+ if (i === 0 || $headerCell) {
1734
+ $tbody?.removeChild($row)
1735
+ const $tableCells = ($row as Element).querySelectorAll('td')
1736
+ for (const $td of $tableCells) {
1770
1737
  // for backwards compatibility since older docs use tds for header cells
1771
- this.changeTag(td, 'th')
1738
+ this.changeTag($td, 'th')
1772
1739
  }
1773
- thead.appendChild(row)
1740
+ $thead.appendChild($row)
1774
1741
  }
1775
1742
  }
1776
1743
  })
1777
1744
 
1778
- if (thead.hasChildNodes()) {
1779
- table.insertBefore(thead, tbody as Element)
1780
- }
1781
- }
1782
-
1783
- private unwrapBody = (body: HTMLElement) => {
1784
- const container = body.querySelector(':scope > sec[sec-type="body"]')
1785
- if (!container) {
1786
- return
1787
- }
1788
- const sections = container.querySelectorAll(':scope > sec')
1789
- sections.forEach((section) => {
1790
- body.appendChild(section.cloneNode(true))
1791
- })
1792
- body.removeChild(container)
1793
- }
1794
-
1795
- private removeBackContainer = (body: HTMLElement) => {
1796
- const container = body.querySelector(':scope > sec[sec-type="backmatter"]')
1797
- if (!container) {
1798
- return
1799
- }
1800
- body.removeChild(container)
1801
- }
1802
- private moveAwards = (front: HTMLElement, body: HTMLElement) => {
1803
- const awardGroups = body.querySelectorAll(':scope > award-group')
1804
- if (!awardGroups.length) {
1805
- return
1806
- }
1807
- const fundingGroup = this.createElement('funding-group')
1808
- awardGroups.forEach((award) => {
1809
- fundingGroup.appendChild(award)
1810
- })
1811
- const articleMeta = front.querySelector(':scope > article-meta')
1812
-
1813
- if (articleMeta) {
1814
- const insertBeforeElement = articleMeta.querySelector(
1815
- ':scope > support-group, :scope > conference, :scope > counts, :scope > custom-meta-group'
1816
- )
1817
- insertBeforeElement
1818
- ? articleMeta.insertBefore(fundingGroup, insertBeforeElement)
1819
- : articleMeta.appendChild(fundingGroup)
1820
- }
1821
- }
1822
-
1823
- private moveAbstracts = (front: HTMLElement, body: HTMLElement) => {
1824
- const abstractSections = this.getAbstractSections(body)
1825
-
1826
- for (const abstractSection of abstractSections) {
1827
- const node =
1828
- abstractSection.nodeName === 'trans-abstract'
1829
- ? this.createTransAbstractNode(abstractSection)
1830
- : this.createAbstractNode(abstractSection)
1831
- abstractSection.remove()
1832
- insertAbstractNode(front, node)
1745
+ if ($thead.hasChildNodes()) {
1746
+ $table.insertBefore($thead, $tbody as Element)
1833
1747
  }
1834
1748
  }
1835
1749
 
1836
- private getAbstractCategories(
1837
- abstractsNode: ManuscriptNode | undefined
1838
- ): string[] {
1839
- const categories: string[] = []
1840
- abstractsNode?.content.descendants((node) => {
1841
- categories.push(node.attrs.category)
1842
- return false
1843
- })
1844
- return categories
1845
- }
1846
-
1847
- private getAbstractSections(body: HTMLElement) {
1848
- {
1849
- const abstractsNode = this.getFirstChildOfType(schema.nodes.abstracts)
1850
- const abstractCategories = this.getAbstractCategories(abstractsNode)
1851
- const sections = Array.from(
1852
- body.querySelectorAll(':scope > sec, :scope > trans-abstract')
1853
- )
1854
- return sections.filter((section) =>
1855
- this.isAbstractSection(section, abstractCategories)
1856
- )
1857
- }
1858
- }
1859
-
1860
- private isAbstractSection(
1861
- section: Element,
1862
- abstractCategories: string[]
1863
- ): boolean {
1864
- const sectionType = section.getAttribute('sec-type')
1865
- return sectionType ? abstractCategories.includes(sectionType) : false
1866
- }
1867
-
1868
- private createTransAbstractNode(transAbstract: Element): Element {
1869
- const transAbstractNode = this.createElement('trans-abstract')
1870
- transAbstractNode.setAttributeNS(
1871
- XML_NAMESPACE,
1872
- 'lang',
1873
- transAbstract.getAttributeNS(XML_NAMESPACE, 'lang') ?? ''
1874
- )
1875
- this.setAbstractType(transAbstractNode, transAbstract)
1876
- transAbstractNode.append(...transAbstract.childNodes)
1877
- return transAbstractNode
1878
- }
1879
-
1880
- private createAbstractNode(abstractSection: Element): Element {
1881
- const abstractNode = this.createElement('abstract')
1882
- for (const node of abstractSection.childNodes) {
1883
- if (node.nodeName !== 'title') {
1884
- abstractNode.appendChild(node.cloneNode(true))
1750
+ private buildAbstracts = () => {
1751
+ const $abstracts: Element[] = []
1752
+ const abstractsNode = this.getFirstChildOfType(schema.nodes.abstracts)
1753
+ abstractsNode?.forEach((child) => {
1754
+ const $abstract = this.serializeNode(child) as Element
1755
+ if ($abstract.nodeName === 'abstract') {
1756
+ $abstract
1757
+ .querySelectorAll(':scope > title')
1758
+ .forEach((title) => title.remove())
1885
1759
  }
1886
- }
1887
- this.setAbstractType(abstractNode, abstractSection)
1888
- return abstractNode
1889
- }
1890
-
1891
- private setAbstractType(abstractNode: Element, abstractSection: Element) {
1892
- const sectionType = abstractSection.getAttribute('sec-type')
1893
- if (sectionType && sectionType !== 'abstract') {
1894
- const abstractType = sectionType.replace('abstract-', '')
1895
- abstractNode.setAttribute('abstract-type', abstractType)
1896
- }
1760
+ $abstracts.push($abstract)
1761
+ })
1762
+ return $abstracts
1897
1763
  }
1898
1764
 
1899
- private moveSectionsToBack = (back: HTMLElement, body: HTMLElement) => {
1900
- const availabilitySection = body.querySelector(
1901
- 'sec[sec-type="availability"]'
1902
- )
1903
-
1904
- if (availabilitySection) {
1905
- back.insertBefore(availabilitySection, back.firstChild)
1906
- }
1907
-
1908
- const ethicsSection = body.querySelector('sec[sec-type="ethics-statement"]')
1909
- if (ethicsSection) {
1910
- back.appendChild(ethicsSection)
1911
- }
1912
-
1913
- const section = body.querySelector('sec[sec-type="acknowledgements"]')
1914
-
1915
- if (section) {
1916
- const ack = this.createElement('ack')
1917
-
1918
- while (section.firstChild) {
1919
- ack.appendChild(section.firstChild)
1920
- }
1921
-
1922
- if (section.parentNode) {
1923
- section.parentNode.removeChild(section)
1924
- }
1925
-
1926
- back.insertBefore(ack, back.firstChild)
1927
- }
1928
- const appendicesSections = body.querySelectorAll(
1929
- 'sec[sec-type="appendices"]'
1930
- )
1931
-
1932
- if (appendicesSections) {
1933
- const appGroup = this.createElement('app-group')
1934
- appendicesSections.forEach((section) => {
1935
- if (section.parentNode) {
1936
- section.parentNode.removeChild(section)
1937
- }
1938
- const app = this.createElement('app')
1939
- app.appendChild(section)
1940
- appGroup.appendChild(app)
1941
- })
1942
- back.insertBefore(appGroup, back.firstChild)
1943
- }
1944
-
1945
- const footNotes = []
1946
-
1947
- const footnoteCategories = [
1948
- 'con',
1949
- 'conflict',
1950
- 'deceased',
1951
- 'equal',
1952
- 'present-address',
1953
- 'presented-at',
1954
- 'previously-at',
1955
- 'supplementary-material',
1956
- 'supported-by',
1957
- 'financial-disclosure',
1958
- 'coi-statement',
1959
- ]
1960
-
1961
- const sections = body.querySelectorAll('sec')
1962
- for (const currentSection of sections) {
1963
- const currentSectionType = currentSection.getAttribute('sec-type')
1964
- if (
1965
- currentSectionType &&
1966
- footnoteCategories.indexOf(currentSectionType) >= 0
1967
- ) {
1968
- footNotes.push(
1969
- this.sectionToFootnote(currentSection, currentSectionType)
1970
- )
1971
- }
1972
- }
1973
-
1974
- if (footNotes.length > 0) {
1975
- const fnGroup = this.createElement('fn-group')
1976
- fnGroup.append(...footNotes)
1977
- back.append(fnGroup)
1765
+ sectionToFootnote = ($section: Element, fnType: string) => {
1766
+ const $fn = this.createElement('fn')
1767
+ $fn.setAttribute('fn-type', fnType)
1768
+ const $title = $section.querySelector(':scope > title')
1769
+ if ($title) {
1770
+ const $label = this.createElement('label')
1771
+ $label.textContent = $title.textContent
1772
+ $section.removeChild($title)
1773
+ $fn.append($label)
1774
+ }
1775
+ $fn.append(...$section.children)
1776
+ if ($section.parentNode) {
1777
+ $section.parentNode.removeChild($section)
1978
1778
  }
1779
+ return $fn
1979
1780
  }
1980
-
1981
- sectionToFootnote = (section: Element, fnType: string) => {
1982
- const footNote = this.createElement('fn')
1983
- footNote.setAttribute('fn-type', fnType)
1984
- const title = section.querySelector('title')
1985
- if (title) {
1986
- const footNoteTitle = this.createElement('label')
1987
- footNoteTitle.textContent = title.textContent
1988
- section.removeChild(title)
1989
- footNote.append(footNoteTitle)
1990
- }
1991
- footNote.append(...section.children)
1992
- if (section.parentNode) {
1993
- section.parentNode.removeChild(section)
1994
- }
1995
- return footNote
1996
- }
1997
- private moveFloatsGroup = (article: HTMLElement) => {
1781
+ private buildFloatsGroup = () => {
1998
1782
  const heroImage = this.getFirstChildOfType(schema.nodes.hero_image)
1999
1783
  if (!heroImage) {
2000
1784
  return
2001
1785
  }
2002
- const floatsGroup = this.createElement('floats-group')
2003
- let figure: HTMLElement | null = null
1786
+ const $floatsGroup = this.createElement('floats-group')
1787
+ let $graphic: Element | null = null
2004
1788
  heroImage.descendants((node) => {
2005
1789
  if (node.type === schema.nodes.figure) {
2006
- figure = this.serializeNode(node) as HTMLElement
2007
- floatsGroup.appendChild(figure)
1790
+ $graphic = this.serializeNode(node) as Element
1791
+ $floatsGroup.appendChild($graphic)
2008
1792
  } else {
2009
- const serializedNode = this.serializeNode(node)
2010
- figure?.appendChild(serializedNode)
1793
+ const $serializedNode = this.serializeNode(node)
1794
+ $graphic?.appendChild($serializedNode)
2011
1795
  }
2012
1796
  return false
2013
1797
  })
2014
1798
 
2015
- if (floatsGroup.children.length > 0) {
2016
- article.appendChild(floatsGroup)
1799
+ if ($floatsGroup.children.length > 0) {
1800
+ return $floatsGroup
2017
1801
  }
2018
1802
  }
2019
1803
 
2020
- private moveCoiStatementToAuthorNotes(back: HTMLElement, front: HTMLElement) {
2021
- const fnGroups = back.querySelectorAll('fn-group')
2022
- fnGroups.forEach((fnGroup) => {
2023
- if (fnGroup) {
2024
- const coiStatement = fnGroup.querySelector(
2025
- 'fn[fn-type="coi-statement"]'
2026
- )
2027
- if (coiStatement) {
2028
- const authorNotes = this.createElement('author-notes')
2029
- authorNotes.append(coiStatement)
2030
- const articleMeta = front.querySelector('article-meta')
2031
- if (articleMeta) {
2032
- const authorNoteEl = articleMeta.querySelector('author-notes')
2033
- if (authorNoteEl) {
2034
- authorNoteEl.append(...authorNotes.childNodes)
2035
- } else {
2036
- const appendableSelectors = [
2037
- 'contrib-group',
2038
- 'title-group',
2039
- 'article-id',
2040
- ]
2041
- const appendable = [
2042
- ...(articleMeta as HTMLElement).querySelectorAll(
2043
- appendableSelectors.join(', ')
2044
- ),
2045
- ]
2046
- for (let i = 0; i < appendableSelectors.length; i++) {
2047
- const sel = appendableSelectors[i]
2048
- const match = appendable.find((el) => el.matches(sel))
2049
- if (match) {
2050
- articleMeta.insertBefore(authorNotes, match.nextSibling)
2051
- break
2052
- }
2053
- }
2054
- }
2055
- }
2056
- if (!fnGroup.hasChildNodes()) {
2057
- fnGroup.remove()
2058
- }
2059
- }
2060
- }
2061
- })
2062
- }
2063
-
2064
- private updateFootnoteTypes(front: HTMLElement, body: HTMLElement) {
2065
- const footnotes: Element[] = [...front.querySelectorAll('fn').values()]
2066
- footnotes.push(...body.querySelectorAll('fn'))
2067
- footnotes.forEach((fn) => {
2068
- const fnType = fn.getAttribute('fn-type')
2069
- if (fnType) {
2070
- fn.setAttribute('fn-type', fnType)
2071
- }
2072
- })
2073
- }
2074
-
2075
1804
  private fillEmptyElements(
2076
- articleElement: Element,
1805
+ $article: Element,
2077
1806
  selector: string,
2078
1807
  tagName = 'p'
2079
1808
  ) {
2080
- const emptyElements = Array.from(
2081
- articleElement.querySelectorAll(selector)
2082
- ).filter((element) => !element.innerHTML)
2083
- emptyElements.forEach((element) =>
2084
- element.appendChild(this.createElement(tagName))
1809
+ const $empty = Array.from($article.querySelectorAll(selector)).filter(
1810
+ ($el) => !$el.innerHTML
1811
+ )
1812
+ $empty.forEach(($element) =>
1813
+ $element.appendChild(this.createElement(tagName))
2085
1814
  )
2086
1815
  }
2087
- private addParagraphsToSections(articleElement: Element) {
2088
- const sections = articleElement.querySelectorAll('sec')
1816
+ private addParagraphsToSections($article: Element) {
1817
+ const $sections = $article.querySelectorAll('sec, abstract')
2089
1818
  const TITLE_TAGS = new Set(['title', 'label', 'sec-meta'])
2090
- for (const section of sections) {
2091
- const hasContent = Array.from(section.children).some(
2092
- (child) => !TITLE_TAGS.has(child.tagName)
1819
+ for (const $section of $sections) {
1820
+ const hasContent = Array.from($section.children).some(
1821
+ ($child) => !TITLE_TAGS.has($child.tagName)
2093
1822
  )
2094
1823
  if (hasContent) {
2095
1824
  continue
2096
1825
  }
2097
- const p = this.createElement('p')
2098
- const insertAfterElement =
2099
- section.querySelector(':scope > title') ??
2100
- section.querySelector(':scope > label') ??
2101
- section.querySelector(':scope > sec-meta')
2102
-
2103
- if (insertAfterElement) {
2104
- insertAfterElement.insertAdjacentElement('afterend', p)
1826
+ const $p = this.createElement('p')
1827
+ const $insertAfter =
1828
+ $section.querySelector(':scope > title') ??
1829
+ $section.querySelector(':scope > label') ??
1830
+ $section.querySelector(':scope > sec-meta')
1831
+
1832
+ if ($insertAfter) {
1833
+ $insertAfter.insertAdjacentElement('afterend', $p)
2105
1834
  } else {
2106
- section.prepend(p)
1835
+ $section.prepend($p)
2107
1836
  }
2108
1837
  }
2109
1838
  }
2110
1839
 
2111
- private fillEmptyFootnotes(articleElement: Element) {
2112
- this.fillEmptyElements(articleElement, 'fn')
1840
+ private fillEmptyFootnotes($article: Element) {
1841
+ this.fillEmptyElements($article, 'fn')
2113
1842
  }
2114
1843
 
2115
- private fillEmptyTableFooters(articleElement: Element) {
2116
- this.fillEmptyElements(articleElement, 'table-wrap-foot')
1844
+ private fillEmptyTableFooters($article: Element) {
1845
+ this.fillEmptyElements($article, 'table-wrap-foot')
2117
1846
  }
2118
1847
 
2119
- private fillEmptyListItem(articleElement: Element) {
2120
- this.fillEmptyElements(articleElement, 'list-item')
1848
+ private fillEmptyListItem($article: Element) {
1849
+ this.fillEmptyElements($article, 'list-item')
2121
1850
  }
2122
1851
  }