From e98f86a6302e814921cf345f8a5f2524925274ab Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 28 May 2023 15:23:40 -0600 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=AA=86=20Unnest=20cross-references=20?= =?UTF-8?q?with=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/short-wolves-warn.md | 5 +++++ packages/myst-transforms/src/enumerate.ts | 27 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/short-wolves-warn.md diff --git a/.changeset/short-wolves-warn.md b/.changeset/short-wolves-warn.md new file mode 100644 index 000000000..80bff50b1 --- /dev/null +++ b/.changeset/short-wolves-warn.md @@ -0,0 +1,5 @@ +--- +'myst-transforms': patch +--- + +Unnest links from cross references diff --git a/packages/myst-transforms/src/enumerate.ts b/packages/myst-transforms/src/enumerate.ts index 822e8a623..d1f3027ca 100644 --- a/packages/myst-transforms/src/enumerate.ts +++ b/packages/myst-transforms/src/enumerate.ts @@ -5,7 +5,15 @@ import type { Container, CrossReference, Heading, Link, Math, Paragraph } from ' import { visit } from 'unist-util-visit'; import { select, selectAll } from 'unist-util-select'; import { findAndReplace } from 'mdast-util-find-and-replace'; -import { createHtmlId, fileWarn, normalizeLabel, setTextAsChild, copyNode } from 'myst-common'; +import type { GenericNode } from 'myst-common'; +import { + createHtmlId, + fileWarn, + normalizeLabel, + setTextAsChild, + copyNode, + liftChildren, +} from 'myst-common'; const TRANSFORM_NAME = 'myst-transforms:enumerate'; @@ -548,6 +556,22 @@ export const resolveReferenceLinksTransform = (tree: Root, opts: StateOptions) = }); }; +/** Cross references cannot contain links, but should retain their content */ +function unnestCrossReferencesTransform(tree: Root) { + const xrefs = selectAll('crossReference', tree) as GenericNode[]; + xrefs.forEach((xref) => { + const children = xref.children as any; + if (!children) return; + const subtree = { type: 'root', children: copyNode(children) } as any; + const nested = select('crossReference,link', subtree); + if (!nested) return; + liftChildren(subtree, 'link'); + liftChildren(subtree, 'crossReference'); + xref.children = subtree.children; + }); + return tree.children as PhrasingContent[]; +} + export const resolveCrossReferencesTransform = (tree: Root, opts: StateOptions) => { visit(tree, 'crossReference', (node: CrossReference) => { opts.state.resolveReferenceContent(node); @@ -558,6 +582,7 @@ export const resolveReferencesTransform = (tree: Root, file: VFile, opts: StateO resolveReferenceLinksTransform(tree, opts); resolveCrossReferencesTransform(tree, opts); addContainerCaptionNumbersTransform(tree, file, opts); + unnestCrossReferencesTransform(tree); }; export const resolveReferencesPlugin: Plugin<[StateOptions], Root, Root> = From a7584e27fd08582f9ab896543a87f2fdddc91228 Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 28 May 2023 15:24:43 -0600 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8C=89=20Add=20support=20for=20gated?= =?UTF-8?q?=20directives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/quiet-bananas-sin.md | 6 ++ packages/myst-cli/src/process/mdast.ts | 2 + packages/myst-transforms/src/index.ts | 1 + .../myst-transforms/src/joinGates.spec.ts | 56 +++++++++++++++++++ packages/myst-transforms/src/joinGates.ts | 55 ++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 .changeset/quiet-bananas-sin.md create mode 100644 packages/myst-transforms/src/joinGates.spec.ts create mode 100644 packages/myst-transforms/src/joinGates.ts diff --git a/.changeset/quiet-bananas-sin.md b/.changeset/quiet-bananas-sin.md new file mode 100644 index 000000000..36cb0d357 --- /dev/null +++ b/.changeset/quiet-bananas-sin.md @@ -0,0 +1,6 @@ +--- +'myst-cli': patch +'myst-transforms': patch +--- + +Add transform for gated directives diff --git a/packages/myst-cli/src/process/mdast.ts b/packages/myst-cli/src/process/mdast.ts index 416e1d04c..137c74e78 100644 --- a/packages/myst-cli/src/process/mdast.ts +++ b/packages/myst-cli/src/process/mdast.ts @@ -21,6 +21,7 @@ import { GithubTransformer, RRIDTransformer, DOITransformer, + joinGatesPlugin, } from 'myst-transforms'; import { unified } from 'unified'; import { VFile } from 'vfile'; @@ -136,6 +137,7 @@ export async function transformMdast( .use(htmlPlugin, { htmlHandlers }) .use(mathPlugin, { macros: frontmatter.math }) .use(enumerateTargetsPlugin, { state }) // This should be after math + .use(joinGatesPlugin) .run(mdast, vfile); // Run the link transformations that can be done without knowledge of other files diff --git a/packages/myst-transforms/src/index.ts b/packages/myst-transforms/src/index.ts index 8299c38b7..e8fc02c8f 100644 --- a/packages/myst-transforms/src/index.ts +++ b/packages/myst-transforms/src/index.ts @@ -39,6 +39,7 @@ export { headingLabelPlugin, headingLabelTransform, } from './targets'; +export { joinGatesPlugin, joinGatesTransform } from './joinGates'; // Enumeration export type { IReferenceState, NumberingOptions, TargetKind, ReferenceKind } from './enumerate'; diff --git a/packages/myst-transforms/src/joinGates.spec.ts b/packages/myst-transforms/src/joinGates.spec.ts new file mode 100644 index 000000000..364791c56 --- /dev/null +++ b/packages/myst-transforms/src/joinGates.spec.ts @@ -0,0 +1,56 @@ +import { unified } from 'unified'; +import { VFile } from 'vfile'; +import { joinGatesPlugin } from './joinGates'; + +describe('Test gate directive joining', () => { + test('Test basic gate', () => { + const file = new VFile(); + const open = { type: 'node', gate: 'start' } as any; + const paragraph = { type: 'paragraph' } as any; + const close = { type: 'node', gate: 'end' } as any; + const mdast = { type: 'root', children: [open, paragraph, close] } as any; + unified().use(joinGatesPlugin).runSync(mdast, file); + expect(file.messages.length).toBe(0); + expect(open.gate).toBeUndefined(); + expect(mdast.children.length).toBe(1); + expect(open.children[0]).toBe(paragraph); + }); + test('Test mismatch gate', () => { + const file = new VFile(); + const open = { type: 'node', gate: 'start' } as any; + const paragraph = { type: 'paragraph' } as any; + const close = { type: 'not-the-same-node', gate: 'end' } as any; + const mdast = { type: 'root', children: [open, paragraph, close] } as any; + unified().use(joinGatesPlugin).runSync(mdast, file); + expect(file.messages.length).toBe(1); // Raise warning + expect(open.gate).toBeUndefined(); + expect(mdast.children.length).toBe(1); + expect(open.children[0]).toBe(paragraph); + }); + test('Test gate without end', () => { + const file = new VFile(); + const open = { type: 'node', gate: 'start' } as any; + const paragraph = { type: 'paragraph' } as any; + const mdast = { type: 'root', children: [open, paragraph] } as any; + unified().use(joinGatesPlugin).runSync(mdast, file); + expect(file.messages.length).toBe(1); // Raise error + expect(open.gate).toBe('start'); + expect(mdast.children.length).toBe(1); + expect(open.children[0]).toBe(paragraph); + }); + test('Test nested gates', () => { + const file = new VFile(); + const open = { type: 'node', gate: 'start' } as any; + const open2 = { type: 'node', gate: 'start' } as any; + const paragraph = { type: 'paragraph' } as any; + const close = { type: 'node', gate: 'end' } as any; + const close2 = { type: 'node', gate: 'end' } as any; + const mdast = { type: 'root', children: [open, open2, paragraph, close, close2] } as any; + unified().use(joinGatesPlugin).runSync(mdast, file); + expect(file.messages.length).toBe(0); + expect(open.gate).toBeUndefined(); + expect(mdast.children.length).toBe(1); + expect(open.children[0]).toBe(open2); + expect(open2.children[0]).toBe(paragraph); + }); +}); diff --git a/packages/myst-transforms/src/joinGates.ts b/packages/myst-transforms/src/joinGates.ts new file mode 100644 index 000000000..b6da78c4d --- /dev/null +++ b/packages/myst-transforms/src/joinGates.ts @@ -0,0 +1,55 @@ +import type { GenericNode, GenericParent } from 'myst-common'; +import { fileWarn, fileError } from 'myst-common'; +import { map } from 'unist-util-map'; +import type { Root } from 'mdast'; +import type { Plugin } from 'unified'; +import type { VFile } from 'vfile'; + +export function joinGatesTransform(tree: GenericParent, file: VFile) { + map(tree, (node) => { + const nodes = ((node as GenericParent).children as GenericParent[])?.reduce( + (children, child) => { + const [last] = children.slice(-1); + const [secondLast] = children.slice(-2); + if (last?.gate !== 'start') return [...children, child]; + if (child.gate === 'start') { + // If we are opening a new gate, add to the stack and the logic below will close it + return [...children, child]; + } + if (child.gate === 'end') { + if (child.type !== last.type) { + fileWarn( + file, + `Gate close ("${child.type}") does not match opening gate (${child.gate}).`, + { node }, + ); + } + // Clean up the gate logic + delete last.gate; + if (secondLast?.gate === 'start') { + // We have two or more open gates (from above), close the current one and append the child + const closed = children.pop() as GenericNode; + secondLast.children = [...(secondLast.children ?? []), closed]; + } + return children; + } + // Append the child to the open gate if no end is found + last.children = [...(last.children ?? []), child]; + return children; + }, + [] as GenericNode[], + ); + const [last] = nodes?.slice(-1) ?? []; + if (last?.gate === 'start') { + fileError(file, `Gated node is not closed, expected a {${last.type}-end} directive.`, { + node, + }); + } + if (nodes !== undefined) (node as GenericParent).children = nodes; + return node; + }); +} + +export const joinGatesPlugin: Plugin<[], Root, Root> = () => (tree, file) => { + joinGatesTransform(tree as GenericParent, file); +}; From 71e9166562b8bb1e3f1167e21f569db57c3b7bbd Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 28 May 2023 15:26:48 -0600 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BC=E2=80=8D?= =?UTF-8?q?=F0=9F=8F=AB=20Add=20support=20for=20sphinx=20exercise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See executablebooks/myst-theme#104 --- .changeset/sweet-rocks-lie.md | 7 + package-lock.json | 41 ++++++ packages/myst-cli/package.json | 1 + packages/myst-cli/src/process/myst.ts | 10 +- packages/myst-ext-exercise/.eslintrc.js | 4 + packages/myst-ext-exercise/CHANGELOG.md | 1 + packages/myst-ext-exercise/README.md | 3 + packages/myst-ext-exercise/jest.config.js | 23 ++++ packages/myst-ext-exercise/package.json | 57 ++++++++ packages/myst-ext-exercise/src/exercise.ts | 127 ++++++++++++++++++ packages/myst-ext-exercise/src/index.ts | 1 + .../myst-ext-exercise/tests/exercise.spec.ts | 69 ++++++++++ packages/myst-ext-exercise/tsconfig.json | 33 +++++ packages/myst-ext-exercise/tsconfig.test.json | 8 ++ packages/myst-transforms/src/enumerate.ts | 18 ++- 15 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 .changeset/sweet-rocks-lie.md create mode 100644 packages/myst-ext-exercise/.eslintrc.js create mode 100644 packages/myst-ext-exercise/CHANGELOG.md create mode 100644 packages/myst-ext-exercise/README.md create mode 100644 packages/myst-ext-exercise/jest.config.js create mode 100644 packages/myst-ext-exercise/package.json create mode 100644 packages/myst-ext-exercise/src/exercise.ts create mode 100644 packages/myst-ext-exercise/src/index.ts create mode 100644 packages/myst-ext-exercise/tests/exercise.spec.ts create mode 100644 packages/myst-ext-exercise/tsconfig.json create mode 100644 packages/myst-ext-exercise/tsconfig.test.json diff --git a/.changeset/sweet-rocks-lie.md b/.changeset/sweet-rocks-lie.md new file mode 100644 index 000000000..c52ed70aa --- /dev/null +++ b/.changeset/sweet-rocks-lie.md @@ -0,0 +1,7 @@ +--- +'myst-cli': patch +'myst-ext-exercise': patch +'myst-transforms': patch +--- + +Initial support for sphinx-exercise diff --git a/package-lock.json b/package-lock.json index f3bb168d2..b5dfb34b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9170,6 +9170,10 @@ "resolved": "packages/myst-ext-card", "link": true }, + "node_modules/myst-ext-exercise": { + "resolved": "packages/myst-ext-exercise", + "link": true + }, "node_modules/myst-ext-grid": { "resolved": "packages/myst-ext-grid", "link": true @@ -13201,6 +13205,7 @@ "myst-common": "^0.0.17", "myst-config": "^0.0.15", "myst-ext-card": "^0.0.7", + "myst-ext-exercise": "^0.0.1", "myst-ext-grid": "^0.0.7", "myst-ext-proof": "^0.0.2", "myst-ext-reactive": "^0.0.7", @@ -13515,6 +13520,25 @@ "typescript": "latest" } }, + "packages/myst-ext-exercise": { + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "myst-common": "^0.0.17" + }, + "devDependencies": { + "@types/jest": "^28.1.6", + "eslint": "^8.21.0", + "eslint-config-curvenote": "latest", + "jest": "28.1.3", + "myst-parser": "^0.0.30", + "npm-run-all": "^4.1.5", + "prettier": "latest", + "rimraf": "^3.0.2", + "ts-jest": "^28.0.7", + "typescript": "latest" + } + }, "packages/myst-ext-grid": { "version": "0.0.7", "license": "MIT", @@ -20405,6 +20429,7 @@ "myst-common": "^0.0.17", "myst-config": "^0.0.15", "myst-ext-card": "^0.0.7", + "myst-ext-exercise": "^0.0.1", "myst-ext-grid": "^0.0.7", "myst-ext-proof": "^0.0.2", "myst-ext-reactive": "^0.0.7", @@ -20624,6 +20649,22 @@ "typescript": "latest" } }, + "myst-ext-exercise": { + "version": "file:packages/myst-ext-exercise", + "requires": { + "@types/jest": "^28.1.6", + "eslint": "^8.21.0", + "eslint-config-curvenote": "latest", + "jest": "28.1.3", + "myst-common": "^0.0.17", + "myst-parser": "^0.0.30", + "npm-run-all": "^4.1.5", + "prettier": "latest", + "rimraf": "^3.0.2", + "ts-jest": "^28.0.7", + "typescript": "latest" + } + }, "myst-ext-grid": { "version": "file:packages/myst-ext-grid", "requires": { diff --git a/packages/myst-cli/package.json b/packages/myst-cli/package.json index 277e73a3f..61a1e5fe0 100644 --- a/packages/myst-cli/package.json +++ b/packages/myst-cli/package.json @@ -77,6 +77,7 @@ "myst-config": "^0.0.15", "myst-ext-card": "^0.0.7", "myst-ext-grid": "^0.0.7", + "myst-ext-exercise": "^0.0.1", "myst-ext-proof": "^0.0.2", "myst-ext-reactive": "^0.0.7", "myst-ext-tabs": "^0.0.7", diff --git a/packages/myst-cli/src/process/myst.ts b/packages/myst-cli/src/process/myst.ts index f850a694e..0f6a87bd0 100644 --- a/packages/myst-cli/src/process/myst.ts +++ b/packages/myst-cli/src/process/myst.ts @@ -3,6 +3,7 @@ import { mystParse } from 'myst-parser'; import { cardDirective } from 'myst-ext-card'; import { gridDirective } from 'myst-ext-grid'; import { proofDirective } from 'myst-ext-proof'; +import { exerciseDirectives } from 'myst-ext-exercise'; import { reactiveDirective, reactiveRole } from 'myst-ext-reactive'; import { tabDirectives } from 'myst-ext-tabs'; import { VFile } from 'vfile'; @@ -14,7 +15,14 @@ export function parseMyst(session: ISession, content: string, file: string): Roo vfile.path = file; const parsed = mystParse(content, { markdownit: { linkify: true }, - directives: [cardDirective, gridDirective, reactiveDirective, proofDirective, ...tabDirectives], + directives: [ + cardDirective, + gridDirective, + reactiveDirective, + proofDirective, + ...exerciseDirectives, + ...tabDirectives, + ], roles: [reactiveRole], vfile, }); diff --git a/packages/myst-ext-exercise/.eslintrc.js b/packages/myst-ext-exercise/.eslintrc.js new file mode 100644 index 000000000..76787609a --- /dev/null +++ b/packages/myst-ext-exercise/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['curvenote'], +}; diff --git a/packages/myst-ext-exercise/CHANGELOG.md b/packages/myst-ext-exercise/CHANGELOG.md new file mode 100644 index 000000000..0f84ff905 --- /dev/null +++ b/packages/myst-ext-exercise/CHANGELOG.md @@ -0,0 +1 @@ +# myst-ext-exercise diff --git a/packages/myst-ext-exercise/README.md b/packages/myst-ext-exercise/README.md new file mode 100644 index 000000000..c1a0bbc2a --- /dev/null +++ b/packages/myst-ext-exercise/README.md @@ -0,0 +1,3 @@ +# myst-ext-exercise + +`mystjs` extension for `exercise` directive diff --git a/packages/myst-ext-exercise/jest.config.js b/packages/myst-ext-exercise/jest.config.js new file mode 100644 index 000000000..816f544aa --- /dev/null +++ b/packages/myst-ext-exercise/jest.config.js @@ -0,0 +1,23 @@ +module.exports = { + rootDir: '../../', + preset: 'ts-jest/presets/js-with-ts', + testMatch: ['/packages/myst-ext-exercise/**/?(*.)+(spec|test).+(ts|tsx|js)'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, + testTimeout: 10000, + moduleNameMapper: { + '#(.*)': '/node_modules/$1', // https://github.com/chalk/chalk/issues/532 + }, + globals: { + 'ts-jest': { + tsconfig: './tsconfig.test.json', + }, + }, + verbose: true, + testEnvironment: 'node', + transformIgnorePatterns: [ + '/node_modules/(?!(vfile|formdata-polyfill|chalk|fetch-blob|vfile-message|unified|bail|trough|zwitch|unist-|hast-|html-|rehype-|mdast-|micromark-|trim-|web-namespaces|property-information|space-separated-tokens|comma-separated-tokens|get-port|stringify-entities|character-entities-html4|ccount|array-iterate))', + ], + testPathIgnorePatterns: ['/node_modules/', '/.yalc/', '/dist/'], +}; diff --git a/packages/myst-ext-exercise/package.json b/packages/myst-ext-exercise/package.json new file mode 100644 index 000000000..d5d29f7f4 --- /dev/null +++ b/packages/myst-ext-exercise/package.json @@ -0,0 +1,57 @@ +{ + "name": "myst-ext-exercise", + "version": "0.0.1", + "sideEffects": false, + "license": "MIT", + "description": "MyST extension for exercise", + "author": "Rowan Cockett ", + "homepage": "https://github.com/executablebooks/mystjs/tree/main/packages/myst-ext-exercise", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/executablebooks/mystjs.git" + }, + "scripts": { + "clean": "rimraf dist", + "build:esm": "tsc --project ./tsconfig.json --module es2015 --outDir dist/esm", + "build:cjs": "tsc --project ./tsconfig.json --module commonjs --outDir dist/cjs", + "declarations": "tsc --project ./tsconfig.json --declaration --emitDeclarationOnly --declarationMap --outDir dist/types", + "build": "npm-run-all -l clean -p build:cjs build:esm declarations", + "lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.js", + "lint:format": "npx prettier --check \"src/**/*.ts\"", + "test": "jest", + "test:watch": "jest --watchAll" + }, + "bugs": { + "url": "https://github.com/executablebooks/mystjs/issues" + }, + "dependencies": { + "myst-common": "^0.0.17" + }, + "devDependencies": { + "@types/jest": "^28.1.6", + "eslint": "^8.21.0", + "eslint-config-curvenote": "latest", + "jest": "28.1.3", + "myst-parser": "^0.0.30", + "npm-run-all": "^4.1.5", + "prettier": "latest", + "rimraf": "^3.0.2", + "ts-jest": "^28.0.7", + "typescript": "latest" + } +} diff --git a/packages/myst-ext-exercise/src/exercise.ts b/packages/myst-ext-exercise/src/exercise.ts new file mode 100644 index 000000000..278d3776a --- /dev/null +++ b/packages/myst-ext-exercise/src/exercise.ts @@ -0,0 +1,127 @@ +import type { DirectiveSpec, DirectiveData, GenericNode } from 'myst-common'; +import { createId, normalizeLabel, ParseTypesEnum } from 'myst-common'; + +export const exerciseDirective: DirectiveSpec = { + name: 'exercise', + alias: ['exercise-start'], + arg: { + type: ParseTypesEnum.parsed, + }, + options: { + label: { + type: ParseTypesEnum.string, + }, + class: { + type: ParseTypesEnum.string, + }, + nonumber: { + type: ParseTypesEnum.boolean, + }, + hidden: { + type: ParseTypesEnum.boolean, + }, + }, + body: { + type: ParseTypesEnum.parsed, + }, + run(data: DirectiveData): GenericNode[] { + const children: GenericNode[] = []; + if (data.arg) { + children.push({ + type: 'admonitionTitle', + children: data.arg as GenericNode[], + }); + } + if (data.body) { + children.push(...(data.body as GenericNode[])); + } + const nonumber = (data.options?.nonumber as boolean) ?? false; + // Numbered, unlabeled exercises still need a label + const backupLabel = nonumber ? undefined : `exercise-${createId()}`; + const rawLabel = (data.options?.label as string) || backupLabel; + const { label, identifier } = normalizeLabel(rawLabel) || {}; + const exercise: GenericNode = { + type: 'exercise', + label, + identifier, + class: data.options?.class as string, + hidden: data.options?.hidden as boolean, + enumerated: !nonumber, + children: children as any[], + }; + if (data.name.endsWith('-start')) { + exercise.gate = 'start'; + } + return [exercise]; + }, +}; + +export const solutionDirective: DirectiveSpec = { + name: 'solution', + alias: ['solution-start'], + arg: { + type: ParseTypesEnum.string, + required: true, + }, + options: { + label: { + type: ParseTypesEnum.string, + }, + class: { + type: ParseTypesEnum.string, + }, + hidden: { + type: ParseTypesEnum.boolean, + }, + }, + body: { + type: ParseTypesEnum.parsed, + }, + run(data: DirectiveData): GenericNode[] { + const children: GenericNode[] = []; + if (data.arg) { + const { label, identifier } = normalizeLabel(data.arg as string) || {}; + children.push({ + type: 'admonitionTitle', + children: [ + { type: 'text', value: 'Solution to ' }, + { type: 'crossReference', label, identifier }, + ], + }); + } + if (data.body) { + children.push(...(data.body as GenericNode[])); + } + const rawLabel = data.options?.label as string; + const { label, identifier } = normalizeLabel(rawLabel) || {}; + const solution: GenericNode = { + type: 'solution', + label, + identifier, + class: data.options?.class as string, + hidden: data.options?.hidden as boolean, + children: children as any[], + }; + if (data.name.endsWith('-start')) { + solution.gate = 'start'; + } + return [solution]; + }, +}; + +export const solutionEndDirective: DirectiveSpec = { + name: 'solution-end', + run: () => [{ type: 'solution', gate: 'end' }], +}; + +export const exerciseEndDirective: DirectiveSpec = { + name: 'exercise-end', + run: () => [{ type: 'exercise', gate: 'end' }], +}; + +export const exerciseDirectives = [ + exerciseDirective, + exerciseEndDirective, + solutionDirective, + solutionEndDirective, +]; diff --git a/packages/myst-ext-exercise/src/index.ts b/packages/myst-ext-exercise/src/index.ts new file mode 100644 index 000000000..bea8a9d3e --- /dev/null +++ b/packages/myst-ext-exercise/src/index.ts @@ -0,0 +1 @@ +export { exerciseDirective, solutionDirective, exerciseDirectives } from './exercise'; diff --git a/packages/myst-ext-exercise/tests/exercise.spec.ts b/packages/myst-ext-exercise/tests/exercise.spec.ts new file mode 100644 index 000000000..620584e07 --- /dev/null +++ b/packages/myst-ext-exercise/tests/exercise.spec.ts @@ -0,0 +1,69 @@ +import { mystParse } from 'myst-parser'; +import { exerciseDirective } from 'myst-ext-exercise'; + +describe('exercise directive', () => { + it('exercise directive parses', async () => { + const content = '```{exercise} Exercise Title\nExercise content\n```'; + const expected = { + type: 'root', + children: [ + { + type: 'mystDirective', + name: 'exercise', + args: 'Exercise Title', + value: 'Exercise content', + position: { + start: { + line: 0, + column: 0, + }, + end: { + line: 3, + column: 0, + }, + }, + children: [ + { + type: 'exercise', + enumerated: true, + children: [ + { + type: 'admonitionTitle', + children: [ + { + type: 'text', + value: 'Exercise Title', + }, + ], + }, + { + type: 'paragraph', + children: [ + { + type: 'text', + value: 'Exercise content', + }, + ], + position: { + end: { + column: 0, + line: 2, + }, + start: { + column: 0, + line: 1, + }, + }, + }, + ], + }, + ], + }, + ], + }; + const output = mystParse(content, { + directives: [exerciseDirective], + }); + expect(output).toEqual(expected); + }); +}); diff --git a/packages/myst-ext-exercise/tsconfig.json b/packages/myst-ext-exercise/tsconfig.json new file mode 100644 index 000000000..8268fe093 --- /dev/null +++ b/packages/myst-ext-exercise/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es6", + // module is overridden from the build:esm/build:cjs scripts + "module": "es2015", + "jsx": "react-jsx", + "lib": ["es2020"], + "esModuleInterop": true, + "noImplicitAny": true, + "strict": true, + "moduleResolution": "node", + "sourceMap": false, + // outDir is overridden from the build:esm/build:cjs scripts + "outDir": "dist/types", + "baseUrl": "src", + "paths": { + "*": ["node_modules/*"] + }, + // Type roots allows it to be included in a workspace + "typeRoots": [ + "./types", + "./node_modules/@types", + "../../node_modules/@types", + "../../../node_modules/@types" + ], + "resolveJsonModule": true, + // Ignore node_modules, etc. + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["tests/**/*"] +} diff --git a/packages/myst-ext-exercise/tsconfig.test.json b/packages/myst-ext-exercise/tsconfig.test.json new file mode 100644 index 000000000..bafe01bbd --- /dev/null +++ b/packages/myst-ext-exercise/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "allowJs": true, + "target": "es6" + }, + "exclude": [] +} diff --git a/packages/myst-transforms/src/enumerate.ts b/packages/myst-transforms/src/enumerate.ts index d1f3027ca..6dc59466e 100644 --- a/packages/myst-transforms/src/enumerate.ts +++ b/packages/myst-transforms/src/enumerate.ts @@ -57,6 +57,20 @@ function getDefaultNumberedReferenceLabel(kind: TargetKind | string) { } } +function getDefaultNamedReferenceLabel(kind: TargetKind | string, hasTitle: boolean) { + const domain = kind.includes(':') ? kind.split(':')[1] : kind; + const name = `${domain.slice(0, 1).toUpperCase()}${domain.slice(1)}`; + switch (kind) { + // TODO: These need to be moved to the directive definition in an extension + case 'proof': + case 'exercise': + return hasTitle ? `${name} ({name})` : name; + default: + if (hasTitle) return '{name}'; + return name; + } +} + export enum ReferenceKind { ref = 'ref', numref = 'numref', @@ -374,7 +388,7 @@ export class ReferenceState implements IReferenceState { } const template = target.node.enumerator ? getDefaultNumberedReferenceLabel(target.kind) - : '{name}'; + : getDefaultNamedReferenceLabel(target.kind, !!title); fillReferenceEnumerators(this.file, node, template, target.node.enumerator, title); } node.resolved = true; @@ -457,7 +471,7 @@ export class MultiPageReferenceState implements IReferenceState { export const enumerateTargetsTransform = (tree: Root, opts: StateOptions) => { opts.state.initializeNumberedHeadingDepths(tree); - const nodes = selectAll('container,math,heading,proof,[identifier]', tree) as ( + const nodes = selectAll('container,math,heading,proof,[identifier],[enumerated=true]', tree) as ( | TargetNodes | IdentifierNodes )[]; From 2bc07cff0d7fb349d21c60a69e2f1aaed81137d0 Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 28 May 2023 15:29:01 -0600 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=96=20Documentation=20for=20Exerci?= =?UTF-8?q?ses=20and=20Solutions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_toc.yml | 1 + docs/exercises.md | 343 +++++++++++++++++++++++++++++++++++ docs/proofs-and-theorems.md | 2 +- docs/thumbnails/exercise.png | Bin 0 -> 50852 bytes 4 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 docs/exercises.md create mode 100644 docs/thumbnails/exercise.png diff --git a/docs/_toc.yml b/docs/_toc.yml index 0271c84e5..b34109d11 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -20,6 +20,7 @@ parts: - file: external-references - file: citations - file: proofs-and-theorems + - file: exercises - file: blocks - file: diagrams - file: dropdowns-cards-and-tabs diff --git a/docs/exercises.md b/docs/exercises.md new file mode 100644 index 000000000..c27c7e498 --- /dev/null +++ b/docs/exercises.md @@ -0,0 +1,343 @@ +--- +title: Exercises and Solutions +short_title: Exercises +description: MyST supports adding exercises and solutions which can cross-reference each-other and include Jupyter Notebook outputs. +thumbnail: ./thumbnails/exercise.png +--- + +There are two directives available to add exercises and solutions to your documents: (1) an `exercise` directive; and (2) a `solution` directive. The exercises are enumerated by default and can take in an optional title argument as well as be "gated" around Jupyter Notebook cells. + +:::{note} Same as Sphinx Exercise 🎉 +:class: dropdown + +The implementation and documentation for exercises and solutions is based on [Sphinx Exercise](https://ebp-sphinx-exercise.readthedocs.io), the syntax can be used interchangeably. We have reused the examples in that extension here to show off the various parts of the MyST extension. + +Changes to the original extension include being able to click on the exercise label (e.g. "Exercise 1"), and having a link to that exercise anchor. We have also updated the styles from both Sphinx and JupyterBook to be more distinct from admonitions. + +You can also reference exercises with any cross-reference syntax (including the `{ref}` and `{numref}` roles). We recommend the markdown link syntax. +::: + +## Exercise Directive + +**Example** + +```{exercise} +:label: my-exercise + +Recall that $n!$ is read as "$n$ factorial" and defined as +$n! = n \times (n - 1) \times \cdots \times 2 \times 1$. + +There are functions to compute this in various modules, but let's +write our own version as an exercise. + +In particular, write a function `factorial` such that `factorial(n)` returns $n!$ +for any positive integer $n$. +``` + +**MyST Syntax** + +````markdown +```{exercise} +:label: my-exercise + +Recall that $n!$ is read as "$n$ factorial" and defined as +$n! = n \times (n - 1) \times \cdots \times 2 \times 1$. + +There are functions to compute this in various modules, but let's +write our own version as an exercise. + +In particular, write a function `factorial` such that `factorial(n)` returns $n!$ +for any positive integer $n$. +``` +```` + +_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) + +The following options for exercise and solution directives are supported: + +- `label`: text + + A unique identifier for your exercise that you can use to reference it with a Markdown link or `{ref}` and `{numref}` roles. Cannot contain spaces or special characters. + +- `class`: text + + Value of the exercise’s class attribute which can be used to add custom CSS or JavaScript. This can also be the optional `dropdown` class to initially hide the exercise. + +- `nonumber`: flag (empty) + + Turns off exercise auto numbering. + +- `hidden` : flag (empty) + + Removes the directive from the final output. + +## Solution Directive + +A solution directive can be included using the `solution` pattern. It takes in the label of the directive it wants to link to as a required argument. Unlike the `exercise` directive, the solution directive is not enumerable as it inherits numbering directly from the linked exercise. The argument for a solution is the label of the linked exercise, which is required. + +**Example** + +````{solution} my-exercise +:label: my-solution + +Here's one solution. + +```{code-block} python +def factorial(n): + k = 1 + for i in range(n): + k = k * (i + 1) + return k + +factorial(4) +``` +```` + +**MyST Syntax** + +`````markdown +````{solution} my-exercise +:label: my-solution + +Here's one solution. + +```{code-block} python +def factorial(n): + k = 1 + for i in range(n): + k = k * (i + 1) + return k + +factorial(4) +``` +```` +````` + +_Source:_ [QuantEcon](https://python-programming.quantecon.org/functions.html#Exercise-1) + +The following options are also supported: + +- `label` : text + + A unique identifier for your solution that you can use to reference it with `{ref}`. Cannot contain spaces or special characters. + +- `class` : text + + Value of the solution’s class attribute which can be used to add custom CSS or JavaScript. + +- `hidden` : flag (empty) + + Removes the directive from the final output. + +## Referencing Exercises & Solutions + +You can refer to an exercise using the standard link syntax: + +- `[](#my-exercise)`, creates [](#my-exercise) +- `[{name}](#nfactorial)`[^note] creates [{name}](#nfactorial) +- `[{number}](#my-exercise)` creates [{number}](#my-exercise) +- `[See Exercise](#my-exercise)` creates [See Exercise](#my-exercise) + +:::{tip} Compatibility with Sphinx Exercise +:class: dropdown + +You can also refer to an exercise using the `{ref}` role like `` {ref}`my-exercise` ``, which will display the title of the exercise directive. In the event that directive does not have a title, the title will be the default "Exercise" or "Exercise {number}" like so: {ref}`my-exercise`. + +Enumerable directives can also be referenced through the `numref` role like `` {numref}`my-exercise` ``, which will display the number of the exercise directive. Referencing the above directive will display {numref}`my-exercise`. In this case it displays the same result as the `{ref}` role as `exerise` notes are (by default) enumerated. + +Furthermore, `numref` can take in three additional placeholders for more customized titles: + +1. _%s_ +2. _{number}_ which get replaced by the exercise number, and +3. _{name}_ by the exercise title.[^note] + +For example,\ +`` {numref}`My custom {number} and {name}` ``. + +[^note]: If the exercise directive does not have a title, the `label` will be used instead. + +::: + +### Referencing Solutions + +You can refer to a solution directly as well using a Markdown link or using the `{ref}` role like: `` {ref}`my-solution` `` the output of which depends on the attributes of the linked directive. If the linked directive is enumerable, the role will replace the solution reference with the linked directive type and its appropriate number like so: {ref}`my-solution`. + +In the event that the directive being referenced is unenumerable, the reference will display its title: {ref}`nfactorial-solution`. + +:::{note} Named Exercise & Solution +:class: dropdown simple +:icon: false + +```{exercise} $n!$ Factorial +:label: nfactorial +:nonumber: + +Write a function `factorial` such that `factorial(int n)` returns $n!$ +for any positive integer $n$. +``` + +````{solution} nfactorial +:label: nfactorial-solution + +Here's a solution in Java. + +```{code-block} java +static int factorial(int n){ + if (n == 0) + return 1; + else { + return(n * factorial(n-1)); + } +} +```` + +::: + +If the title of the linked directive being reference does not exist, it will default to {ref}`nfactorial-notitle-solution`. + +:::{note} Unnumbered Exercise & Solution +:class: dropdown simple +:icon: false + +```{exercise} +:label: nfactorial-notitle +:nonumber: + +Write a function `factorial` such that `factorial(int n)` returns $n!$ +for any positive integer $n$. +``` + +````{solution} nfactorial-notitle +:label: nfactorial-notitle-solution + +Here's a solution in Java. + +```{code-block} java +static int factorial(int n){ + if (n == 0) + return 1; + else { + return(n * factorial(n-1)); + } +} +```` + +::: + +## Alternative Gated Syntax + +To be able to be viewed as Jupyter Notebooks (e.g. in [JupyterLab MyST](./quickstart-jupyter-lab-myst.md)) `code-cell` directives must be at the root level of the document for them to be executed. This maintains direct compatibility with the `jupyter notebook` and enables tools like `jupytext` to convert between `myst` and `ipynb` files. + +As a result **executable** `code-cell` directives cannot be nested inside of exercises or solution directives. + +The solution to this is to use the **gated syntax**. + +```{note} +This syntax can also be a convenient way of surrounding blocks of text that may include other directives that you wish +to include in an exercise or solution admonition. +``` + +**Basic Syntax** + +````markdown +```{exercise-start} +:label: ex1 +``` + +```{code-cell} +# Some setup code that needs executing +``` + +and maybe you wish to add a figure + +```{figure} https://source.unsplash.com/random/400x200?beach,ocean + +``` + +```{exercise-end} + +``` +```` + +```{exercise-start} +:label: ex1 +``` + +```{code-cell} +# Some setup code that needs executing +``` + +and maybe you wish to add a figure + +```{figure} https://source.unsplash.com/random/400x200?beach,ocean + +``` + +```{exercise-end} + +``` + +This can also be completed for solutions with `solution-start` and `solution-end` directives. The `solution-start` and `exercise-start` directives have the same options as original directive. + +```{warning} Mismatched Start & End +:class: dropdown +If there are missing `-start` and `-end` directives, this will cause an extension error, +alongside feedback to diagnose the issue in document structure. +``` + +## Hiding Directive Content + +To visually hide the content, simply add `:class: dropdown` as a directive option, similar to an admonition. + +**Example** + +```{exercise} +:class: dropdown + +Recall that $n!$ is read as "$n$ factorial" and defined as +$n! = n \times (n - 1) \times \cdots \times 2 \times 1$. + +There are functions to compute this in various modules, but let's +write our own version as an exercise. + +In particular, write a function `factorial` such that `factorial(n)` returns $n!$ +for any positive integer $n$. +``` + +**MyST Syntax**: + +````markdown +```{exercise} +:class: dropdown + +Recall that $n!$ is read as "$n$ factorial" and defined as +$n! = n \times (n - 1) \times \cdots \times 2 \times 1$. + +There are functions to compute this in various modules, but let's +write our own version as an exercise. + +In particular, write a function `factorial` such that `factorial(n)` returns $n!$ +for any positive integer $n$. +``` +```` + +### Remove Directives + +Any specific directive can be hidden by introducing the `:hidden:` option. For example, the following example will not be displayed + +````markdown +```{exercise} +:hidden: + +This is a hidden exercise directive. +``` +```` + +```{exercise} +:hidden: + +This is a hidden exercise directive. +``` + +% TODO: Remove All Solutions +% TODO: Custom CSS diff --git a/docs/proofs-and-theorems.md b/docs/proofs-and-theorems.md index 4d98f90f1..d727ffe86 100644 --- a/docs/proofs-and-theorems.md +++ b/docs/proofs-and-theorems.md @@ -42,7 +42,7 @@ The following options for proof directives are supported: - `label`: text - A unique identifier for your theorem that you can use to reference it with `{prf:ref}`. Cannot contain spaces or special characters. + A unique identifier for your theorem that you can use to reference it with a Markdown link or the `{prf:ref}` role. Cannot contain spaces or special characters. - `class`: text diff --git a/docs/thumbnails/exercise.png b/docs/thumbnails/exercise.png new file mode 100644 index 0000000000000000000000000000000000000000..514973e24788d0d4223756c9ab7ce6dcdef98e85 GIT binary patch literal 50852 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 literal 0 HcmV?d00001 From dd88590f7f831d63f798ffdc3ae6f8680efa4a53 Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 28 May 2023 15:43:59 -0600 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20exercise=20test=20with?= =?UTF-8?q?=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/myst-ext-exercise/tests/exercise.spec.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/myst-ext-exercise/tests/exercise.spec.ts b/packages/myst-ext-exercise/tests/exercise.spec.ts index 620584e07..92cb91e96 100644 --- a/packages/myst-ext-exercise/tests/exercise.spec.ts +++ b/packages/myst-ext-exercise/tests/exercise.spec.ts @@ -3,13 +3,16 @@ import { exerciseDirective } from 'myst-ext-exercise'; describe('exercise directive', () => { it('exercise directive parses', async () => { - const content = '```{exercise} Exercise Title\nExercise content\n```'; + const content = '```{exercise} Exercise Title\n:label: ex-1\nExercise content\n```'; const expected = { type: 'root', children: [ { type: 'mystDirective', name: 'exercise', + options: { + label: 'ex-1', + }, args: 'Exercise Title', value: 'Exercise content', position: { @@ -18,7 +21,7 @@ describe('exercise directive', () => { column: 0, }, end: { - line: 3, + line: 4, column: 0, }, }, @@ -26,6 +29,8 @@ describe('exercise directive', () => { { type: 'exercise', enumerated: true, + identifier: 'ex-1', + label: 'ex-1', children: [ { type: 'admonitionTitle', @@ -47,11 +52,11 @@ describe('exercise directive', () => { position: { end: { column: 0, - line: 2, + line: 3, }, start: { column: 0, - line: 1, + line: 2, }, }, },