├── .nvmrc ├── .husky └── pre-commit ├── CHANGELOG.md ├── .npmignore ├── .lintstagedrc.json ├── .prettierignore ├── src ├── vite-env.d.ts ├── dev │ ├── main.tsx │ ├── App.tsx │ └── theme.utils.ts ├── index.ts ├── components │ ├── icons │ │ ├── Check.tsx │ │ └── Copy.tsx │ └── CodeBlockWithCopy.tsx ├── index.css ├── utils │ └── utils.ts └── TiptapParser.tsx ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── automerge.yml │ ├── publish.yml │ ├── add-badges.yml │ └── release.yml ├── example ├── .prettierignore ├── src │ ├── vite-env.d.ts │ ├── index.css │ ├── main.tsx │ └── App.tsx ├── public │ └── favicon.ico ├── .yarn │ └── install-state.gz ├── postcss.config ├── tailwind.config ├── docs │ └── Note.md ├── tsconfig.node.json ├── .prettierrc.cjs ├── .gitignore ├── vite.config.ts ├── tsconfig.json ├── index.html ├── README.md ├── package.json └── eslint.config.mjs ├── public └── favicon.ico ├── screenshots └── screenshot.png ├── .yarnrc.yml ├── tsconfig.build.json ├── .markdown-link-check-config.json ├── tsconfig.build-esm.json ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── tsconfig.node.json ├── vite.config.ts ├── .cspell.json ├── .prettierrc.cjs ├── tsconfig.json ├── index.html ├── CONTRIBUTING.md ├── .gitignore ├── package.json ├── README.md └── eslint.config.mjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.5 (2024-10-12) 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .nyc_output/ 3 | coverage/ -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx}": "eslint --fix" 3 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /example/.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build -------------------------------------------------------------------------------- /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiavina-mika/tiptap-parser/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiavina-mika/tiptap-parser/HEAD/example/public/favicon.ico -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiavina-mika/tiptap-parser/HEAD/screenshots/screenshot.png -------------------------------------------------------------------------------- /example/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiavina-mika/tiptap-parser/HEAD/example/.yarn/install-state.gz -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | npmAlwaysAuth: true 4 | 5 | npmAuthToken: "${NODE_AUTH_TOKEN-fallback}" 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src"], 4 | "exclude": ["./src/dev"] 5 | } -------------------------------------------------------------------------------- /example/postcss.config: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.markdown-link-check-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https://tiptap.dev/" 5 | } 6 | ], 7 | "timeout": "10s", 8 | "retryOn429": true 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.build.json", 3 | "compilerOptions": { 4 | "target": "ES2018", 5 | "module": "ES2020", 6 | "outDir": "./dist/esm" 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "streetsidesoftware.code-spell-checker", 6 | "yzhang.markdown-all-in-one" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /example/tailwind.config: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /example/docs/Note.md: -------------------------------------------------------------------------------- 1 | ## Link utils 2 | - [Vite package build template](https://github.com/jasonsturges/vite-typescript-npm-package/tree/main) 3 | - [Issue with yarn when run yarn in nested project](https://stackoverflow.com/questions/64048830/yarn-2-init-add-failing) 4 | -------------------------------------------------------------------------------- /src/dev/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig, defineConfig } from 'vite' 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | open: true, 9 | }, 10 | } satisfies UserConfig) 11 | -------------------------------------------------------------------------------- /example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "octocat", 6 | "automerge", 7 | "tiptap", 8 | "Tiptap" 9 | ], 10 | "flagWords": [], 11 | "ignorePaths": [ 12 | "package.json", 13 | "package-lock.json", 14 | "yarn.lock", 15 | "tsconfig.json", 16 | "node_modules/**" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import parse from 'html-react-parser'; 2 | import TiptapParser from './TiptapParser'; 3 | 4 | export { 5 | type TiptapProps, 6 | type ClassNamesProps, 7 | } from './TiptapParser'; 8 | 9 | export { 10 | parse, 11 | }; 12 | 13 | export { 14 | type HTMLReactParserOptions, 15 | } from 'html-react-parser'; 16 | 17 | export default TiptapParser; 18 | -------------------------------------------------------------------------------- /example/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Remember to restart VSCode after making 3 | * any changes here and saving this file. 4 | */ 5 | module.exports = { 6 | arrowParens: 'always', 7 | quoteProps: 'preserve', 8 | bracketSameLine: false, 9 | endOfLine: "lf", 10 | printWidth: 80, 11 | semi: true, 12 | singleQuote: true, 13 | tabWidth: 2, 14 | trailingComma: 'all', 15 | useTabs: false 16 | }; 17 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env.preprod 27 | .env.prod 28 | -------------------------------------------------------------------------------- /src/components/icons/Check.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Check = () => { 3 | return ( 4 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Check; 21 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Remember to restart VSCode after making 3 | * any changes here and saving this file. 4 | */ 5 | module.exports = { 6 | arrowParens: 'always', 7 | quoteProps: 'preserve', 8 | bracketSameLine: false, 9 | endOfLine: "lf", 10 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 11 | plugins: [ 12 | "prettier-plugin-organize-imports" 13 | ], 14 | printWidth: 80, 15 | semi: true, 16 | singleQuote: true, 17 | tabWidth: 2, 18 | trailingComma: 'all', 19 | useTabs: false, 20 | }; 21 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | import tsconfigPaths from 'vite-tsconfig-paths' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | resolve: { 9 | alias: { 10 | "@/": path.resolve(__dirname, "src"), 11 | "assets": path.resolve(__dirname, "public") 12 | }, 13 | }, 14 | server: { 15 | open: true, 16 | port: 5174 17 | }, 18 | plugins: [ 19 | react(), 20 | tsconfigPaths(), 21 | ], 22 | }) 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "pwa-node", 7 | "request": "launch", 8 | "name": "Debug Current Test File", 9 | "autoAttachChildProcesses": true, 10 | "skipFiles": ["/**", "**/node_modules/**"], 11 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 12 | "args": ["run", "${relativeFile}"], 13 | "smartStep": true, 14 | "console": "integratedTerminal" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/components/icons/Copy.tsx: -------------------------------------------------------------------------------- 1 | 2 | const Copy = () => { 3 | return ( 4 | 15 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default Copy; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["ES2015", "DOM", "DOM.Iterable"], 5 | "module": "CommonJS", 6 | "skipLibCheck": true, 7 | 8 | // Build options 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "outDir": "./dist", 14 | "sourceMap": false, 15 | "esModuleInterop": true, 16 | 17 | // Linting 18 | "strict": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "declaration": true, 21 | "allowJs": false, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["./src", "vite.config.ts", "dts-bundle-generator.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ### Is your feature request related to a problem? Please describe. 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ### Describe the solution you'd like 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | ### Describe alternatives you've considered 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | ### Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | tiptap-parser 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "types": ["vite/client"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "paths": { 25 | "@/*": ["src/*"], 26 | "assets*": ["public/*"], 27 | }, 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | App using tiptap-parser 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css'; 2 | 3 | .code-container { 4 | font-size: 1rem/* 12px */; 5 | line-height: 1.4em; 6 | border-radius: 0.375rem/* 6px */; 7 | padding-top: 1.5rem/* 24px */ !important; 8 | padding-bottom: 1.5rem/* 24px */ !important; 9 | } 10 | 11 | .h1 { 12 | color: red; 13 | } 14 | .pre-container { 15 | position: relative; 16 | } 17 | .pre-container button { 18 | position: absolute; 19 | right: 1rem; 20 | top: 1rem; 21 | background: transparent; 22 | border: none; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | border: 1px solid rgb(255 255 255 / 0.3); 27 | width: 2rem; 28 | height: 2rem; 29 | cursor: pointer; 30 | z-index: 1000; 31 | color: #fff; 32 | font-size: 12px; 33 | border-radius: .25rem; 34 | } 35 | 36 | .pre-container button svg { 37 | width: 1rem; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { Children, isValidElement, ReactNode } from 'react'; 2 | 3 | /** 4 | * transform the children array to a string 5 | * @param children 6 | * @returns 7 | */ 8 | export const childrenToString = (children: ReactNode): string => { 9 | if (!children) return ''; // Return empty string if no children 10 | // Using React.Children to map over the children 11 | const array = Children.map(children, (child) => { 12 | if (!child) return ''; // Return empty string if the child is null or undefined 13 | // Convert the child to string by checking the type 14 | if (isValidElement(child)) { 15 | // For React elements, convert them back to their string representation 16 | return child.props.children ? childrenToString(child.props.children) : ''; 17 | } 18 | return child; // Return strings, numbers, etc. directly 19 | }) || []; 20 | 21 | return array.join(''); // Join all parts into a single string 22 | }; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ### Describe the bug 10 | 11 | A clear and concise description of what the bug is. 12 | 13 | Before filing a bug, please confirm that you have: 14 | 15 | ### To Reproduce 16 | 17 | Please include a CodeSandbox demo of the problem if possible. (You can fork [this CodeSandbox](https://codesandbox.io/p/github/tiavina-mika/tiptap-parser-demo).) 18 | 19 | Steps to reproduce the behavior: 20 | 21 | 1. 22 | 2. 23 | 3. 24 | 25 | ### Expected behavior 26 | 27 | A clear and concise description of what you expected to happen. 28 | 29 | ### Screenshots 30 | 31 | If applicable, add screenshots to help explain your problem. 32 | 33 | ### System (please complete the following information) 34 | 35 | - tiptap-parser version: [e.g. 2.0.0] 36 | - Browser: [e.g. Chrome, Firefox] 37 | - Node version: [e.g 16.4.2] 38 | - React version: [e.g 18.0.2] 39 | - OS: [e.g. Ubuntu 22.04, macOS 11.4] 40 | 41 | ```tsx 42 | 43 | ``` 44 | 45 | ### Additional context 46 | 47 | Add any other context about the problem here. 48 | -------------------------------------------------------------------------------- /src/components/CodeBlockWithCopy.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ReactNode, useState } from 'react'; 4 | import Check from './icons/Check'; 5 | import Copy from './icons/Copy'; 6 | import { childrenToString } from '../utils/utils'; 7 | 8 | /** 9 | * 10 | * add a copy button to the code block 11 | * @returns 12 | */ 13 | type Props = { 14 | children: ReactNode; 15 | }; 16 | 17 | const CodeBlockWithCopy = ({ children }: Props) => { 18 | const [isCopied, setIsCopied] = useState(false); 19 | 20 | const copyToClipboard = () => { 21 | const codeContent = childrenToString(children); 22 | 23 | if (!codeContent) return; 24 | navigator.clipboard.writeText(codeContent).then(() => { 25 | setIsCopied(true); 26 | setTimeout(() => setIsCopied(false), 2000); // "Copied!" message for 2 seconds 27 | }); 28 | }; 29 | 30 | return ( 31 |
32 | 38 |
{children}
39 |
40 | ); 41 | }; 42 | 43 | export default CodeBlockWithCopy; 44 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- # 2 | # ------------------------ 2 ------------------------ # 3 | # --------------------------------------------------- # 4 | 5 | name: Approve PR and delete branch 6 | on: pull_request_target 7 | 8 | env: 9 | PR_BRANCH: release-ci-${{ github.sha }} 10 | 11 | jobs: 12 | approve_pr_branch: 13 | name: Approve PR branch 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | steps: 18 | # Automatically approve GitHub pull requests. 19 | # see: https://github.com/hmarr/auto-approve-action 20 | - uses: hmarr/auto-approve-action@v4 21 | 22 | merge_pr_branch: 23 | name: Merge PR branch 24 | # we need to run this job after the approve_pr_branch job 25 | needs: approve_pr_branch 26 | runs-on: ubuntu-latest 27 | permissions: 28 | pull-requests: write 29 | 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Merge and delete Changelog PR 35 | run: | 36 | gh pr merge --squash --auto --delete-branch ${{ env.PR_BRANCH }} 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 39 | -------------------------------------------------------------------------------- /src/dev/App.tsx: -------------------------------------------------------------------------------- 1 | import TiptapParser from '../TiptapParser'; 2 | 3 | const html = ` 4 |

