UNPKG

myst-to-jats

Version:
937 lines (936 loc) 32.9 kB
import { VFile } from 'vfile'; import { xml2js } from 'xml-js'; import { RuleId, copyNode, extractPart, fileError } from 'myst-common'; import { renderEquation } from 'myst-transforms'; import { SourceFileKind } from 'myst-spec-ext'; import { Tags, RefType } from 'jats-tags'; import { serializeJatsXml } from 'jats-utils'; import { getBack } from './backmatter.js'; import { getArticleMeta, getFront } from './frontmatter.js'; import { ACKNOWLEDGMENT_PARTS, ABSTRACT_PARTS } from './types.js'; import { basicTransformations, referenceResolutionTransform, referenceTargetTransform, } from './transforms/index.js'; import { affiliationIdTransform } from './transforms/frontmatter.js'; import { sectionAttrsFromBlock } from './transforms/sections.js'; import { inlineExpression } from './inlineExpression.js'; function escapeForXML(text) { return (text .replace(/&(?!amp;)/g, '&amp;') .replace(/</g, '&lt;') // eslint-disable-next-line no-irregular-whitespace .replace(/​/g, '') // Replace no-width spaces, used in links ); } function referenceKindToRefType(kind) { switch (kind?.split(':')[0]) { case 'heading': return RefType.sec; case 'figure': return RefType.fig; case 'subequation': case 'equation': return RefType.dispFormula; case 'table': return RefType.table; case 'proof': return RefType.statement; default: return RefType.custom; } } function renderLabel(node, state, template = (s) => s) { const { enumerated, enumerator } = node; if (enumerated !== false && enumerator) { state.openNode('label'); state.text(template(enumerator)); state.closeNode(); } } function alternativesFromMinifiedOutput(output, state) { state.openNode('alternatives'); if (output.output_type === 'error') { state.openNode('media', { 'specific-use': 'error', mimetype: 'text', 'mime-subtype': 'plain', 'xlink:href': output.path, }); state.openNode('caption'); state.openNode('title'); state.text(output.ename); state.closeNode(); state.openNode('p'); state.text(output.evalue); state.closeNode(); state.closeNode(); state.closeNode(); } else if (output.output_type === 'stream') { state.addLeaf('media', { 'specific-use': 'stream', mimetype: 'text', 'mime-subtype': 'plain', 'xlink:href': output.path, }); } else if (['display_data', 'execute_result', 'update_display_data'].includes(output.output_type)) { Object.entries(output.data ?? {}).forEach(([mimeType, value]) => { let leafType; let specificUse; if (mimeType.startsWith('image/')) { leafType = 'graphic'; specificUse = 'print'; } else if (mimeType === 'text/html') { leafType = 'media'; specificUse = 'web'; } else if (mimeType === 'text/plain') { leafType = 'media'; specificUse = 'text'; } else { leafType = 'media'; specificUse = 'original-format'; } state.addLeaf(leafType, { 'specific-use': specificUse, mimetype: mimeType.split('/')[0], 'mime-subtype': mimeType.split('/').slice(1).join('/'), 'xlink:href': value.path, }); }); } state.closeNode(); } function addMmlAndRemoveAnnotation(el) { if (el?.name) el.name = `mml:${el.name}`; if (!el?.elements) return; el.elements = el.elements.filter((child) => child.name !== 'annotation'); el.elements.forEach((child) => { addMmlAndRemoveAnnotation(child); }); } function mathToMml(node) { const math = copyNode(node); // TODO: add macros, log errors renderEquation(new VFile(), math, { mathML: true }); const katexJs = math.html ? xml2js(math.html, { compact: false }) : undefined; const spanElement = katexJs?.elements?.[0]; const mathElement = spanElement?.elements?.[0]; if (!mathElement) return; const inline = node.type === 'inlineMath'; if (inline) mathElement.attributes = { ...mathElement.attributes, display: 'inline' }; delete mathElement.attributes?.xmlns; addMmlAndRemoveAnnotation(mathElement); // Remove the wrapping `<mml:semantics><mml:mrow>` if it is the only element if (mathElement?.elements?.length === 1 && mathElement.elements[0].name === 'mml:semantics') { mathElement.elements = mathElement.elements[0].elements; } if (mathElement?.elements?.length === 1 && mathElement.elements[0].name === 'mml:mrow') { mathElement.elements = mathElement.elements[0].elements; } return mathElement; } /** * Remove comments and consolidate to one line */ function cleanLatex(value) { if (!value) return; return value .split('\n') .map((s) => s.replace(/%(.*)/, '').trim()) .join(' ') .trim(); } // TODO: this should be based on some information of the proof, or myst config function capitalize(kind) { if (!kind) return ''; return kind.slice(0, 1).toUpperCase() + kind.slice(1); } const handlers = { text(node, state) { state.text(node.value); }, paragraph(node, state) { state.renderInline(node, 'p'); }, section(node, state) { state.renderInline(node, 'sec', sectionAttrsFromBlock(node)); }, heading(node, state) { renderLabel(node, state); state.renderInline(node, 'title'); }, block(node, state) { if (node.visibility === 'remove' || node.visibility === 'hide') return; state.renderChildren(node); }, blockquote(node, state) { state.renderInline(node, 'disp-quote'); }, definitionList(node, state) { state.renderInline(node, 'def-list'); }, definitionItem(node, state) { state.renderInline(node, 'def-item'); }, definitionTerm(node, state) { state.renderInline(node, 'term'); }, definitionDescription(node, state) { state.openNode('def'); if (node.children?.[0].type === 'paragraph') { state.renderChildren(node); } else { state.renderInline(node, 'p'); } state.closeNode(); }, code(node, state) { const { lang, executable, identifier } = node; const attrs = { language: lang }; if (executable) attrs.executable = 'yes'; if (identifier) attrs.id = identifier; state.renderInline(node, 'code', attrs); }, list(node, state) { // https://jats.nlm.nih.gov/archiving/tag-library/1.3/element/list.html state.renderInline(node, 'list', { 'list-type': node.ordered ? 'order' : 'bullet' }); }, listItem(node, state) { state.openNode('list-item'); if (node.children?.[0].type === 'paragraph') { state.renderChildren(node); } else { state.renderInline(node, 'p'); } state.closeNode(); }, thematicBreak(node, state) { // Don't include the HR state.warn('The use of thematic breaks should be restricted to use inside table cells.', node, 'thematicBreak', { url: 'https://jats.nlm.nih.gov/archiving/tag-library/1.3/element/hr.html', }); }, inlineMath(node, state) { const inlineFormulaAttrs = {}; if (node.identifier) { inlineFormulaAttrs.id = node.identifier; } state.openNode('inline-formula', inlineFormulaAttrs); state.openNode('alternatives'); state.pushNode(mathToMml(node)); state.openNode('tex-math'); state.addLeaf('cdata', { cdata: cleanLatex(node.value) }); state.closeNode(); state.closeNode(); state.closeNode(); }, math(node, state) { const dispFormulaAttrs = {}; if (node.identifier) { dispFormulaAttrs.id = node.identifier; } state.openNode('disp-formula', dispFormulaAttrs); state.openNode('alternatives'); state.pushNode(mathToMml(node)); state.openNode('tex-math'); state.addLeaf('cdata', { cdata: cleanLatex(node.value) }); state.closeNode(); state.closeNode(); // The label can be anywhere, and many publishers like this at the end (which is closer to reading order) renderLabel(node, state, (enumerator) => `(${enumerator})`); state.closeNode(); }, mathGroup(node, state) { const attrs = {}; if (node.identifier) { attrs.id = node.identifier; } state.openNode('disp-formula-group', attrs); // The disp-formula-group must have the label first renderLabel(node, state, (enumerator) => `(${enumerator})`); state.renderChildren(node); state.closeNode(); }, mystRole(node, state) { state.renderChildren(node); }, mystDirective(node, state) { state.renderChildren(node); }, comment() { // Do not archive comments }, strong(node, state) { // TODO: potentially add `{ role: 'strong' }`? state.renderInline(node, 'bold'); }, emphasis(node, state) { state.renderInline(node, 'italic'); }, underline(node, state) { state.renderInline(node, 'underline'); }, inlineCode(node, state) { state.renderInline(node, 'monospace'); }, subscript(node, state) { state.renderInline(node, 'sub'); }, superscript(node, state) { state.renderInline(node, 'sup'); }, delete(node, state) { state.renderInline(node, 'strike'); }, smallcaps(node, state) { state.renderInline(node, 'sc'); }, span(node, state) { // Basic support for span at the moment state.renderChildren(node); }, break(node, state, parent) { if (parent.type === 'paragraph' || parent.type === 'listItem') { state.warn(`There are no breaks allowed in ${parent.type}s.`, node, 'break', { url: 'https://jats.nlm.nih.gov/archiving/tag-library/1.3/element/break.html', }); return; } // https://jats.nlm.nih.gov/archiving/tag-library/1.3/element/break.html state.addLeaf('break'); }, abbreviation(node, state) { state.renderInline(node, 'abbrev', { alt: node.title }); }, link(node, state) { state.renderInline(node, 'ext-link', { 'ext-link-type': 'uri', 'xlink:href': node.url, }); }, admonition(node, state) { // This is `boxed-text`, the content-type is used to differentiate it // https://jats.nlm.nih.gov/archiving/tag-library/1.3/element/boxed-text.html state.renderInline(node, 'boxed-text', { 'content-type': node.kind }); }, admonitionTitle(node, state) { state.openNode('caption'); state.renderInline(node, 'title'); state.closeNode(); }, attrib(node, state) { // This is used inside of disp-quotes state.renderInline(node, 'attrib'); }, table(node, state) { state.renderInline(node, 'table'); }, tableHead(node, state) { state.renderInline(node, 'thead'); }, tableBody(node, state) { state.renderInline(node, 'tbody'); }, tableFooter(node, state) { state.renderInline(node, 'tfoot'); }, tableRow(node, state) { state.renderInline(node, 'tr'); }, tableCell(node, state) { const { align, colspan, rowspan } = node; state.renderInline(node, node.header ? 'th' : 'td', { align, colspan: colspan ? String(colspan) : undefined, rowspan: rowspan ? String(rowspan) : undefined, }); }, image(node, state) { if (node.url?.startsWith('http')) { state.warn(`Image URL is remote (${node.url})`, node, 'image'); } if (state.data.isInContainer && node.alt && !node.data?.altTextIsAutoGenerated) { state.openNode('alt-text'); state.text(node.alt); state.closeNode(); } const attrs = { mimetype: 'image' }; const ext = node.url ? node.url.split('.').slice(-1)?.[0] : ''; if (ext) attrs['mime-subtype'] = ext; attrs['xlink:href'] = node.url; // TOOD: identifier? if (node.placeholder) state.openNode('alternatives'); state.addLeaf('graphic', attrs); if (node.placeholder) state.closeNode(); }, container(node, state) { state.data.isInContainer = true; switch (node.kind) { case 'figure': { state.renderInline(node, 'fig'); break; } case 'table': { state.renderInline(node, 'table-wrap'); break; } case 'quote': { // This is transformed in containers.ts state.renderChildren(node); break; } case 'code': { // This is transformed in containers.ts state.renderInline(node, 'boxed-text', { 'content-type': node.kind }); break; } default: { state.error(`Unhandled container kind of ${node.kind}`, node, 'container'); state.renderChildren(node); } } delete state.data.isInContainer; }, caption(node, state) { state.renderInline(node, 'caption'); }, captionNumber(node, state) { delete node.identifier; state.renderInline(node, 'label'); }, crossReference(node, state) { // Look up reference and add the text const { identifier, kind } = node; const attrs = { 'ref-type': referenceKindToRefType(kind), rid: identifier }; if (attrs['ref-type'] === RefType.custom && kind) { attrs['custom-type'] = kind; } state.renderInline(node, 'xref', attrs); }, citeGroup(node, state) { state.renderChildren(node); }, cite(node, state) { const { label } = node; const attrs = { 'ref-type': 'bibr', rid: label, }; if (!state.referenceOrder.includes(label)) { state.referenceOrder.push(label); } state.renderInline(node, 'xref', attrs); }, footnoteReference(node, state) { const { identifier, enumerator } = node; const attrs = { 'ref-type': 'fn', rid: identifier, }; state.openNode('xref', attrs); state.text(enumerator); state.closeNode(); }, footnoteDefinition(node, state) { const { identifier, enumerator } = node; state.openNode('fn', { id: identifier }); state.openNode('label'); state.text(enumerator); state.closeNode(); state.renderChildren(node); const element = state.stack.pop(); if (element) state.footnotes.push(element); }, si(node, state) { // <named-content content-type="quantity">5 <abbrev content-type="unit" alt="milli meter">mm</abbrev></named-content> const hasNumber = node.number != null; if (hasNumber) { state.openNode('named-content', { 'content-type': 'quantity' }); state.text(`${node.number} `); } state.openNode('abbrev', { 'content-type': 'unit', alt: node.alt }); state.text(node.unit); state.closeNode(); if (hasNumber) state.closeNode(); }, proof(node, state) { state.openNode('statement', { 'specific-use': node.kind, id: node.identifier }); const [title, ...rest] = node.children ?? []; const useTitle = title && title.type === 'admonitionTitle'; if (node.enumerated) { state.openNode('label'); state.text(`${capitalize(node.kind)} ${node.enumerator}`); state.closeNode(); } if (useTitle) { state.openNode('title'); state.renderChildren(title); state.closeNode(); } state.renderChildren(useTitle ? rest : node.children); state.closeNode(); }, algorithmLine(node, state) { state.openNode('p', { 'specific-use': 'line' }); if (node.enumerator) { state.openNode('x'); state.text(`${node.enumerator}: `); state.closeNode(); } state.text(Array(node.indent).fill(' ').join('')); state.renderChildren(node); state.closeNode(); }, outputs(node, state) { state.renderChildren(node); }, output(node, state) { if (state.data.isInContainer) { if (!node.jupyter_data) return; alternativesFromMinifiedOutput(node.jupyter_data, state); return; } const { identifier } = node; const attrs = { 'sec-type': 'notebook-output' }; if (node.jupyter_data) { state.openNode('sec', { ...attrs, id: identifier && !state.data.isNotebookArticleRep ? identifier : undefined, }); alternativesFromMinifiedOutput(node.jupyter_data, state); state.closeNode(); } }, embed(node, state) { if (state.data.isInContainer) { // embedded figure content is handled in container transform return; } state.renderChildren(node); }, supplementaryMaterial(node, state) { const smNode = node; state.openNode('p'); const smAttrs = {}; if (smNode.figIdentifier) { smAttrs.id = `${smNode.figIdentifier}-source`; } smAttrs['specific-use'] = 'notebook'; state.openNode('supplementary-material', smAttrs); renderLabel(node, state, (s) => `Figure ${s} - Notebook.`); state.openNode('caption'); state.openNode('title'); state.text('Analysis for '); if (smNode.figIdentifier) { state.openNode('xref', { 'ref-type': 'fig', rid: smNode.figIdentifier }); } state.text('Figure' + (smNode.enumerator ? ` ${smNode.enumerator}` : '')); if (smNode.figIdentifier) { state.closeNode(); } state.text('.'); state.closeNode(); state.openNode('p'); state.text('See methods'); if (smNode.sourceSlug) { state.text(' in '); state.openNode('xref', { 'ref-type': 'custom', 'custom-type': 'notebook', rid: smNode.sourceSlug, }); state.text('notebook'); state.closeNode(); } if (smNode.embedIdentifier) { state.text(' from '); state.openNode('xref', { 'ref-type': 'custom', 'custom-type': 'notebook-code', rid: smNode.embedIdentifier, }); state.text('cell'); state.closeNode(); } state.text('.'); state.closeNode(); state.closeNode(); state.closeNode(); state.closeNode(); }, inlineExpression, }; function createText(text) { return { type: 'text', text: escapeForXML(text) }; } function renderPart(vfile, mdast, part, opts) { const { frontmatterParts, ...otherOpts } = opts ?? {}; const partMdast = extractPart(mdast, part, { removePartData: true, frontmatterParts, }); if (!partMdast) return undefined; const serializer = new JatsSerializer(vfile, partMdast, otherOpts); return serializer.render(true).elements(); } function renderAbstract(vfile, mdast, def, opts) { const elements = renderPart(vfile, mdast, def.part, opts); if (!elements) return undefined; const abstract = { type: 'element', name: 'abstract', elements }; if (def.title) abstract.elements = [ { type: 'element', name: 'title', elements: [{ type: 'text', text: def.title }] }, ...abstract.elements, ]; if (def.type) abstract.attributes = { 'abstract-type': def.type }; return abstract; } function renderAcknowledgments(vfile, mdast, opts) { const elements = renderPart(vfile, mdast, ACKNOWLEDGMENT_PARTS, opts); if (!elements) return undefined; const acknowledgments = { type: 'element', name: 'ack', elements }; return acknowledgments; } function renderBackSection(vfile, mdast, def, opts) { const elements = renderPart(vfile, mdast, def.part, opts); if (!elements) return undefined; const sec = { type: 'element', name: 'sec', elements }; if (def.title) sec.elements = [ { type: 'element', name: 'title', elements: [{ type: 'text', text: def.title }] }, ...sec.elements, ]; if (def.type) sec.attributes = { 'sec-type': def.type }; return sec; } export class JatsSerializer { file; data; handlers; mdast; stack; footnotes; expressions; referenceOrder; opts; constructor(file, mdast, opts) { this.file = file; this.data = { isNotebookArticleRep: opts?.isNotebookArticleRep, slug: opts?.slug, }; this.stack = [{ type: 'element', elements: [] }]; this.footnotes = []; this.expressions = []; this.referenceOrder = []; this.handlers = opts?.handlers ?? handlers; this.mdast = copyNode(mdast); this.opts = opts ?? {}; basicTransformations(this.mdast, opts ?? {}); } render(ignoreParts) { if (!ignoreParts) { if (this.opts?.extractAbstract) { const abstractParts = this.opts.abstractParts ?? ABSTRACT_PARTS.map((part) => { return { part }; }); this.data.abstracts = abstractParts .map((def) => renderAbstract(this.file, this.mdast, def, this.opts)) .filter((e) => !!e); } this.data.acknowledgments = renderAcknowledgments(this.file, this.mdast, this.opts); const backSections = this.opts?.backSections ?? []; this.data.backSections = backSections .map((def) => renderBackSection(this.file, this.mdast, def, this.opts)) .filter((e) => !!e); } this.renderChildren(this.mdast); while (this.stack.length > 1) this.closeNode(); return this; } top() { return this.stack[this.stack.length - 1]; } warn(message, node, source, opts) { fileError(this.file, message, { ...opts, node, source: source ? `myst-to-jats:${source}` : 'myst-to-jats', ruleId: RuleId.jatsRenders, }); } error(message, node, source, opts) { fileError(this.file, message, { ...opts, node, source: source ? `myst-to-jats:${source}` : 'myst-to-jats', ruleId: RuleId.jatsRenders, }); } pushNode(el) { const top = this.top(); if (this.stack.length && el && 'elements' in top) top.elements?.push(el); return el; } text(text) { const top = this.top(); const value = text; if (!value || !this.stack.length || !('elements' in top)) return; const last = top.elements?.[top.elements.length - 1]; if (last?.type === 'text') { // The last node is also text, merge it last.text += `${escapeForXML(value)}`; return last; } const node = createText(value); top.elements?.push(node); return node; } renderChildren(node) { const parent = Array.isArray(node) ? { children: node } : node; const children = Array.isArray(node) ? node : node.children; children?.forEach((child) => { const handler = this.handlers[child.type]; if (handler) { handler(child, this, parent); } else { fileError(this.file, `Unhandled JATS conversion for node of "${child.type}"`, { node: child, source: 'myst-to-jats', }); } }); } renderInline(node, name, attributes) { this.openNode(name, { id: name !== 'xref' && node.identifier ? node.identifier : undefined, ...attributes, }); if ('children' in node) { this.renderChildren(node); } else if ('value' in node && node.value) { this.text(node.value); } this.closeNode(); } addLeaf(name, attributes) { this.openNode(name, attributes, true); this.closeNode(); } openNode(name, attributes, isLeaf = false) { const node = name === 'cdata' ? { type: 'cdata', cdata: attributes?.cdata } : { type: 'element', name, attributes }; if (!isLeaf) node.elements = []; this.stack.push(node); } closeNode() { const node = this.stack.pop(); return this.pushNode(node); } elements() { return this.stack[0].elements ?? []; } } export class JatsDocument { file; options; content; constructor(file, content, opts) { this.file = file; this.options = opts ?? {}; this.content = content; } article(articleType, specificUse) { const attributes = { 'xmlns:mml': 'http://www.w3.org/1998/Math/MathML', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:ali': 'http://www.niso.org/schemas/ali/1.0/', // Used for the licensing 'dtd-version': '1.3', 'xml:lang': 'en', }; if (articleType) attributes['article-type'] = articleType; if (specificUse) attributes['specific-use'] = specificUse; const isNotebookArticleRep = this.content.kind === SourceFileKind.Notebook; if (this.content.slug) { attributes.id = `${this.content.slug}${isNotebookArticleRep ? '-article' : ''}`; } const articleState = new JatsSerializer(this.file, this.content.mdast, { ...this.options, isNotebookArticleRep, extractAbstract: true, frontmatterParts: this.content.frontmatter?.parts, }); const inventory = {}; referenceTargetTransform(articleState.mdast, inventory, this.content.citations); const subArticles = this.options.subArticles ?? []; if (isNotebookArticleRep) { subArticles.unshift({ mdast: copyNode(this.content.mdast), kind: this.content.kind, frontmatter: this.content.frontmatter, slug: this.content.slug, // No citations here - don't want duplicates in the jats }); } affiliationIdTransform([this.content.frontmatter, ...subArticles.map((a) => a.frontmatter)].filter((fm) => !!fm), 'aff'); const subArticleStates = subArticles.map((subArticle, ind) => { const subArticleState = this.subArticleState(subArticle, ind === 0 && isNotebookArticleRep); referenceTargetTransform(subArticleState.mdast, inventory, subArticle.citations); return subArticleState; }); [articleState, ...subArticleStates].forEach((state) => { referenceResolutionTransform(state.mdast, inventory); state.render(); }); const elements = [ ...getFront(this.content.frontmatter, articleState), this.body(articleState), ...getBack(articleState, { citations: this.content.citations, footnotes: articleState.footnotes, expressions: articleState.expressions, referenceOrder: articleState.referenceOrder, }), ...subArticleStates.map((state, ind) => { return this.subArticle(state, subArticles[ind], ind === 0 && isNotebookArticleRep); }), ]; const article = { type: 'element', name: Tags.article, attributes, elements, }; return article; } frontStub(frontmatter, state, notebookRep) { const stubFrontmatter = {}; if (frontmatter) { // Do not duplicate frontmatter fields that are already in the article front Object.entries(frontmatter).forEach(([key, val]) => { const articleVal = this.content.frontmatter?.[key]; if (articleVal == null || JSON.stringify(val) !== JSON.stringify(articleVal)) { stubFrontmatter[key] = val; } }); } const articleMeta = getArticleMeta(stubFrontmatter, state); let elements = articleMeta?.elements ?? []; if (notebookRep) { const articleVersion = { type: 'element', name: 'article-version', attributes: { 'article-version-type': 'alt representation' }, elements: [{ type: 'text', text: 'notebook' }], }; // For valid JATS, article-id must be first if it is present and article-version must be next if (elements[0]?.name === 'article-id') { elements = [elements[0], articleVersion, ...elements.slice(1)]; } else { elements.unshift(articleVersion); } } return [{ type: 'element', name: 'front-stub', elements }]; } subArticleState(content, notebookRep) { return new JatsSerializer(this.file, content.mdast, { ...this.options, isNotebookArticleRep: false, isSubArticle: true, slug: content.slug, extractAbstract: !notebookRep, frontmatterParts: content.frontmatter?.parts, }); } subArticle(subArticleState, content, notebookRep) { const elements = [ ...this.frontStub(content.frontmatter, subArticleState, notebookRep), { type: 'element', name: 'body', elements: subArticleState.elements() }, ...getBack(subArticleState, { citations: content.citations, footnotes: subArticleState.footnotes, expressions: subArticleState.expressions, referenceOrder: subArticleState.referenceOrder, }), ]; const attributes = {}; if (content.slug) attributes.id = content.slug; return { type: 'element', name: 'sub-article', elements, attributes }; } body(state) { if (!state) { state = new JatsSerializer(this.file, this.content.mdast, this.options); state.render(); } return { type: 'element', name: 'body', elements: state.elements() }; } } export function writeJats(file, content, opts) { const doc = new JatsDocument(file, content, opts); const element = opts?.writeFullArticle ? { type: 'element', elements: [ { type: 'doctype', doctype: 'article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Archiving and Interchange DTD with MathML3 v1.3 20210610//EN" "http://jats.nlm.nih.gov/publishing/1.3/JATS-archivearticle1-3-mathml3.dtd"', }, doc.article(), ], declaration: { attributes: { version: '1.0', encoding: 'UTF-8' } }, } : doc.body(); const xml = serializeJatsXml(element, opts); file.result = xml; return file; } const plugin = function (kind, frontmatter, citations, slug, opts) { this.Compiler = (node, file) => { return writeJats(file, { mdast: node, kind, frontmatter, citations, slug }, opts); }; return (node) => { // Preprocess return node; }; }; export default plugin;