diff --git a/.changeset/lazy-houses-tell.md b/.changeset/lazy-houses-tell.md new file mode 100644 index 000000000..b37eb3fdc --- /dev/null +++ b/.changeset/lazy-houses-tell.md @@ -0,0 +1,7 @@ +--- +"myst-ext-button": patch +"myst-spec-ext": patch +"myst-cli": patch +--- + +Add new button role diff --git a/docs/directives.mjs b/docs/directives.mjs index 4f28ca3d7..d2007009a 100644 --- a/docs/directives.mjs +++ b/docs/directives.mjs @@ -3,6 +3,7 @@ import { mystParse } from 'myst-parser'; import { defaultDirectives } from 'myst-directives'; import { defaultRoles } from 'myst-roles'; import { cardDirective } from 'myst-ext-card'; +import { buttonRole } from 'myst-ext-button'; import { gridDirectives } from 'myst-ext-grid'; import { proofDirective } from 'myst-ext-proof'; import { exerciseDirectives } from 'myst-ext-exercise'; @@ -17,7 +18,7 @@ const allDirectives = [ cardDirective, proofDirective, ]; -const allRoles = [...defaultRoles]; +const allRoles = [...defaultRoles, buttonRole]; /** * @param {import('myst-common').OptionDefinition} option diff --git a/docs/dropdowns-cards-and-tabs.md b/docs/dropdowns-cards-and-tabs.md index 224cfbe68..5fb0b5adc 100644 --- a/docs/dropdowns-cards-and-tabs.md +++ b/docs/dropdowns-cards-and-tabs.md @@ -27,7 +27,7 @@ You can also hide the body of your admonition blocks so that users must click a To turn an admonition into a dropdown, add the option `:class: dropdown` to them. See [](#admonition-dropdown) for more information. ``` -### Cards +## Cards Cards provide an easy way for you to content into a standard “header”, “body”, “footer” structure that has a similar alignment and visual style. It is useful for creating galleries or high-visibility collections of links and information. For example, a card with a header, title, body, and footer: @@ -76,25 +76,24 @@ Footer Note that, card headers and footers are optional. If you don’t include ^^^ or +++ in your card, they will not show up. ```` +:::{myst:directive} card +::: -### `card` reference - -**Arguments** _(optional, markdown)_ -: The `card` can take a single argument that is the title as a string. - -**Options** -: No options for the `card` are required +## Buttons - header _(optional, markdown)_ - : Styled content at the top of the card +A button is an element with text content that triggers an action to navigate to an internal or external reference upon a user click. Use the {myst:role}`button` role followed by the text content and target path to create a button. - footer _(optional, markdown)_ - : Styled content at the bottom of the card +```{myst} +{button}`MyST Role Spec ` +``` - link _(optional, string)_ - : If given, clicking the card will direct you to the URL given here. +```{myst} +{button}`MyST-MD GitHub ` +``` +:::{myst:role} button +::: -### Grids +## Grids Grids allow you to structure arbitrary chunks of content in a grid-like system. @@ -124,6 +123,9 @@ Execute notebook cells, store results, and insert outputs across pages. :::: ``` +:::{myst:directive} grid +::: + ## Tabs You can also produce tabbed content. This allows you to display a variety of tabbed content blocks that users can click on. @@ -154,23 +156,8 @@ Synced content for tab 2 ``` ```` -### `tab-item` reference - -**Arguments** _(required: `1`, string)_ -: The `tab-item` requires a single argument that is the title as a string. - - ```{warning} - :class: dropdown - # Note: the `tab-item` title is not currently not parsed - - The current implementation does not parse the tab title properly, and markup in this field will not be parsed. - ``` - -**Options** -: No options for the `tab-item` are required - - sync _(optional, string)_ - : A key that is used to sync the selected tab across multiple tab-sets. +:::{myst:directive} tab-set +::: - selected _(flag, no-value)_ - : a flag indicating whether the tab should be selected by default. +:::{myst:directive} tab-item +::: diff --git a/docs/roles.md b/docs/roles.md index 56dbac85d..28990e3f7 100644 --- a/docs/roles.md +++ b/docs/roles.md @@ -6,6 +6,9 @@ description: A full list of the roles included in MyST Markdown by default. :::{myst:role} abbreviation ::: +:::{myst:role} button +::: + :::{myst:role} chemicalFormula ::: diff --git a/package-lock.json b/package-lock.json index 40bbd6411..e7c3028bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10032,6 +10032,10 @@ "resolved": "packages/myst-execute", "link": true }, + "node_modules/myst-ext-button": { + "resolved": "packages/myst-ext-button", + "link": true + }, "node_modules/myst-ext-card": { "resolved": "packages/myst-ext-card", "link": true @@ -15482,6 +15486,7 @@ "myst-common": "^1.7.6", "myst-config": "^1.7.6", "myst-execute": "^0.1.2", + "myst-ext-button": "^0.0.0", "myst-ext-card": "^1.0.9", "myst-ext-exercise": "^1.0.9", "myst-ext-grid": "^1.0.9", @@ -15776,6 +15781,17 @@ "node": "^16.13.0 || >=18.0.0" } }, + "packages/myst-ext-button": { + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "myst-common": "^1.7.2", + "myst-spec-ext": "^1.7.6" + }, + "devDependencies": { + "myst-parser": "^1.5.7" + } + }, "packages/myst-ext-card": { "version": "1.0.9", "license": "MIT", diff --git a/packages/myst-cli/package.json b/packages/myst-cli/package.json index d9e8133b5..29e78d42d 100644 --- a/packages/myst-cli/package.json +++ b/packages/myst-cli/package.json @@ -72,6 +72,7 @@ "myst-common": "^1.7.6", "myst-config": "^1.7.6", "myst-execute": "^0.1.2", + "myst-ext-button": "^0.0.0", "myst-ext-card": "^1.0.9", "myst-ext-exercise": "^1.0.9", "myst-ext-grid": "^1.0.9", diff --git a/packages/myst-cli/src/process/myst.ts b/packages/myst-cli/src/process/myst.ts index 175139819..389b65e2a 100644 --- a/packages/myst-cli/src/process/myst.ts +++ b/packages/myst-cli/src/process/myst.ts @@ -1,4 +1,5 @@ import { mystParse } from 'myst-parser'; +import { buttonRole } from 'myst-ext-button'; import { cardDirective } from 'myst-ext-card'; import { gridDirectives } from 'myst-ext-grid'; import { proofDirective } from 'myst-ext-proof'; @@ -47,7 +48,7 @@ export function parseMyst( extensions: { frontmatter: !opts?.ignoreFrontmatter, }, - roles: [...(session.plugins?.roles ?? [])], + roles: [buttonRole, ...(session.plugins?.roles ?? [])], vfile, }); logMessagesFromVFile(session, vfile); diff --git a/packages/myst-ext-button/.eslintrc.cjs b/packages/myst-ext-button/.eslintrc.cjs new file mode 100644 index 000000000..76787609a --- /dev/null +++ b/packages/myst-ext-button/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['curvenote'], +}; diff --git a/packages/myst-ext-button/README.md b/packages/myst-ext-button/README.md new file mode 100644 index 000000000..bdf031fd0 --- /dev/null +++ b/packages/myst-ext-button/README.md @@ -0,0 +1,3 @@ +# myst-ext-button + +`mystmd` extension for `button` role diff --git a/packages/myst-ext-button/package.json b/packages/myst-ext-button/package.json new file mode 100644 index 000000000..24770f392 --- /dev/null +++ b/packages/myst-ext-button/package.json @@ -0,0 +1,41 @@ +{ + "name": "myst-ext-button", + "version": "0.0.0", + "sideEffects": false, + "license": "MIT", + "description": "MyST extension for button role", + "author": "Jenny Wong ", + "homepage": "https://github.com/jupyter-book/mystmd/tree/main/packages/myst-ext-button", + "type": "module", + "exports": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/jupyter-book/mystmd.git" + }, + "scripts": { + "clean": "rimraf dist", + "lint": "eslint \"src/**/!(*.spec).ts\" -c ./.eslintrc.cjs", + "lint:format": "npx prettier --check \"src/**/*.ts\"", + "test": "vitest run", + "test:watch": "vitest watch", + "build:esm": "tsc", + "build": "npm-run-all -l clean -p build:esm" + }, + "bugs": { + "url": "https://github.com/jupyter-book/mystmd/issues" + }, + "dependencies": { + "myst-common": "^1.7.2", + "myst-spec-ext": "^1.7.6" + }, + "devDependencies": { + "myst-parser": "^1.5.7" + } +} diff --git a/packages/myst-ext-button/src/index.ts b/packages/myst-ext-button/src/index.ts new file mode 100644 index 000000000..830878c13 --- /dev/null +++ b/packages/myst-ext-button/src/index.ts @@ -0,0 +1,28 @@ +import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; +import type { Link } from 'myst-spec-ext'; + +const REF_PATTERN = /^(.+?)<([^<>]+)>$/; + +export const buttonRole: RoleSpec = { + name: 'button', + doc: 'Button element with an action to navigate to internal or external links.', + body: { + type: String, + doc: 'The body of the button.', + required: true, + }, + run(data: RoleData): GenericNode[] { + const body = data.body as string; + const match = REF_PATTERN.exec(body); + const [, modified, rawLabel] = match ?? []; + const url = rawLabel ?? body; + const node: Link = { + type: 'link', + url, + children: [], + class: 'button', // TODO: allow users to extend this + }; + if (modified) node.children = [{ type: 'text', value: modified.trim() }]; + return [node]; + }, +}; diff --git a/packages/myst-ext-button/tests/button.spec.ts b/packages/myst-ext-button/tests/button.spec.ts new file mode 100644 index 000000000..8dceb74e5 --- /dev/null +++ b/packages/myst-ext-button/tests/button.spec.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { buttonRole } from '../src'; +import { VFile } from 'vfile'; + +describe('Button component', () => { + it('should process button role correctly', () => { + const result = buttonRole.run( + { name: 'button', body: 'Click me' }, + new VFile(), + ); + + expect(result).toEqual([ + { + type: 'link', + class: 'button', + url: 'http://example.com', + children: [{ type: 'text', value: 'Click me' }], + }, + ]); + }); + + it('should process button role without label correctly', () => { + const result = buttonRole.run({ name: 'button', body: 'http://example.com' }, new VFile()); + expect(result).toEqual([ + { + type: 'link', + class: 'button', + url: 'http://example.com', + children: [], + }, + ]); + }); +}); diff --git a/packages/myst-ext-button/tsconfig.json b/packages/myst-ext-button/tsconfig.json new file mode 100644 index 000000000..1c5c0f1c4 --- /dev/null +++ b/packages/myst-ext-button/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules", "src/**/*.spec.ts", "tests"] +} diff --git a/packages/myst-spec-ext/src/types.ts b/packages/myst-spec-ext/src/types.ts index b808b82be..0a2d793b9 100644 --- a/packages/myst-spec-ext/src/types.ts +++ b/packages/myst-spec-ext/src/types.ts @@ -275,6 +275,7 @@ export type CrossReference = SpecCrossReference & { dataUrl?: string; remoteBaseUrl?: string; html_id?: string; + class?: Image['class']; }; export type Link = SpecLink & { @@ -284,6 +285,7 @@ export type Link = SpecLink & { static?: true; protocol?: string; error?: true; + class?: Image['class']; }; // Search types