@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,1260 @@
1
+ /*!
2
+ * © 2020 Atypon Systems LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { DOMParser, Fragment, ParseOptions, Schema } from 'prosemirror-model'
17
+
18
+ import {
19
+ dateToTimestamp,
20
+ getCreditRole,
21
+ getHTMLContent,
22
+ getTrimmedTextContent,
23
+ } from '../../lib/utils'
24
+ import { XLINK_NAMESPACE, XML_NAMESPACE } from '../../lib/xml'
25
+ import {
26
+ BibliographyItemAttrs,
27
+ BibliographyItemType,
28
+ ManuscriptNode,
29
+ MarkRule,
30
+ NodeRule,
31
+ SectionCategory,
32
+ } from '../../schema'
33
+ import { DEFAULT_PROFILE_ID } from './jats-comments'
34
+
35
+ export class JATSDOMParser {
36
+ private parser: DOMParser
37
+
38
+ constructor(
39
+ private sectionCategories: SectionCategory[],
40
+ private schema: Schema
41
+ ) {
42
+ this.parser = new DOMParser(this.schema, [...this.marks, ...this.nodes])
43
+ }
44
+
45
+ public parse(doc: Node, options?: ParseOptions) {
46
+ return this.parser.parse(doc, options)
47
+ }
48
+
49
+ private isMatchingCategory(
50
+ secType: string | null,
51
+ titleNode: Element | null,
52
+ category: SectionCategory
53
+ ) {
54
+ if (
55
+ (secType && category.synonyms.includes(secType)) ||
56
+ category.id === secType
57
+ ) {
58
+ return true
59
+ }
60
+ if (titleNode && titleNode.nodeName === 'title' && titleNode.textContent) {
61
+ const textContent = titleNode.textContent.trim().toLowerCase()
62
+ if (category.synonyms.includes(textContent)) {
63
+ return true
64
+ }
65
+ }
66
+ return false
67
+ }
68
+
69
+ private chooseSectionCategory(section: HTMLElement) {
70
+ const secType = section.getAttribute('sec-type')
71
+ const abstractType = section.getAttribute('abstract-type')
72
+ const effectiveSecType =
73
+ secType || (abstractType ? `abstract-${abstractType}` : null)
74
+ const titleNode = section.firstElementChild
75
+
76
+ for (const category of this.sectionCategories) {
77
+ if (this.isMatchingCategory(effectiveSecType, titleNode, category)) {
78
+ return category.id
79
+ }
80
+ }
81
+ }
82
+
83
+ private parsePriority = (priority: string | null) => {
84
+ if (!priority) {
85
+ return undefined
86
+ }
87
+ return parseInt(priority)
88
+ }
89
+
90
+ private getEquationContent = (p: string | HTMLElement) => {
91
+ const element = p as HTMLElement
92
+ const id = element.getAttribute('id')
93
+ const container = element.querySelector('alternatives') ?? element
94
+ let contents: string | null = ''
95
+ let format: string | null = ''
96
+ for (const child of container.childNodes) {
97
+ const nodeName = child.nodeName.replace(/^[a-z]:/, '')
98
+
99
+ switch (nodeName) {
100
+ case 'tex-math':
101
+ contents = child.textContent
102
+ format = 'tex'
103
+ break
104
+ case 'mml:math':
105
+ contents = (child as Element).outerHTML
106
+ format = 'mathml'
107
+ break
108
+ }
109
+ }
110
+ return { id, format, contents }
111
+ }
112
+
113
+ private parseHistoryDates = (historyNode: Element | null) => {
114
+ if (!historyNode) {
115
+ return undefined
116
+ }
117
+ const history: {
118
+ acceptanceDate?: number
119
+ correctionDate?: number
120
+ retractionDate?: number
121
+ revisionRequestDate?: number
122
+ revisionReceiveDate?: number
123
+ receiveDate?: number
124
+ } = {}
125
+
126
+ for (const date of historyNode.children) {
127
+ const dateType = date.getAttribute('date-type')
128
+ switch (dateType) {
129
+ case 'received': {
130
+ history.receiveDate = dateToTimestamp(date)
131
+ break
132
+ }
133
+ case 'rev-recd': {
134
+ history.revisionReceiveDate = dateToTimestamp(date)
135
+ break
136
+ }
137
+ case 'accepted': {
138
+ history.acceptanceDate = dateToTimestamp(date)
139
+ break
140
+ }
141
+ case 'rev-request': {
142
+ history.revisionRequestDate = dateToTimestamp(date)
143
+ break
144
+ }
145
+ case 'retracted': {
146
+ history.retractionDate = dateToTimestamp(date)
147
+ break
148
+ }
149
+ case 'corrected': {
150
+ history.correctionDate = dateToTimestamp(date)
151
+ break
152
+ }
153
+ }
154
+ }
155
+ return history
156
+ }
157
+
158
+ private getEmail = (element: HTMLElement) => {
159
+ const email = element.querySelector('email')
160
+ if (email) {
161
+ return {
162
+ href: email.getAttributeNS(XLINK_NAMESPACE, 'href') ?? '',
163
+ text: getTrimmedTextContent(email) ?? '',
164
+ }
165
+ }
166
+ }
167
+
168
+ private getFigAttrs = (node: HTMLElement | string | Node) => {
169
+ const element = node as HTMLElement
170
+ const extLink = element.querySelector('ext-link')
171
+ const parentElement = element.parentElement
172
+ return {
173
+ id: element.getAttribute('id'),
174
+ type:
175
+ parentElement?.getAttribute('fig-type') ??
176
+ element.getAttribute('content-type') ??
177
+ '',
178
+ src: element.getAttributeNS(XLINK_NAMESPACE, 'href'),
179
+ extLink: extLink?.getAttributeNS(XLINK_NAMESPACE, 'href'),
180
+ }
181
+ }
182
+
183
+ private getInstitutionDetails = (element: HTMLElement) => {
184
+ let department = ''
185
+ let institution = ''
186
+ for (const node of element.querySelectorAll('institution')) {
187
+ const content = getTrimmedTextContent(node)
188
+ if (!content) {
189
+ continue
190
+ }
191
+ const type = node.getAttribute('content-type')
192
+ if (type === 'dept') {
193
+ department = content
194
+ } else {
195
+ institution = content
196
+ }
197
+ }
198
+ return { department, institution }
199
+ }
200
+
201
+ private getAddressLine = (element: HTMLElement, index: number) => {
202
+ return (
203
+ getTrimmedTextContent(element, `addr-line:nth-of-type(${index})`) || ''
204
+ )
205
+ }
206
+
207
+ private getSurname = (element: HTMLElement) => {
208
+ const surname = getTrimmedTextContent(element, 'surname')
209
+ if (surname) {
210
+ return surname
211
+ }
212
+
213
+ const stringName = element.querySelector('string-name')
214
+ if (stringName && !stringName.querySelector('given-names')) {
215
+ return getTrimmedTextContent(element, 'string-name')
216
+ }
217
+ }
218
+
219
+ private getRefType = (element: Element): BibliographyItemType => {
220
+ const citation = element.querySelector('element-citation, mixed-citation')
221
+ const type = citation?.getAttribute('publication-type')
222
+ if (citation?.getAttribute('specific-use') === 'unstructured-citation') {
223
+ return 'literal'
224
+ }
225
+ if (!type) {
226
+ return 'article-journal'
227
+ }
228
+ switch (type) {
229
+ case 'journal':
230
+ return 'article-journal'
231
+ case 'web':
232
+ return 'webpage'
233
+ case 'data':
234
+ return 'dataset'
235
+ default:
236
+ return type as BibliographyItemType
237
+ }
238
+ }
239
+
240
+ private getNameContent(element: Element, type: string) {
241
+ const query = `person-group[person-group-type="${type}"] > *`
242
+ const groups = [...element.querySelectorAll(query)]
243
+ if (!groups.length) {
244
+ return
245
+ }
246
+ return groups.map((node) => ({
247
+ given: getTrimmedTextContent(node, 'given-names'),
248
+ family: getTrimmedTextContent(node, 'surname'),
249
+ literal:
250
+ node.nodeName === 'collab' ? getTrimmedTextContent(node) : undefined,
251
+ })) as CSL.Person[]
252
+ }
253
+
254
+ private getDateContent(
255
+ element: Element,
256
+ query: string
257
+ ): CSL.Date | undefined {
258
+ const date = element.querySelector(query)
259
+ if (date) {
260
+ const isoDate = date.getAttribute('iso-8601-date')
261
+ if (!isoDate) {
262
+ return
263
+ }
264
+
265
+ const parsedDate = new Date(Date.parse(isoDate))
266
+
267
+ const parts: [number, number, number] = [
268
+ parsedDate.getFullYear(),
269
+ parsedDate.getMonth() + 1, //month is 0 indexed.
270
+ parsedDate.getDate(),
271
+ ]
272
+
273
+ return {
274
+ 'date-parts': [parts],
275
+ }
276
+ }
277
+ }
278
+
279
+ private getIssuedDateContent(element: Element): CSL.Date | undefined {
280
+ const year = getTrimmedTextContent(element, ':scope > * > year')
281
+ if (!year) {
282
+ return
283
+ }
284
+ const month = getTrimmedTextContent(element, ':scope > * > month')
285
+ const day = getTrimmedTextContent(element, ':scope > * > day')
286
+ return {
287
+ 'date-parts': [[year, month ?? '', day ?? '']],
288
+ }
289
+ }
290
+
291
+ private getFigContent = (node: Node) => {
292
+ const element = node as HTMLElement
293
+ const content = [this.schema.nodes.figure.create(this.getFigAttrs(element))]
294
+ const altText = element.querySelector('alt-text')
295
+ if (altText) {
296
+ const altTextNode = this.schema.nodes.alt_text.create()
297
+ content.push(this.parse(altText, { topNode: altTextNode }))
298
+ }
299
+ const longDesc = element.querySelector('long-desc')
300
+ if (longDesc) {
301
+ const longDescNode = this.schema.nodes.long_desc.create()
302
+ content.push(this.parse(longDesc, { topNode: longDescNode }))
303
+ }
304
+ return Fragment.from(content)
305
+ }
306
+
307
+ private getCaptionContent = (node: Node) => {
308
+ const element = node as HTMLElement
309
+ const content = Array.from(element.querySelectorAll('p')).map((paragraph) =>
310
+ this.parse(paragraph, { topNode: this.schema.nodes.text_block.create() })
311
+ )
312
+ return Fragment.from(
313
+ content.length > 0 ? content : this.schema.nodes.text_block.create()
314
+ )
315
+ }
316
+
317
+ private parseRefPages = (element: Element) => {
318
+ const fpage = getTrimmedTextContent(element, 'fpage')
319
+ const lpage = getTrimmedTextContent(element, 'lpage')
320
+ if (fpage) {
321
+ return lpage ? `${fpage}-${lpage}` : fpage
322
+ }
323
+ }
324
+
325
+ private parseRef = (element: Element): BibliographyItemAttrs => {
326
+ return {
327
+ id: element.id,
328
+ type: this.getRefType(element),
329
+ comment: getTrimmedTextContent(element, 'comment'),
330
+ volume: getTrimmedTextContent(element, 'volume'),
331
+ issue: getTrimmedTextContent(element, 'issue'),
332
+ supplement: getTrimmedTextContent(element, 'supplement'),
333
+ DOI: getTrimmedTextContent(element, 'pub-id[pub-id-type="doi"]'),
334
+ URL: getTrimmedTextContent(element, 'ext-link[ext-link-type="uri"]'),
335
+ std: getTrimmedTextContent(
336
+ element,
337
+ 'pub-id[pub-id-type="std-designation"]'
338
+ ),
339
+ 'collection-title': getTrimmedTextContent(element, 'series'),
340
+ edition: getTrimmedTextContent(element, 'edition'),
341
+ publisher: getTrimmedTextContent(element, 'publisher-name'),
342
+ 'publisher-place': getTrimmedTextContent(element, 'publisher-loc'),
343
+ event: getTrimmedTextContent(element, 'conf-name'),
344
+ 'event-place': getTrimmedTextContent(element, 'conf-loc'),
345
+ 'number-of-pages': getTrimmedTextContent(element, 'size[units="pages"]'),
346
+ institution: getTrimmedTextContent(element, 'institution'),
347
+ locator: getTrimmedTextContent(element, 'elocation-id'),
348
+ 'container-title': getHTMLContent(element, 'source'),
349
+ title: getHTMLContent(element, 'article-title, data-title, part-title'),
350
+ author: this.getNameContent(element, 'author'),
351
+ editor: this.getNameContent(element, 'editor'),
352
+ literal: getTrimmedTextContent(element, 'mixed-citation'),
353
+ accessed: this.getDateContent(element, 'date-in-citation'),
354
+ 'event-date': this.getDateContent(element, 'conf-date'),
355
+ issued: this.getIssuedDateContent(element),
356
+ page: this.parseRefPages(element),
357
+ }
358
+ }
359
+
360
+ private nodes: NodeRule[] = [
361
+ {
362
+ tag: 'long-desc',
363
+ node: 'long_desc',
364
+ },
365
+ {
366
+ tag: 'alt-text',
367
+ node: 'alt_text',
368
+ },
369
+ {
370
+ tag: 'attachments > self-uri',
371
+ node: 'attachment',
372
+ getAttrs: (node) => {
373
+ const element = node as HTMLElement
374
+ return {
375
+ id: element.getAttribute('id'),
376
+ href: element.getAttributeNS(XLINK_NAMESPACE, 'href') || '',
377
+ type: element.getAttribute('content-type') || '',
378
+ }
379
+ },
380
+ },
381
+ {
382
+ tag: 'article',
383
+ node: 'manuscript',
384
+ getAttrs: (node) => {
385
+ const element = node as HTMLElement
386
+ const doi = element.querySelector(
387
+ 'front > article-meta > article-id[pub-id-type="doi"]'
388
+ )
389
+ const history = element.querySelector('history')
390
+ const dates = this.parseHistoryDates(history)
391
+ return {
392
+ doi: getTrimmedTextContent(doi),
393
+ articleType: element.getAttribute('article-type') || '',
394
+ primaryLanguageCode: element.getAttributeNS(XML_NAMESPACE, 'lang'),
395
+ ...dates,
396
+ }
397
+ },
398
+ },
399
+ {
400
+ tag: 'subtitle',
401
+ node: 'subtitle',
402
+ getAttrs: (node) => {
403
+ const element = node as HTMLElement
404
+ return {
405
+ id: element.getAttribute('id'),
406
+ }
407
+ },
408
+ },
409
+ {
410
+ tag: 'alt-title[alt-title-type]',
411
+ node: 'alt_title',
412
+ getAttrs: (node) => {
413
+ const element = node as HTMLElement
414
+ return {
415
+ id: element.getAttribute('id'),
416
+ type: element.getAttribute('alt-title-type'),
417
+ }
418
+ },
419
+ },
420
+ {
421
+ tag: 'article-title',
422
+ node: 'title',
423
+ getAttrs: (node) => {
424
+ const element = node as HTMLElement
425
+ return {
426
+ id: element.getAttribute('id'),
427
+ type: element.getAttribute('type') || '',
428
+ }
429
+ },
430
+ },
431
+
432
+ {
433
+ tag: 'highlight-marker',
434
+ node: 'highlight_marker',
435
+ getAttrs: (node) => {
436
+ const element = node as HTMLElement
437
+ return {
438
+ id: element.id,
439
+ position: element.getAttribute('position'),
440
+ }
441
+ },
442
+ },
443
+ {
444
+ tag: 'comments',
445
+ node: 'comments',
446
+ },
447
+ {
448
+ tag: 'comment',
449
+ node: 'comment',
450
+ getAttrs: (node) => {
451
+ const element = node as HTMLElement
452
+ return {
453
+ id: element.getAttribute('id'),
454
+ target: element.getAttribute('target-id'),
455
+ contents: getTrimmedTextContent(element),
456
+ userID: DEFAULT_PROFILE_ID,
457
+ timestamp: Date.now(),
458
+ }
459
+ },
460
+ },
461
+ {
462
+ tag: 'author-notes',
463
+ node: 'author_notes',
464
+ getAttrs: (node) => {
465
+ const element = node as HTMLElement
466
+ if (!getTrimmedTextContent(element)) {
467
+ return false
468
+ }
469
+ return {
470
+ id: element.getAttribute('id'),
471
+ }
472
+ },
473
+ },
474
+ {
475
+ tag: 'funding-group',
476
+ node: 'awards',
477
+ },
478
+ {
479
+ tag: 'award-group',
480
+ node: 'award',
481
+ getAttrs: (node) => {
482
+ const element = node as HTMLElement
483
+ return {
484
+ id: element.getAttribute('id'),
485
+ recipient: getTrimmedTextContent(
486
+ element,
487
+ 'principal-award-recipient'
488
+ ),
489
+ code: Array.from(element.querySelectorAll('award-id'))
490
+ .map((awardID) => getTrimmedTextContent(awardID))
491
+ .reduce((acc, text) => (acc ? `${acc};${text}` : text), ''),
492
+ source: getTrimmedTextContent(element, 'funding-source'),
493
+ }
494
+ },
495
+ },
496
+ {
497
+ tag: 'fn[fn-type]', // all supported fn-types should be moved out by the time we parse into prosemirror
498
+ context: 'author_notes/',
499
+ ignore: true,
500
+ },
501
+ {
502
+ tag: 'fn:not([fn-type])',
503
+ node: 'footnote',
504
+ context: 'author_notes/',
505
+ getAttrs: (node) => {
506
+ const element = node as HTMLElement
507
+ return {
508
+ id: element.getAttribute('id'),
509
+ kind: 'footnote',
510
+ }
511
+ },
512
+ },
513
+ {
514
+ tag: 'corresp',
515
+ node: 'corresp',
516
+ getAttrs: (node) => {
517
+ const element = node as HTMLElement
518
+ const label = element.querySelector('label')
519
+ if (label) {
520
+ label.remove()
521
+ }
522
+ return {
523
+ id: element.getAttribute('id'),
524
+ label: getTrimmedTextContent(label),
525
+ }
526
+ },
527
+ getContent: (node) => {
528
+ const element = node as HTMLElement
529
+ return Fragment.from(
530
+ this.schema.text(getTrimmedTextContent(element) || '')
531
+ )
532
+ },
533
+ },
534
+ {
535
+ tag: 'contrib[contrib-type="author"]',
536
+ node: 'contributor',
537
+ getAttrs: (node) => {
538
+ const element = node as HTMLElement
539
+ const footnoteIDs: string[] = []
540
+ const affiliationIDs: string[] = []
541
+ const correspIDs: string[] = []
542
+
543
+ const xrefs = element.querySelectorAll('xref')
544
+ for (const xref of xrefs) {
545
+ const rid = xref.getAttribute('rid')
546
+ const type = xref.getAttribute('ref-type')
547
+ if (!rid) {
548
+ continue
549
+ }
550
+ switch (type) {
551
+ case 'fn':
552
+ footnoteIDs.push(rid)
553
+ break
554
+ case 'corresp':
555
+ correspIDs.push(rid)
556
+ break
557
+ case 'aff':
558
+ affiliationIDs.push(rid)
559
+ break
560
+ }
561
+ }
562
+
563
+ return {
564
+ id: element.getAttribute('id') || undefined,
565
+ role: getTrimmedTextContent(element, 'role'),
566
+ affiliationIDs,
567
+ correspIDs,
568
+ footnoteIDs,
569
+ isCorresponding: element.getAttribute('corresp') === 'yes',
570
+ given: getTrimmedTextContent(element, 'given-names'),
571
+ family: this.getSurname(element),
572
+ prefix: getTrimmedTextContent(element, 'prefix'),
573
+ suffix: getTrimmedTextContent(element, 'suffix'),
574
+ degrees:
575
+ Array.from(element.querySelectorAll('degrees')).map((degree) =>
576
+ getTrimmedTextContent(degree)
577
+ ) || [],
578
+ ORCID: getTrimmedTextContent(
579
+ element,
580
+ 'contrib-id[contrib-id-type="orcid"]'
581
+ ),
582
+ isAuthenticated: !!element.querySelector(
583
+ 'contrib-id[contrib-id-type="orcid"][authenticated="true"]'
584
+ ),
585
+ creditRoles: getCreditRole(element),
586
+ priority: this.parsePriority(element.getAttribute('priority')),
587
+ email: getTrimmedTextContent(element, 'email') || '',
588
+ }
589
+ },
590
+ getContent: () => {
591
+ return Fragment.from(this.schema.text('_'))
592
+ },
593
+ },
594
+ {
595
+ tag: 'affiliations',
596
+ node: 'affiliations',
597
+ },
598
+ {
599
+ tag: 'aff',
600
+ node: 'affiliation',
601
+ context: 'affiliations/',
602
+ getAttrs: (node) => {
603
+ const element = node as HTMLElement
604
+
605
+ const { department, institution } = this.getInstitutionDetails(element)
606
+
607
+ return {
608
+ id: element.getAttribute('id'),
609
+ institution: institution ?? '',
610
+ department: department ?? '',
611
+ addressLine1: this.getAddressLine(element, 1),
612
+ addressLine2: this.getAddressLine(element, 2),
613
+ addressLine3: this.getAddressLine(element, 3),
614
+ postCode: getTrimmedTextContent(element, 'postal-code') ?? '',
615
+ country: getTrimmedTextContent(element, 'country') ?? '',
616
+ county: getTrimmedTextContent(element, 'state') ?? '',
617
+ city: getTrimmedTextContent(element, 'city') ?? '',
618
+ email: this.getEmail(element),
619
+ priority: this.parsePriority(element.getAttribute('priority')),
620
+ }
621
+ },
622
+ getContent: () => {
623
+ return Fragment.from(this.schema.text('_'))
624
+ },
625
+ },
626
+
627
+ {
628
+ tag: 'attrib',
629
+ node: 'attribution',
630
+ },
631
+ {
632
+ tag: 'back',
633
+ ignore: true,
634
+ },
635
+ {
636
+ tag: 'history',
637
+ ignore: true,
638
+ },
639
+ {
640
+ tag: 'break',
641
+ node: 'hard_break',
642
+ },
643
+ {
644
+ tag: 'caption > p',
645
+ context: 'table_element/',
646
+ ignore: true,
647
+ },
648
+ {
649
+ tag: 'caption > title',
650
+ node: 'caption_title',
651
+ },
652
+ {
653
+ tag: 'caption',
654
+ node: 'caption',
655
+ context: 'image_element/|figure_element/',
656
+ getContent: this.getCaptionContent,
657
+ },
658
+ {
659
+ tag: 'caption',
660
+ node: 'caption',
661
+ context: 'listing_element/|embed/|supplement/',
662
+ getContent: (node) => {
663
+ const element = node as HTMLElement
664
+ const title = element.querySelector('title')
665
+ const caption_title = title
666
+ ? this.parse(title, {
667
+ topNode: this.schema.nodes.caption_title.create(),
668
+ })
669
+ : this.schema.nodes.caption_title.create()
670
+ const caption = this.schema.nodes.caption.create(
671
+ undefined,
672
+ this.getCaptionContent(node)
673
+ )
674
+ return Fragment.from([caption_title, caption])
675
+ },
676
+ },
677
+ {
678
+ tag: 'code',
679
+ node: 'listing',
680
+ context: 'listing_element/',
681
+ getAttrs: (node) => {
682
+ const element = node as HTMLElement
683
+
684
+ return {
685
+ id: element.getAttribute('id'),
686
+ language: element.getAttribute('language') ?? '',
687
+ contents: getTrimmedTextContent(element),
688
+ }
689
+ },
690
+ },
691
+ {
692
+ tag: 'inline-formula',
693
+ node: 'inline_equation',
694
+ getAttrs: (node) => {
695
+ const element = node as HTMLElement
696
+ return this.getEquationContent(element)
697
+ },
698
+ },
699
+ {
700
+ tag: 'disp-formula',
701
+ node: 'equation_element',
702
+ getAttrs: (node) => {
703
+ const element = node as HTMLElement
704
+ return {
705
+ id: element.getAttribute('id'),
706
+ }
707
+ },
708
+ getContent: (node, schema) => {
709
+ const element = node as HTMLElement
710
+ const attrs = this.getEquationContent(element)
711
+ return Fragment.from([
712
+ schema.nodes.equation.createChecked({ ...attrs }),
713
+ ]) as Fragment
714
+ },
715
+ },
716
+ {
717
+ tag: 'disp-quote[content-type=quote]',
718
+ node: 'blockquote_element',
719
+ getAttrs: (node) => {
720
+ const element = node as HTMLElement
721
+
722
+ return {
723
+ id: element.getAttribute('id'),
724
+ }
725
+ },
726
+ },
727
+ {
728
+ tag: 'disp-quote',
729
+ node: 'pullquote_element',
730
+ getAttrs: (node) => {
731
+ const element = node as HTMLElement
732
+
733
+ return {
734
+ id: element.getAttribute('id'),
735
+ }
736
+ },
737
+ },
738
+ {
739
+ tag: 'graphic',
740
+ node: 'quote_image',
741
+ context: 'pullquote_element/',
742
+ getAttrs: this.getFigAttrs,
743
+ },
744
+ {
745
+ tag: 'ext-link',
746
+ node: 'link',
747
+ getAttrs: (node) => {
748
+ const element = node as HTMLElement
749
+
750
+ return {
751
+ href: element.getAttributeNS(XLINK_NAMESPACE, 'href') || '',
752
+ title: element.getAttributeNS(XLINK_NAMESPACE, 'title') || '',
753
+ }
754
+ },
755
+ },
756
+ {
757
+ tag: 'fig[fig-type=equation]',
758
+ node: 'equation_element',
759
+ getAttrs: (node) => {
760
+ const element = node as HTMLElement
761
+
762
+ return {
763
+ id: element.getAttribute('id'),
764
+ }
765
+ },
766
+ },
767
+ {
768
+ tag: 'media',
769
+ node: 'embed',
770
+ getAttrs: (node) => {
771
+ const element = node as HTMLElement
772
+ return {
773
+ id: element.getAttribute('id'),
774
+ href: element.getAttributeNS(XLINK_NAMESPACE, 'href'),
775
+ mimetype: element.getAttribute('mimetype'),
776
+ mimeSubtype: element.getAttribute('mime-subtype'),
777
+ }
778
+ },
779
+ },
780
+ {
781
+ tag: 'fig[fig-type=listing]',
782
+ node: 'listing_element',
783
+ getAttrs: (node) => {
784
+ const element = node as HTMLElement
785
+
786
+ return {
787
+ id: element.getAttribute('id'),
788
+ }
789
+ },
790
+ },
791
+ {
792
+ tag: 'graphic[specific-use=MISSING]',
793
+ node: 'missing_figure',
794
+ getAttrs: (node) => {
795
+ const element = node as HTMLElement
796
+
797
+ return {
798
+ id: element.getAttribute('id'),
799
+ }
800
+ },
801
+ },
802
+ {
803
+ tag: 'graphic[content-type=leading]',
804
+ node: 'hero_image',
805
+ getContent: this.getFigContent,
806
+ },
807
+ {
808
+ tag: 'graphic',
809
+ node: 'figure',
810
+ context: 'figure_element/',
811
+ getAttrs: this.getFigAttrs,
812
+ },
813
+ {
814
+ tag: 'graphic',
815
+ node: 'image_element',
816
+ getContent: (node) => {
817
+ const element = node as HTMLElement
818
+ const caption = this.schema.nodes.caption.create(
819
+ undefined,
820
+ this.getCaptionContent(node)
821
+ )
822
+ const fig = this.getFigContent(element).content
823
+ return Fragment.from([...fig.slice(0, 1), caption, ...fig.slice(1)])
824
+ },
825
+ getAttrs: this.getFigAttrs,
826
+ },
827
+ {
828
+ tag: 'fig',
829
+ node: 'figure_element',
830
+ getAttrs: (node) => {
831
+ const element = node as HTMLElement
832
+ const attrib = element.querySelector('attrib')
833
+ const attribution = attrib
834
+ ? {
835
+ literal: getTrimmedTextContent(attrib) ?? '',
836
+ }
837
+ : undefined
838
+ return {
839
+ id: element.getAttribute('id'),
840
+ attribution,
841
+ }
842
+ },
843
+ },
844
+ {
845
+ tag: 'fn-group',
846
+ node: 'footnotes_element',
847
+ context: 'footnotes_section/|table_element_footer/',
848
+ getAttrs: (node) => {
849
+ const element = node as HTMLElement
850
+
851
+ return {
852
+ id: element.getAttribute('id'),
853
+ kind: 'footnote', // TODO: 'endnote' depending on position or attribute?
854
+ }
855
+ },
856
+ },
857
+ {
858
+ tag: 'table-wrap-foot',
859
+ node: 'table_element_footer',
860
+ getAttrs: (node) => {
861
+ const element = node as HTMLElement
862
+
863
+ return {
864
+ id: element.getAttribute('id'),
865
+ }
866
+ },
867
+ },
868
+ {
869
+ tag: 'general-table-footnote',
870
+ node: 'general_table_footnote',
871
+ context: 'table_element_footer/',
872
+ getAttrs: (node) => {
873
+ const element = node as HTMLElement
874
+ return {
875
+ id: element.getAttribute('id'),
876
+ }
877
+ },
878
+ getContent: (node) => {
879
+ const paragraphs: ManuscriptNode[] = []
880
+ node.childNodes.forEach((p) => {
881
+ const paragraph = this.schema.nodes.paragraph.create()
882
+ const content = this.parse(p, {
883
+ topNode: paragraph,
884
+ })
885
+ paragraphs.push(content)
886
+ })
887
+ return Fragment.from([...paragraphs]) as Fragment
888
+ },
889
+ },
890
+ {
891
+ tag: 'fn',
892
+ node: 'footnote',
893
+ context: 'footnotes_element/|table_element_footer/',
894
+ getAttrs: (node) => {
895
+ const element = node as HTMLElement
896
+
897
+ return {
898
+ id: element.getAttribute('id'),
899
+ kind: 'footnote', // TODO: 'endnote' depending on position or attribute?
900
+ }
901
+ },
902
+ },
903
+ {
904
+ tag: 'front',
905
+ ignore: true,
906
+ },
907
+ {
908
+ tag: 'list',
909
+ node: 'list',
910
+ getAttrs: (node) => {
911
+ const element = node as HTMLElement
912
+
913
+ return {
914
+ id: element.getAttribute('id'),
915
+ listStyleType: element.getAttribute('list-type'),
916
+ }
917
+ },
918
+ },
919
+ {
920
+ tag: 'list-item',
921
+ node: 'list_item',
922
+ },
923
+ {
924
+ tag: 'p',
925
+ node: 'paragraph',
926
+ context: 'section/',
927
+ getAttrs: (node) => {
928
+ const element = node as HTMLElement
929
+
930
+ return {
931
+ id: element.getAttribute('id'),
932
+ }
933
+ },
934
+ },
935
+ {
936
+ tag: 'p',
937
+ node: 'paragraph',
938
+ },
939
+ {
940
+ tag: 'sec[sec-type="endnotes"]',
941
+ node: 'footnotes_section', // NOTE: higher priority than 'section'
942
+ getAttrs: (node) => {
943
+ const element = node as HTMLElement
944
+
945
+ return {
946
+ id: element.getAttribute('id'),
947
+ // category: chooseSectionCategory(element), // 'MPSectionCategory:endnotes',
948
+ }
949
+ },
950
+ },
951
+ {
952
+ tag: 'sec[sec-type="keywords"]',
953
+ node: 'keywords', // NOTE: higher priority than 'section'
954
+ },
955
+ {
956
+ tag: 'sec[sec-type="supplementary-material"]',
957
+ node: 'supplements', // NOTE: higher priority than 'section'
958
+ },
959
+ {
960
+ tag: 'supplementary-material',
961
+ node: 'supplement', // NOTE: higher priority than 'section'
962
+ getAttrs: (node) => {
963
+ const element = node as HTMLElement
964
+ return {
965
+ id: element.getAttribute('id'),
966
+ href: element.getAttributeNS(XLINK_NAMESPACE, 'href'),
967
+ mimeType: element.getAttribute('mimetype'),
968
+ mimeSubType: element.getAttribute('mime-subtype'),
969
+ }
970
+ },
971
+ },
972
+ {
973
+ tag: 'trans-abstract[sec-type="abstract-graphical"]',
974
+ node: 'trans_graphical_abstract',
975
+ getAttrs: (node) => {
976
+ const element = node as HTMLElement
977
+ return {
978
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
979
+ category: this.chooseSectionCategory(element),
980
+ }
981
+ },
982
+ },
983
+ {
984
+ tag: 'trans-abstract[sec-type="abstract-key-image"]',
985
+ node: 'trans_graphical_abstract',
986
+ getAttrs: (node) => {
987
+ const element = node as HTMLElement
988
+ return {
989
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
990
+ category: this.chooseSectionCategory(element),
991
+ }
992
+ },
993
+ },
994
+ {
995
+ tag: 'trans-abstract',
996
+ node: 'trans_abstract',
997
+ getAttrs: (node) => {
998
+ const element = node as HTMLElement
999
+ return {
1000
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
1001
+ category: this.chooseSectionCategory(element),
1002
+ }
1003
+ },
1004
+ },
1005
+ {
1006
+ tag: 'sec[sec-type="abstracts"]',
1007
+ node: 'abstracts',
1008
+ },
1009
+ {
1010
+ tag: 'sec[sec-type="body"]',
1011
+ node: 'body',
1012
+ },
1013
+ {
1014
+ tag: 'sec[sec-type="backmatter"]',
1015
+ node: 'backmatter',
1016
+ },
1017
+ {
1018
+ tag: 'boxed-text',
1019
+ node: 'box_element',
1020
+ getAttrs: (node) => {
1021
+ const element = node as HTMLElement
1022
+ return {
1023
+ id: element.getAttribute('id'),
1024
+ }
1025
+ },
1026
+ },
1027
+ {
1028
+ tag: 'sec[sec-type="bibliography"]',
1029
+ node: 'bibliography_section',
1030
+ },
1031
+ {
1032
+ tag: 'ref-list',
1033
+ context: 'bibliography_section/',
1034
+ node: 'bibliography_element',
1035
+ },
1036
+ {
1037
+ tag: 'ref',
1038
+ context: 'bibliography_element/',
1039
+ node: 'bibliography_item',
1040
+ getAttrs: (node) => this.parseRef(node as Element),
1041
+ },
1042
+ {
1043
+ tag: 'sec[sec-type="abstract-graphical"]',
1044
+ node: 'graphical_abstract_section',
1045
+ getAttrs: (node) => {
1046
+ const element = node as HTMLElement
1047
+ return {
1048
+ category: this.chooseSectionCategory(element),
1049
+ }
1050
+ },
1051
+ },
1052
+ {
1053
+ tag: 'sec[sec-type="abstract-key-image"]',
1054
+ node: 'graphical_abstract_section',
1055
+ getAttrs: (node) => {
1056
+ const element = node as HTMLElement
1057
+ return {
1058
+ category: this.chooseSectionCategory(element),
1059
+ }
1060
+ },
1061
+ },
1062
+ {
1063
+ tag: 'sec',
1064
+ node: 'section',
1065
+ getAttrs: (node) => {
1066
+ const element = node as HTMLElement
1067
+ return {
1068
+ id: element.getAttribute('id'),
1069
+ category: this.chooseSectionCategory(element),
1070
+ }
1071
+ },
1072
+ },
1073
+ {
1074
+ tag: 'kwd-group-list',
1075
+ context: 'keywords/',
1076
+ node: 'keywords_element',
1077
+ },
1078
+ {
1079
+ tag: 'kwd-group',
1080
+ context: 'keywords_element/',
1081
+ node: 'keyword_group',
1082
+ getAttrs: (node) => {
1083
+ const element = node as HTMLElement
1084
+ return {
1085
+ id: element.id,
1086
+ type: element.getAttribute('kwd-group-type'),
1087
+ }
1088
+ },
1089
+ },
1090
+ {
1091
+ tag: 'kwd',
1092
+ context: 'keyword_group//',
1093
+ node: 'keyword',
1094
+ },
1095
+ {
1096
+ tag: 'label',
1097
+ context: 'section/',
1098
+ node: 'section_label',
1099
+ },
1100
+ {
1101
+ tag: 'table',
1102
+ node: 'table',
1103
+ // TODO: count thead and tfoot rows
1104
+ getAttrs: (node) => {
1105
+ const element = node as HTMLElement
1106
+
1107
+ return {
1108
+ id: element.getAttribute('id'),
1109
+ }
1110
+ },
1111
+ },
1112
+ {
1113
+ tag: 'table-wrap',
1114
+ node: 'table_element',
1115
+ getAttrs: (node) => {
1116
+ const element = node as HTMLElement
1117
+ return {
1118
+ id: element.getAttribute('id'),
1119
+ }
1120
+ },
1121
+ },
1122
+ {
1123
+ tag: 'title',
1124
+ node: 'section_title',
1125
+ context:
1126
+ 'section/|footnotes_section/|bibliography_section/|keywords/|supplements/|author_notes/|graphical_abstract_section/|trans_abstract/|trans_graphical_abstract/',
1127
+ },
1128
+ {
1129
+ tag: 'tr',
1130
+ node: 'table_row',
1131
+ },
1132
+ {
1133
+ tag: 'td',
1134
+ node: 'table_cell',
1135
+ getAttrs: (node) => {
1136
+ const element = node as HTMLElement
1137
+ const colspan = parseInt(element.getAttribute('colspan') || '1')
1138
+ const rowspan = parseInt(element.getAttribute('rowspan') || '1')
1139
+ return {
1140
+ ...(colspan && { colspan }),
1141
+ ...(rowspan && { rowspan }),
1142
+ valign: element.getAttribute('valign'),
1143
+ align: element.getAttribute('align'),
1144
+ scope: element.getAttribute('scope'),
1145
+ style: element.getAttribute('style'),
1146
+ }
1147
+ },
1148
+ },
1149
+ {
1150
+ tag: 'th',
1151
+ node: 'table_header',
1152
+ getAttrs: (node) => {
1153
+ const element = node as HTMLElement
1154
+ const colspan = parseInt(element.getAttribute('colspan') || '1')
1155
+ const rowspan = parseInt(element.getAttribute('rowspan') || '1')
1156
+ return {
1157
+ ...(colspan && { colspan }),
1158
+ ...(rowspan && { rowspan }),
1159
+ valign: element.getAttribute('valign'),
1160
+ align: element.getAttribute('align'),
1161
+ scope: element.getAttribute('scope'),
1162
+ style: element.getAttribute('style'),
1163
+ }
1164
+ },
1165
+ },
1166
+ {
1167
+ tag: 'col',
1168
+ node: 'table_col',
1169
+ getAttrs: (node) => {
1170
+ const element = node as HTMLElement
1171
+
1172
+ return {
1173
+ width: element.getAttribute('width'),
1174
+ }
1175
+ },
1176
+ },
1177
+ {
1178
+ tag: 'colgroup',
1179
+ node: 'table_colgroup',
1180
+ },
1181
+ {
1182
+ tag: 'xref[ref-type="bibr"]',
1183
+ node: 'citation',
1184
+ getAttrs: (node) => {
1185
+ const element = node as HTMLElement
1186
+ return {
1187
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1188
+ contents: getTrimmedTextContent(element),
1189
+ }
1190
+ },
1191
+ },
1192
+ {
1193
+ tag: 'xref[ref-type="fn"]',
1194
+ node: 'inline_footnote',
1195
+ getAttrs: (node) => {
1196
+ const element = node as HTMLElement
1197
+
1198
+ return {
1199
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1200
+ }
1201
+ },
1202
+ },
1203
+ {
1204
+ tag: 'xref',
1205
+ node: 'cross_reference',
1206
+ getAttrs: (node) => {
1207
+ const element = node as HTMLElement
1208
+ return {
1209
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1210
+ }
1211
+ },
1212
+ },
1213
+ {
1214
+ tag: 'label',
1215
+ ignore: true,
1216
+ },
1217
+ ]
1218
+
1219
+ private marks: MarkRule[] = [
1220
+ {
1221
+ tag: 'bold',
1222
+ mark: 'bold',
1223
+ },
1224
+ {
1225
+ tag: 'code',
1226
+ mark: 'code',
1227
+ },
1228
+ {
1229
+ tag: 'italic',
1230
+ mark: 'italic',
1231
+ },
1232
+ {
1233
+ tag: 'sc',
1234
+ mark: 'smallcaps',
1235
+ },
1236
+ {
1237
+ tag: 'strike',
1238
+ mark: 'strikethrough',
1239
+ },
1240
+ {
1241
+ tag: 'styled-content',
1242
+ mark: 'styled',
1243
+ getAttrs: (node) => ({
1244
+ style: (node as Element).getAttribute('style'),
1245
+ }),
1246
+ },
1247
+ {
1248
+ tag: 'sub',
1249
+ mark: 'subscript',
1250
+ },
1251
+ {
1252
+ tag: 'sup',
1253
+ mark: 'superscript',
1254
+ },
1255
+ {
1256
+ tag: 'underline',
1257
+ mark: 'underline',
1258
+ },
1259
+ ]
1260
+ }