Here is an exemple of code

5 |

This is a stringified html with code

6 |
7 |
import Link from 'next/link';
 8 | 
 9 | import Title from '@/components/typography/Title';
10 | 
11 | // some comment here
12 | 
13 | const NotFound = () => (
14 |   <html lang="en">
15 |     <body className="">
16 |       <div className="flex min-h-screen flex-col items-center justify-center space-y-8">
17 |         <Title className="text-4xl font-semibold">404 - Page Not Found</Title>
18 | 
19 |         <div className="space-x-4">
20 |           <Link
21 |             className="text-blue-600 underline duration-300 hover:text-red-500"
22 |             href="/"
23 |           >
24 |             Homepage
25 |           </Link>
26 |           <Link
27 |             className="text-blue-600 underline duration-300 hover:text-red-500"
28 |             href="/contact"
29 |           >
30 |             Contact Us
31 |           </Link>
32 |         </div>
33 |       </div>
34 |     </body>
35 |   </html>
36 | );
37 | export default NotFound;
38 | 

39 | `; 40 | 41 | const App = () => { 42 | return ( 43 | 44 | ); 45 | }; 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import TiptapParser from 'tiptap-parser'; 2 | 3 | const html = ` 4 |

Here is an exemple of code

5 |

This is a stringified html with code

6 |
7 |
import Link from 'next/link';
 8 | 
 9 | import Title from '@/components/typography/Title';
10 | 
11 | // some comment here
12 | 
13 | const NotFound = () => (
14 |   <html lang="en">
15 |     <body className="">
16 |       <div className="flex min-h-screen flex-col items-center justify-center space-y-8">
17 |         <Title className="text-4xl font-semibold">404 - Page Not Found</Title>
18 | 
19 |         <div className="space-x-4">
20 |           <Link
21 |             className="text-blue-600 underline duration-300 hover:text-red-500"
22 |             href="/"
23 |           >
24 |             Homepage
25 |           </Link>
26 |           <Link
27 |             className="text-blue-600 underline duration-300 hover:text-red-500"
28 |             href="/contact"
29 |           >
30 |             Contact Us
31 |           </Link>
32 |         </div>
33 |       </div>
34 |     </body>
35 |   </html>
36 | );
37 | export default NotFound;
38 | 

