diff --git a/package-lock.json b/package-lock.json index 883b38b44..e1a95da83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15414,6 +15414,7 @@ "license": "MIT", "dependencies": { "classnames": "^2.3.2", + "csv-parse": "^5.5.5", "js-yaml": "^4.1.0", "myst-common": "^1.2.0", "myst-spec-ext": "^1.2.0", @@ -15422,6 +15423,11 @@ "vfile": "^5.3.7" } }, + "packages/myst-directives/node_modules/csv-parse": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.5.tgz", + "integrity": "sha512-erCk7tyU3yLWAhk6wvKxnyPtftuy/6Ak622gOO7BCJ05+TYffnPCJF905wmOQm+BpkX54OdAl8pveJwUdpnCXQ==" + }, "packages/myst-execute": { "version": "0.0.5", "license": "MIT", diff --git a/packages/myst-directives/package.json b/packages/myst-directives/package.json index de00a65ae..47c9294e7 100644 --- a/packages/myst-directives/package.json +++ b/packages/myst-directives/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "classnames": "^2.3.2", + "csv-parse": "^5.5.5", "js-yaml": "^4.1.0", "myst-common": "^1.2.0", "myst-spec-ext": "^1.2.0", diff --git a/packages/myst-directives/src/table.ts b/packages/myst-directives/src/table.ts index 48cd1ee36..a49f59f25 100644 --- a/packages/myst-directives/src/table.ts +++ b/packages/myst-directives/src/table.ts @@ -1,6 +1,7 @@ -import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import type { DirectiveSpec, DirectiveData, DirectiveContext, GenericNode } from 'myst-common'; import { fileError, normalizeLabel, RuleId } from 'myst-common'; import type { VFile } from 'vfile'; +import { parse } from 'csv-parse/sync'; export const tableDirective: DirectiveSpec = { name: 'table', @@ -162,3 +163,94 @@ export const listTableDirective: DirectiveSpec = { return [container]; }, }; + +export const csvTableDirective: DirectiveSpec = { + name: 'csv-table', + arg: { + type: 'myst', + }, + options: { + label: { + type: String, + alias: ['name'], + }, + 'header-rows': { + type: Number, + // nonnegative int + }, + class: { + type: String, + // class_option: list of strings? + doc: `CSS classes to add to your table. Special classes include: + +- \`full-width\`: changes the table environment to cover two columns in LaTeX`, + }, + align: { + type: String, + // choice(['left', 'center', 'right']) + }, + delim: { + type: String, + }, + escape: { + type: String, + }, + keepspace: { + type: Boolean, + }, + quote: { + type: String, + }, + }, + body: { + type: String, + required: true, + }, + run(data: DirectiveData, vfile: VFile, ctx: DirectiveContext): GenericNode[] { + const delimiter = (data.options?.delimiter ?? ',') as string; + const records = parse(data.body as string, { + delimiter, + ltrim: !data.options?.keepspace, + escape: (data.options?.escape ?? delimiter) as string, + quote: (data.options?.quote ?? '"') as string, + }); + + const { label, identifier } = normalizeLabel(data.options?.label as string | undefined) || {}; + + let headerCount = (data.options?.['header-rows'] as number) || 0; + const rows = records.map((record: any, recordIndex: number) => { + const cells = record.map((cell: string) => { + const rawCells = ctx.parseMyst(cell, recordIndex); + if (!(rawCells.length === 1 && rawCells[0].type === 'paragraph')) { + throw new Error(`Expected a single paragraph node, encountered ${rawCells[0].type}`); + } + return { + type: 'tableCell', + header: headerCount > 0 ? true : undefined, + children: rawCells[0].children, + }; + }); + headerCount -= 1; + // Parsing produes multiple nodes + return { + type: 'tableRow', + children: cells, + }; + }); + const table = { + type: 'table', + align: data.options?.align, + children: rows, + }; + const container = { + type: 'container', + kind: 'table', + identifier: identifier, + label: label, + class: data.options?.class, + children: [...(data.arg as GenericNode[]), table], + }; + + return [container]; + }, +};