@manuscripts/transform 4.3.53 → 4.4.1

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 (196) hide show
  1. package/dist/cjs/jats/__tests__/jats-roundtrip.test.js +3 -3
  2. package/dist/cjs/jats/exporter/jats-exporter.js +3 -0
  3. package/dist/cjs/jats/importer/jats-dom-parser.js +41 -1
  4. package/dist/cjs/schema/index.js +9 -0
  5. package/dist/cjs/schema/nodes/headshot_element.js +38 -0
  6. package/dist/cjs/schema/nodes/headshot_grid.js +37 -0
  7. package/dist/cjs/schema/nodes/headshot_image.js +51 -0
  8. package/dist/cjs/transformer/node-names.js +2 -0
  9. package/dist/cjs/transformer/node-title.js +6 -0
  10. package/dist/cjs/version.js +1 -1
  11. package/dist/es/jats/__tests__/jats-roundtrip.test.js +3 -3
  12. package/dist/es/jats/exporter/jats-exporter.js +3 -0
  13. package/dist/es/jats/importer/jats-dom-parser.js +41 -1
  14. package/dist/es/schema/index.js +9 -0
  15. package/dist/es/schema/nodes/headshot_element.js +34 -0
  16. package/dist/es/schema/nodes/headshot_grid.js +33 -0
  17. package/dist/es/schema/nodes/headshot_image.js +48 -0
  18. package/dist/es/transformer/node-names.js +2 -0
  19. package/dist/es/transformer/node-title.js +6 -0
  20. package/dist/es/version.js +1 -1
  21. package/dist/types/jats/__tests__/utils.d.ts +1 -1
  22. package/dist/types/schema/index.d.ts +3 -0
  23. package/dist/types/schema/nodes/headshot_element.d.ts +25 -0
  24. package/dist/types/schema/nodes/headshot_grid.d.ts +24 -0
  25. package/dist/types/schema/nodes/headshot_image.d.ts +26 -0
  26. package/dist/types/schema/types.d.ts +1 -1
  27. package/dist/types/version.d.ts +1 -1
  28. package/package.json +22 -21
  29. package/src/errors.ts +28 -0
  30. package/src/getVersion.ts +21 -0
  31. package/src/index.ts +28 -0
  32. package/src/jats/__tests__/__fixtures__/debug-example.xml +136 -0
  33. package/src/jats/__tests__/__fixtures__/jats-abstract-no-body.xml +163 -0
  34. package/src/jats/__tests__/__fixtures__/jats-citations.xml +472 -0
  35. package/src/jats/__tests__/__fixtures__/jats-comments.xml +46 -0
  36. package/src/jats/__tests__/__fixtures__/jats-document.xml +134 -0
  37. package/src/jats/__tests__/__fixtures__/jats-example-doc.xml +1235 -0
  38. package/src/jats/__tests__/__fixtures__/jats-example-front-only.xml +26 -0
  39. package/src/jats/__tests__/__fixtures__/jats-example-full.xml +209 -0
  40. package/src/jats/__tests__/__fixtures__/jats-example-no-back.xml +156 -0
  41. package/src/jats/__tests__/__fixtures__/jats-example-no-body.xml +91 -0
  42. package/src/jats/__tests__/__fixtures__/jats-example.xml +221 -0
  43. package/src/jats/__tests__/__fixtures__/jats-fn-group.xml +60 -0
  44. package/src/jats/__tests__/__fixtures__/jats-import-no-refs.xml +1077 -0
  45. package/src/jats/__tests__/__fixtures__/jats-import.xml +1578 -0
  46. package/src/jats/__tests__/__fixtures__/jats-roundtrip.xml +133 -0
  47. package/src/jats/__tests__/__fixtures__/jats-tables-example.xml +2043 -0
  48. package/src/jats/__tests__/__fixtures__/math-fragment.xml +12 -0
  49. package/src/jats/__tests__/__snapshots__/citeproc.test.ts.snap +57 -0
  50. package/src/jats/__tests__/__snapshots__/create-article-node.test.ts.snap +65 -0
  51. package/src/jats/__tests__/__snapshots__/jats-exporter.test.ts.snap +7 -0
  52. package/src/jats/__tests__/__snapshots__/jats-importer.test.ts.snap +59796 -0
  53. package/src/jats/__tests__/__snapshots__/jats-roundtrip.test.ts.snap +18 -0
  54. package/src/jats/__tests__/citations.ts +25 -0
  55. package/src/jats/__tests__/citeproc.test.ts +195 -0
  56. package/src/jats/__tests__/create-article-node.test.ts +29 -0
  57. package/src/jats/__tests__/data/section-categories.ts +212 -0
  58. package/src/jats/__tests__/files.ts +24 -0
  59. package/src/jats/__tests__/jats-exporter.test.ts +223 -0
  60. package/src/jats/__tests__/jats-importer.test.ts +567 -0
  61. package/src/jats/__tests__/jats-roundtrip.test.ts +66 -0
  62. package/src/jats/__tests__/utils.ts +90 -0
  63. package/src/jats/exporter/citeproc.ts +265 -0
  64. package/src/jats/exporter/jats-exporter.ts +2093 -0
  65. package/src/jats/exporter/jats-versions.ts +50 -0
  66. package/src/jats/exporter/labels.ts +101 -0
  67. package/src/jats/importer/create-article-node.ts +52 -0
  68. package/src/jats/importer/jats-comments.ts +111 -0
  69. package/src/jats/importer/jats-dom-parser.ts +1306 -0
  70. package/src/jats/importer/jats-parser-utils.ts +234 -0
  71. package/src/jats/importer/jats-transformations.ts +591 -0
  72. package/src/jats/importer/parse-jats-article.ts +106 -0
  73. package/src/jats/index.ts +21 -0
  74. package/src/jats/types.ts +16 -0
  75. package/src/lib/__tests__/footnotes.test.ts +36 -0
  76. package/src/lib/citeproc.ts +30 -0
  77. package/src/lib/credit-roles.ts +91 -0
  78. package/src/lib/deafults.ts +17 -0
  79. package/src/lib/footnotes.ts +85 -0
  80. package/src/lib/html.ts +56 -0
  81. package/src/lib/section-categories.ts +27 -0
  82. package/src/lib/utils.ts +145 -0
  83. package/src/lib/xml.ts +28 -0
  84. package/src/schema/__tests__/docs.ts +1911 -0
  85. package/src/schema/__tests__/groups.test.ts +61 -0
  86. package/src/schema/__tests__/migration.test.ts +23 -0
  87. package/src/schema/groups.ts +36 -0
  88. package/src/schema/index.ts +295 -0
  89. package/src/schema/marks.ts +254 -0
  90. package/src/schema/migration/migrate.ts +77 -0
  91. package/src/schema/migration/migration-script.ts +26 -0
  92. package/src/schema/migration/migration-scripts/1.2.5.ts +44 -0
  93. package/src/schema/migration/migration-scripts/2.3.22.ts +52 -0
  94. package/src/schema/migration/migration-scripts/3.0.12.ts +33 -0
  95. package/src/schema/migration/migration-scripts/3.0.21.ts +50 -0
  96. package/src/schema/migration/migration-scripts/3.0.30.ts +65 -0
  97. package/src/schema/migration/migration-scripts/3.0.31.ts +40 -0
  98. package/src/schema/migration/migration-scripts/3.0.41.ts +37 -0
  99. package/src/schema/migration/migration-scripts/3.0.55.ts +42 -0
  100. package/src/schema/migration/migration-scripts/3.0.56.ts +114 -0
  101. package/src/schema/migration/migration-scripts/4.2.13.ts +37 -0
  102. package/src/schema/migration/migration-scripts/4.2.15.ts +48 -0
  103. package/src/schema/migration/migration-scripts/4.3.23.ts +55 -0
  104. package/src/schema/migration/migration-scripts/4.3.34.ts +132 -0
  105. package/src/schema/migration/migration-scripts/4.3.35.ts +41 -0
  106. package/src/schema/migration/migration-scripts/index.ts +49 -0
  107. package/src/schema/nodes/abstracts.ts +29 -0
  108. package/src/schema/nodes/affiliation.ts +88 -0
  109. package/src/schema/nodes/affiliations.ts +30 -0
  110. package/src/schema/nodes/alt_text.ts +36 -0
  111. package/src/schema/nodes/alt_title.ts +40 -0
  112. package/src/schema/nodes/alt_titles_section.ts +58 -0
  113. package/src/schema/nodes/attachment.ts +46 -0
  114. package/src/schema/nodes/attachments.ts +45 -0
  115. package/src/schema/nodes/attribution.ts +38 -0
  116. package/src/schema/nodes/author_notes.ts +44 -0
  117. package/src/schema/nodes/award.ts +52 -0
  118. package/src/schema/nodes/awards.ts +45 -0
  119. package/src/schema/nodes/backmatter.ts +31 -0
  120. package/src/schema/nodes/bibliography_element.ts +58 -0
  121. package/src/schema/nodes/bibliography_item.ts +108 -0
  122. package/src/schema/nodes/bibliography_section.ts +60 -0
  123. package/src/schema/nodes/blockquote_element.ts +79 -0
  124. package/src/schema/nodes/body.ts +29 -0
  125. package/src/schema/nodes/box_element.ts +67 -0
  126. package/src/schema/nodes/caption.ts +67 -0
  127. package/src/schema/nodes/caption_title.ts +67 -0
  128. package/src/schema/nodes/citation.ts +70 -0
  129. package/src/schema/nodes/comment.ts +50 -0
  130. package/src/schema/nodes/comments.ts +28 -0
  131. package/src/schema/nodes/contributor.ts +87 -0
  132. package/src/schema/nodes/contributors.ts +40 -0
  133. package/src/schema/nodes/core_section.ts +27 -0
  134. package/src/schema/nodes/corresp.ts +49 -0
  135. package/src/schema/nodes/cross_reference.ts +64 -0
  136. package/src/schema/nodes/doc.ts +21 -0
  137. package/src/schema/nodes/embed.ts +55 -0
  138. package/src/schema/nodes/equation.ts +65 -0
  139. package/src/schema/nodes/equation_element.ts +61 -0
  140. package/src/schema/nodes/figure.ts +65 -0
  141. package/src/schema/nodes/figure_element.ts +73 -0
  142. package/src/schema/nodes/footnote.ts +95 -0
  143. package/src/schema/nodes/footnotes_element.ts +69 -0
  144. package/src/schema/nodes/footnotes_section.ts +61 -0
  145. package/src/schema/nodes/general_table_footnote.ts +42 -0
  146. package/src/schema/nodes/graphical_abstract_section.ts +62 -0
  147. package/src/schema/nodes/hard_break.ts +36 -0
  148. package/src/schema/nodes/headshot_element.ts +48 -0
  149. package/src/schema/nodes/headshot_grid.ts +46 -0
  150. package/src/schema/nodes/headshot_image.ts +65 -0
  151. package/src/schema/nodes/hero_image.ts +46 -0
  152. package/src/schema/nodes/highlight_marker.ts +68 -0
  153. package/src/schema/nodes/image_element.ts +49 -0
  154. package/src/schema/nodes/inline_equation.ts +68 -0
  155. package/src/schema/nodes/inline_footnote.ts +66 -0
  156. package/src/schema/nodes/keyword.ts +61 -0
  157. package/src/schema/nodes/keyword_group.ts +63 -0
  158. package/src/schema/nodes/keywords.ts +59 -0
  159. package/src/schema/nodes/keywords_element.ts +64 -0
  160. package/src/schema/nodes/link.ts +79 -0
  161. package/src/schema/nodes/list.ts +134 -0
  162. package/src/schema/nodes/listing.ts +91 -0
  163. package/src/schema/nodes/listing_element.ts +61 -0
  164. package/src/schema/nodes/long_desc.ts +36 -0
  165. package/src/schema/nodes/manuscript.ts +86 -0
  166. package/src/schema/nodes/missing_figure.ts +59 -0
  167. package/src/schema/nodes/paragraph.ts +77 -0
  168. package/src/schema/nodes/placeholder.ts +62 -0
  169. package/src/schema/nodes/placeholder_element.ts +60 -0
  170. package/src/schema/nodes/pullquote_element.ts +81 -0
  171. package/src/schema/nodes/quote_image.ts +64 -0
  172. package/src/schema/nodes/section.ts +69 -0
  173. package/src/schema/nodes/section_label.ts +39 -0
  174. package/src/schema/nodes/section_title.ts +58 -0
  175. package/src/schema/nodes/subtitle.ts +40 -0
  176. package/src/schema/nodes/subtitles.ts +51 -0
  177. package/src/schema/nodes/supplement.ts +77 -0
  178. package/src/schema/nodes/supplements.ts +60 -0
  179. package/src/schema/nodes/table.ts +144 -0
  180. package/src/schema/nodes/table_col.ts +76 -0
  181. package/src/schema/nodes/table_element.ts +67 -0
  182. package/src/schema/nodes/table_element_footer.ts +42 -0
  183. package/src/schema/nodes/text.ts +31 -0
  184. package/src/schema/nodes/title.ts +43 -0
  185. package/src/schema/nodes/trans_abstract.ts +58 -0
  186. package/src/schema/nodes/trans_graphical_abstract.ts +67 -0
  187. package/src/schema/types.ts +243 -0
  188. package/src/tests.ts +33 -0
  189. package/src/transformer/id.ts +23 -0
  190. package/src/transformer/index.ts +21 -0
  191. package/src/transformer/node-names.ts +75 -0
  192. package/src/transformer/node-title.ts +116 -0
  193. package/src/transformer/node-types.ts +39 -0
  194. package/src/transformer/node-validator.ts +82 -0
  195. package/src/types.ts +25 -0
  196. package/src/version.ts +1 -0