39 | `; 40 | 41 | const App = () => { 42 | return ( 43 | 44 | ); 45 | }; 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | 32 | ### issues 33 | https://stackoverflow.com/questions/63190725/delete-cr-only-for-ts-tsx-files-prettier-eslint-on-vscode-1-46 -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-using-tiptap-parser", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --fix", 10 | "preview": "vite preview", 11 | "clean-cache": "yarn cache clean --all", 12 | "upgrade": "yarn add tiptap-parser@latest" 13 | }, 14 | "dependencies": { 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "tiptap-parser": "^0.2.1" 18 | }, 19 | "devDependencies": { 20 | "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", 21 | "@eslint/compat": "^1.2.0", 22 | "@eslint/eslintrc": "^3.1.0", 23 | "@eslint/js": "^9.12.0", 24 | "@stylistic/eslint-plugin": "^2.9.0", 25 | "@types/eslint-plugin-jsx-a11y": "^6", 26 | "@types/eslint__js": "^8.42.3", 27 | "@types/react": "^18.2.43", 28 | "@types/react-dom": "^18.2.17", 29 | "@typescript-eslint/eslint-plugin": "^6.14.0", 30 | "@typescript-eslint/parser": "^6.20.0", 31 | "@vitejs/plugin-react": "^4.2.1", 32 | "autoprefixer": "^10.4.19", 33 | "babel-plugin-module-resolver": "^5.0.0", 34 | "eslint": "^9.12.0", 35 | "eslint-config-prettier": "^9.1.0", 36 | "eslint-import-resolver-babel-module": "^5.3.2", 37 | "eslint-plugin-eslint-comments": "^3.2.0", 38 | "eslint-plugin-jsx-a11y": "^6.10.0", 39 | "eslint-plugin-prefer-arrow-functions": "^3.2.4", 40 | "eslint-plugin-prettier": "^5.2.1", 41 | "eslint-plugin-react-hooks": "^4.6.2", 42 | "eslint-plugin-react-refresh": "^0.4.12", 43 | "globals": "^15.11.0", 44 | "postcss": "^8.4.38", 45 | "prettier": "^3.2.2", 46 | "tailwindcss": "^3.4.3", 47 | "typescript": "5.5.3", 48 | "typescript-eslint": "^8.8.1", 49 | "vite": "5.0.8", 50 | "vite-tsconfig-paths": "^4.3.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "// editor": "-------------------------------------", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit", 5 | "source.organizeImports": "never" 6 | }, 7 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 8 | "editor.formatOnSave": true, 9 | "editor.tabSize": 2, 10 | "eslint.format.enable": true, 11 | "[javascript]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[javascriptreact]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[typescriptreact]": { 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[markdown]": { 24 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint" 25 | }, 26 | "// files": "-------------------------------------", 27 | "files.trimTrailingWhitespace": true, 28 | "files.insertFinalNewline": true, 29 | "files.associations": { 30 | "*.ignore": "plaintext", 31 | "*.txt": "plaintext", 32 | "*.tsx": "typescriptreact", // Associe .tsx à TypeScript pour React 33 | "*.ts": "typescript" 34 | }, 35 | "files.exclude": { 36 | "**/*.js": { 37 | "when": "$(basename).ts" 38 | } // Cache les fichiers .js générés si vous utilisez TypeScript 39 | }, 40 | "// typescript": "-------------------------------------", 41 | "typescript.tsdk": "./node_modules/typescript/lib", 42 | "typescript.enablePromptUseWorkspaceTsdk": true, 43 | "typescript.validate.enable": true, 44 | "// eslint": "-------------------------------------", 45 | "eslint.validate": [ 46 | "javascript", 47 | "javascriptreact", 48 | "typescript", 49 | "typescriptreact", 50 | "html", 51 | "markdown", 52 | "json", 53 | "jsonc", 54 | "yaml" 55 | ], 56 | "prettier.enable": true, 57 | "typescript.tsserver.allowLocalPluginLoads": true, 58 | "typescript.tsserver.pluginPaths": ["./"] 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- # 2 | # ------------------------ 3 ------------------------ # 3 | # --------------------------------------------------- # 4 | name: Publish to NPM 5 | run-name: ${{ github.actor }} publish to npm 🚀 6 | 7 | on: 8 | # we are using workflow_run to trigger the workflow 9 | # because we want to run this workflow only when the Approve PR and delete branch workflow is completed 10 | # GitHub prevents workflows from running on events that were caused by other workflows to prevent unlimited recursion 11 | # so we do not use the release event 12 | # issue: https://github.com/orgs/community/discussions/25281 13 | # see: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run 14 | workflow_run: 15 | # the workflow we want to trigger this workflow (see .github/workflows/add-badges.yml) 16 | workflows: [Add Badges] 17 | types: [completed] 18 | # run on release creation 19 | # release: 20 | # types: [created] 21 | 22 | jobs: 23 | publish: 24 | runs-on: ubuntu-latest 25 | # if: github.actor == 'dependabot[bot]' 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | persist-credentials: false 31 | 32 | # need this for using yarn v4 33 | - name: Enable Corepack 34 | run: corepack enable 35 | 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: '20.x' 39 | registry-url: 'https://registry.npmjs.org' 40 | 41 | - name: Install dependencies 42 | run: yarn 43 | 44 | - name: Build 45 | run: yarn build 46 | 47 | # npm publish 48 | - name: Publish package on NPM 📦 49 | # if using yarn v4 50 | run: yarn npm publish 51 | env: 52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 53 | 54 | - run: echo "Published to NPM 🚀- https://www.npmjs.com/package/${{ github.event.repository.name }}" 55 | -------------------------------------------------------------------------------- /.github/workflows/add-badges.yml: -------------------------------------------------------------------------------- 1 | # This action use https://github.com/wow-actions/add-badges?tab=readme-ov-file to add badges to your README.md file. 2 | name: Add Badges 3 | 4 | on: 5 | # we are using workflow_run to trigger the workflow 6 | # because we want to run this workflow only when the Approve PR and delete branch workflow is completed 7 | # GitHub prevents workflows from running on events that were caused by other workflows to prevent unlimited recursion 8 | # so we do not use the release event 9 | # issue: https://github.com/orgs/community/discussions/25281 10 | # see: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run 11 | workflow_run: 12 | # the workflow we want to trigger this workflow (see .github/workflows/automerge.yml) 13 | workflows: [Approve PR and delete branch] 14 | types: [completed] 15 | 16 | jobs: 17 | run: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: wow-actions/add-badges@v1 21 | env: 22 | repo_url: ${{ github.event.repository.html_url }} 23 | repo_name: ${{ github.event.repository.name }} 24 | repo_owner: ${{ github.event.repository.owner.login }} 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 27 | badges: | 28 | [ 29 | { 30 | "badge": "https://img.shields.io/npm/v/${{ env.repo_name }}?style=flat-square", 31 | "alt": "NPM Version", 32 | "link": "https://www.npmjs.com/package/${{ env.repo_name }}" 33 | }, 34 | { 35 | "badge": "https://img.shields.io/badge/language-TypeScript-blue.svg?style=flat-square", 36 | "alt": "Language", 37 | "link": "https://www.typescriptlang.org" 38 | } 39 | ] 40 | # if adding the last commit badge 41 | # { 42 | # "badge": "https://img.shields.io/github/last-commit/${{ env.repo_owner }}/${{ env.repo_name }}?style=flat-square", 43 | # "alt": "Last Commit", 44 | # "link": "${{ env.repo_url }}/commits/master" 45 | # } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | _Pull requests, bug reports, and all other forms of contribution are welcomed and highly encouraged!_ :octocat: 4 | 5 | ## How can I contribute? 6 | 7 | ### GitHub issues 8 | 9 | If you encounter a problem with this library or if you have a new feature you'd like to see in this project, please create [a new issue](https://github.com/tiavina-mika/tiptap-parser/issues/new/choose). 10 | 11 | ### GitHub pull requests 12 | 13 | Please leverage the repository's own tools to make sure the code is aligned with our standards. If you're using VSCode, it's easiest to use the recommended extensions (`.vscode/extensions.json`) to get integrated linting and autoformatting. 14 | 15 | It's recommended to run all check commands before submitting the PR (`type:check`, `format:check`, `lint:check`, `spell:check`, `yarn lint`). 16 | 17 | ## Development setup 18 | 19 | 1. Set up [yarn](https://yarnpkg.com/getting-started/install) 20 | 2. Run `yarn` 21 | 3. Run `yarn dev` and view the demo site at the printed localhost URL 22 | 23 | This package uses Vite with Hot Module Replacement (HMR), so file edits should reflect immediately in your browser during local development. 24 | 25 | To instead test a "built" version of this package which is installed into an "external" module, you can run `yarn example`, which runs a server with the separate application in the `example/` directory. 26 | 27 | ## Releasing a new version (for maintainers) 28 | 29 | When a new version should be cut since some new changes have landed on the `main` branch, do the following to publish it: 30 | 31 | 1. Go to the `main` branch and pull in the latest changes. 32 | 2. Commit change using (commit convention)[https://www.conventionalcommits.org] 33 | 3. Push the commit (ex: `git push origin main`) 34 | 4. The `release.yml` GitHub Actions workflow will auto-generate a tag, change log, release note and a PR branch 35 | 5. The `automerge.yml` GitHub Actions workflow will merge and delete the PR branch 36 | 6. The `publish.yml` GitHub Actions workflow will publish the package to the npm registry 37 | 38 | ## Issue possibly encountered during installation 39 | - [with yarn](https://stackoverflow.com/questions/67062308/getting-yn0028-the-lockfile-would-have-been-modified-by-this-install-which-is-e) 40 | - solution: 41 | . Remove `yarn.lock` 42 | . `yarn cache clean --all` 43 | . Create an empty `yarn.lock` 44 | . run `yarn` 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiptap-parser", 3 | "version": "0.2.5", 4 | "description": "HTML parser to React component built on the top of html-react-parser with code syntax highlighting", 5 | "keywords": [ 6 | "tiptap", 7 | "prosemirror", 8 | "rich text editor", 9 | "react", 10 | "wysiwyg", 11 | "quill", 12 | "draftjs", 13 | "slate", 14 | "lowlight", 15 | "highlight", 16 | "html string to react", 17 | "code editor" 18 | ], 19 | "homepage": "https://github.com/tiavina-mika/tiptap-parser", 20 | "bugs": { 21 | "url": "https://github.com/tiavina-mika/tiptap-parser/issues" 22 | }, 23 | "author": { 24 | "name": "Tiavina Michael Ralainirina", 25 | "email": "tiavinamika@gmail.com", 26 | "github": "https://github.com/tiavina-mika" 27 | }, 28 | "license": "MIT", 29 | "type": "module", 30 | "types": "./dist/index.d.ts", 31 | "main": "dist/index.js", 32 | "module": "dist/esm/index.js", 33 | "exports": { 34 | ".": { 35 | "types": "./dist/index.d.ts", 36 | "require": "./dist/index.js", 37 | "import": "./dist/esm/index.js" 38 | } 39 | }, 40 | "files": [ 41 | "/dist" 42 | ], 43 | "publishConfig": { 44 | "access": "public" 45 | }, 46 | "scripts": { 47 | "clean": "rimraf dist", 48 | "dev": "vite", 49 | "build": "yarn clean && tsc --project tsconfig.build.json && tsc --project tsconfig.build-esm.json && yarn copy-files && yarn copy-files-esm && yarn fixImportExtension", 50 | "prepare": "husky", 51 | "lint-staged": "lint-staged", 52 | "preview": "vite preview", 53 | "type:check": "tsc --noEmit", 54 | "format": "prettier --write .", 55 | "format:check": "prettier --check .", 56 | "lint:check": "eslint --max-warnings 0 --ext .js,.jsx,.ts,.tsx src", 57 | "lint": "eslint . --fix", 58 | "md-link:check": "markdown-link-check -v -p README.md -v -p CONTRIBUTING.md -v -p .github/**/*.md -c .markdown-link-check-config.json", 59 | "spell:check": "cspell \"{README.md,CONTRIBUTING.md,.github/*.md}\"", 60 | "visualize": "npx vite-bundle-visualizer", 61 | "publish": "yarn build && yarn npm publish", 62 | "copy-files": "copyfiles -u 1 src/*.css dist/", 63 | "copy-files-esm": "copyfiles -u 1 src/*.css dist/esm", 64 | "clean-cache": "yarn cache clean --all", 65 | "fixImportExtension": "ts-add-js-extension --dir=dist", 66 | "example": "cd example && yarn upgrade && rimraf ./example/node_modules && yarn && yarn dev" 67 | }, 68 | "dependencies": { 69 | "hast-util-to-html": "^9.0.1", 70 | "html-react-parser": "^5.1.10", 71 | "lowlight": "3.1.0" 72 | }, 73 | "peerDependencies": { 74 | "react": "^16.8.0 || ^17.0.2 || ^18.2.0", 75 | "react-dom": "^16.8.0 || ^17.0.2 || ^18.2.0" 76 | }, 77 | "devDependencies": { 78 | "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", 79 | "@eslint/compat": "^1.2.0", 80 | "@eslint/eslintrc": "^3.1.0", 81 | "@eslint/js": "^9.12.0", 82 | "@stylistic/eslint-plugin": "^2.9.0", 83 | "@types/eslint-plugin-jsx-a11y": "^6", 84 | "@types/eslint__js": "^8.42.3", 85 | "@types/react": "^18.2.43", 86 | "@types/react-dom": "^18.2.17", 87 | "@vitejs/plugin-react": "^4.2.1", 88 | "babel-plugin-module-resolver": "^5.0.0", 89 | "copyfiles": "^2.4.1", 90 | "cspell": "^6.31.1", 91 | "eslint": "^9.12.0", 92 | "eslint-config-prettier": "^9.1.0", 93 | "eslint-import-resolver-babel-module": "^5.3.2", 94 | "eslint-plugin-eslint-comments": "^3.2.0", 95 | "eslint-plugin-jsx-a11y": "^6.10.0", 96 | "eslint-plugin-prefer-arrow-functions": "^3.2.4", 97 | "eslint-plugin-prettier": "^5.2.1", 98 | "eslint-plugin-react-hooks": "^4.6.2", 99 | "eslint-plugin-react-refresh": "^0.4.12", 100 | "globals": "^15.11.0", 101 | "husky": "^9.1.6", 102 | "lint-staged": "^15.2.10", 103 | "markdown-link-check": "^3.12.1", 104 | "prettier": "^3.3.3", 105 | "prettier-plugin-organize-imports": "^4.1.0", 106 | "react": "^18.2.0", 107 | "react-dom": "^18.2.0", 108 | "rimraf": "^5.0.5", 109 | "ts-add-js-extension": "^1.6.4", 110 | "typescript": "5.5.3", 111 | "typescript-eslint": "^8.8.1", 112 | "vite": "5.0.8", 113 | "vite-tsconfig-paths": "^4.3.1" 114 | }, 115 | "packageManager": "yarn@4.2.1" 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tiptap-parser 2 | 3 |

4 | HTML parser for Tiptap editor build on the of html-react-parser with code syntax highlighting. 5 |

6 | 7 | 8 | 9 | [![NPM Version](https://img.shields.io/npm/v/tiptap-parser?style=flat-square)](https://www.npmjs.com/package/tiptap-parser) 10 | [![Language](https://img.shields.io/badge/language-TypeScript-blue.svg?style=flat-square)](https://www.typescriptlang.org) 11 | 12 | 13 | ## Use case 14 | The Tiptap editor is primarily used as a text editor for blogging, particularly in back-office applications. However, if the goal is simply to display content (for example, on a Next.js website), it may be excessive to install either the Tiptap or the mui-tiptap-editor along with the entire MUI library. 15 | 16 | This library is specifically designed to display the contents of the mui-tiptap-editor, which saves HTML as text. If you're using a developer-oriented blogging platform like Medium, this library is ideal for you. 17 | 18 | ## Demo 19 | Try it yourself in this **[CodeSandbox live demo](https://codesandbox.io/p/github/tiavina-mika/tiptap-parser-demo)** 20 | 21 | Screenshot 22 | 23 | ## Installation 24 | 25 | ```shell 26 | 27 | npm install tiptap-parser 28 | 29 | ``` 30 | or 31 | ```shell 32 | 33 | yarn add tiptap-parser 34 | ``` 35 | 36 | 37 | ## Get started 38 | 39 | #### Simple usage 40 | 41 | ```tsx 42 | import TiptapParser from "tiptap-parser"; 43 | 44 | const html = `

Hello world

`; 45 | 46 | function App() { 47 | return ( 48 | 49 | ); 50 | } 51 | ``` 52 | 53 | #### Content with code 54 | 55 | ```tsx 56 | const html = `<>

Hello there

console.log("Log something here")
`; 57 | 58 | 59 | ``` 60 | 61 | #### Customization 62 | 63 | ```tsx 64 | const html = `

Hello there

`; 65 | 66 | 76 | ``` 77 | 78 | ## Props 79 | 80 | |props |type | Default value | Description | 81 | |----------------|-------------------------------|-----------------------------|-----------------------------| 82 | |content|`string`|empty| html string to be displayed 83 | |containerClassName|`string`|empty| styles of the container 84 | |classNames|`ClassNameProps`|empty| class names of each element withing the container 85 | |language|`string`|javascript| language of the code. [see the list](https://github.com/wooorm/lowlight?tab=readme-ov-file#data) 86 | 87 | ## Types 88 | 89 | |props |type | Default value | Description | 90 | |----------------|-------------------------------|-----------------------------|-----------------------------| 91 | |codeClassName|`string`|empty| class name of code element 92 | |h1ClassName|`string`|empty| class name of h1 element 93 | |h2ClassName|`string`|empty| class name of h2 element 94 | |h3ClassName|`string`|empty| class name of h3 element 95 | |h4ClassName|`string`|empty| class name of h4 element 96 | |h5ClassName|`string`|empty| class name of h5 element 97 | |h6ClassName|`string`|empty| class name of h6 element 98 | |pClassName|`string`|empty| class name of p element 99 | |ulClassName|`string`|empty| class name of ul element 100 | |spanClassName|`string`|empty| class name of span element 101 | |divClassName|`string`|empty| class name of div element 102 | |aClassName|`string`|empty| class name of a element 103 | |tableClassName|`string`|empty| class name of table element 104 | |imageClassName|`string`|empty| class name of image element 105 | |other props|`HTMLReactParserOptions`|empty| [all html-react-parser props](https://www.npmjs.com/package/html-react-parser) 106 | 107 | 108 | ## Contributing 109 | 110 | Get started [here](https://github.com/tiavina-mika/tiptap-parser/blob/main/CONTRIBUTING.md). 111 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- # 2 | # ------------------------ 1 ------------------------ # 3 | # --------------------------------------------------- # 4 | name: Releases 5 | run-name: ${{ github.actor }} releases 🚀 6 | 7 | # run this workflow on push to main branch and pull requests 8 | on: 9 | push: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: write 15 | packages: write 16 | pull-requests: write 17 | 18 | env: 19 | APP_NAME: tiptap-parser 20 | 21 | jobs: 22 | lint: 23 | name: Lint 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | # need this for using yarn v4 31 | - name: Enable Corepack 32 | run: corepack enable 33 | 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: '20' 38 | 39 | - name: Install dependencies 40 | run: yarn 41 | 42 | - name: Lint 43 | run: yarn lint 44 | 45 | changelog: 46 | name: Changelog 47 | needs: 48 | - lint 49 | if: github.event_name != 'pull_request' 50 | runs-on: ubuntu-latest 51 | 52 | outputs: 53 | skipped: ${{ steps.changelog.outputs.skipped }} 54 | tag: ${{ steps.changelog.outputs.tag }} 55 | clean_changelog: ${{ steps.changelog.outputs.clean_changelog }} 56 | version: ${{ steps.changelog.outputs.version }} 57 | 58 | env: 59 | PR_BRANCH: release-ci-${{ github.sha }} 60 | 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v4 64 | 65 | - name: Create Branch 66 | run: | 67 | git checkout -b ${{ env.PR_BRANCH }} 68 | 69 | # This action will bump version, tag commit and generate a changelog with conventional commits. 70 | # see: https://github.com/TriPSs/conventional-changelog-action 71 | - name: Create Changelog 72 | uses: TriPSs/conventional-changelog-action@v5 73 | id: changelog 74 | with: 75 | preset: 'conventionalcommits' 76 | github-token: ${{ secrets.GH_TOKEN }} 77 | git-user-name: "github-actions[bot]" 78 | git-user-email: "github-actions[bot]@users.noreply.github.com" 79 | git-branch: ${{ env.PR_BRANCH }} 80 | skip-git-pull: true 81 | skip-on-empty: false 82 | # this will update the version in package.json 83 | # and therefore the npm package version 84 | version-file: './package.json' 85 | create-summary: true 86 | 87 | # if the changelog is skipped, we do not need to create a PR 88 | # create a PR with the changelog and the new version 89 | # see: https://blog.kubesimplify.com/automated-github-releases-with-github-actions-and-conventional-commits 90 | - name: Create Changelog PR 91 | if: steps.changelog.outputs.skipped == 'false' 92 | run: | 93 | gh pr create --base main --head ${{ env.PR_BRANCH }} --title 'chore(release): ${{ steps.changelog.outputs.tag }} [skip-ci]' --body '${{ steps.changelog.outputs.clean_changelog }}' 94 | env: 95 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 96 | # the PR is created, we can now merge and close it 97 | # see: .github/workflows/automerge.yml 98 | 99 | release: 100 | name: Release 101 | needs: changelog 102 | if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target' 103 | runs-on: ubuntu-latest 104 | 105 | 106 | steps: 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | 110 | # Create GitHub Releases 111 | # see: https://github.com/softprops/action-gh-release 112 | - name: Create Release 113 | uses: softprops/action-gh-release@v2 114 | # it needs the tag to create the release 115 | if: needs.changelog.outputs.tag != '' 116 | with: 117 | tag_name: ${{ needs.changelog.outputs.tag }} 118 | name: ${{ needs.changelog.outputs.tag }} 119 | prerelease: false 120 | draft: false 121 | files: CHANGELOG.md 122 | generate_release_notes: true 123 | body: | 124 |
125 | 🤖 ${{ env.APP_NAME }} Changelog 126 | 127 | ${{ needs.changelog.outputs.clean_changelog }} 128 |
129 | -------------------------------------------------------------------------------- /src/TiptapParser.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * documentations: 4 | * https://www.npmjs.com/package/html-react-parser 5 | * https://github.com/wooorm/lowlight?tab=readme-ov-file 6 | * https://github.com/GeoffSelby/tailwind-highlightjs 7 | * https://highlightjs.org/examples (list of all available themes) 8 | * https://medium.com/@hizacharylee/simplify-syntax-highlighting-with-highlight-js-b65af3bdc509 (custom css theme, not used here) 9 | * 10 | */ 11 | import parse, { 12 | HTMLReactParserOptions, DOMNode, Element, attributesToProps, domToReact, 13 | } from 'html-react-parser'; 14 | import { common, createLowlight } from 'lowlight'; 15 | import { toHtml } from 'hast-util-to-html'; 16 | import { ElementType } from 'react'; 17 | import './index.css'; 18 | import CodeBlockWithCopy from './components/CodeBlockWithCopy'; 19 | 20 | const lowlight = createLowlight(common); 21 | 22 | export type ClassNamesProps = { 23 | codeClassName?: string; 24 | h1ClassName?: string; 25 | h2ClassName?: string; 26 | h3ClassName?: string; 27 | h4ClassName?: string; 28 | h5ClassName?: string; 29 | h6ClassName?: string; 30 | pClassName?: string; 31 | ulClassName?: string; 32 | liClassName?: string; 33 | spanClassName?: string; 34 | divClassName?: string; 35 | aClassName?: string; 36 | tableClassName?: string; 37 | imageClassName?: string; 38 | }; 39 | 40 | const parseHtml = ( 41 | /** 42 | * The stringified html to be parsed (to HTML). 43 | * example: `

