diff --git a/.changeset/quiet-taxis-grab.md b/.changeset/quiet-taxis-grab.md new file mode 100644 index 000000000..cd9ae9603 --- /dev/null +++ b/.changeset/quiet-taxis-grab.md @@ -0,0 +1,8 @@ +--- +'myst-transforms': patch +'myst-ext-proof': patch +'jats-to-myst': patch +'myst-to-tex': patch +--- + +Add support for proofs in LaTeX diff --git a/package-lock.json b/package-lock.json index 8d581c584..5f5133c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13755,7 +13755,8 @@ "version": "1.0.4", "license": "MIT", "dependencies": { - "myst-common": "^1.1.7" + "myst-common": "^1.1.7", + "myst-spec": "^0.0.4" }, "devDependencies": { "myst-parser": "^1.0.8" @@ -14053,6 +14054,7 @@ "license": "MIT", "dependencies": { "myst-common": "^1.1.8", + "myst-ext-proof": "^1.0.3", "myst-frontmatter": "^1.1.8", "myst-spec-ext": "^1.1.8", "unist-util-remove": "^3.1.0", diff --git a/packages/jats-to-myst/package.json b/packages/jats-to-myst/package.json index 99f424f21..c2ae86acc 100644 --- a/packages/jats-to-myst/package.json +++ b/packages/jats-to-myst/package.json @@ -49,5 +49,8 @@ "unist-util-remove": "^3.1.0", "vfile": "^5.0.0", "vfile-reporter": "^7.0.4" + }, + "devDependencies": { + "myst-to-tex": "^1.0.7" } } diff --git a/packages/myst-ext-proof/package.json b/packages/myst-ext-proof/package.json index e94d9dd36..ce32c7e16 100644 --- a/packages/myst-ext-proof/package.json +++ b/packages/myst-ext-proof/package.json @@ -32,6 +32,7 @@ "url": "https://github.com/executablebooks/mystmd/issues" }, "dependencies": { + "myst-spec": "^0.0.4", "myst-common": "^1.1.7" }, "devDependencies": { diff --git a/packages/myst-ext-proof/src/index.ts b/packages/myst-ext-proof/src/index.ts index 14b7cd30f..59d6723fa 100644 --- a/packages/myst-ext-proof/src/index.ts +++ b/packages/myst-ext-proof/src/index.ts @@ -1,2 +1,2 @@ export { proofDirective } from './proof.js'; -export { ProofKind } from './types.js'; +export type { ProofKind, ProofContainer } from './types.js'; diff --git a/packages/myst-ext-proof/src/types.ts b/packages/myst-ext-proof/src/types.ts index d16fffdf7..e1f41af0b 100644 --- a/packages/myst-ext-proof/src/types.ts +++ b/packages/myst-ext-proof/src/types.ts @@ -1,17 +1,26 @@ -export enum ProofKind { - proof = 'proof', - axiom = 'axiom', - lemma = 'lemma', - definition = 'definition', - criterion = 'criterion', - remark = 'remark', - conjecture = 'conjecture', - corollary = 'corollary', - algorithm = 'algorithm', - example = 'example', - property = 'property', - observation = 'observation', - proposition = 'proposition', - assumption = 'assumption', - theorem = 'theorem', -} +import type { Container } from 'myst-spec'; + +const PROOF_KINDS = [ + 'proof', + 'axiom', + 'lemma', + 'definition', + 'criterion', + 'remark', + 'conjecture', + 'corollary', + 'algorithm', + 'example', + 'property', + 'observation', + 'proposition', + 'assumption', + 'theorem', +] as const; +type ProofKinds = typeof PROOF_KINDS; + +export type ProofKind = ProofKinds[number]; + +export type ProofContainer = Omit & { + kind: ProofKind; +}; diff --git a/packages/myst-to-tex/package.json b/packages/myst-to-tex/package.json index 9efbdc9d7..a4ffdd84a 100644 --- a/packages/myst-to-tex/package.json +++ b/packages/myst-to-tex/package.json @@ -40,6 +40,7 @@ "myst-common": "^1.1.8", "myst-frontmatter": "^1.1.8", "myst-spec-ext": "^1.1.8", + "myst-ext-proof": "^1.0.3", "unist-util-remove": "^3.1.0", "unist-util-select": "^4.0.3", "vfile-reporter": "^7.0.4" diff --git a/packages/myst-to-tex/src/index.ts b/packages/myst-to-tex/src/index.ts index 294194512..6811fb76a 100644 --- a/packages/myst-to-tex/src/index.ts +++ b/packages/myst-to-tex/src/index.ts @@ -17,6 +17,7 @@ import MATH_HANDLERS, { withRecursiveCommands } from './math.js'; import { selectAll } from 'unist-util-select'; import type { FootnoteDefinition } from 'myst-spec-ext'; import { transformLegends } from './legends.js'; +import { TexProofSerializer, proofHandler } from './proof.js'; export type { LatexResult } from './types.js'; @@ -332,6 +333,7 @@ const handlers: Record = { state.closeBlock(node); }, container: containerHandler, + proof: proofHandler, caption: captionHandler, captionNumber: () => undefined, crossReference(node, state, parent) { @@ -598,14 +600,20 @@ const plugin: Plugin<[Options?], Root, VFile> = function (opts) { transformLegends(node); const state = new TexSerializer(file, node, opts ?? { handlers }); - let tex = (file.result as string).trim(); - const glossaryState = new TexGlossaryAndAcronymSerializer(state.glossary, state.abbreviations); + + const preamble: string[] = []; + if (state.data.hasProofs) { + preamble.push(new TexProofSerializer().preamble); + } + preamble.push(opts?.printGlossaries ? glossaryState.preamble : ''); + + let tex = (file.result as string).trim(); tex += opts?.printGlossaries ? '\n' + glossaryState.printedDefinitions : ''; const result: LatexResult = { imports: [...state.data.imports], - preamble: opts?.printGlossaries ? glossaryState.preamble : '', + preamble: preamble.join('\n\n'), commands: withRecursiveCommands(state), value: tex, }; diff --git a/packages/myst-to-tex/src/proof.ts b/packages/myst-to-tex/src/proof.ts new file mode 100644 index 000000000..72c024199 --- /dev/null +++ b/packages/myst-to-tex/src/proof.ts @@ -0,0 +1,108 @@ +import { fileError, writeTexLabelledComment, RuleId } from 'myst-common'; +import type { GenericNode } from 'myst-common'; +import type { Text } from 'myst-spec'; +import type { ProofContainer, ProofKind } from 'myst-ext-proof'; +import { select } from 'unist-util-select'; +import { remove } from 'unist-util-remove'; +import type { Handler } from './types.js'; + +function kindToEnvironment(kind: ProofKind): string { + switch (kind) { + case 'theorem': + return 'theorem'; + case 'proof': + return 'proof'; + case 'proposition': + return 'proposition'; + case 'definition': + return 'definition'; + case 'example': + return 'example'; + case 'remark': + return 'remark'; + case 'axiom': + return 'axiom'; + case 'conjecture': + return 'conjecture'; + case 'lemma': + return 'lemma'; + case 'observation': + return 'observation'; + default: + return ''; + } +} + +export const proofHandler: Handler = (node, state) => { + state.usePackages('amsthm'); + + const p = node as ProofContainer; + const env = kindToEnvironment(p.kind); + if (!env) { + fileError(state.file, `Unhandled LaTeX proof environment "${p.kind}"`, { + node, + source: 'myst-to-tex', + ruleId: RuleId.texRenders, + }); + return; + } + + const t = select('admonitionTitle > text', node); + if (t) { + // Do not render the title twice + t.type = '__delete__'; + } + const newNode = remove(node, '__delete__') as GenericNode; + + state.write('\\begin{'); + state.write(env); + state.write('}'); + if (t) { + state.write('['); + state.write((t as Text).value); + state.write(']'); + } + if (newNode.identifier && newNode.identifier.length > 0) { + state.write('\\label{'); + state.write(newNode.identifier); + state.write('}'); + } + state.renderChildren(newNode, true); + state.write('\\end{'); + state.write(env); + state.write('}'); + + state.data.hasProofs = true; +}; + +export class TexProofSerializer { + static COMMENT_LENGTH = 50; + + preamble: string; + + constructor() { + this.preamble = this.renderThmDefinitions(); + } + + private renderThmDefinitions(): string { + const definitions = [ + '\\newtheorem{theorem}{Theorem}[section]', + '\\newtheorem{corollary}{Corollary}[theorem]', + '\\newtheorem{lemma}[theorem]{Lemma}', + '\\newtheorem{proposition}{Proposition}[section]', + '\\newtheorem{definition}{Definition}[section]', + '\\newtheorem{example}{Example}[section]', + '\\newtheorem{remark}{Remark}[section]', + '\\newtheorem{axiom}{Axiom}[section]', + '\\newtheorem{conjecture}{Conjecture}[section]', + '\\newtheorem{observation}{Observation}[section]', + ]; + const block = writeTexLabelledComment( + 'theorem', + definitions, + TexProofSerializer.COMMENT_LENGTH, + ); + const percents = ''.padEnd(TexProofSerializer.COMMENT_LENGTH, '%'); + return `${percents}\n${block}${percents}`; + } +} diff --git a/packages/myst-to-tex/src/types.ts b/packages/myst-to-tex/src/types.ts index d49ffd31d..5969e37e5 100644 --- a/packages/myst-to-tex/src/types.ts +++ b/packages/myst-to-tex/src/types.ts @@ -33,6 +33,7 @@ export type StateData = { nextCaptionNumbered?: boolean; nextHeadingIsFrameTitle?: boolean; nextCaptionId?: string; + hasProofs?: boolean; mathPlugins: Required['math']; imports: Set; }; diff --git a/packages/myst-to-tex/tests/proofs.yml b/packages/myst-to-tex/tests/proofs.yml new file mode 100644 index 000000000..4ad1fa8b6 --- /dev/null +++ b/packages/myst-to-tex/tests/proofs.yml @@ -0,0 +1,209 @@ +title: myst-to-tex basic features +cases: + - title: theorem after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a theorem' + - type: proof + kind: theorem + label: theo-t1 + identifier: theo-t1 + enumerated: true + children: + - type: text + value: 'Given this, then that.' + latex: |- + Some text before a theorem + + \begin{theorem}\label{theo-t1}Given this, then that.\end{theorem} + - title: theorem ad proof after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a theorem' + - type: proof + kind: theorem + label: theo-t1 + identifier: theo-t1 + enumerated: true + children: + - type: text + value: 'Given this, then that.' + - type: proof + kind: proof + label: proof-t1 + identifier: proof-t1 + children: + - type: text + value: 'qed.' + latex: |- + Some text before a theorem + + \begin{theorem}\label{theo-t1}Given this, then that.\end{theorem}\begin{proof}\label{proof-t1}qed.\end{proof} + - title: proposition after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a proposition' + - type: proof + kind: proposition + label: prop-t1 + identifier: prop-t1 + enumerated: true + children: + - type: text + value: 'Given this, then that.' + latex: |- + Some text before a proposition + + \begin{proposition}\label{prop-t1}Given this, then that.\end{proposition} + - title: definition after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a definition' + - type: proof + kind: definition + label: def-t1 + identifier: def-t1 + enumerated: true + children: + - type: text + value: 'Given this, then that.' + latex: |- + Some text before a definition + + \begin{definition}\label{def-t1}Given this, then that.\end{definition} + - title: example after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before an example' + - type: proof + kind: example + label: ex-t1 + identifier: ex-t1 + enumerated: true + children: + - type: text + value: 'This is it.' + latex: |- + Some text before an example + + \begin{example}\label{ex-t1}This is it.\end{example} + - title: remark after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a remark' + - type: proof + kind: remark + label: rem-t1 + identifier: rem-t1 + enumerated: true + children: + - type: text + value: 'This is it.' + latex: |- + Some text before a remark + + \begin{remark}\label{rem-t1}This is it.\end{remark} + - title: axiom after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before an axiom' + - type: proof + kind: axiom + label: ax-t1 + identifier: ax-t1 + enumerated: true + children: + - type: text + value: 'This is it.' + latex: |- + Some text before an axiom + + \begin{axiom}\label{ax-t1}This is it.\end{axiom} + - title: conjecture after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a conjecture' + - type: proof + kind: conjecture + label: conj-t1 + identifier: conj-t1 + enumerated: true + children: + - type: text + value: 'This is it.' + latex: |- + Some text before a conjecture + + \begin{conjecture}\label{conj-t1}This is it.\end{conjecture} + - title: lemma after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before a lemma' + - type: proof + kind: lemma + label: lem-t1 + identifier: lem-t1 + enumerated: true + children: + - type: text + value: 'When this, then that.' + latex: |- + Some text before a lemma + + \begin{lemma}\label{lem-t1}When this, then that.\end{lemma} + - title: observation after paragraph + mdast: + type: root + children: + - type: paragraph + children: + - type: text + value: 'Some text before an observation' + - type: proof + kind: observation + label: obs-t1 + identifier: obs-t1 + enumerated: true + children: + - type: text + value: 'This is it.' + latex: |- + Some text before an observation + + \begin{observation}\label{obs-t1}This is it.\end{observation} diff --git a/packages/myst-transforms/src/abbreviations.ts b/packages/myst-transforms/src/abbreviations.ts index 564eef3e0..0cdf4851d 100644 --- a/packages/myst-transforms/src/abbreviations.ts +++ b/packages/myst-transforms/src/abbreviations.ts @@ -2,7 +2,7 @@ import type { Plugin } from 'unified'; import type { GenericParent } from 'myst-common'; import { toText } from 'myst-common'; import { selectAll } from 'unist-util-select'; -import type { Abbreviation, StaticPhrasingContent, Text } from 'myst-spec'; +import type { Abbreviation, Text } from 'myst-spec'; import { u } from 'unist-builder'; import type { FindAndReplaceSchema, RegExpMatchObject } from 'mdast-util-find-and-replace'; import { findAndReplace } from 'mdast-util-find-and-replace';