@@ -0,0 +1,2093 @@
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
+ headshot_grid: () => ['p', { 'content-type': 'headshots'}, 0],
704
+ headshot_element: (node) => createImage(node),
705
+ headshot_image: () => '',
706
+ alt_text: (node) => {
707
+ if (node.textContent) {
708
+ const altText = this.createElement('alt-text')
709
+ altText.textContent = node.textContent
710
+ return altText
711
+ }
712
+ return ''
713
+ },
714
+ long_desc: (node) => {
715
+ if (node.textContent) {
716
+ const longDesc = this.createElement('long-desc')
717
+ longDesc.textContent = node.textContent
718
+ return longDesc
719
+ }
720
+ return ''
721
+ },
722
+ attachment: () => '',
723
+ attachments: () => '',
724
+ image_element: (node) => createImage(node),
725
+ embed: (node) => {
726
+ const { id, href, mimetype, mimeSubtype } = node.attrs
727
+ if (!href) {
728
+ return ''
729
+ }
730
+ const mediaElement = this.createElement('media')
731
+ mediaElement.setAttribute('id', normalizeID(id))
732
+ mediaElement.setAttributeNS(XLINK_NAMESPACE, 'show', 'embed')
733
+ mediaElement.setAttributeNS(XLINK_NAMESPACE, 'href', href)
734
+ if (mimetype) {
735
+ mediaElement.setAttribute('mimetype', node.attrs.mimetype)
736
+ }
737
+ if (mimeSubtype) {
738
+ mediaElement.setAttribute('mime-subtype', node.attrs.mimeSubtype)
739
+ }
740
+ appendLabels(mediaElement, node)
741
+ this.appendChildNodeOfType(mediaElement, node, schema.nodes.alt_text)
742
+ this.appendChildNodeOfType(mediaElement, node, schema.nodes.long_desc)
743
+ this.appendCaption(mediaElement, node)
744
+ return mediaElement
745
+ },
746
+ awards: () => ['funding-group', 0],
747
+ award: (node) => {
748
+ const awardGroup = node as AwardNode
749
+ const awardGroupElement = this.createElement('award-group')
750
+ awardGroupElement.setAttribute('id', normalizeID(awardGroup.attrs.id))
751
+ appendChildIfPresent(
752
+ awardGroupElement,
753
+ 'funding-source',
754
+ awardGroup.attrs.source
755
+ )
756
+ awardGroup.attrs.code
757
+ ?.split(';')
758
+ .forEach((code) =>
759
+ appendChildIfPresent(awardGroupElement, 'award-id', code)
760
+ )
761
+ appendChildIfPresent(
762
+ awardGroupElement,
763
+ 'principal-award-recipient',
764
+ awardGroup.attrs.recipient
765
+ )
766
+
767
+ return awardGroupElement
768
+ },
769
+ box_element: (node) => createBoxElement(node),
770
+ author_notes: () => '',
771
+ corresp: () => '',
772
+ title: () => '',
773
+ alt_title: () => '',
774
+ alt_titles: () => '',
775
+ subtitle: () => '',
776
+ subtitles: () => '',
777
+ text_block: (node) => nodes.paragraph(node),
778
+ affiliations: () => '',
779
+ contributors: () => '',
780
+ table_element_footer: (node) =>
781
+ node.childCount == 0
782
+ ? ['table-wrap-foot', ['fn-group', ['fn', ['p']]]]
783
+ : ['table-wrap-foot', 0],
784
+ contributor: () => '',
785
+ affiliation: () => '',
786
+ attribution: () => ['attrib', 0],
787
+ bibliography_element: () => '',
788
+ bibliography_item: () => '',
789
+ comments: () => '',
790
+ keyword_group: () => '',
791
+ body: () => ['body', 0],
792
+ abstracts: () => ['abstract', 0],
793
+ backmatter: () => ['backmatter', 0],
794
+ supplement: () => '',
795
+ supplements: () => '',
796
+ bibliography_section: () => '',
797
+ blockquote_element: () => ['disp-quote', { 'content-type': 'quote' }, 0],
798
+ list: (node) => [
799
+ 'list',
800
+ {
801
+ 'list-type': node.attrs.listStyleType ?? 'bullet',
802
+ },
803
+ 0,
804
+ ],
805
+ caption: () => ['p', 0],
806
+ caption_title: (node) => {
807
+ if (!node.textContent) {
808
+ return ''
809
+ }
810
+ return ['title', 0]
811
+ },
812
+ citation: (node) => {
813
+ const citation = node as CitationNode
814
+ const rids = citation.attrs.rids
815
+ if (!rids.length) {
816
+ return ''
817
+ }
818
+
819
+ const xref = this.createElement('xref')
820
+ xref.setAttribute('ref-type', 'bibr')
821
+ xref.setAttribute('rid', normalizeID(rids.join(' ')))
822
+ const fragment = this.renderedCitations.get(node.attrs.id)
823
+ if (fragment) {
824
+ xref.innerHTML = fragment
825
+ }
826
+ return xref
827
+ },
828
+ cross_reference: (node) => {
829
+ const cross = node as CrossReferenceNode
830
+ const rids = cross.attrs.rids
831
+ if (!rids.length) {
832
+ return cross.attrs.label ?? ''
833
+ }
834
+
835
+ const rid = rids[0]
836
+ const text = cross.attrs.label || this.labelTargets.get(rid)?.label
837
+
838
+ const target = findChildrenByAttr(
839
+ this.manuscriptNode,
840
+ (attrs) => attrs.id === rid
841
+ )[0]?.node
842
+ if (!target) {
843
+ return text ?? ''
844
+ }
845
+
846
+ const xref = this.createElement('xref')
847
+
848
+ const type = chooseRefType(target.type)
849
+ if (type) {
850
+ xref.setAttribute('ref-type', type)
851
+ }
852
+
853
+ xref.setAttribute('rid', normalizeID(rids.join(' ')))
854
+ xref.textContent = text ?? ''
855
+
856
+ return xref
857
+ },
858
+ doc: () => '',
859
+ equation: (node) => {
860
+ return node.attrs.contents ? this.createEquation(node) : ''
861
+ },
862
+ general_table_footnote: (node) => {
863
+ const el = this.createElement('general-table-footnote')
864
+ el.setAttribute('id', normalizeID(node.attrs.id))
865
+ processChildNodes(el, node, schema.nodes.general_table_footnote)
866
+ return el
867
+ },
868
+ inline_equation: (node) => {
869
+ if (!node.attrs.contents) {
870
+ return ''
871
+ }
872
+ const eqElement = this.createElement('inline-formula')
873
+ const equation = this.createEquation(node, true)
874
+ eqElement.append(equation)
875
+ return eqElement
876
+ },
877
+ equation_element: (node) => {
878
+ const eqElement = this.createElement('disp-formula')
879
+ eqElement.setAttribute('id', normalizeID(node.attrs.id))
880
+ appendLabels(eqElement, node)
881
+ processChildNodes(eqElement, node, schema.nodes.equation)
882
+ return eqElement
883
+ },
884
+ figure: (node) => createGraphic(node),
885
+ figure_element: (node) =>
886
+ createFigureElement(node, node.type.schema.nodes.figure),
887
+ footnote: (node) => {
888
+ const attrs: Attrs = {}
889
+
890
+ if (node.attrs.id) {
891
+ attrs.id = normalizeID(node.attrs.id)
892
+ }
893
+ if (node.attrs.category) {
894
+ attrs['fn-type'] = node.attrs.category
895
+ }
896
+ return ['fn', attrs, 0]
897
+ },
898
+ footnotes_element: (node) =>
899
+ node.childCount == 0
900
+ ? ['fn-group', { id: normalizeID(node.attrs.id) }, ['fn', ['p']]]
901
+ : ['fn-group', { id: normalizeID(node.attrs.id) }, 0],
902
+ footnotes_section: (node) => {
903
+ const attrs: { [key: string]: string } = {
904
+ id: normalizeID(node.attrs.id),
905
+ 'sec-type': 'endnotes', // chooseSecType(node.attrs.category),
906
+ }
907
+
908
+ return ['sec', attrs, 0]
909
+ },
910
+ hard_break: () => '',
911
+ highlight_marker: () => '',
912
+ inline_footnote: (node) => {
913
+ const rids: string[] = node.attrs.rids
914
+ const xref = this.createElement('xref')
915
+ xref.setAttribute('ref-type', 'fn')
916
+ xref.setAttribute('rid', normalizeID(rids.join(' ')))
917
+ xref.textContent = rids
918
+ .map((rid) => this.footnoteLabels.get(rid))
919
+ .join(', ')
920
+ return xref
921
+ },
922
+ keyword: () => '',
923
+ keywords_element: () => '',
924
+ keywords: () => '',
925
+ link: (node) => {
926
+ const text = node.textContent
927
+
928
+ if (!text) {
929
+ return ''
930
+ }
931
+
932
+ if (!node.attrs.href) {
933
+ return text
934
+ }
935
+
936
+ const linkNode = this.createElement('ext-link')
937
+ linkNode.setAttribute('ext-link-type', 'uri')
938
+ linkNode.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.href)
939
+ linkNode.textContent = text
940
+
941
+ if (node.attrs.title) {
942
+ linkNode.setAttributeNS(
943
+ XLINK_NAMESPACE,
944
+ 'xlink:title',
945
+ node.attrs.title
946
+ )
947
+ }
948
+
949
+ return linkNode
950
+ },
951
+ list_item: () => ['list-item', 0],
952
+ listing: (node) => {
953
+ const code = this.createElement('code')
954
+ code.setAttribute('id', normalizeID(node.attrs.id))
955
+ code.setAttribute('language', node.attrs.languageKey)
956
+ code.textContent = node.attrs.contents
957
+
958
+ return code
959
+ },
960
+ listing_element: (node) =>
961
+ createFigureElement(node, node.type.schema.nodes.listing),
962
+ manuscript: (node) => ['article', { id: normalizeID(node.attrs.id) }, 0],
963
+ missing_figure: () => {
964
+ const graphic = this.createElement('graphic')
965
+ graphic.setAttribute('specific-use', 'MISSING')
966
+ graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', '')
967
+ return graphic
968
+ },
969
+ paragraph: (node) => {
970
+ if (!node.childCount) {
971
+ return ''
972
+ }
973
+
974
+ const attrs: Attrs = {}
975
+
976
+ if (node.attrs.id) {
977
+ attrs.id = normalizeID(node.attrs.id)
978
+ }
979
+
980
+ if (node.attrs.contentType) {
981
+ attrs['content-type'] = node.attrs.contentType
982
+ }
983
+
984
+ return ['p', attrs, 0]
985
+ },
986
+ placeholder: () => {
987
+ return this.createElement('boxed-text')
988
+ },
989
+ placeholder_element: () => {
990
+ return this.createElement('boxed-text')
991
+ },
992
+ pullquote_element: (node) => {
993
+ let type = 'pullquote'
994
+ if (node.firstChild?.type === schema.nodes.quote_image) {
995
+ type = 'quote-with-image'
996
+ }
997
+ return ['disp-quote', { 'content-type': type }, 0]
998
+ },
999
+ quote_image: (node) => {
1000
+ const img = node as QuoteImageNode
1001
+ if (img.attrs.src) {
1002
+ return createGraphic(node)
1003
+ }
1004
+ return ''
1005
+ },
1006
+ graphical_abstract_section: (node) => {
1007
+ const attrs: { [key: string]: string } = {
1008
+ id: normalizeID(node.attrs.id),
1009
+ }
1010
+ if (node.attrs.category) {
1011
+ attrs['sec-type'] = node.attrs.category
1012
+ }
1013
+ return ['sec', attrs, 0]
1014
+ },
1015
+ section: (node) => {
1016
+ const attrs: { [key: string]: string } = {
1017
+ id: normalizeID(node.attrs.id),
1018
+ }
1019
+
1020
+ if (node.attrs.category) {
1021
+ attrs['sec-type'] = node.attrs.category
1022
+ }
1023
+
1024
+ return ['sec', attrs, 0]
1025
+ },
1026
+ section_label: () => ['label', 0],
1027
+ section_title: () => ['title', 0],
1028
+ section_title_plain: () => ['title', 0],
1029
+ table: (node) => ['table', { id: normalizeID(node.attrs.id) }, 0],
1030
+ table_element: (node) => {
1031
+ const element = createTableElement(node)
1032
+ element.setAttribute('position', 'anchor')
1033
+ return element
1034
+ },
1035
+ table_cell: (node) => [
1036
+ 'td',
1037
+ {
1038
+ valign: node.attrs.valign,
1039
+ align: node.attrs.align,
1040
+ scope: node.attrs.scope,
1041
+ style: node.attrs.style,
1042
+ ...(node.attrs.rowspan > 1 && { rowspan: node.attrs.rowspan }),
1043
+ ...(node.attrs.colspan > 1 && { colspan: node.attrs.colspan }),
1044
+ },
1045
+ 0,
1046
+ ],
1047
+ table_header: (node) => [
1048
+ 'th',
1049
+ {
1050
+ valign: node.attrs.valign,
1051
+ align: node.attrs.align,
1052
+ scope: node.attrs.scope,
1053
+ style: node.attrs.style,
1054
+ ...(node.attrs.rowspan > 1 && { rowspan: node.attrs.rowspan }),
1055
+ ...(node.attrs.colspan > 1 && { colspan: node.attrs.colspan }),
1056
+ },
1057
+ 0,
1058
+ ],
1059
+ table_row: () => ['tr', 0],
1060
+ table_col: (node) => ['col', { width: node.attrs.width }],
1061
+ table_colgroup: () => ['colgroup', 0],
1062
+ text: (node) => node.text as string,
1063
+ comment: () => '',
1064
+ }
1065
+
1066
+ const marks: MarkSpecs = {
1067
+ bold: () => ['bold'],
1068
+ code: () => ['code', { position: 'anchor' }],
1069
+ italic: () => ['italic'],
1070
+ smallcaps: () => ['sc'],
1071
+ strikethrough: () => ['strike'],
1072
+ //I couldn't find any examples for this to test
1073
+ styled: () => ['styled-content'],
1074
+ superscript: () => ['sup'],
1075
+ subscript: () => ['sub'],
1076
+ underline: () => ['underline'],
1077
+ tracked_insert: () => ['ins'],
1078
+ tracked_delete: () => ['del'],
1079
+ }
1080
+
1081
+ this.serializer = new DOMSerializer(nodes, marks)
1082
+ const appendChildIfPresent = (
1083
+ parent: Element,
1084
+ tagName: string,
1085
+ textContent: string
1086
+ ) => {
1087
+ if (!textContent) {
1088
+ return
1089
+ }
1090
+ const element = this.createElement(tagName)
1091
+ element.textContent = textContent
1092
+ parent.appendChild(element)
1093
+ }
1094
+ const processChildNodes = (
1095
+ element: HTMLElement,
1096
+ node: ManuscriptNode,
1097
+ contentNodeType: ManuscriptNodeType
1098
+ ) => {
1099
+ node.forEach((childNode) => {
1100
+ if (childNode.type === contentNodeType) {
1101
+ if (childNode.attrs.id) {
1102
+ element.appendChild(this.serializeNode(childNode))
1103
+ }
1104
+ } else if (childNode.type === node.type.schema.nodes.paragraph) {
1105
+ element.appendChild(this.serializeNode(childNode))
1106
+ } else if (childNode.type === node.type.schema.nodes.missing_figure) {
1107
+ element.appendChild(this.serializeNode(childNode))
1108
+ }
1109
+ })
1110
+ }
1111
+ const createElement = (node: ManuscriptNode, nodeName: string) => {
1112
+ const element = this.createElement(nodeName)
1113
+ element.setAttribute('id', normalizeID(node.attrs.id))
1114
+ return element
1115
+ }
1116
+
1117
+ const appendLabels = (element: HTMLElement, node: ManuscriptNode) => {
1118
+ if (this.labelTargets) {
1119
+ const target = this.labelTargets.get(node.attrs.id)
1120
+
1121
+ if (target) {
1122
+ const label = this.createElement('label')
1123
+ label.textContent = target.label
1124
+ element.appendChild(label)
1125
+ }
1126
+ }
1127
+ }
1128
+ const appendAttributions = (element: HTMLElement, node: ManuscriptNode) => {
1129
+ if (node.attrs.attribution) {
1130
+ const attribution = this.createElement('attrib')
1131
+ attribution.textContent = node.attrs.attribution.literal
1132
+ element.appendChild(attribution)
1133
+ }
1134
+ }
1135
+
1136
+ const appendTable = (element: HTMLElement, node: ManuscriptNode) => {
1137
+ const tableNode = this.getFirstChildOfType(schema.nodes.table, node)
1138
+ const colGroupNode = this.getFirstChildOfType(
1139
+ schema.nodes.table_colgroup,
1140
+ node
1141
+ )
1142
+ if (!tableNode) {
1143
+ return
1144
+ }
1145
+ const table = this.serializeNode(tableNode)
1146
+ const tbodyElement = this.createElement('tbody')
1147
+
1148
+ while (table.firstChild) {
1149
+ const child = table.firstChild
1150
+ table.removeChild(child)
1151
+ tbodyElement.appendChild(child)
1152
+ }
1153
+ table.appendChild(tbodyElement)
1154
+ if (colGroupNode) {
1155
+ const colGroup = this.serializeNode(colGroupNode)
1156
+ table.insertBefore(colGroup, table.firstChild)
1157
+ }
1158
+
1159
+ element.appendChild(table)
1160
+ }
1161
+ const createBoxElement = (node: ManuscriptNode) => {
1162
+ const element = createElement(node, 'boxed-text')
1163
+ appendLabels(element, node)
1164
+ const child = node.firstChild
1165
+ if (child?.type === schema.nodes.caption_title) {
1166
+ this.appendCaption(element, node)
1167
+ }
1168
+
1169
+ processChildNodes(element, node, node.type.schema.nodes.section)
1170
+ return element
1171
+ }
1172
+
1173
+ const isChildOfNodeType = (
1174
+ targetID: string,
1175
+ type: NodeType,
1176
+ descend = false
1177
+ ): boolean => {
1178
+ const nodes = this.getChildrenOfType(type)
1179
+ return nodes.some((node) => {
1180
+ const result = findChildrenByAttr(
1181
+ node,
1182
+ (attrs) => attrs.id === targetID,
1183
+ descend
1184
+ )[0]
1185
+ return !!result
1186
+ })
1187
+ }
1188
+
1189
+ const createImage = (node: ManuscriptNode) => {
1190
+ const graphicNode = node.content.firstChild
1191
+ if (!graphicNode) {
1192
+ return ''
1193
+ }
1194
+ const graphicElement = createGraphic(graphicNode)
1195
+ if (node.attrs.extLink) {
1196
+ const extLink = this.appendElement(graphicElement, 'ext-link')
1197
+ extLink.setAttributeNS(XLINK_NAMESPACE, 'href', node.attrs.extLink)
1198
+ }
1199
+ this.appendCaption(graphicElement, node)
1200
+ this.appendChildNodeOfType(graphicElement, node, schema.nodes.alt_text)
1201
+ this.appendChildNodeOfType(graphicElement, node, schema.nodes.long_desc)
1202
+ return graphicElement
1203
+ }
1204
+
1205
+ const createGraphic = (node: ManuscriptNode) => {
1206
+ const graphic = this.createElement('graphic')
1207
+ graphic.setAttributeNS(XLINK_NAMESPACE, 'xlink:href', node.attrs.src)
1208
+
1209
+ if (isChildOfNodeType(node.attrs.id, schema.nodes.hero_image)) {
1210
+ graphic.setAttribute('content-type', 'leading')
1211
+ } else if (
1212
+ !isChildOfNodeType(node.attrs.id, schema.nodes.figure_element) &&
1213
+ node.attrs.type
1214
+ ) {
1215
+ graphic.setAttribute('content-type', node.attrs.type)
1216
+ }
1217
+ return graphic
1218
+ }
1219
+ const createFigureElement = (
1220
+ node: ManuscriptNode,
1221
+ contentNodeType: ManuscriptNodeType
1222
+ ) => {
1223
+ const element = createElement(node, 'fig')
1224
+ const figNode = this.getFirstChildOfType(schema.nodes.figure, node)
1225
+ const figType = figNode?.attrs.type
1226
+ if (figType) {
1227
+ element.setAttribute('fig-type', figType)
1228
+ }
1229
+ appendLabels(element, node)
1230
+ this.appendCaption(element, node)
1231
+ this.appendChildNodeOfType(element, node, schema.nodes.alt_text)
1232
+ this.appendChildNodeOfType(element, node, schema.nodes.long_desc)
1233
+ this.appendChildNodeOfType(
1234
+ element,
1235
+ node,
1236
+ node.type.schema.nodes.footnotes_element
1237
+ )
1238
+ processChildNodes(element, node, contentNodeType)
1239
+ appendAttributions(element, node)
1240
+ if (isExecutableNodeType(node.type)) {
1241
+ processExecutableNode(node, element)
1242
+ }
1243
+ moveAltTextAndLongDescToGraphics(element)
1244
+ return element
1245
+ }
1246
+
1247
+ const moveAltTextAndLongDescToGraphics = (element: Element) => {
1248
+ const altText = element.querySelector('alt-text')
1249
+ const longDesc = element.querySelector('long-desc')
1250
+ const graphics = element.querySelectorAll('graphic')
1251
+
1252
+ if (graphics.length === 0) {
1253
+ return
1254
+ }
1255
+
1256
+ graphics.forEach((graphic) => {
1257
+ if (longDesc) {
1258
+ graphic.prepend(longDesc.cloneNode(true))
1259
+ }
1260
+ if (altText) {
1261
+ graphic.prepend(altText.cloneNode(true))
1262
+ }
1263
+ })
1264
+
1265
+ altText?.remove()
1266
+ longDesc?.remove()
1267
+ }
1268
+
1269
+ const createTableElement = (node: ManuscriptNode) => {
1270
+ const nodeName = 'table-wrap'
1271
+ const element = createElement(node, nodeName)
1272
+ appendLabels(element, node)
1273
+ this.appendCaption(element, node)
1274
+ this.appendChildNodeOfType(element, node, schema.nodes.alt_text)
1275
+ this.appendChildNodeOfType(element, node, schema.nodes.long_desc)
1276
+ appendTable(element, node)
1277
+ this.appendChildNodeOfType(
1278
+ element,
1279
+ node,
1280
+ node.type.schema.nodes.table_element_footer
1281
+ )
1282
+ if (isExecutableNodeType(node.type)) {
1283
+ processExecutableNode(node, element)
1284
+ }
1285
+ return element
1286
+ }
1287
+ const processExecutableNode = (node: ManuscriptNode, element: Element) => {
1288
+ const listingNode = this.getFirstChildOfType(schema.nodes.listing, node)
1289
+
1290
+ if (listingNode) {
1291
+ const { contents, languageKey } = listingNode.attrs
1292
+
1293
+ if (contents && languageKey) {
1294
+ const listing = this.createElement('fig')
1295
+ listing.setAttribute('specific-use', 'source')
1296
+ element.appendChild(listing)
1297
+
1298
+ const code = this.createElement('code')
1299
+ code.setAttribute('executable', 'true')
1300
+ code.setAttribute('language', languageKey)
1301
+ code.textContent = contents
1302
+ listing.appendChild(code)
1303
+ }
1304
+ }
1305
+ }
1306
+ }
1307
+
1308
+ private createEquation(node: ManuscriptNode, isInline = false) {
1309
+ if (node.attrs.format === 'tex') {
1310
+ const texMath = this.createElement('tex-math')
1311
+ texMath.setAttribute('notation', 'LaTeX')
1312
+ texMath.setAttribute('version', 'MathJax')
1313
+ if (node.attrs.contents.includes('<![CDATA[')) {
1314
+ texMath.innerHTML = node.attrs.contents
1315
+ } else {
1316
+ texMath.innerHTML = `<![CDATA[ ${node.attrs.contents} ]]>`
1317
+ }
1318
+ return texMath
1319
+ } else {
1320
+ const math = this.nodeFromJATS(node.attrs.contents)
1321
+ const mathml = math as Element
1322
+ if (!isInline) {
1323
+ mathml.setAttribute('id', normalizeID(node.attrs.id))
1324
+ }
1325
+ return mathml
1326
+ }
1327
+ }
1328
+
1329
+ protected serializeNode = (node: ManuscriptNode) =>
1330
+ this.serializer.serializeNode(node, {
1331
+ document: this.document,
1332
+ })
1333
+
1334
+ private buildContributors = (articleMeta: Node) => {
1335
+ const contributors = this.getChildrenOfType<ContributorNode>(
1336
+ schema.nodes.contributor
1337
+ ).sort(sortContributors)
1338
+
1339
+ if (!contributors.length) {
1340
+ return
1341
+ }
1342
+
1343
+ const labels = new Map<string, number>()
1344
+ const createLabel = (id: string) => {
1345
+ let label = labels.get(id)
1346
+ if (!label) {
1347
+ label = labels.size + 1
1348
+ labels.set(id, label)
1349
+ }
1350
+ const sup = this.createElement('sup')
1351
+ sup.textContent = String(label)
1352
+ return sup
1353
+ }
1354
+
1355
+ const $contribGroup = this.createElement('contrib-group')
1356
+ $contribGroup.setAttribute('content-type', 'authors')
1357
+ articleMeta.appendChild($contribGroup)
1358
+
1359
+ contributors.forEach((contributor) => {
1360
+ const $contrib = this.createElement('contrib')
1361
+ $contrib.setAttribute('contrib-type', 'author')
1362
+ $contrib.setAttribute('id', normalizeID(contributor.attrs.id))
1363
+
1364
+ if (contributor.attrs.isCorresponding) {
1365
+ $contrib.setAttribute('corresp', 'yes')
1366
+ }
1367
+
1368
+ if (contributor.attrs.role) {
1369
+ this.appendElement($contrib, 'role', contributor.attrs.role)
1370
+ }
1371
+
1372
+ if (contributor.attrs.ORCID) {
1373
+ const contribIdAttrs: Record<string, string> = {
1374
+ 'contrib-id-type': 'orcid',
1375
+ }
1376
+ if (contributor.attrs.isAuthenticated) {
1377
+ contribIdAttrs['authenticated'] = 'true'
1378
+ }
1379
+ this.appendElement(
1380
+ $contrib,
1381
+ 'contrib-id',
1382
+ contributor.attrs.ORCID,
1383
+ contribIdAttrs
1384
+ )
1385
+ }
1386
+
1387
+ const $name = this.buildContributorName(contributor)
1388
+ $contrib.appendChild($name)
1389
+
1390
+ if (contributor.attrs.suffix) {
1391
+ this.appendElement($name, 'suffix', contributor.attrs.suffix)
1392
+ }
1393
+
1394
+ contributor.attrs.degrees?.forEach((degree) => {
1395
+ this.appendElement($contrib, 'degrees', degree)
1396
+ })
1397
+
1398
+ if (contributor.attrs.email) {
1399
+ this.appendElement($contrib, 'email', contributor.attrs.email)
1400
+ }
1401
+
1402
+ contributor.attrs.affiliationIDs?.forEach((rid) => {
1403
+ const $xref = this.appendElement($contrib, 'xref', '', {
1404
+ 'ref-type': 'aff',
1405
+ rid: normalizeID(rid),
1406
+ })
1407
+ $xref.appendChild(createLabel(rid))
1408
+ })
1409
+
1410
+ contributor.attrs.footnoteIDs?.forEach((rid) => {
1411
+ const $xref = this.appendElement($contrib, 'xref', '', {
1412
+ 'ref-type': 'fn',
1413
+ rid: normalizeID(rid),
1414
+ })
1415
+ $xref.appendChild(createLabel(rid))
1416
+ })
1417
+
1418
+ contributor.attrs.correspIDs?.forEach((rid) => {
1419
+ const $xref = this.appendElement($contrib, 'xref', '', {
1420
+ 'ref-type': 'corresp',
1421
+ rid: normalizeID(rid),
1422
+ })
1423
+ $xref.appendChild(createLabel(rid))
1424
+ })
1425
+
1426
+ contributor.attrs.creditRoles?.forEach((credit) => {
1427
+ const url = CreditRoleUrls.get(credit.vocabTerm)
1428
+ if (!url) {
1429
+ return
1430
+ }
1431
+ this.appendElement($contrib, 'role', credit.vocabTerm, {
1432
+ 'vocab-identifier': 'http://credit.niso.org/',
1433
+ vocab: 'CRediT',
1434
+ 'vocab-term': credit.vocabTerm,
1435
+ 'vocab-term-identifier': url,
1436
+ })
1437
+ })
1438
+ $contribGroup.appendChild($contrib)
1439
+ })
1440
+
1441
+ const affiliationIDs = contributors.flatMap((n) => n.attrs.affiliationIDs)
1442
+ const sortAffiliations = (a: AffiliationNode, b: AffiliationNode) =>
1443
+ affiliationIDs.indexOf(a.attrs.id) - affiliationIDs.indexOf(b.attrs.id)
1444
+
1445
+ this.getChildrenOfType<AffiliationNode>(schema.nodes.affiliation)
1446
+ .filter((a) => affiliationIDs.includes(a.attrs.id))
1447
+ .sort(sortAffiliations)
1448
+ .forEach((affiliation) => {
1449
+ const $content = []
1450
+
1451
+ if (affiliation.attrs.department) {
1452
+ const $institution = this.createElement(
1453
+ 'institution',
1454
+ affiliation.attrs.department,
1455
+ {
1456
+ 'content-type': 'dept',
1457
+ }
1458
+ )
1459
+ $content.push($institution)
1460
+ }
1461
+
1462
+ if (affiliation.attrs.institution) {
1463
+ const $institution = this.createElement(
1464
+ 'institution',
1465
+ affiliation.attrs.institution
1466
+ )
1467
+ $content.push($institution)
1468
+ }
1469
+
1470
+ if (affiliation.attrs.addressLine1) {
1471
+ const $addrLine = this.createElement(
1472
+ 'addr-line',
1473
+ affiliation.attrs.addressLine1
1474
+ )
1475
+ $content.push($addrLine)
1476
+ }
1477
+
1478
+ if (affiliation.attrs.addressLine2) {
1479
+ const $addrLine = this.createElement(
1480
+ 'addr-line',
1481
+ affiliation.attrs.addressLine2
1482
+ )
1483
+ $content.push($addrLine)
1484
+ }
1485
+
1486
+ if (affiliation.attrs.addressLine3) {
1487
+ const $addrLine = this.createElement(
1488
+ 'addr-line',
1489
+ affiliation.attrs.addressLine3
1490
+ )
1491
+ $content.push($addrLine)
1492
+ }
1493
+
1494
+ if (affiliation.attrs.city) {
1495
+ const $city = this.createElement('city', affiliation.attrs.city)
1496
+ $content.push($city)
1497
+ }
1498
+
1499
+ if (affiliation.attrs.county) {
1500
+ const $state = this.createElement('state', affiliation.attrs.county)
1501
+ $content.push($state)
1502
+ }
1503
+
1504
+ if (affiliation.attrs.country) {
1505
+ const $country = this.createElement(
1506
+ 'country',
1507
+ affiliation.attrs.country
1508
+ )
1509
+ $content.push($country)
1510
+ }
1511
+
1512
+ if (affiliation.attrs.postCode) {
1513
+ const $postalCode = this.createElement(
1514
+ 'postal-code',
1515
+ affiliation.attrs.postCode
1516
+ )
1517
+ $content.push($postalCode)
1518
+ }
1519
+
1520
+ const $aff = this.createElement('aff')
1521
+ $aff.setAttribute('id', normalizeID(affiliation.attrs.id))
1522
+ $contribGroup.appendChild($aff)
1523
+
1524
+ const label = labels.get(affiliation.attrs.id)
1525
+ if (label) {
1526
+ this.appendElement($aff, 'label', String(label))
1527
+ }
1528
+
1529
+ $content.forEach((node, i) => {
1530
+ if (i > 0) {
1531
+ $aff.appendChild(this.document.createTextNode(', '))
1532
+ }
1533
+ $aff.appendChild(node)
1534
+ })
1535
+ })
1536
+
1537
+ const $authorNotes = this.createAuthorNotesElement(contributors)
1538
+ if ($authorNotes) {
1539
+ articleMeta.insertBefore($authorNotes, $contribGroup.nextSibling)
1540
+ }
1541
+ }
1542
+
1543
+ private buildContributorName = (contributor: ContributorNode) => {
1544
+ const { given, family } = contributor.attrs
1545
+ if (Boolean(given) !== Boolean(family)) {
1546
+ return this.createElement('string-name', given || family)
1547
+ }
1548
+
1549
+ const name = this.createElement('name')
1550
+
1551
+ if (contributor.attrs.family) {
1552
+ this.appendElement(name, 'surname', contributor.attrs.family)
1553
+ }
1554
+
1555
+ if (contributor.attrs.given) {
1556
+ this.appendElement(name, 'given-names', contributor.attrs.given)
1557
+ }
1558
+
1559
+ if (contributor.attrs.prefix) {
1560
+ this.appendElement(name, 'prefix', contributor.attrs.prefix)
1561
+ }
1562
+
1563
+ return name
1564
+ }
1565
+
1566
+ private createAuthorNotesElement = (contributors: ContributorNode[]) => {
1567
+ const authorNotes = this.getFirstChildOfType<AuthorNotesNode>(
1568
+ schema.nodes.author_notes
1569
+ )
1570
+ if (!authorNotes || !authorNotes.childCount) {
1571
+ return
1572
+ }
1573
+
1574
+ const $authorNotes = this.createElement('author-notes')
1575
+
1576
+ const used = contributors.flatMap((c) => c.attrs.correspIDs)
1577
+
1578
+ authorNotes.descendants((node) => {
1579
+ switch (node.type) {
1580
+ case schema.nodes.paragraph: {
1581
+ const $p = this.writeParagraph(node as ParagraphNode)
1582
+ $authorNotes.append($p)
1583
+ break
1584
+ }
1585
+ case schema.nodes.footnote: {
1586
+ const $fn = this.writeFootnote(node as FootnoteNode)
1587
+ $authorNotes.append($fn)
1588
+ break
1589
+ }
1590
+ case schema.nodes.corresp: {
1591
+ if (used.includes(node.attrs.id)) {
1592
+ const $corresp = this.writeCorresp(node as CorrespNode)
1593
+ $authorNotes.append($corresp)
1594
+ }
1595
+ break
1596
+ }
1597
+ }
1598
+ return false
1599
+ })
1600
+ return $authorNotes.hasChildNodes() ? $authorNotes : undefined
1601
+ }
1602
+
1603
+ private writeCorresp = (corresp: CorrespNode) => {
1604
+ const $corresp = this.createElement('corresp')
1605
+ $corresp.setAttribute('id', normalizeID(corresp.attrs.id))
1606
+ if (corresp.attrs.label) {
1607
+ this.appendElement($corresp, 'label', corresp.attrs.label)
1608
+ }
1609
+ $corresp.append(corresp.textContent)
1610
+ return $corresp
1611
+ }
1612
+
1613
+ private writeParagraph = (paragraph: ParagraphNode) => {
1614
+ const dom = new DOMParser().parseFromString(
1615
+ paragraph.textContent,
1616
+ 'text/html'
1617
+ )
1618
+ const $p = this.createElement('p')
1619
+ $p.setAttribute('id', normalizeID(paragraph.attrs.id))
1620
+ if (dom.body.innerHTML.length) {
1621
+ $p.innerHTML = dom.body.innerHTML
1622
+ }
1623
+ return $p
1624
+ }
1625
+
1626
+ private writeFootnote = (footnote: FootnoteNode) => {
1627
+ const $fn = this.createElement('fn')
1628
+ $fn.setAttribute('id', normalizeID(footnote.attrs.id))
1629
+ let content = footnote.textContent
1630
+ if (!content.includes('<p>')) {
1631
+ content = `<p>${content}</p>`
1632
+ }
1633
+ $fn.innerHTML = content
1634
+ return $fn
1635
+ }
1636
+
1637
+ private buildKeywords(articleMeta: Node) {
1638
+ const keywordGroups = this.getChildrenOfType(schema.nodes.keyword_group)
1639
+
1640
+ keywordGroups.forEach((group) => {
1641
+ const kwdGroup = this.createElement('kwd-group')
1642
+ if (group.attrs.type) {
1643
+ kwdGroup.setAttribute('kwd-group-type', group.attrs.type)
1644
+ }
1645
+ articleMeta.appendChild(kwdGroup)
1646
+ group.content.forEach((keyword) => {
1647
+ const kwd = this.createElement('kwd')
1648
+ kwd.textContent = keyword.textContent
1649
+ kwdGroup.appendChild(kwd)
1650
+ })
1651
+ articleMeta.appendChild(kwdGroup)
1652
+ })
1653
+ }
1654
+
1655
+ private fixBody = (body: Element) => {
1656
+ this.manuscriptNode.descendants((node) => {
1657
+ if (node.attrs.id) {
1658
+ if (
1659
+ isNodeType<TableElementFooterNode>(node, 'general_table_footnote')
1660
+ ) {
1661
+ const generalTableFootnote = body.querySelector(
1662
+ `#${normalizeID(node.attrs.id)}`
1663
+ )
1664
+ if (generalTableFootnote) {
1665
+ Array.from(generalTableFootnote.childNodes).forEach((cn) => {
1666
+ generalTableFootnote.before(cn)
1667
+ })
1668
+ generalTableFootnote.remove()
1669
+ }
1670
+ }
1671
+ // move captions to the top of tables
1672
+ if (isNodeType<TableElementNode>(node, 'table_element')) {
1673
+ const tableElement = body.querySelector(
1674
+ `#${normalizeID(node.attrs.id)}`
1675
+ )
1676
+
1677
+ if (tableElement) {
1678
+ for (const childNode of tableElement.childNodes) {
1679
+ switch (childNode.nodeName) {
1680
+ case 'caption': {
1681
+ const label = tableElement.querySelector('label')
1682
+
1683
+ tableElement.insertBefore(
1684
+ childNode,
1685
+ label ? label.nextSibling : tableElement.firstChild
1686
+ )
1687
+ break
1688
+ }
1689
+
1690
+ case 'table': {
1691
+ this.fixTable(childNode)
1692
+ break
1693
+ }
1694
+ }
1695
+ }
1696
+ }
1697
+ }
1698
+ }
1699
+ })
1700
+ }
1701
+
1702
+ private changeTag = (node: Element, tag: string) => {
1703
+ const clone = this.createElement(tag)
1704
+ for (const attr of node.attributes) {
1705
+ clone.setAttributeNS(null, attr.name, attr.value)
1706
+ }
1707
+ while (node.firstChild) {
1708
+ clone.appendChild(node.firstChild)
1709
+ }
1710
+ node.replaceWith(clone)
1711
+ return clone
1712
+ }
1713
+
1714
+ private fixTable = (table: ChildNode) => {
1715
+ let tbody: Element | undefined
1716
+
1717
+ Array.from(table.childNodes).forEach((child) => {
1718
+ if (child instanceof Element && child.tagName.toLowerCase() === 'tbody') {
1719
+ tbody = child
1720
+ }
1721
+ })
1722
+
1723
+ if (!tbody) {
1724
+ return
1725
+ }
1726
+
1727
+ const tbodyRows = Array.from(tbody.childNodes)
1728
+ const thead = this.createElement('thead')
1729
+
1730
+ tbodyRows.forEach((row, i) => {
1731
+ const isRow = row instanceof Element && row.tagName.toLowerCase() === 'tr'
1732
+ if (isRow) {
1733
+ // we assume that <th scope="col | colgroup"> always belongs to <thead>
1734
+ const headerCell = (row as Element).querySelector(
1735
+ 'th[scope="col"], th[scope="colgroup"]'
1736
+ )
1737
+ if (i === 0 || headerCell) {
1738
+ tbody?.removeChild(row)
1739
+ const tableCells = (row as Element).querySelectorAll('td')
1740
+ for (const td of tableCells) {
1741
+ // for backwards compatibility since older docs use tds for header cells
1742
+ this.changeTag(td, 'th')
1743
+ }
1744
+ thead.appendChild(row)
1745
+ }
1746
+ }
1747
+ })
1748
+
1749
+ if (thead.hasChildNodes()) {
1750
+ table.insertBefore(thead, tbody as Element)
1751
+ }
1752
+ }
1753
+
1754
+ private unwrapBody = (body: HTMLElement) => {
1755
+ const container = body.querySelector(':scope > sec[sec-type="body"]')
1756
+ if (!container) {
1757
+ return
1758
+ }
1759
+ const sections = container.querySelectorAll(':scope > sec')
1760
+ sections.forEach((section) => {
1761
+ body.appendChild(section.cloneNode(true))
1762
+ })
1763
+ body.removeChild(container)
1764
+ }
1765
+
1766
+ private removeBackContainer = (body: HTMLElement) => {
1767
+ const container = body.querySelector(':scope > sec[sec-type="backmatter"]')
1768
+ if (!container) {
1769
+ return
1770
+ }
1771
+ body.removeChild(container)
1772
+ }
1773
+ private moveAwards = (front: HTMLElement, body: HTMLElement) => {
1774
+ const awardGroups = body.querySelectorAll(':scope > award-group')
1775
+ if (!awardGroups.length) {
1776
+ return
1777
+ }
1778
+ const fundingGroup = this.createElement('funding-group')
1779
+ awardGroups.forEach((award) => {
1780
+ fundingGroup.appendChild(award)
1781
+ })
1782
+ const articleMeta = front.querySelector(':scope > article-meta')
1783
+
1784
+ if (articleMeta) {
1785
+ const insertBeforeElement = articleMeta.querySelector(
1786
+ ':scope > support-group, :scope > conference, :scope > counts, :scope > custom-meta-group'
1787
+ )
1788
+ insertBeforeElement
1789
+ ? articleMeta.insertBefore(fundingGroup, insertBeforeElement)
1790
+ : articleMeta.appendChild(fundingGroup)
1791
+ }
1792
+ }
1793
+
1794
+ private moveAbstracts = (front: HTMLElement, body: HTMLElement) => {
1795
+ const abstractSections = this.getAbstractSections(body)
1796
+
1797
+ for (const abstractSection of abstractSections) {
1798
+ const node =
1799
+ abstractSection.nodeName === 'trans-abstract'
1800
+ ? this.createTransAbstractNode(abstractSection)
1801
+ : this.createAbstractNode(abstractSection)
1802
+ abstractSection.remove()
1803
+ insertAbstractNode(front, node)
1804
+ }
1805
+ }
1806
+
1807
+ private getAbstractCategories(
1808
+ abstractsNode: ManuscriptNode | undefined
1809
+ ): string[] {
1810
+ const categories: string[] = []
1811
+ abstractsNode?.content.descendants((node) => {
1812
+ categories.push(node.attrs.category)
1813
+ return false
1814
+ })
1815
+ return categories
1816
+ }
1817
+
1818
+ private getAbstractSections(body: HTMLElement) {
1819
+ {
1820
+ const abstractsNode = this.getFirstChildOfType(schema.nodes.abstracts)
1821
+ const abstractCategories = this.getAbstractCategories(abstractsNode)
1822
+ const sections = Array.from(
1823
+ body.querySelectorAll(':scope > sec, :scope > trans-abstract')
1824
+ )
1825
+ return sections.filter((section) =>
1826
+ this.isAbstractSection(section, abstractCategories)
1827
+ )
1828
+ }
1829
+ }
1830
+
1831
+ private isAbstractSection(
1832
+ section: Element,
1833
+ abstractCategories: string[]
1834
+ ): boolean {
1835
+ const sectionType = section.getAttribute('sec-type')
1836
+ return sectionType ? abstractCategories.includes(sectionType) : false
1837
+ }
1838
+
1839
+ private createTransAbstractNode(transAbstract: Element): Element {
1840
+ const transAbstractNode = this.createElement('trans-abstract')
1841
+ transAbstractNode.setAttributeNS(
1842
+ XML_NAMESPACE,
1843
+ 'lang',
1844
+ transAbstract.getAttributeNS(XML_NAMESPACE, 'lang') ?? ''
1845
+ )
1846
+ this.setAbstractType(transAbstractNode, transAbstract)
1847
+ transAbstractNode.append(...transAbstract.childNodes)
1848
+ return transAbstractNode
1849
+ }
1850
+
1851
+ private createAbstractNode(abstractSection: Element): Element {
1852
+ const abstractNode = this.createElement('abstract')
1853
+ for (const node of abstractSection.childNodes) {
1854
+ if (node.nodeName !== 'title') {
1855
+ abstractNode.appendChild(node.cloneNode(true))
1856
+ }
1857
+ }
1858
+ this.setAbstractType(abstractNode, abstractSection)
1859
+ return abstractNode
1860
+ }
1861
+
1862
+ private setAbstractType(abstractNode: Element, abstractSection: Element) {
1863
+ const sectionType = abstractSection.getAttribute('sec-type')
1864
+ if (sectionType && sectionType !== 'abstract') {
1865
+ const abstractType = sectionType.replace('abstract-', '')
1866
+ abstractNode.setAttribute('abstract-type', abstractType)
1867
+ }
1868
+ }
1869
+
1870
+ private moveSectionsToBack = (back: HTMLElement, body: HTMLElement) => {
1871
+ const availabilitySection = body.querySelector(
1872
+ 'sec[sec-type="availability"]'
1873
+ )
1874
+
1875
+ if (availabilitySection) {
1876
+ back.insertBefore(availabilitySection, back.firstChild)
1877
+ }
1878
+
1879
+ const ethicsSection = body.querySelector('sec[sec-type="ethics-statement"]')
1880
+ if (ethicsSection) {
1881
+ back.appendChild(ethicsSection)
1882
+ }
1883
+
1884
+ const section = body.querySelector('sec[sec-type="acknowledgements"]')
1885
+
1886
+ if (section) {
1887
+ const ack = this.createElement('ack')
1888
+
1889
+ while (section.firstChild) {
1890
+ ack.appendChild(section.firstChild)
1891
+ }
1892
+
1893
+ if (section.parentNode) {
1894
+ section.parentNode.removeChild(section)
1895
+ }
1896
+
1897
+ back.insertBefore(ack, back.firstChild)
1898
+ }
1899
+ const appendicesSections = body.querySelectorAll(
1900
+ 'sec[sec-type="appendices"]'
1901
+ )
1902
+
1903
+ if (appendicesSections) {
1904
+ const appGroup = this.createElement('app-group')
1905
+ appendicesSections.forEach((section) => {
1906
+ if (section.parentNode) {
1907
+ section.parentNode.removeChild(section)
1908
+ }
1909
+ const app = this.createElement('app')
1910
+ app.appendChild(section)
1911
+ appGroup.appendChild(app)
1912
+ })
1913
+ back.insertBefore(appGroup, back.firstChild)
1914
+ }
1915
+
1916
+ const footNotes = []
1917
+
1918
+ const footnoteCategories = [
1919
+ 'con',
1920
+ 'conflict',
1921
+ 'deceased',
1922
+ 'equal',
1923
+ 'present-address',
1924
+ 'presented-at',
1925
+ 'previously-at',
1926
+ 'supplementary-material',
1927
+ 'supported-by',
1928
+ 'financial-disclosure',
1929
+ 'coi-statement',
1930
+ ]
1931
+
1932
+ const sections = body.querySelectorAll('sec')
1933
+ for (const currentSection of sections) {
1934
+ const currentSectionType = currentSection.getAttribute('sec-type')
1935
+ if (
1936
+ currentSectionType &&
1937
+ footnoteCategories.indexOf(currentSectionType) >= 0
1938
+ ) {
1939
+ footNotes.push(
1940
+ this.sectionToFootnote(currentSection, currentSectionType)
1941
+ )
1942
+ }
1943
+ }
1944
+
1945
+ if (footNotes.length > 0) {
1946
+ const fnGroup = this.createElement('fn-group')
1947
+ fnGroup.append(...footNotes)
1948
+ back.append(fnGroup)
1949
+ }
1950
+ }
1951
+
1952
+ sectionToFootnote = (section: Element, fnType: string) => {
1953
+ const footNote = this.createElement('fn')
1954
+ footNote.setAttribute('fn-type', fnType)
1955
+ const title = section.querySelector('title')
1956
+ if (title) {
1957
+ const footNoteTitle = this.createElement('label')
1958
+ footNoteTitle.textContent = title.textContent
1959
+ section.removeChild(title)
1960
+ footNote.append(footNoteTitle)
1961
+ }
1962
+ footNote.append(...section.children)
1963
+ if (section.parentNode) {
1964
+ section.parentNode.removeChild(section)
1965
+ }
1966
+ return footNote
1967
+ }
1968
+ private moveFloatsGroup = (article: HTMLElement) => {
1969
+ const heroImage = this.getFirstChildOfType(schema.nodes.hero_image)
1970
+ if (!heroImage) {
1971
+ return
1972
+ }
1973
+ const floatsGroup = this.createElement('floats-group')
1974
+ let figure: HTMLElement | null = null
1975
+ heroImage.descendants((node) => {
1976
+ if (node.type === schema.nodes.figure) {
1977
+ figure = this.serializeNode(node) as HTMLElement
1978
+ floatsGroup.appendChild(figure)
1979
+ } else {
1980
+ const serializedNode = this.serializeNode(node)
1981
+ figure?.appendChild(serializedNode)
1982
+ }
1983
+ return false
1984
+ })
1985
+
1986
+ if (floatsGroup.children.length > 0) {
1987
+ article.appendChild(floatsGroup)
1988
+ }
1989
+ }
1990
+
1991
+ private moveCoiStatementToAuthorNotes(back: HTMLElement, front: HTMLElement) {
1992
+ const fnGroups = back.querySelectorAll('fn-group')
1993
+ fnGroups.forEach((fnGroup) => {
1994
+ if (fnGroup) {
1995
+ const coiStatement = fnGroup.querySelector(
1996
+ 'fn[fn-type="coi-statement"]'
1997
+ )
1998
+ if (coiStatement) {
1999
+ const authorNotes = this.createElement('author-notes')
2000
+ authorNotes.append(coiStatement)
2001
+ const articleMeta = front.querySelector('article-meta')
2002
+ if (articleMeta) {
2003
+ const authorNoteEl = articleMeta.querySelector('author-notes')
2004
+ if (authorNoteEl) {
2005
+ authorNoteEl.append(...authorNotes.childNodes)
2006
+ } else {
2007
+ const appendableSelectors = [
2008
+ 'contrib-group',
2009
+ 'title-group',
2010
+ 'article-id',
2011
+ ]
2012
+ const appendable = [
2013
+ ...(articleMeta as HTMLElement).querySelectorAll(
2014
+ appendableSelectors.join(', ')
2015
+ ),
2016
+ ]
2017
+ for (let i = 0; i < appendableSelectors.length; i++) {
2018
+ const sel = appendableSelectors[i]
2019
+ const match = appendable.find((el) => el.matches(sel))
2020
+ if (match) {
2021
+ articleMeta.insertBefore(authorNotes, match.nextSibling)
2022
+ break
2023
+ }
2024
+ }
2025
+ }
2026
+ }
2027
+ if (!fnGroup.hasChildNodes()) {
2028
+ fnGroup.remove()
2029
+ }
2030
+ }
2031
+ }
2032
+ })
2033
+ }
2034
+
2035
+ private updateFootnoteTypes(front: HTMLElement, body: HTMLElement) {
2036
+ const footnotes: Element[] = [...front.querySelectorAll('fn').values()]
2037
+ footnotes.push(...body.querySelectorAll('fn'))
2038
+ footnotes.forEach((fn) => {
2039
+ const fnType = fn.getAttribute('fn-type')
2040
+ if (fnType) {
2041
+ fn.setAttribute('fn-type', fnType)
2042
+ }
2043
+ })
2044
+ }
2045
+
2046
+ private fillEmptyElements(
2047
+ articleElement: Element,
2048
+ selector: string,
2049
+ tagName = 'p'
2050
+ ) {
2051
+ const emptyElements = Array.from(
2052
+ articleElement.querySelectorAll(selector)
2053
+ ).filter((element) => !element.innerHTML)
2054
+ emptyElements.forEach((element) =>
2055
+ element.appendChild(this.createElement(tagName))
2056
+ )
2057
+ }
2058
+ private addParagraphsToSections(articleElement: Element) {
2059
+ const sections = articleElement.querySelectorAll('sec')
2060
+ const TITLE_TAGS = new Set(['title', 'label', 'sec-meta'])
2061
+ for (const section of sections) {
2062
+ const hasContent = Array.from(section.children).some(
2063
+ (child) => !TITLE_TAGS.has(child.tagName)
2064
+ )
2065
+ if (hasContent) {
2066
+ continue
2067
+ }
2068
+ const p = this.createElement('p')
2069
+ const insertAfterElement =
2070
+ section.querySelector(':scope > title') ??
2071
+ section.querySelector(':scope > label') ??
2072
+ section.querySelector(':scope > sec-meta')
2073
+
2074
+ if (insertAfterElement) {
2075
+ insertAfterElement.insertAdjacentElement('afterend', p)
2076
+ } else {
2077
+ section.prepend(p)
2078
+ }
2079
+ }
2080
+ }
2081
+
2082
+ private fillEmptyFootnotes(articleElement: Element) {
2083
+ this.fillEmptyElements(articleElement, 'fn')
2084
+ }
2085
+
2086
+ private fillEmptyTableFooters(articleElement: Element) {
2087
+ this.fillEmptyElements(articleElement, 'table-wrap-foot')
2088
+ }
2089
+
2090
+ private fillEmptyListItem(articleElement: Element) {
2091
+ this.fillEmptyElements(articleElement, 'list-item')
2092
+ }
2093
+ }