From 6ede39858e89531992ef54dce12380706a0089f1 Mon Sep 17 00:00:00 2001 From: Franklin Koch Date: Tue, 21 Apr 2026 20:43:17 -0600 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=AA=91=20Add=20chair=20contributor=5F?= =?UTF-8?q?type=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/perfect-numbers-beg.md | 5 +++++ README.md | 4 +++- src/cli/deposit.ts | 20 +++++++++++++++++++- src/contributors.ts | 8 ++++++-- tests/contributors.spec.ts | 26 ++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 .changeset/perfect-numbers-beg.md create mode 100644 tests/contributors.spec.ts diff --git a/.changeset/perfect-numbers-beg.md b/.changeset/perfect-numbers-beg.md new file mode 100644 index 0000000..5ca7a9a --- /dev/null +++ b/.changeset/perfect-numbers-beg.md @@ -0,0 +1,5 @@ +--- +"crossref-utils": patch +--- + +Add chair contributor_type option diff --git a/README.md b/README.md index 2cbc7d2..cbd1591 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ This will prompt the user to select new DOIs, if they are not present in MyST me - `--name`, `--email`: Depositor name and email. Default Depositor is Curvenote. - `--registrant`: Registrant organization. Default is `Crossref` - likely this should not be changed. - `--id`: Unique ID for the deposit. By default, a random ID will be autogenerated - likely this should not be changed. +- `--contributor-type`: For **conference** deposits only, sets the `contributor_role` for people listed under MyST `editors` in the proceedings metadata. Values are `editor` (default) or `chair`. Other deposit types ignore this flag. ### Deposit types @@ -85,7 +86,8 @@ You may also specify in the frontmatter: - `venue.issn` - series issn - `venue.doi` - series doi - `volume.subject` - proceedings subject -- `editors` - proceedings editors +- `volume.doi` - proceedings doi +- `editors` - proceedings editors (or "chairs" if `--contributor-type chair` is specified) #### Dataset diff --git a/src/cli/deposit.ts b/src/cli/deposit.ts index 35af3da..9bf631a 100644 --- a/src/cli/deposit.ts +++ b/src/cli/deposit.ts @@ -57,6 +57,7 @@ type DepositOptions = { journalAbbr?: string; journalDoi?: string; prefix?: string; + contributorType?: 'editor' | 'chair'; }; type DepositSource = { @@ -369,7 +370,11 @@ function issueDataFromArticles( } } if (editors?.length && !proceedingsEditors) { - proceedingsEditors = contributorsXmlFromMystEditors(frontmatter); + const proceedingsEditorRole = + opts.type === 'conference' && opts.contributorType === 'chair' ? 'chair' : 'editor'; + proceedingsEditors = contributorsXmlFromMystEditors(frontmatter, { + contributor_role: proceedingsEditorRole, + }); } }); if (!publicationDate && (volumeNumber || volumeDoi || issueNumber || issueDoi)) { @@ -419,6 +424,11 @@ export async function deposit(session: ISession, opts: DepositOptions) { if (!depositType) { throw new Error('No deposit type specified'); } + if (depositType !== 'conference' && opts.contributorType === 'chair') { + session.log.warn( + '`--contributor-type chair` only applies to conference deposits; using editor role for this deposit type.', + ); + } if (!name) { const resp = await inquirer.prompt([ { @@ -661,6 +671,14 @@ function makeDepositCLI(program: Command) { .addOption(new Option('--registrant ', 'Registrant organization').default('Crossref')) .addOption(new Option('-o, --output ', 'Output file')) .addOption(new Option('--prefix ', 'Prefix for new DOIs')) + .addOption( + new Option( + '--contributor-type ', + 'Contributor role for myst `editors` in conference proceedings (conference only; default editor)', + ) + .choices(['editor', 'chair']) + .default('editor'), + ) .action(clirun(deposit, { program, getSession: (logger) => new Session({ logger }) })); return command; } diff --git a/src/contributors.ts b/src/contributors.ts index 23ae205..90074fb 100644 --- a/src/contributors.ts +++ b/src/contributors.ts @@ -70,7 +70,11 @@ export function contributorsXmlFromMystAuthors( ); } -export function contributorsXmlFromMystEditors(myst: PageFrontmatter): Element | undefined { +export function contributorsXmlFromMystEditors( + myst: PageFrontmatter, + opts?: { contributor_role?: ContributorOptions['contributor_role'] }, +): Element | undefined { + const contributor_role = opts?.contributor_role ?? 'editor'; const editors = myst.editors ?.map((editor) => myst.contributors?.find(({ id }) => editor === id)) @@ -88,7 +92,7 @@ export function contributorsXmlFromMystEditors(myst: PageFrontmatter): Element | contributorXml({ ...(editor as ContributorOptions), sequence: index === 0 ? 'first' : 'additional', - contributor_role: 'editor', + contributor_role, }), ), ); diff --git a/tests/contributors.spec.ts b/tests/contributors.spec.ts new file mode 100644 index 0000000..c31f3cb --- /dev/null +++ b/tests/contributors.spec.ts @@ -0,0 +1,26 @@ +import { describe, test, expect } from 'vitest'; +import { toXml } from 'xast-util-to-xml'; +import type { PageFrontmatter } from 'myst-frontmatter'; +import { contributorsXmlFromMystEditors } from '../src/contributors.js'; + +describe('contributorsXmlFromMystEditors', () => { + const baseMyst = { + editors: ['ed1'], + contributors: [ + { + id: 'ed1', + nameParsed: { given: 'Pat', family: 'Organizer', literal: 'Pat Organizer' }, + }, + ], + } as unknown as PageFrontmatter; + + test('defaults myst editors to Crossref contributor_role editor', () => { + const el = contributorsXmlFromMystEditors(baseMyst); + expect(toXml(el!)).toContain('contributor_role="editor"'); + }); + + test('supports chair role for proceedings editors', () => { + const el = contributorsXmlFromMystEditors(baseMyst, { contributor_role: 'chair' }); + expect(toXml(el!)).toContain('contributor_role="chair"'); + }); +}); From 4ed24586488b2d7a432ad624064464e608010899 Mon Sep 17 00:00:00 2001 From: Franklin Koch Date: Tue, 21 Apr 2026 23:54:44 -0600 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=A7=20Remove=20xrefs=20from=20abst?= =?UTF-8?q?ract=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/pretty-otters-fix.md | 5 +++++ src/cli/deposit.ts | 27 ++++++++++++++++----------- src/cli/utils.ts | 24 ++++++++++++++++++++++++ tests/utils.spec.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 .changeset/pretty-otters-fix.md diff --git a/.changeset/pretty-otters-fix.md b/.changeset/pretty-otters-fix.md new file mode 100644 index 0000000..51fa4cf --- /dev/null +++ b/.changeset/pretty-otters-fix.md @@ -0,0 +1,5 @@ +--- +"crossref-utils": patch +--- + +Remove xrefs from abstract tree diff --git a/src/cli/deposit.ts b/src/cli/deposit.ts index 9bf631a..8b19657 100644 --- a/src/cli/deposit.ts +++ b/src/cli/deposit.ts @@ -33,6 +33,7 @@ import { transformCiteToText, transformNewlineToSpace, transformXrefToLink, + unwrapJatsXrefElements, } from './utils.js'; import type { ProjectFrontmatter } from 'myst-frontmatter'; import { selectNewDois } from './generate.js'; @@ -131,11 +132,13 @@ export async function depositArticleFromSource(session: ISession, depositSource: transformNewlineToSpace(abstractPart); const serializer = new JatsSerializer(new VFile(), abstractPart as any); const jats = serializer.render(true).elements(); - abstract = u( - 'element', - { name: 'jats:abstract' }, - jats.map((e) => element2JatsUnist(e)), - ) as Element; + abstract = unwrapJatsXrefElements( + u( + 'element', + { name: 'jats:abstract' }, + jats.map((e) => element2JatsUnist(e)), + ) as Element, + ); } else if (description) { // Use the project description as the fallback for the abstract abstractPart = { @@ -145,11 +148,13 @@ export async function depositArticleFromSource(session: ISession, depositSource: transformNewlineToSpace(abstractPart); const serializer = new JatsSerializer(new VFile(), abstractPart as any); const jats = serializer.render(true).elements(); - abstract = u( - 'element', - { name: 'jats:abstract' }, - jats.map((e) => element2JatsUnist(e)), - ) as Element; + abstract = unwrapJatsXrefElements( + u( + 'element', + { name: 'jats:abstract' }, + jats.map((e) => element2JatsUnist(e)), + ) as Element, + ); } return { frontmatter: frontmatter ?? {}, dois, abstract, configFile }; } @@ -674,7 +679,7 @@ function makeDepositCLI(program: Command) { .addOption( new Option( '--contributor-type ', - 'Contributor role for myst `editors` in conference proceedings (conference only; default editor)', + 'Contributor role for myst `editors` in conference proceedings', ) .choices(['editor', 'chair']) .default('editor'), diff --git a/src/cli/utils.ts b/src/cli/utils.ts index 105b07d..a2a6212 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import { u } from 'unist-builder'; import { selectAll } from 'unist-util-select'; import { liftChildren, type GenericNode, type GenericParent } from 'myst-common'; +import type { Element, ElementContent } from 'xast'; type JatsAttributes = Record; @@ -30,6 +31,29 @@ export function element2JatsUnist(element: JatsElement): Node { throw new Error(`Invalid Jats element: ${element}`); } +/** + * Remove `jats:xref` wrappers from a JATS xast subtree, keeping only their children. + */ +export function unwrapJatsXrefElements(node: Element): Element { + const children = node.children ?? []; + const newChildren: ElementContent[] = []; + for (const child of children) { + if (child.type === 'element') { + const processed = unwrapJatsXrefElements(child); + if (processed.name === 'jats:xref') { + for (const inner of processed.children) { + newChildren.push(inner.type === 'element' ? unwrapJatsXrefElements(inner) : inner); + } + } else { + newChildren.push(processed); + } + } else { + newChildren.push(child); + } + } + return { ...node, children: newChildren }; +} + /** * Transform to handle xrefs that resolve to external sites * diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index cfeb09b..e524e69 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -1,6 +1,8 @@ import { describe, test, expect } from 'vitest'; +import { u } from 'unist-builder'; import { toXml } from 'xast-util-to-xml'; import { publicationDateXml } from '../src'; +import { unwrapJatsXrefElements } from '../src/cli/utils.js'; import type { Element } from 'xast'; describe('CrossRef Utilities', () => { @@ -29,3 +31,31 @@ describe('CrossRef Utilities', () => { } }); }); + +describe('unwrapJatsXrefElements', () => { + test('removes jats:xref wrapper, keeps children', () => { + const tree = u('element', { name: 'jats:p', attributes: {} }, [ + u('text', 'See '), + u('element', { name: 'jats:xref', attributes: { 'ref-type': 'fig', rid: 'f1' } }, [ + u('element', { name: 'jats:bold', attributes: {} }, [u('text', 'Figure 1')]), + ]), + u('text', ' for details.'), + ]) as Element; + const out = unwrapJatsXrefElements(tree); + const xml = toXml(out); + expect(xml).not.toContain('xref'); + expect(xml).toContain('Figure 1'); + expect(xml).toContain('See '); + expect(xml).toContain('for details.'); + }); + + test('unwraps nested jats:xref', () => { + const tree = u('element', { name: 'jats:p', attributes: {} }, [ + u('element', { name: 'jats:xref', attributes: {} }, [ + u('element', { name: 'jats:xref', attributes: {} }, [u('text', 'inner')]), + ]), + ]) as Element; + const out = unwrapJatsXrefElements(tree); + expect(toXml(out)).toBe('inner'); + }); +});