@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,1306 @@
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/|headshot_element/',
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',
793
+ node: 'headshot_image',
794
+ context: 'headshot_element/',
795
+ getAttrs: this.getFigAttrs,
796
+ },
797
+ {
798
+ tag: 'graphic',
799
+ node: 'headshot_element',
800
+ context: 'headshot_grid/',
801
+ getContent: (node) => {
802
+ const element = node as HTMLElement
803
+
804
+ const innerGraphic = element.cloneNode(false) as HTMLElement
805
+
806
+ // build wrapper: [graphic(shallow), caption, alt-text, long-desc]
807
+ const wrapper = element.ownerDocument!.createElement('div')
808
+ wrapper.appendChild(innerGraphic)
809
+
810
+ const caption = element.querySelector('caption')
811
+ if (caption) wrapper.appendChild(caption)
812
+
813
+ const altText = element.querySelector('alt-text')
814
+ if (altText) {
815
+ if (altText.childNodes.length === 0) {
816
+ // fill empty alt-text with `caption > title` content
817
+ const title = caption?.querySelector(':scope > title')
818
+ if (title) {
819
+ altText.append(...Array.from(title.childNodes).map(n => n.cloneNode(true)))
820
+ }
821
+ }
822
+ wrapper.appendChild(altText)
823
+ }
824
+
825
+ const longDesc = element.querySelector('long-desc')
826
+ if (longDesc) wrapper.appendChild(longDesc)
827
+
828
+ return this.parse(wrapper, {
829
+ topNode: this.schema.nodes.headshot_element.create(),
830
+ }).content
831
+ }
832
+ },
833
+ {
834
+ tag: 'graphic[specific-use=MISSING]',
835
+ node: 'missing_figure',
836
+ getAttrs: (node) => {
837
+ const element = node as HTMLElement
838
+
839
+ return {
840
+ id: element.getAttribute('id'),
841
+ }
842
+ },
843
+ },
844
+ {
845
+ tag: 'graphic[content-type=leading]',
846
+ node: 'hero_image',
847
+ getContent: this.getFigContent,
848
+ },
849
+ {
850
+ tag: 'graphic',
851
+ node: 'figure',
852
+ context: 'figure_element/',
853
+ getAttrs: this.getFigAttrs,
854
+ },
855
+ {
856
+ tag: 'graphic',
857
+ node: 'image_element',
858
+ getContent: (node) => {
859
+ const element = node as HTMLElement
860
+ const caption = this.schema.nodes.caption.create(
861
+ undefined,
862
+ this.getCaptionContent(node)
863
+ )
864
+ const fig = this.getFigContent(element).content
865
+ return Fragment.from([...fig.slice(0, 1), caption, ...fig.slice(1)])
866
+ },
867
+ getAttrs: this.getFigAttrs,
868
+ },
869
+ {
870
+ tag: 'fig',
871
+ node: 'figure_element',
872
+ getAttrs: (node) => {
873
+ const element = node as HTMLElement
874
+ const attrib = element.querySelector('attrib')
875
+ const attribution = attrib
876
+ ? {
877
+ literal: getTrimmedTextContent(attrib) ?? '',
878
+ }
879
+ : undefined
880
+ return {
881
+ id: element.getAttribute('id'),
882
+ attribution,
883
+ }
884
+ },
885
+ },
886
+ {
887
+ tag: 'fn-group',
888
+ node: 'footnotes_element',
889
+ context: 'footnotes_section/|table_element_footer/',
890
+ getAttrs: (node) => {
891
+ const element = node as HTMLElement
892
+
893
+ return {
894
+ id: element.getAttribute('id'),
895
+ kind: 'footnote', // TODO: 'endnote' depending on position or attribute?
896
+ }
897
+ },
898
+ },
899
+ {
900
+ tag: 'table-wrap-foot',
901
+ node: 'table_element_footer',
902
+ getAttrs: (node) => {
903
+ const element = node as HTMLElement
904
+
905
+ return {
906
+ id: element.getAttribute('id'),
907
+ }
908
+ },
909
+ },
910
+ {
911
+ tag: 'general-table-footnote',
912
+ node: 'general_table_footnote',
913
+ context: 'table_element_footer/',
914
+ getAttrs: (node) => {
915
+ const element = node as HTMLElement
916
+ return {
917
+ id: element.getAttribute('id'),
918
+ }
919
+ },
920
+ getContent: (node) => {
921
+ const paragraphs: ManuscriptNode[] = []
922
+ node.childNodes.forEach((p) => {
923
+ const paragraph = this.schema.nodes.paragraph.create()
924
+ const content = this.parse(p, {
925
+ topNode: paragraph,
926
+ })
927
+ paragraphs.push(content)
928
+ })
929
+ return Fragment.from([...paragraphs]) as Fragment
930
+ },
931
+ },
932
+ {
933
+ tag: 'fn',
934
+ node: 'footnote',
935
+ context: 'footnotes_element/|table_element_footer/',
936
+ getAttrs: (node) => {
937
+ const element = node as HTMLElement
938
+
939
+ return {
940
+ id: element.getAttribute('id'),
941
+ kind: 'footnote', // TODO: 'endnote' depending on position or attribute?
942
+ }
943
+ },
944
+ },
945
+ {
946
+ tag: 'front',
947
+ ignore: true,
948
+ },
949
+ {
950
+ tag: 'list',
951
+ node: 'list',
952
+ getAttrs: (node) => {
953
+ const element = node as HTMLElement
954
+
955
+ return {
956
+ id: element.getAttribute('id'),
957
+ listStyleType: element.getAttribute('list-type'),
958
+ }
959
+ },
960
+ },
961
+ {
962
+ tag: 'list-item',
963
+ node: 'list_item',
964
+ },
965
+ {
966
+ tag: 'p[content-type="headshots"]',
967
+ node: 'headshot_grid',
968
+ },
969
+ {
970
+ tag: 'p',
971
+ node: 'paragraph',
972
+ context: 'section/',
973
+ getAttrs: (node) => {
974
+ const element = node as HTMLElement
975
+
976
+ return {
977
+ id: element.getAttribute('id'),
978
+ }
979
+ },
980
+ },
981
+ {
982
+ tag: 'p',
983
+ node: 'paragraph',
984
+ },
985
+ {
986
+ tag: 'sec[sec-type="endnotes"]',
987
+ node: 'footnotes_section', // NOTE: higher priority than 'section'
988
+ getAttrs: (node) => {
989
+ const element = node as HTMLElement
990
+
991
+ return {
992
+ id: element.getAttribute('id'),
993
+ // category: chooseSectionCategory(element), // 'MPSectionCategory:endnotes',
994
+ }
995
+ },
996
+ },
997
+ {
998
+ tag: 'sec[sec-type="keywords"]',
999
+ node: 'keywords', // NOTE: higher priority than 'section'
1000
+ },
1001
+ {
1002
+ tag: 'sec[sec-type="supplementary-material"]',
1003
+ node: 'supplements', // NOTE: higher priority than 'section'
1004
+ },
1005
+ {
1006
+ tag: 'supplementary-material',
1007
+ node: 'supplement', // NOTE: higher priority than 'section'
1008
+ getAttrs: (node) => {
1009
+ const element = node as HTMLElement
1010
+ return {
1011
+ id: element.getAttribute('id'),
1012
+ href: element.getAttributeNS(XLINK_NAMESPACE, 'href'),
1013
+ mimeType: element.getAttribute('mimetype'),
1014
+ mimeSubType: element.getAttribute('mime-subtype'),
1015
+ }
1016
+ },
1017
+ },
1018
+ {
1019
+ tag: 'trans-abstract[sec-type="abstract-graphical"]',
1020
+ node: 'trans_graphical_abstract',
1021
+ getAttrs: (node) => {
1022
+ const element = node as HTMLElement
1023
+ return {
1024
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
1025
+ category: this.chooseSectionCategory(element),
1026
+ }
1027
+ },
1028
+ },
1029
+ {
1030
+ tag: 'trans-abstract[sec-type="abstract-key-image"]',
1031
+ node: 'trans_graphical_abstract',
1032
+ getAttrs: (node) => {
1033
+ const element = node as HTMLElement
1034
+ return {
1035
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
1036
+ category: this.chooseSectionCategory(element),
1037
+ }
1038
+ },
1039
+ },
1040
+ {
1041
+ tag: 'trans-abstract',
1042
+ node: 'trans_abstract',
1043
+ getAttrs: (node) => {
1044
+ const element = node as HTMLElement
1045
+ return {
1046
+ lang: element.getAttributeNS(XML_NAMESPACE, 'lang') ?? '',
1047
+ category: this.chooseSectionCategory(element),
1048
+ }
1049
+ },
1050
+ },
1051
+ {
1052
+ tag: 'sec[sec-type="abstracts"]',
1053
+ node: 'abstracts',
1054
+ },
1055
+ {
1056
+ tag: 'sec[sec-type="body"]',
1057
+ node: 'body',
1058
+ },
1059
+ {
1060
+ tag: 'sec[sec-type="backmatter"]',
1061
+ node: 'backmatter',
1062
+ },
1063
+ {
1064
+ tag: 'boxed-text',
1065
+ node: 'box_element',
1066
+ getAttrs: (node) => {
1067
+ const element = node as HTMLElement
1068
+ return {
1069
+ id: element.getAttribute('id'),
1070
+ }
1071
+ },
1072
+ },
1073
+ {
1074
+ tag: 'sec[sec-type="bibliography"]',
1075
+ node: 'bibliography_section',
1076
+ },
1077
+ {
1078
+ tag: 'ref-list',
1079
+ context: 'bibliography_section/',
1080
+ node: 'bibliography_element',
1081
+ },
1082
+ {
1083
+ tag: 'ref',
1084
+ context: 'bibliography_element/',
1085
+ node: 'bibliography_item',
1086
+ getAttrs: (node) => this.parseRef(node as Element),
1087
+ },
1088
+ {
1089
+ tag: 'sec[sec-type="abstract-graphical"]',
1090
+ node: 'graphical_abstract_section',
1091
+ getAttrs: (node) => {
1092
+ const element = node as HTMLElement
1093
+ return {
1094
+ category: this.chooseSectionCategory(element),
1095
+ }
1096
+ },
1097
+ },
1098
+ {
1099
+ tag: 'sec[sec-type="abstract-key-image"]',
1100
+ node: 'graphical_abstract_section',
1101
+ getAttrs: (node) => {
1102
+ const element = node as HTMLElement
1103
+ return {
1104
+ category: this.chooseSectionCategory(element),
1105
+ }
1106
+ },
1107
+ },
1108
+ {
1109
+ tag: 'sec',
1110
+ node: 'section',
1111
+ getAttrs: (node) => {
1112
+ const element = node as HTMLElement
1113
+ return {
1114
+ id: element.getAttribute('id'),
1115
+ category: this.chooseSectionCategory(element),
1116
+ }
1117
+ },
1118
+ },
1119
+ {
1120
+ tag: 'kwd-group-list',
1121
+ context: 'keywords/',
1122
+ node: 'keywords_element',
1123
+ },
1124
+ {
1125
+ tag: 'kwd-group',
1126
+ context: 'keywords_element/',
1127
+ node: 'keyword_group',
1128
+ getAttrs: (node) => {
1129
+ const element = node as HTMLElement
1130
+ return {
1131
+ id: element.id,
1132
+ type: element.getAttribute('kwd-group-type'),
1133
+ }
1134
+ },
1135
+ },
1136
+ {
1137
+ tag: 'kwd',
1138
+ context: 'keyword_group//',
1139
+ node: 'keyword',
1140
+ },
1141
+ {
1142
+ tag: 'label',
1143
+ context: 'section/',
1144
+ node: 'section_label',
1145
+ },
1146
+ {
1147
+ tag: 'table',
1148
+ node: 'table',
1149
+ // TODO: count thead and tfoot rows
1150
+ getAttrs: (node) => {
1151
+ const element = node as HTMLElement
1152
+
1153
+ return {
1154
+ id: element.getAttribute('id'),
1155
+ }
1156
+ },
1157
+ },
1158
+ {
1159
+ tag: 'table-wrap',
1160
+ node: 'table_element',
1161
+ getAttrs: (node) => {
1162
+ const element = node as HTMLElement
1163
+ return {
1164
+ id: element.getAttribute('id'),
1165
+ }
1166
+ },
1167
+ },
1168
+ {
1169
+ tag: 'title',
1170
+ node: 'section_title',
1171
+ context:
1172
+ 'section/|footnotes_section/|bibliography_section/|keywords/|supplements/|author_notes/|graphical_abstract_section/|trans_abstract/|trans_graphical_abstract/',
1173
+ },
1174
+ {
1175
+ tag: 'tr',
1176
+ node: 'table_row',
1177
+ },
1178
+ {
1179
+ tag: 'td',
1180
+ node: 'table_cell',
1181
+ getAttrs: (node) => {
1182
+ const element = node as HTMLElement
1183
+ const colspan = parseInt(element.getAttribute('colspan') || '1')
1184
+ const rowspan = parseInt(element.getAttribute('rowspan') || '1')
1185
+ return {
1186
+ ...(colspan && { colspan }),
1187
+ ...(rowspan && { rowspan }),
1188
+ valign: element.getAttribute('valign'),
1189
+ align: element.getAttribute('align'),
1190
+ scope: element.getAttribute('scope'),
1191
+ style: element.getAttribute('style'),
1192
+ }
1193
+ },
1194
+ },
1195
+ {
1196
+ tag: 'th',
1197
+ node: 'table_header',
1198
+ getAttrs: (node) => {
1199
+ const element = node as HTMLElement
1200
+ const colspan = parseInt(element.getAttribute('colspan') || '1')
1201
+ const rowspan = parseInt(element.getAttribute('rowspan') || '1')
1202
+ return {
1203
+ ...(colspan && { colspan }),
1204
+ ...(rowspan && { rowspan }),
1205
+ valign: element.getAttribute('valign'),
1206
+ align: element.getAttribute('align'),
1207
+ scope: element.getAttribute('scope'),
1208
+ style: element.getAttribute('style'),
1209
+ }
1210
+ },
1211
+ },
1212
+ {
1213
+ tag: 'col',
1214
+ node: 'table_col',
1215
+ getAttrs: (node) => {
1216
+ const element = node as HTMLElement
1217
+
1218
+ return {
1219
+ width: element.getAttribute('width'),
1220
+ }
1221
+ },
1222
+ },
1223
+ {
1224
+ tag: 'colgroup',
1225
+ node: 'table_colgroup',
1226
+ },
1227
+ {
1228
+ tag: 'xref[ref-type="bibr"]',
1229
+ node: 'citation',
1230
+ getAttrs: (node) => {
1231
+ const element = node as HTMLElement
1232
+ return {
1233
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1234
+ contents: getTrimmedTextContent(element),
1235
+ }
1236
+ },
1237
+ },
1238
+ {
1239
+ tag: 'xref[ref-type="fn"]',
1240
+ node: 'inline_footnote',
1241
+ getAttrs: (node) => {
1242
+ const element = node as HTMLElement
1243
+
1244
+ return {
1245
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1246
+ }
1247
+ },
1248
+ },
1249
+ {
1250
+ tag: 'xref',
1251
+ node: 'cross_reference',
1252
+ getAttrs: (node) => {
1253
+ const element = node as HTMLElement
1254
+ return {
1255
+ rids: element.getAttribute('rid')?.split(/\s+/) || [],
1256
+ }
1257
+ },
1258
+ },
1259
+ {
1260
+ tag: 'label',
1261
+ ignore: true,
1262
+ },
1263
+ ]
1264
+
1265
+ private marks: MarkRule[] = [
1266
+ {
1267
+ tag: 'bold',
1268
+ mark: 'bold',
1269
+ },
1270
+ {
1271
+ tag: 'code',
1272
+ mark: 'code',
1273
+ },
1274
+ {
1275
+ tag: 'italic',
1276
+ mark: 'italic',
1277
+ },
1278
+ {
1279
+ tag: 'sc',
1280
+ mark: 'smallcaps',
1281
+ },
1282
+ {
1283
+ tag: 'strike',
1284
+ mark: 'strikethrough',
1285
+ },
1286
+ {
1287
+ tag: 'styled-content',
1288
+ mark: 'styled',
1289
+ getAttrs: (node) => ({
1290
+ style: (node as Element).getAttribute('style'),
1291
+ }),
1292
+ },
1293
+ {
1294
+ tag: 'sub',
1295
+ mark: 'subscript',
1296
+ },
1297
+ {
1298
+ tag: 'sup',
1299
+ mark: 'superscript',
1300
+ },
1301
+ {
1302
+ tag: 'underline',
1303
+ mark: 'underline',
1304
+ },
1305
+ ]
1306
+ }