Hello there

console.log('Hello, World!')

` 44 | */ 45 | text: string, 46 | classNames?: ClassNamesProps, 47 | language = 'javascript', 48 | options?: HTMLReactParserOptions 49 | ) => { 50 | const { codeClassName } = classNames || {}; 51 | /** 52 | * Parse the html string content and highlight the code snippets 53 | */ 54 | 55 | const defaultOptions = { 56 | /** 57 | * Replace the `` with the highlighted code snippet 58 | * the string content of the `` tag is transformed to stringified html using the lowlight library 59 | * the stringified html is then highlighted using the `highlight` theme 60 | * for that to work with tailwindcss, we are use tailwindcss plugin `tailwind-highlightjs` 61 | * hast trees as returned by lowlight can be serialized to HTML using `hast-util-to-html` 62 | * finally the highlighted code snippet is parsed to react component using the `parse` function of `html-react-parser` 63 | * @param domProps 64 | * @returns 65 | */ 66 | replace: (domProps: DOMNode) => { 67 | const { name, children, attribs } = domProps as Element; 68 | const Component = name as ElementType; 69 | 70 | if (name) { 71 | const props = attributesToProps(attribs); 72 | 73 | /* 74 | * do not replace the `
` tag
 75 |          * if (name === 'pre') return
 76 |          */
 77 |         if (name === 'pre') {
 78 |           return (
 79 |             // pre with the copy button
 80 |             
 81 |               {domToReact(children as DOMNode[], {
 82 |                 replace: (codeNode: any) => {
 83 |                   /**
 84 |                    * Replace the `` tag with the highlighted code snippet
 85 |                    * with the `hljs` class name to apply the highlight theme (see: https://highlightjs.org/examples/)
 86 |                    */
 87 |                   if (codeNode.name === 'code') {
 88 |                     const codeStr = domToReact(codeNode.children as DOMNode[]) as string;
 89 |                     const tree = lowlight.highlight(language, codeStr);
 90 | 
 91 |                     return (
 92 |                       
 93 |                         {parse(toHtml(tree))}
 94 |                       
 95 |                     );
 96 |                   }
 97 |                 },
 98 |               })}
 99 |             
100 |           );
101 |         }
102 | 
103 |         if (name === 'image') {
104 |           return ;
105 |         }
106 | 
107 |         if (['br', 'hr', 'img'].find((currentName: string) => currentName === name)) {
108 |           return ;
109 |         }
110 | 
111 |         return (
112 |           
113 |             {domToReact(children as DOMNode[])}
114 |           
115 |         );
116 |       }
117 |     },
118 |   };
119 | 
120 |   /*
121 |    * If the `` tag is not found in the html string content
122 |    * it means that there are no code snippets to be highlighted.
123 |    */
124 |   return parse(text, { ...defaultOptions, ...options });
125 | };
126 | 
127 | /*
128 |  * ------------------------------ //
129 |  * ---------- main props -------- //
130 |  * ------------------------------ //
131 |  */
132 | export type TiptapProps = {
133 |   /**
134 |    * the stringified html content to be parsed
135 |    */
136 |   content: string;
137 |   /**
138 |    * object that contains the class names for the html tags
139 |    */
140 |   classNames?: ClassNamesProps;
141 |   /**
142 |    * the programming language of the code snippets to be highlighted
143 |    * default: `javascript`
144 |    * see all available languages here: https://highlightjs.org/examples
145 |    */
146 |   language?: string;
147 |   /**
148 |    * the class name of the container div
149 |    */
150 |   containerClassName?: string;
151 |   /**
152 |    * HTMLReactParserOptions: the options of the `html-react-parser` library
153 |    */
154 | } & HTMLReactParserOptions;
155 | const TiptapParser = ({
156 |   classNames, containerClassName, language, content, ...rest
157 | }: TiptapProps) => {
158 |   return (
159 |     
160 | {parseHtml(content, classNames, language, rest)} 161 |
162 | ); 163 | }; 164 | 165 | export default TiptapParser; 166 | -------------------------------------------------------------------------------- /src/dev/theme.utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadgeProps, PaletteMode, PaletteOptions, Theme, createTheme, 3 | } from '@mui/material'; 4 | import { grey, teal } from '@mui/material/colors'; 5 | 6 | export const DASHBOARD_BACKGROUND_COLOR = '#FAFBFB'; 7 | export const DEFAULT_THEME_COLOR = 'green'; 8 | 9 | 10 | const textDarkColor = { 11 | color: '#fff', 12 | }; 13 | 14 | const defaultTheme = { 15 | palette: { 16 | secondary: { 17 | light: '#ff7961', 18 | main: '#f44336', 19 | dark: '#ba000d', 20 | contrastText: '#000', 21 | }, 22 | success: { 23 | main: '#00C292', 24 | }, 25 | info: { 26 | main: '#0BB2FB', 27 | }, 28 | error: { 29 | main: '#E46A76', 30 | }, 31 | }, 32 | typography: { 33 | fontFamily: [ 34 | 'DM Sans', 35 | '-apple-system', 36 | 'BlinkMacSystemFont', 37 | '"Segoe UI"', 38 | 'Roboto', 39 | '"Helvetica Neue"', 40 | 'Arial', 41 | 'sans-serif', 42 | '"Apple Color Emoji"', 43 | '"Segoe UI Emoji"', 44 | '"Segoe UI Symbol"', 45 | ].join(','), 46 | }, 47 | components: { 48 | MuiSwitch: { 49 | styleOverrides: { 50 | root: ({ theme }: { theme: Theme; ownerState: BadgeProps }) => ({ 51 | width: 38, 52 | height: 20, 53 | padding: 0, 54 | display: 'flex', 55 | '&:active': { 56 | '& .MuiSwitch-thumb': { 57 | width: 19, 58 | }, 59 | '& .MuiSwitch-switchBase.Mui-checked': { 60 | transform: 'translateX(9px)', 61 | }, 62 | }, 63 | '& .MuiSwitch-switchBase': { 64 | padding: 1, 65 | '&.Mui-checked': { 66 | transform: 'translateX(16px)', 67 | color: '#fff', 68 | '& + .MuiSwitch-track': { 69 | opacity: 1, 70 | backgroundColor: theme.palette.mode === 'dark' ? '#177ddc' : '#1890ff', 71 | }, 72 | }, 73 | }, 74 | '& .MuiSwitch-thumb': { 75 | boxShadow: '0 2px 4px 0 rgb(0 35 11 / 20%)', 76 | width: 18, 77 | height: 18, 78 | borderRadius: 20 / 2, 79 | transition: theme.transitions.create(['width'], { 80 | duration: 200, 81 | }), 82 | }, 83 | '& .MuiSwitch-track': { 84 | borderRadius: 20 / 2, 85 | opacity: 1, 86 | backgroundColor: 87 | theme.palette.mode === 'dark' ? 'rgba(255,255,255,.35)' : 'rgba(0,0,0,.25)', 88 | boxSizing: 'border-box', 89 | }, 90 | }), 91 | }, 92 | }, 93 | MuiChip: { 94 | styleOverrides: { 95 | root: { 96 | padding: '0px 3px', 97 | fontWeight: 500, 98 | fontSize: 12, 99 | }, 100 | filled: { 101 | borderRadius: 6, 102 | color: '#fff', 103 | }, 104 | label: { 105 | paddingLeft: 8, 106 | paddingRight: 8, 107 | }, 108 | }, 109 | }, 110 | MuiBadge: { 111 | styleOverrides: { 112 | badge: ({ theme, ownerState }: { theme: Theme; ownerState: BadgeProps }) => { 113 | if (ownerState.overlap === 'rectangular') { 114 | return { 115 | color: '#fff', 116 | top: -2, 117 | left: 0, 118 | border: `1px solid ${theme.palette.background.paper}`, 119 | }; 120 | } 121 | }, 122 | }, 123 | }, 124 | MuiButton: { 125 | styleOverrides: { 126 | root: ({ theme }: { theme: Theme }) => ({ 127 | fontWeight: 400, 128 | fontStyle: 'normal', 129 | fontSize: 16, 130 | textTransform: 'initial', 131 | padding: '12px 24px', 132 | borderRadius: 60, 133 | // border: 'none', 134 | '&.Mui-disabled': { 135 | color: '#fff', 136 | backgroundColor: theme.palette.grey[300], 137 | }, 138 | }), 139 | contained: ({ theme }: { theme: Theme }) => ({ 140 | backgroundColor: theme.palette.primary.main, 141 | color: 'white', 142 | boxShadow: '0px 0px 4px rgba(0, 0, 0, 0.25)', 143 | }), 144 | }, 145 | }, 146 | MuiStack: { 147 | defaultProps: { 148 | useFlexGap: true, 149 | }, 150 | /* 151 | * mui stack has no, so overrides in the variants instead 152 | * ISSUE: https://stackoverflow.com/questions/72382224/styleoverrides-not-being-applied-with-styled-components-in-mui 153 | */ 154 | variants: [ 155 | { 156 | props: {}, 157 | style: { 158 | flexWrap: 'wrap', 159 | }, 160 | }, 161 | ], 162 | }, 163 | MuiDialog: { 164 | styleOverrides: { 165 | paper: { 166 | paddingTop: 4, 167 | paddingBottom: 4, 168 | }, 169 | }, 170 | }, 171 | MuiCard: { 172 | styleOverrides: { 173 | root: ({ theme }: { theme: Theme }) => ({ 174 | [theme.breakpoints.up('sm')]: { 175 | boxShadow: 'rgba(0, 0, 0, 0.1) 0px 4px 12px', 176 | }, 177 | [theme.breakpoints.down('sm')]: { 178 | boxShadow: 'none', 179 | }, 180 | }), 181 | }, 182 | }, 183 | MuiTextField: { 184 | styleOverrides: { 185 | root: { 186 | '& .MuiFormHelperText-root': { 187 | marginLeft: 0, 188 | }, 189 | }, 190 | }, 191 | }, 192 | MuiAutocomplete: { 193 | styleOverrides: { 194 | popper: ({ theme }: { theme: Theme }) => ({ 195 | '& .MuiAutocomplete-listbox': { 196 | '& li': { 197 | fontSize: '16px !important', 198 | paddingTop: 14 + ' !important', 199 | paddingBottom: 14 + ' !important', 200 | fontFamily: theme.typography.fontFamily + ' !important', 201 | '&:hover': { 202 | fontWeight: 500, 203 | }, 204 | '&:not(:last-child)': { 205 | borderBottom: '1px solid ' + theme.palette.grey[100] + ' !important', 206 | }, 207 | }, 208 | }, 209 | }), 210 | }, 211 | }, 212 | }, 213 | }; 214 | 215 | export const websitePalette = { 216 | primary: { 217 | light: teal[50], 218 | main: '#03C9D7', 219 | dark: teal[900], 220 | contrastText: '#fff', 221 | }, 222 | secondary: { 223 | light: '#ff7961', 224 | main: '#222222', 225 | dark: '#ba000d', 226 | contrastText: '#000', 227 | }, 228 | success: { 229 | main: '#00C292', 230 | }, 231 | info: { 232 | main: '#0BB2FB', 233 | }, 234 | error: { 235 | main: '#E46A76', 236 | }, 237 | }; 238 | 239 | export const boPalette: PaletteOptions = { 240 | primary: { 241 | light: teal[50], 242 | main: '#03C9D7', 243 | dark: teal[900], 244 | contrastText: '#fff', 245 | }, 246 | ...defaultTheme.palette, 247 | }; 248 | 249 | const lightTheme = { 250 | ...defaultTheme, 251 | palette: { 252 | ...defaultTheme.palette, 253 | background: { 254 | default: '#FAFBFB', 255 | }, 256 | }, 257 | components: { 258 | ...defaultTheme.components, 259 | MuiListSubheader: { 260 | styleOverrides: { 261 | root: { color: grey[700] }, 262 | }, 263 | }, 264 | }, 265 | }; 266 | 267 | const DEFAULT_BG = '#20232A'; 268 | const darkTheme = { 269 | ...defaultTheme, 270 | palette: { 271 | ...defaultTheme.palette, 272 | background: { 273 | paper: '#282C34', 274 | default: DEFAULT_BG, 275 | }, 276 | }, 277 | components: { 278 | ...defaultTheme.components, 279 | MuiTypography: { 280 | styleOverrides: { 281 | root: textDarkColor, 282 | }, 283 | }, 284 | MuiListItemText: { 285 | styleOverrides: { 286 | primary: textDarkColor, 287 | }, 288 | }, 289 | MuiListSubheader: { 290 | styleOverrides: { 291 | root: textDarkColor, 292 | }, 293 | }, 294 | MuiFormControl: { 295 | styleOverrides: { 296 | root: { backgroundColor: 'transparent !important' }, 297 | }, 298 | }, 299 | MuiInputBase: { 300 | styleOverrides: { 301 | input: { 302 | backgroundColor: DEFAULT_BG, 303 | }, 304 | inputMultiline: { 305 | backgroundColor: DEFAULT_BG, 306 | }, 307 | root: { 308 | backgroundColor: DEFAULT_BG, 309 | }, 310 | }, 311 | }, 312 | MuiLink: { 313 | styleOverrides: { 314 | root: { color: '#fff !important' }, 315 | }, 316 | }, 317 | }, 318 | }; 319 | 320 | export const getTheme = (mode: PaletteMode = 'light'): Theme => { 321 | const defaultTheme = mode === 'light' ? lightTheme : darkTheme; 322 | 323 | // @ts-expect-error - unknown type 324 | const theme = createTheme({ 325 | ...defaultTheme, 326 | palette: { 327 | mode, 328 | primary: { 329 | light: teal[50], 330 | main: '#03C9D7', 331 | dark: teal[900], 332 | contrastText: '#fff', 333 | }, 334 | ...defaultTheme.palette, 335 | }, 336 | }); 337 | 338 | theme.typography.h6 = { 339 | [theme.breakpoints.down('md')]: { 340 | fontSize: 14, 341 | }, 342 | }; 343 | 344 | return theme; 345 | }; 346 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import stylistic from '@stylistic/eslint-plugin' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | import tseslint from 'typescript-eslint' 7 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 8 | import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions"; 9 | import eslint from '@eslint/js'; 10 | import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; 11 | import { fixupPluginRules } from "@eslint/compat"; 12 | import jsxA11y from "eslint-plugin-jsx-a11y"; 13 | 14 | export default tseslint.config( 15 | { 16 | ignores: [ 17 | "**/dist", 18 | "**/node_modules/", 19 | "**/build", 20 | "**/vite.config.ts", 21 | "**/.prettierrc.cjs", 22 | "**/example/", 23 | ] 24 | }, 25 | eslint.configs.recommended, 26 | ...tseslint.configs.recommendedTypeChecked, 27 | ...tseslint.configs.stylisticTypeChecked, 28 | { 29 | languageOptions: { 30 | globals: { 31 | ...globals.browser, 32 | Parse: "readonly", 33 | // ...globals.node, 34 | }, 35 | parserOptions: { 36 | // projectService: true, 37 | projectService: { 38 | allowDefaultProject: ['*.mjs', '*.js'], 39 | defaultProject: 'tsconfig.json', 40 | }, 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 42 | tsconfigRootDir: import.meta.dirname, 43 | }, 44 | sourceType: "module", 45 | ecmaVersion: 2023, 46 | }, 47 | }, 48 | jsxA11y.flatConfigs.recommended, 49 | { 50 | extends: [ 51 | js.configs.recommended, 52 | eslintPluginPrettierRecommended, 53 | comments.recommended, 54 | ], 55 | files: ['**/*.ts', '**/*.tsx'], 56 | plugins: { 57 | "react-hooks": fixupPluginRules(reactHooks), 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 59 | 'react-refresh': reactRefresh, 60 | "prefer-arrow-functions": preferArrowFunctions, 61 | '@stylistic': stylistic 62 | }, 63 | rules: { 64 | ...reactHooks.configs.recommended.rules, 65 | ...stylistic.configs["recommended-flat"].rules, 66 | "@typescript-eslint/no-empty-object-type": "off", 67 | 'react-refresh/only-export-components': [ 68 | 'warn', 69 | { allowConstantExport: true }, 70 | ], 71 | "prettier/prettier": ["off", { 72 | singleQuote: true, 73 | }], 74 | "@typescript-eslint/no-explicit-any": "off", 75 | "import/no-extraneous-dependencies": "off", 76 | "import/extensions": "off", 77 | "no-await-in-loop": "off", 78 | "import/no-cycle": "off", 79 | "no-plusplus": "off", 80 | "no-param-reassign": "off", 81 | "prefer-template": "off", 82 | "react/react-in-jsx-scope": "off", 83 | "no-console": "off", 84 | "import/prefer-default-export": "off", 85 | "global-require": "off", 86 | "react/require-default-props": "off", 87 | "react/jsx-props-no-spreading": "off", 88 | "jsx-a11y/label-has-associated-control": "off", 89 | "react/no-unescaped-entities": "off", 90 | "jsx-a11y/control-has-associated-label": "off", 91 | "react/function-component-definition": "off", 92 | "react/prop-types": "off", 93 | "max-len": "off", 94 | "consistent-return": "off", 95 | "react/no-array-index-key": "off", 96 | "no-restricted-syntax": "off", 97 | "arrow-body-style": "off", 98 | "prefer-arrow-callback": "off", 99 | "no-unsafe-optional-chaining": "error", 100 | "prefer-arrow-functions/prefer-arrow-functions": ["warn", { 101 | allowNamedFunctions: false, 102 | classPropertiesAllowed: false, 103 | disallowPrototype: false, 104 | returnStyle: "unchanged", 105 | singleReturnOnly: false, 106 | }], 107 | "require-await": "error", 108 | "no-use-before-define": "error", 109 | "array-bracket-spacing": ["error", "never"], 110 | "block-spacing": "error", 111 | "no-unused-vars": "off", 112 | "@eslint-community/eslint-comments/disable-enable-pair": "off", 113 | // ------------------------------------ // 114 | // ------------ @stylistic ------------ // 115 | // ------------------------------------ // 116 | "@stylistic/eol-last": "error", 117 | '@stylistic/semi': 'error', 118 | "no-useless-return": "error", 119 | "@stylistic/indent": ["error", 2], 120 | "@stylistic/keyword-spacing": ["error", { "after": true, "before": true }], 121 | "@stylistic/key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 122 | "@stylistic/lines-around-comment": ["error", { "beforeBlockComment": false }], 123 | "@stylistic/multiline-comment-style": ["error", "starred-block"], 124 | "@stylistic/multiline-ternary": "off", 125 | "@stylistic/no-extra-semi": "error", 126 | "@stylistic/no-floating-decimal": "error", 127 | "@stylistic/no-mixed-operators": "error", 128 | "@stylistic/no-mixed-spaces-and-tabs": "error", 129 | "@stylistic/no-multi-spaces": "error", 130 | "@stylistic/no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 0 }], 131 | "@stylistic/no-trailing-spaces": "error", 132 | 133 | "@stylistic/max-len": ["error", { 134 | "code": 110, 135 | // Ignore objects when enforcing line length (to avoid conflicts with object-curly-newline) 136 | "ignorePattern": "ImportDeclaration", 137 | "ignoreUrls": true, // Optionally, you can ignore long URLs 138 | "ignoreStrings": true, // Ignore long strings (optional) 139 | "ignoreComments": true, 140 | "ignoreTrailingComments": true, 141 | }], 142 | "@stylistic/object-curly-newline": ["error", { 143 | // e.g: const a = {}; 144 | "ObjectExpression": { "consistent": true, "multiline": true, "minProperties": 0 }, 145 | // "ObjectExpression": { "consistent": true, "multiline": true, "minProperties": 3 }, 146 | // e.g: const { a } = obj; 147 | "ObjectPattern": { "consistent": true, "multiline": true, "minProperties": 4 }, 148 | // e.g: import { a } from 'module'; 149 | "ImportDeclaration": { "consistent": true, "minProperties": 4 }, 150 | // e.g: export { a } from 'module'; 151 | "ExportDeclaration": { "consistent": true, "multiline": true, "minProperties": 3 } 152 | }], 153 | "@stylistic/object-curly-spacing": ["error", "always"], 154 | "@stylistic/quote-props": ["error", "as-needed"], 155 | "@stylistic/quotes": ["error", "single"], 156 | "@stylistic/rest-spread-spacing": ["error", "never"], 157 | "@stylistic/semi-spacing": "error", 158 | "@stylistic/space-before-blocks": "error", 159 | "@stylistic/space-in-parens": ["error", "never"], 160 | "@stylistic/space-unary-ops": "error", 161 | "@stylistic/spaced-comment": ["error", "always"], 162 | "@stylistic/template-curly-spacing": "error", 163 | "@stylistic/member-delimiter-style": "error", 164 | "@stylistic/padding-line-between-statements": [ 165 | "error", 166 | { blankLine: "always", prev: ["const", "let", "var"], next: "*"}, 167 | { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]} 168 | ], 169 | "@typescript-eslint/no-unused-vars": [ 170 | "error", 171 | { 172 | "args": "all", 173 | "argsIgnorePattern": "^_", 174 | "caughtErrors": "all", 175 | "caughtErrorsIgnorePattern": "^_", 176 | "destructuredArrayIgnorePattern": "^_", 177 | "varsIgnorePattern": "^_", 178 | "ignoreRestSiblings": true 179 | } 180 | ], 181 | "@stylistic/comma-dangle": ["error", { 182 | "arrays": "always-multiline", // Trailing commas for arrays with multiple lines 183 | "objects": "always-multiline", // Trailing commas for objects with multiple lines 184 | "imports": "always-multiline", // Trailing commas in multi-line import statements 185 | "exports": "always-multiline", // Trailing commas in multi-line export statements 186 | "functions": "never" // No trailing commas for function parameters 187 | }], 188 | "@stylistic/operator-linebreak": [ 189 | "error", 190 | "after", 191 | { 192 | "overrides": { 193 | "?": "before", // Example: Optional for other operators 194 | ":": "before" 195 | } 196 | } 197 | ], 198 | // ------------------------------------ // 199 | // ------------ typescript ------------ // 200 | // ------------------------------------ // 201 | "default-param-last": "off", 202 | "@typescript-eslint/default-param-last": "error", 203 | // Note: you must disable the base rule as it can report incorrect errors 204 | "max-params": "off", 205 | "@typescript-eslint/no-unsafe-assignment": "off", 206 | "@typescript-eslint/max-params": ["error", { "max": 4 }], 207 | "@typescript-eslint/method-signature-style": ["error", "property"], 208 | "@typescript-eslint/no-array-delete": "error", 209 | "@typescript-eslint/no-duplicate-enum-values": "error", 210 | "@typescript-eslint/no-duplicate-type-constituents": "error", 211 | "@typescript-eslint/no-mixed-enums": "error", 212 | "@typescript-eslint/no-require-imports": "error", 213 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": ["error", { "allowComparingNullableBooleansToTrue": false }], 214 | "@typescript-eslint/no-unnecessary-template-expression": "error", 215 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 216 | "@typescript-eslint/no-unsafe-function-type": "error", 217 | "@typescript-eslint/no-useless-empty-export": "error", 218 | // Note: you must disable the base rule as it can report incorrect errors 219 | "no-throw-literal": "off", 220 | "@typescript-eslint/only-throw-error": "error", 221 | "@typescript-eslint/prefer-find": "error", 222 | "@typescript-eslint/prefer-for-of": "error", 223 | "@typescript-eslint/prefer-optional-chain": "error", 224 | "@typescript-eslint/array-type": "error", 225 | "@typescript-eslint/explicit-function-return-type": "off", 226 | // these 2 following rules cause performance issue 227 | "@typescript-eslint/await-thenable": "off", 228 | "@typescript-eslint/no-floating-promises": "off", 229 | "@typescript-eslint/no-unsafe-call": "off", 230 | "@typescript-eslint/no-unsafe-member-access": "off", 231 | "@typescript-eslint/no-unsafe-argument": "off", 232 | "@typescript-eslint/no-unsafe-return": "off", 233 | "@typescript-eslint/no-unsafe-enum-comparison": "off", 234 | "@typescript-eslint/no-redundant-type-constituents": "off", 235 | // This rule will not work as expected if strictNullChecks is not enabled, so it is disabled by default. 236 | "@typescript-eslint/prefer-nullish-coalescing": "off", 237 | "@typescript-eslint/no-misused-promises": "off", 238 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 239 | "@typescript-eslint/consistent-type-definitions": "off", 240 | "@stylistic/type-generic-spacing": ["error"], 241 | // --------- naming-convention --------- // 242 | "camelcase": "off", 243 | "@typescript-eslint/naming-convention": [ 244 | "error", 245 | { 246 | "selector": "variable", 247 | "modifiers": ["const"], 248 | "format": ["camelCase", "UPPER_CASE", "PascalCase"] 249 | }, 250 | { 251 | "selector": "import", 252 | "format": ["camelCase", "PascalCase", "UPPER_CASE"] 253 | }, 254 | { 255 | "selector": "variable", 256 | "types": ["boolean"], 257 | "format": ["PascalCase"], 258 | // e.g: isReady, hasError, shouldFetch, ... 259 | "prefix": ["is", "should", "has", "can", "did", "will", "open"], 260 | "filter": { 261 | // exception. e.g: loadingProjects, openDialog, LOCAL 262 | "regex": "^(?:[A-Z_]+|.*loading.*|open.*)$", 263 | "match": false 264 | } 265 | }, 266 | { "selector": "typeLike", "format": ["PascalCase"] }, 267 | // e.g: IProject, IUser, IProjectData, ... 268 | { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }, 269 | { "selector": "typeAlias", "format": ["PascalCase"] }, 270 | { "selector": "enumMember", "format": ["UPPER_CASE"] }, 271 | { "selector": "class", "format": ["PascalCase"] }, 272 | { "selector": "classProperty", "format": ["camelCase", "UPPER_CASE"] }, 273 | { "selector": "classMethod", "format": ["camelCase"] }, 274 | { "selector": "parameter", "format": ["camelCase"], "leadingUnderscore": "allow" }, 275 | { "selector": "function", "format": ["camelCase"] }, 276 | { "selector": "enum", "format": ["PascalCase"] }, 277 | ], 278 | 279 | // ------------------------------------ // 280 | // ---------------- jsx --------------- // 281 | // ------------------------------------ // 282 | // Enforce closing bracket location in JSX 283 | '@stylistic/jsx-closing-bracket-location': [1, 'line-aligned'], 284 | '@stylistic/jsx-one-expression-per-line': [1, { allow: 'non-jsx' }], 285 | "@stylistic/jsx-pascal-case": [1, { allowAllCaps: false, allowNamespace: true, allowLeadingUnderscore: false }], 286 | 287 | "@stylistic/jsx-equals-spacing": ["error", "never"], 288 | 289 | "@stylistic/jsx-max-props-per-line": [ 290 | "error", 291 | { 292 | // Maximum number of props per line 293 | "maximum": { 'single': 3, 'multi': 1 }, 294 | } 295 | ], 296 | // Force new line after opening tag if multiline 297 | "@stylistic/jsx-first-prop-new-line": [2, "multiline-multiprop"], 298 | "@stylistic/jsx-props-no-multi-spaces": "error", 299 | "@stylistic/jsx-tag-spacing": ["error", { 300 | "closingSlash": "never", 301 | "beforeSelfClosing": "always", 302 | "afterOpening": "never", 303 | "beforeClosing": "never" 304 | }], 305 | /** 306 | * Sort props in JSX 307 | * NOTE: comments between props (line) is not supported 308 | * solution: name="John" // User name 309 | */ 310 | "@stylistic/jsx-sort-props": ["error", { 311 | "callbacksLast": true, 312 | "shorthandFirst": true, 313 | "shorthandLast": false, 314 | "ignoreCase": true, 315 | "reservedFirst": true, 316 | "multiline": "last", 317 | }], 318 | "@stylistic/dot-location": ["error", "property"], 319 | "@stylistic/curly-newline": ["error", { "minElements": 1 }], 320 | // Wrap multiline JSX expressions in parentheses, e.g: (
...
) 321 | "@stylistic/jsx-wrap-multilines": ["error", { 322 | "declaration": "parens-new-line", 323 | "assignment": "parens-new-line", 324 | "return": "parens-new-line", 325 | "arrow": "parens-new-line", 326 | "condition": "parens-new-line", 327 | "logical": "parens-new-line", 328 | "prop": "parens-new-line", 329 | "propertyValue": "parens-new-line" 330 | }], 331 | }, 332 | }, 333 | ) 334 | -------------------------------------------------------------------------------- /example/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import stylistic from '@stylistic/eslint-plugin' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | import tseslint from 'typescript-eslint' 7 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; 8 | import preferArrowFunctions from "eslint-plugin-prefer-arrow-functions"; 9 | import eslint from '@eslint/js'; 10 | import comments from "@eslint-community/eslint-plugin-eslint-comments/configs"; 11 | import { fixupPluginRules } from "@eslint/compat"; 12 | import jsxA11y from "eslint-plugin-jsx-a11y"; 13 | 14 | export default tseslint.config( 15 | { 16 | ignores: [ 17 | "**/dist", 18 | "**/node_modules/", 19 | "**/build", 20 | "**/vite.config.ts", 21 | "**/.prettierrc.cjs", 22 | "**/example/", 23 | ] 24 | }, 25 | eslint.configs.recommended, 26 | ...tseslint.configs.recommendedTypeChecked, 27 | ...tseslint.configs.stylisticTypeChecked, 28 | { 29 | languageOptions: { 30 | globals: { 31 | ...globals.browser, 32 | Parse: "readonly", 33 | // ...globals.node, 34 | }, 35 | parserOptions: { 36 | // projectService: true, 37 | projectService: { 38 | allowDefaultProject: ['*.mjs', '*.js'], 39 | defaultProject: 'tsconfig.json', 40 | }, 41 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 42 | tsconfigRootDir: import.meta.dirname, 43 | }, 44 | sourceType: "module", 45 | ecmaVersion: 2023, 46 | }, 47 | }, 48 | jsxA11y.flatConfigs.recommended, 49 | { 50 | extends: [ 51 | js.configs.recommended, 52 | eslintPluginPrettierRecommended, 53 | comments.recommended, 54 | ], 55 | files: ['**/*.ts', '**/*.tsx'], 56 | plugins: { 57 | "react-hooks": fixupPluginRules(reactHooks), 58 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 59 | 'react-refresh': reactRefresh, 60 | "prefer-arrow-functions": preferArrowFunctions, 61 | '@stylistic': stylistic 62 | }, 63 | rules: { 64 | ...reactHooks.configs.recommended.rules, 65 | ...stylistic.configs["recommended-flat"].rules, 66 | "@typescript-eslint/no-empty-object-type": "off", 67 | 'react-refresh/only-export-components': [ 68 | 'warn', 69 | { allowConstantExport: true }, 70 | ], 71 | "prettier/prettier": ["off", { 72 | singleQuote: true, 73 | }], 74 | "@typescript-eslint/no-explicit-any": "off", 75 | "import/no-extraneous-dependencies": "off", 76 | "import/extensions": "off", 77 | "no-await-in-loop": "off", 78 | "import/no-cycle": "off", 79 | "no-plusplus": "off", 80 | "no-param-reassign": "off", 81 | "prefer-template": "off", 82 | "react/react-in-jsx-scope": "off", 83 | "no-console": "off", 84 | "import/prefer-default-export": "off", 85 | "global-require": "off", 86 | "react/require-default-props": "off", 87 | "react/jsx-props-no-spreading": "off", 88 | "jsx-a11y/label-has-associated-control": "off", 89 | "react/no-unescaped-entities": "off", 90 | "jsx-a11y/control-has-associated-label": "off", 91 | "react/function-component-definition": "off", 92 | "react/prop-types": "off", 93 | "max-len": "off", 94 | "consistent-return": "off", 95 | "react/no-array-index-key": "off", 96 | "no-restricted-syntax": "off", 97 | "arrow-body-style": "off", 98 | "prefer-arrow-callback": "off", 99 | "no-unsafe-optional-chaining": "error", 100 | "prefer-arrow-functions/prefer-arrow-functions": ["warn", { 101 | allowNamedFunctions: false, 102 | classPropertiesAllowed: false, 103 | disallowPrototype: false, 104 | returnStyle: "unchanged", 105 | singleReturnOnly: false, 106 | }], 107 | "require-await": "error", 108 | "no-use-before-define": "error", 109 | "array-bracket-spacing": ["error", "never"], 110 | "block-spacing": "error", 111 | "no-unused-vars": "off", 112 | "@eslint-community/eslint-comments/disable-enable-pair": "off", 113 | // ------------------------------------ // 114 | // ------------ @stylistic ------------ // 115 | // ------------------------------------ // 116 | "@stylistic/eol-last": "error", 117 | '@stylistic/semi': 'error', 118 | "no-useless-return": "error", 119 | "@stylistic/indent": ["error", 2], 120 | "@stylistic/keyword-spacing": ["error", { "after": true, "before": true }], 121 | "@stylistic/key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 122 | "@stylistic/lines-around-comment": ["error", { "beforeBlockComment": false }], 123 | "@stylistic/multiline-comment-style": ["error", "starred-block"], 124 | "@stylistic/multiline-ternary": "off", 125 | "@stylistic/no-extra-semi": "error", 126 | "@stylistic/no-floating-decimal": "error", 127 | "@stylistic/no-mixed-operators": "error", 128 | "@stylistic/no-mixed-spaces-and-tabs": "error", 129 | "@stylistic/no-multi-spaces": "error", 130 | "@stylistic/no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 0 }], 131 | "@stylistic/no-trailing-spaces": "error", 132 | 133 | "@stylistic/max-len": ["error", { 134 | "code": 110, 135 | // Ignore objects when enforcing line length (to avoid conflicts with object-curly-newline) 136 | "ignorePattern": "ImportDeclaration", 137 | "ignoreUrls": true, // Optionally, you can ignore long URLs 138 | "ignoreStrings": true, // Ignore long strings (optional) 139 | "ignoreComments": true, 140 | "ignoreTrailingComments": true, 141 | }], 142 | "@stylistic/object-curly-newline": ["error", { 143 | // e.g: const a = {}; 144 | "ObjectExpression": { "consistent": true, "multiline": true, "minProperties": 0 }, 145 | // "ObjectExpression": { "consistent": true, "multiline": true, "minProperties": 3 }, 146 | // e.g: const { a } = obj; 147 | "ObjectPattern": { "consistent": true, "multiline": true, "minProperties": 4 }, 148 | // e.g: import { a } from 'module'; 149 | "ImportDeclaration": { "consistent": true, "minProperties": 4 }, 150 | // e.g: export { a } from 'module'; 151 | "ExportDeclaration": { "consistent": true, "multiline": true, "minProperties": 3 } 152 | }], 153 | "@stylistic/object-curly-spacing": ["error", "always"], 154 | "@stylistic/quote-props": ["error", "as-needed"], 155 | "@stylistic/quotes": ["error", "single"], 156 | "@stylistic/rest-spread-spacing": ["error", "never"], 157 | "@stylistic/semi-spacing": "error", 158 | "@stylistic/space-before-blocks": "error", 159 | "@stylistic/space-in-parens": ["error", "never"], 160 | "@stylistic/space-unary-ops": "error", 161 | "@stylistic/spaced-comment": ["error", "always"], 162 | "@stylistic/template-curly-spacing": "error", 163 | "@stylistic/member-delimiter-style": "error", 164 | "@stylistic/padding-line-between-statements": [ 165 | "error", 166 | { blankLine: "always", prev: ["const", "let", "var"], next: "*"}, 167 | { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]} 168 | ], 169 | "@typescript-eslint/no-unused-vars": [ 170 | "error", 171 | { 172 | "args": "all", 173 | "argsIgnorePattern": "^_", 174 | "caughtErrors": "all", 175 | "caughtErrorsIgnorePattern": "^_", 176 | "destructuredArrayIgnorePattern": "^_", 177 | "varsIgnorePattern": "^_", 178 | "ignoreRestSiblings": true 179 | } 180 | ], 181 | "@stylistic/comma-dangle": ["error", { 182 | "arrays": "always-multiline", // Trailing commas for arrays with multiple lines 183 | "objects": "always-multiline", // Trailing commas for objects with multiple lines 184 | "imports": "always-multiline", // Trailing commas in multi-line import statements 185 | "exports": "always-multiline", // Trailing commas in multi-line export statements 186 | "functions": "never" // No trailing commas for function parameters 187 | }], 188 | "@stylistic/operator-linebreak": [ 189 | "error", 190 | "after", 191 | { 192 | "overrides": { 193 | "?": "before", // Example: Optional for other operators 194 | ":": "before" 195 | } 196 | } 197 | ], 198 | // ------------------------------------ // 199 | // ------------ typescript ------------ // 200 | // ------------------------------------ // 201 | "default-param-last": "off", 202 | "@typescript-eslint/default-param-last": "error", 203 | // Note: you must disable the base rule as it can report incorrect errors 204 | "max-params": "off", 205 | "@typescript-eslint/no-unsafe-assignment": "off", 206 | "@typescript-eslint/max-params": ["error", { "max": 4 }], 207 | "@typescript-eslint/method-signature-style": ["error", "property"], 208 | "@typescript-eslint/no-array-delete": "error", 209 | "@typescript-eslint/no-duplicate-enum-values": "error", 210 | "@typescript-eslint/no-duplicate-type-constituents": "error", 211 | "@typescript-eslint/no-mixed-enums": "error", 212 | "@typescript-eslint/no-require-imports": "error", 213 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": ["error", { "allowComparingNullableBooleansToTrue": false }], 214 | "@typescript-eslint/no-unnecessary-template-expression": "error", 215 | "@typescript-eslint/no-unnecessary-type-arguments": "error", 216 | "@typescript-eslint/no-unsafe-function-type": "error", 217 | "@typescript-eslint/no-useless-empty-export": "error", 218 | // Note: you must disable the base rule as it can report incorrect errors 219 | "no-throw-literal": "off", 220 | "@typescript-eslint/only-throw-error": "error", 221 | "@typescript-eslint/prefer-find": "error", 222 | "@typescript-eslint/prefer-for-of": "error", 223 | "@typescript-eslint/prefer-optional-chain": "error", 224 | "@typescript-eslint/array-type": "error", 225 | "@typescript-eslint/explicit-function-return-type": "off", 226 | // these 2 following rules cause performance issue 227 | "@typescript-eslint/await-thenable": "off", 228 | "@typescript-eslint/no-floating-promises": "off", 229 | "@typescript-eslint/no-unsafe-call": "off", 230 | "@typescript-eslint/no-unsafe-member-access": "off", 231 | "@typescript-eslint/no-unsafe-argument": "off", 232 | "@typescript-eslint/no-unsafe-return": "off", 233 | "@typescript-eslint/no-unsafe-enum-comparison": "off", 234 | "@typescript-eslint/no-redundant-type-constituents": "off", 235 | // This rule will not work as expected if strictNullChecks is not enabled, so it is disabled by default. 236 | "@typescript-eslint/prefer-nullish-coalescing": "off", 237 | "@typescript-eslint/no-misused-promises": "off", 238 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 239 | "@typescript-eslint/consistent-type-definitions": "off", 240 | "@stylistic/type-generic-spacing": ["error"], 241 | // --------- naming-convention --------- // 242 | "camelcase": "off", 243 | "@typescript-eslint/naming-convention": [ 244 | "error", 245 | { 246 | "selector": "variable", 247 | "modifiers": ["const"], 248 | "format": ["camelCase", "UPPER_CASE", "PascalCase"] 249 | }, 250 | { 251 | "selector": "import", 252 | "format": ["camelCase", "PascalCase", "UPPER_CASE"] 253 | }, 254 | { 255 | "selector": "variable", 256 | "types": ["boolean"], 257 | "format": ["PascalCase"], 258 | // e.g: isReady, hasError, shouldFetch, ... 259 | "prefix": ["is", "should", "has", "can", "did", "will", "open"], 260 | "filter": { 261 | // exception. e.g: loadingProjects, openDialog, LOCAL 262 | "regex": "^(?:[A-Z_]+|.*loading.*|open.*)$", 263 | "match": false 264 | } 265 | }, 266 | { "selector": "typeLike", "format": ["PascalCase"] }, 267 | // e.g: IProject, IUser, IProjectData, ... 268 | { "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }, 269 | { "selector": "typeAlias", "format": ["PascalCase"] }, 270 | { "selector": "enumMember", "format": ["UPPER_CASE"] }, 271 | { "selector": "class", "format": ["PascalCase"] }, 272 | { "selector": "classProperty", "format": ["camelCase", "UPPER_CASE"] }, 273 | { "selector": "classMethod", "format": ["camelCase"] }, 274 | { "selector": "parameter", "format": ["camelCase"], "leadingUnderscore": "allow" }, 275 | { "selector": "function", "format": ["camelCase"] }, 276 | { "selector": "enum", "format": ["PascalCase"] }, 277 | ], 278 | 279 | // ------------------------------------ // 280 | // ---------------- jsx --------------- // 281 | // ------------------------------------ // 282 | // Enforce closing bracket location in JSX 283 | '@stylistic/jsx-closing-bracket-location': [1, 'line-aligned'], 284 | '@stylistic/jsx-one-expression-per-line': [1, { allow: 'non-jsx' }], 285 | "@stylistic/jsx-pascal-case": [1, { allowAllCaps: false, allowNamespace: true, allowLeadingUnderscore: false }], 286 | 287 | "@stylistic/jsx-equals-spacing": ["error", "never"], 288 | 289 | "@stylistic/jsx-max-props-per-line": [ 290 | "error", 291 | { 292 | // Maximum number of props per line 293 | "maximum": { 'single': 3, 'multi': 1 }, 294 | } 295 | ], 296 | // Force new line after opening tag if multiline 297 | "@stylistic/jsx-first-prop-new-line": [2, "multiline-multiprop"], 298 | "@stylistic/jsx-props-no-multi-spaces": "error", 299 | "@stylistic/jsx-tag-spacing": ["error", { 300 | "closingSlash": "never", 301 | "beforeSelfClosing": "always", 302 | "afterOpening": "never", 303 | "beforeClosing": "never" 304 | }], 305 | /** 306 | * Sort props in JSX 307 | * NOTE: comments between props (line) is not supported 308 | * solution: name="John" // User name 309 | */ 310 | "@stylistic/jsx-sort-props": ["error", { 311 | "callbacksLast": true, 312 | "shorthandFirst": true, 313 | "shorthandLast": false, 314 | "ignoreCase": true, 315 | "reservedFirst": true, 316 | "multiline": "last", 317 | }], 318 | "@stylistic/dot-location": ["error", "property"], 319 | "@stylistic/curly-newline": ["error", { "minElements": 1 }], 320 | // Wrap multiline JSX expressions in parentheses, e.g: (
...
) 321 | "@stylistic/jsx-wrap-multilines": ["error", { 322 | "declaration": "parens-new-line", 323 | "assignment": "parens-new-line", 324 | "return": "parens-new-line", 325 | "arrow": "parens-new-line", 326 | "condition": "parens-new-line", 327 | "logical": "parens-new-line", 328 | "prop": "parens-new-line", 329 | "propertyValue": "parens-new-line" 330 | }], 331 | }, 332 | }, 333 | ) 334 | --------------------------------------------------------------------------------