@manuscripts/transform 4.3.52 → 4.4.0

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