diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9dd1ac9c..d1e6f4be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,6 +16,7 @@ on: - 'packages/editor' - 'packages/latex-extension' - 'packages/page-constructor-extension' + - 'packages/viewer' tag: description: 'Publish with tag' required: true diff --git a/.release-please/config.json b/.release-please/config.json index 992f3204..7d91c756 100644 --- a/.release-please/config.json +++ b/.release-please/config.json @@ -14,6 +14,7 @@ "packages": { "packages/editor": {}, "packages/latex-extension": {}, - "packages/page-constructor-extension": {} + "packages/page-constructor-extension": {}, + "packages/viewer": {} } } \ No newline at end of file diff --git a/.release-please/manifest.json b/.release-please/manifest.json index 36fa0faf..70c98311 100644 --- a/.release-please/manifest.json +++ b/.release-please/manifest.json @@ -1,5 +1,6 @@ { "packages/editor": "15.40.0", "packages/latex-extension": "0.1.0", - "packages/page-constructor-extension": "0.1.0" + "packages/page-constructor-extension": "0.1.0", + "packages/viewer": "0.0.0" } \ No newline at end of file diff --git a/demo/package.json b/demo/package.json index 70b8f30b..ac6805f0 100644 --- a/demo/package.json +++ b/demo/package.json @@ -34,6 +34,7 @@ "@gravity-ui/markdown-editor": "workspace:*", "@gravity-ui/markdown-editor-latex-extension": "workspace:*", "@gravity-ui/markdown-editor-page-constructor-extension": "workspace:*", + "@gravity-ui/markdown-viewer": "workspace:*", "@gravity-ui/page-constructor": "catalog:peer-gravity", "@gravity-ui/uikit": "catalog:peer-gravity", "markdown-it": "catalog:peers" diff --git a/demo/src/stories/viewer/MarkdownViewer.stories.tsx b/demo/src/stories/viewer/MarkdownViewer.stories.tsx new file mode 100644 index 00000000..4b2bdcf2 --- /dev/null +++ b/demo/src/stories/viewer/MarkdownViewer.stories.tsx @@ -0,0 +1,17 @@ +import type {StoryObj} from '@storybook/react'; + +import {markup} from '../../defaults/content'; + +import {Viewer} from './MarkdownViewer'; + +export const Story: StoryObj = { + args: { + markdown: markup, + }, +}; +Story.storyName = 'Markdown Viewer'; + +export default { + title: 'Viewer / Markdown Viewer', + component: Viewer, +}; diff --git a/demo/src/stories/viewer/MarkdownViewer.tsx b/demo/src/stories/viewer/MarkdownViewer.tsx new file mode 100644 index 00000000..9e601062 --- /dev/null +++ b/demo/src/stories/viewer/MarkdownViewer.tsx @@ -0,0 +1,28 @@ +import {useEffect, useState} from 'react'; + +import transform from '@diplodoc/transform'; +import {MarkdownViewer, cnYFM} from '@gravity-ui/markdown-viewer'; + +export type ViewerProps = { + markdown: string; +}; + +export const Viewer: React.FC = ({markdown}) => { + const [html, setHtml] = useState(''); + + useEffect(() => { + setHtml(transform(markdown).result.html); + }, [markdown]); + + return ( + + ); +}; diff --git a/packages/viewer/.prettierignore b/packages/viewer/.prettierignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/packages/viewer/.prettierignore @@ -0,0 +1 @@ +build diff --git a/packages/viewer/README.md b/packages/viewer/README.md new file mode 100644 index 00000000..8ce1b69c --- /dev/null +++ b/packages/viewer/README.md @@ -0,0 +1,84 @@ +# @gravity-ui/markdown-viewer · [![npm package](https://img.shields.io/npm/v/@gravity-ui/markdown-viewer)](https://www.npmjs.com/package/@gravity-ui/markdown-viewer) + +Markdown viewer component for [@gravity-ui/markdown-editor](https://github.com/gravity-ui/markdown-editor). Renders pre-transformed HTML from `@diplodoc/transform` with support for YFM (Yandex Flavored Markdown) styles and runtime extensions. + +## Installation + +```bash +npm install @gravity-ui/markdown-viewer +``` + +### Required peer dependencies + +```bash +npm install @gravity-ui/uikit react react-dom +``` + +## Usage + +### Basic rendering + +```tsx +import {MarkdownViewer} from '@gravity-ui/markdown-viewer'; + +export function Preview({html}: {html: string}) { + return ; +} +``` + +### With YFM styles + +To apply YFM styles, pass the `cnYFM()` class to the `className` prop. Import the stylesheets manually: + +```tsx +import {MarkdownViewer, cnYFM} from '@gravity-ui/markdown-viewer'; + +import '@diplodoc/transform/dist/yfm.css'; + +export function Preview({html}: {html: string}) { + return ; +} +``` + +### With YFM modifiers + +`cnYFM()` accepts optional modifier flags: + +```tsx + +``` + +Available modifiers: + +| Modifier | Description | +|---|---| +| `no-list-reset` | Disable list counter reset | +| `no-stripe-table` | Disable striped table rows | + +## API + +### `MarkdownViewer` + +| Prop | Type | Description | +|---|---|---| +| `html` | `string` | Pre-transformed HTML string to render. The component does not sanitize it — make sure to sanitize before passing. | +| `className` | `string` | CSS class applied to the inner content element | +| `style` | `CSSProperties` | Inline styles applied to the inner content element | +| `ref` | `Ref` | Ref forwarded to the inner content element | +| `children` | `ReactNode` | Additional nodes rendered alongside the content (e.g. runtime extension portals) | +| `...props` | `HTMLAttributes` | Any other props are forwarded to the inner content `div` | + +### `cnYFM(mods?, mix?)` + +BEM class name helper for the `.yfm` scope. Use it to enable YFM styles and modifiers on the viewer. + +```ts +import {cnYFM, type YFMMods} from '@gravity-ui/markdown-viewer'; + +cnYFM() // => 'yfm' +cnYFM({'no-list-reset': true}) // => 'yfm yfm_no-list-reset' +``` + +## License + +MIT diff --git a/packages/viewer/gulpfile.mjs b/packages/viewer/gulpfile.mjs new file mode 100644 index 00000000..5ae14b32 --- /dev/null +++ b/packages/viewer/gulpfile.mjs @@ -0,0 +1,20 @@ +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; + +import {series, task} from '@markdown-editor/gulp-tasks'; +import {registerBuildTasks} from '@markdown-editor/gulp-tasks/build'; + +import pkg from './package.json' with {type: 'json'}; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const BUILD_DIR = resolve('build'); +const NODE_MODULES_DIR = resolve(__dirname, 'node_modules'); + +registerBuildTasks({ + version: pkg.version, + buildDir: BUILD_DIR, + nodeModulesDir: NODE_MODULES_DIR, +}); + +task('default', series('clean', 'build')); diff --git a/packages/viewer/package.json b/packages/viewer/package.json new file mode 100644 index 00000000..dcdfa4af --- /dev/null +++ b/packages/viewer/package.json @@ -0,0 +1,71 @@ +{ + "name": "@gravity-ui/markdown-viewer", + "version": "0.0.0", + "description": "Viewer for markdown content from @gravity-ui/markdown-editor", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/gravity-ui/markdown-editor" + }, + "keywords": [ + "md", + "yfm", + "markdown" + ], + "scripts": { + "clean": "gulp clean", + "build": "gulp build", + "typecheck": "tsc -p tsconfig.json --noEmit", + "lint": "run-p -cs lint:*", + "lint:js": "eslint './**/*.{js,jsx,mjs,ts,tsx}'", + "lint:styles": "stylelint './**/*.{css,scss}' --allow-empty-input", + "lint:prettier": "prettier --check './**/*.{js,jsx,mjs,ts,tsx,css,scss}'", + "test": "exit 0", + "prepublishOnly": "nx lint && nx clean && nx build" + }, + "exports": { + ".": { + "import": { + "types": "./build/esm/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/cjs/index.d.ts", + "default": "./build/cjs/index.js" + } + } + }, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/esm/index.d.ts", + "files": [ + "build", + "README.md" + ], + "dependencies": { + "@bem-react/classname": "^1.6.0", + "tslib": "catalog:ts" + }, + "devDependencies": { + "@gravity-ui/uikit": "catalog:peer-gravity", + "@markdown-editor/gulp-tasks": "workspace:*", + "@markdown-editor/linters": "workspace:*", + "@markdown-editor/tsconfig": "workspace:*", + "@types/react": "catalog:react", + "@types/react-dom": "catalog:react", + "gulp-cli": "catalog:", + "npm-run-all": "^4.1.5", + "react": "catalog:react", + "react-dom": "catalog:react", + "typescript": "catalog:ts" + }, + "peerDependencies": { + "@gravity-ui/uikit": "^7.1.0", + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "sideEffects": [ + "*.css", + "*.scss" + ] +} diff --git a/packages/viewer/src/components/MarkdownViewer/MarkdownViewer.tsx b/packages/viewer/src/components/MarkdownViewer/MarkdownViewer.tsx new file mode 100644 index 00000000..d872f7d2 --- /dev/null +++ b/packages/viewer/src/components/MarkdownViewer/MarkdownViewer.tsx @@ -0,0 +1,32 @@ +import {type HTMLAttributes, type PropsWithChildren, forwardRef} from 'react'; + +import type {QAProps} from '@gravity-ui/uikit'; + +import {cn} from '../../utils/classname'; + +const b = cn('viewer'); + +export type MarkdownViewerProps = PropsWithChildren< + Omit, 'dangerouslySetInnerHTML'> & + QAProps & { + /** Pre-transformed HTML string. The component does not sanitize it — make sure to sanitize before passing. */ + html: string; + } +>; + +export const MarkdownViewer = forwardRef( + function MarkdownViewer({html, className, qa, children, ...restProps}, ref) { + return ( +
+
+ {children} +
+ ); + }, +); diff --git a/packages/viewer/src/components/MarkdownViewer/index.ts b/packages/viewer/src/components/MarkdownViewer/index.ts new file mode 100644 index 00000000..f7405bcb --- /dev/null +++ b/packages/viewer/src/components/MarkdownViewer/index.ts @@ -0,0 +1 @@ +export * from './MarkdownViewer'; diff --git a/packages/viewer/src/components/index.ts b/packages/viewer/src/components/index.ts new file mode 100644 index 00000000..f7405bcb --- /dev/null +++ b/packages/viewer/src/components/index.ts @@ -0,0 +1 @@ +export * from './MarkdownViewer'; diff --git a/packages/viewer/src/index.ts b/packages/viewer/src/index.ts new file mode 100644 index 00000000..3f8c388a --- /dev/null +++ b/packages/viewer/src/index.ts @@ -0,0 +1,2 @@ +export {MarkdownViewer, type MarkdownViewerProps} from './components'; +export {type YFMMods, cnYFM} from './utils/cn-yfm'; diff --git a/packages/viewer/src/utils/classname.ts b/packages/viewer/src/utils/classname.ts new file mode 100644 index 00000000..c3e4d706 --- /dev/null +++ b/packages/viewer/src/utils/classname.ts @@ -0,0 +1,3 @@ +import {withNaming} from '@bem-react/classname'; + +export const cn = withNaming({n: 'g-md-', e: '__', m: '_', v: '_'}); diff --git a/packages/viewer/src/utils/cn-yfm.ts b/packages/viewer/src/utils/cn-yfm.ts new file mode 100644 index 00000000..d85314d6 --- /dev/null +++ b/packages/viewer/src/utils/cn-yfm.ts @@ -0,0 +1,13 @@ +import {type ClassNameList, cn} from '@bem-react/classname'; + +const b = cn('yfm'); + +export type YFMMods = { + /** Disable list counter reset */ + 'no-list-reset'?: boolean; + /** Disable striped table rows */ + 'no-stripe-table'?: boolean; + [key: string]: string | boolean | number | undefined; +}; + +export const cnYFM: (mods?: YFMMods | null, mix?: ClassNameList) => string = b; diff --git a/packages/viewer/tsconfig.json b/packages/viewer/tsconfig.json new file mode 100644 index 00000000..ce1eae76 --- /dev/null +++ b/packages/viewer/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@markdown-editor/tsconfig/tsconfig", + "compilerOptions": { + "outDir": "build/esm", + "baseUrl": ".", + "paths": {} + }, + "include": ["src/**/*"], + "exclude": ["build"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3be70448..8b99e260 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: '@gravity-ui/markdown-editor-page-constructor-extension': specifier: workspace:* version: link:../packages/page-constructor-extension + '@gravity-ui/markdown-viewer': + specifier: workspace:* + version: link:../packages/viewer '@gravity-ui/page-constructor': specifier: catalog:peer-gravity version: 7.25.0(@diplodoc/transform@4.69.0(highlight.js@11.8.0)(react@18.2.0))(@gravity-ui/uikit@7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.0.28)(js-yaml@4.1.1)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) @@ -777,6 +780,49 @@ importers: specifier: catalog:ts version: 5.9.3 + packages/viewer: + dependencies: + '@bem-react/classname': + specifier: ^1.6.0 + version: 1.6.0 + tslib: + specifier: catalog:ts + version: 2.8.1 + devDependencies: + '@gravity-ui/uikit': + specifier: catalog:peer-gravity + version: 7.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@markdown-editor/gulp-tasks': + specifier: workspace:* + version: link:../../infra/gulp-tasks + '@markdown-editor/linters': + specifier: workspace:* + version: link:../../infra/linters + '@markdown-editor/tsconfig': + specifier: workspace:* + version: link:../../infra/tsconfig + '@types/react': + specifier: catalog:react + version: 18.0.28 + '@types/react-dom': + specifier: catalog:react + version: 18.0.11 + gulp-cli: + specifier: 'catalog:' + version: 3.1.0 + npm-run-all: + specifier: ^4.1.5 + version: 4.1.5 + react: + specifier: catalog:react + version: 18.2.0 + react-dom: + specifier: catalog:react + version: 18.2.0(react@18.2.0) + typescript: + specifier: catalog:ts + version: 5.9.3 + packages: '@aashutoshrathi/word-wrap@1.2.6':