diff --git a/.changeset/dirty-bugs-drop.md b/.changeset/dirty-bugs-drop.md new file mode 100644 index 000000000..567262c21 --- /dev/null +++ b/.changeset/dirty-bugs-drop.md @@ -0,0 +1,5 @@ +--- +'myst-ext-icon': minor +--- + +Add support for parsing icon roles diff --git a/package-lock.json b/package-lock.json index c9b044933..565da35b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9820,6 +9820,10 @@ "resolved": "packages/myst-ext-grid", "link": true }, + "node_modules/myst-ext-icon": { + "resolved": "packages/myst-ext-icon", + "link": true + }, "node_modules/myst-ext-proof": { "resolved": "packages/myst-ext-proof", "link": true @@ -15220,6 +15224,7 @@ "myst-ext-card": "^1.0.7", "myst-ext-exercise": "^1.0.7", "myst-ext-grid": "^1.0.7", + "myst-ext-icon": "^0.0.0", "myst-ext-proof": "^1.0.10", "myst-ext-reactive": "^1.0.7", "myst-ext-tabs": "^1.0.7", @@ -15600,6 +15605,16 @@ "myst-parser": "^1.5.1" } }, + "packages/myst-ext-icon": { + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "myst-common": "^1.5.1" + }, + "devDependencies": { + "myst-parser": "^1.5.1" + } + }, "packages/myst-ext-proof": { "version": "1.0.10", "license": "MIT", @@ -15983,6 +15998,17 @@ "myst-cli": "^1.3.5" } }, + "packages/mystmd-ext-icon": { + "version": "0.0.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "myst-common": "^1.5.1" + }, + "devDependencies": { + "myst-parser": "^1.5.1" + } + }, "packages/mystmd/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", diff --git a/packages/myst-cli/package.json b/packages/myst-cli/package.json index 5da4ea10d..7d0b43169 100644 --- a/packages/myst-cli/package.json +++ b/packages/myst-cli/package.json @@ -75,6 +75,7 @@ "myst-ext-card": "^1.0.7", "myst-ext-exercise": "^1.0.7", "myst-ext-grid": "^1.0.7", + "myst-ext-icon": "^0.0.0", "myst-ext-proof": "^1.0.10", "myst-ext-reactive": "^1.0.7", "myst-ext-tabs": "^1.0.7", diff --git a/packages/myst-cli/src/process/myst.ts b/packages/myst-cli/src/process/myst.ts index ca1172d61..d9e9a7747 100644 --- a/packages/myst-cli/src/process/myst.ts +++ b/packages/myst-cli/src/process/myst.ts @@ -2,6 +2,7 @@ import { mystParse } from 'myst-parser'; import { cardDirective } from 'myst-ext-card'; import { gridDirectives } from 'myst-ext-grid'; import { proofDirective } from 'myst-ext-proof'; +import { iconRole } from 'myst-ext-icon'; import { exerciseDirectives } from 'myst-ext-exercise'; import { reactiveDirective, reactiveRole } from 'myst-ext-reactive'; import { tabDirectives } from 'myst-ext-tabs'; diff --git a/packages/myst-ext-icon/.eslintrc.cjs b/packages/myst-ext-icon/.eslintrc.cjs new file mode 100644 index 000000000..76787609a --- /dev/null +++ b/packages/myst-ext-icon/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['curvenote'], +}; diff --git a/packages/myst-ext-icon/README.md b/packages/myst-ext-icon/README.md new file mode 100644 index 000000000..189dc7d65 --- /dev/null +++ b/packages/myst-ext-icon/README.md @@ -0,0 +1,3 @@ +# myst-ext-icon + +`mystmd` extension for `icon` role diff --git a/packages/myst-ext-icon/package.json b/packages/myst-ext-icon/package.json new file mode 100644 index 000000000..7e7470c22 --- /dev/null +++ b/packages/myst-ext-icon/package.json @@ -0,0 +1,40 @@ +{ + "name": "myst-ext-icon", + "version": "0.0.0", + "sideEffects": false, + "license": "MIT", + "description": "MyST extension for icon roles", + "author": "Angus Hollands ", + "homepage": "https://github.com/jupyter-book/mystmd/tree/main/packages/myst-ext-icon", + "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.5.1" + }, + "devDependencies": { + "myst-parser": "^1.5.1" + } +} diff --git a/packages/myst-ext-icon/src/index.ts b/packages/myst-ext-icon/src/index.ts new file mode 100644 index 000000000..922b307b9 --- /dev/null +++ b/packages/myst-ext-icon/src/index.ts @@ -0,0 +1,52 @@ +import type { RoleSpec, RoleData, GenericNode } from 'myst-common'; + +export const LEGACY_ICON_ALIASES: Record = { + octicon: 'oct', + fas: 'fas', + far: 'far', + fab: 'fab', + 'material-twotone': 'mtt', + 'material-sharp': 'msp', + 'material-regular': 'mrg', + 'material-round': 'mrd', + 'material-outline': 'mol', +}; + +export const iconRole: RoleSpec = { + name: 'icon:oct', + alias: [ + 'icon:mrg', + 'icon:mol', + 'icon:mrd', + 'icon:msp', + 'icon:mtt', + 'icon:fab', + 'icon:far', + 'icon:fas', + ...Object.keys(LEGACY_ICON_ALIASES), + ], + body: { + type: String, + required: true, + }, + run(data: RoleData): GenericNode[] { + let kind: string; + + const roleName = data.name as string; + const alias = LEGACY_ICON_ALIASES[roleName]; + + if (alias !== undefined) { + kind = alias; + } else { + const kindMatch = (data.name as string).match(/icon:(.*)/)!; + kind = kindMatch[1]; + } + const name = data.body as string; + const icon = { + type: 'icon', + kind, + name, + }; + return [icon]; + }, +}; diff --git a/packages/myst-ext-icon/tests/icon.spec.ts b/packages/myst-ext-icon/tests/icon.spec.ts new file mode 100644 index 000000000..453e50d79 --- /dev/null +++ b/packages/myst-ext-icon/tests/icon.spec.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { mystParse } from 'myst-parser'; +import { iconRole, LEGACY_ICON_ALIASES } from '../src'; +import { selectAll } from 'unist-util-select'; + +function deletePositions(tree: any) { + selectAll('*', tree).forEach((n) => { + delete n.position; + }); + return tree; +} + +describe('icon role', () => { + it.each(['fab', 'far', 'fas', 'mtt', 'mrg', 'mrd', 'mol', 'msp', 'oct'])( + 'icon:%s role parses', + async (kind) => { + const role = `icon:${kind}`; + const icon = 'any-icon'; + + const markup = `{${role}}\`${icon}\``; + + const expected = { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'mystRole', + name: role, + value: icon, + children: [ + { + type: 'icon', + kind: kind, + name: icon, + }, + ], + }, + ], + }, + ], + }; + const output = mystParse(markup, { + roles: [iconRole], + }); + expect(deletePositions(output)).toEqual(expected); + }, + ); + it.each(Object.entries(LEGACY_ICON_ALIASES))( + 'legacy %s role parses', + async (legacyName, kind) => { + const icon = 'any-icon'; + + const markup = `{${legacyName}}\`${icon}\``; + + const expected = { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'mystRole', + name: legacyName, + value: icon, + children: [ + { + type: 'icon', + kind: kind, + name: icon, + }, + ], + }, + ], + }, + ], + }; + const output = mystParse(markup, { + roles: [iconRole], + }); + expect(deletePositions(output)).toEqual(expected); + }, + ); +}); diff --git a/packages/myst-ext-icon/tsconfig.json b/packages/myst-ext-icon/tsconfig.json new file mode 100644 index 000000000..288cc4366 --- /dev/null +++ b/packages/myst-ext-icon/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules", "src/**/*.spec.ts", "tests"], +}