feat(MarkdownEditor): enhance Markdown rendering with new plugins and components

- Integrated rehype-raw and rehype-slug for improved Markdown processing.
- Added remark-toc for automatic table of contents generation.
- Refactored Markdown components for better styling and functionality.
- Updated package.json to include new dependencies for enhanced Markdown features.
This commit is contained in:
Julien Froidefond
2025-10-24 09:49:56 +02:00
parent b60e74b1ff
commit f7f77a49dc
3 changed files with 467 additions and 353 deletions

View File

@@ -40,14 +40,17 @@
"mermaid": "^11.12.0",
"next": "15.5.3",
"next-auth": "^4.24.11",
"prism-react-renderer": "^2.4.1",
"prisma": "^6.16.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^3.2.1",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-toc": "^9.0.0",
"tailwind-merge": "^3.3.1",
"twemoji": "^14.0.2"
},

217
pnpm-lock.yaml generated
View File

@@ -53,6 +53,9 @@ importers:
next-auth:
specifier: ^4.24.11
version: 4.24.11(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
prism-react-renderer:
specifier: ^2.4.1
version: 2.4.1(react@19.1.0)
prisma:
specifier: ^6.16.1
version: 6.17.1(typescript@5.9.3)
@@ -68,15 +71,21 @@ importers:
recharts:
specifier: ^3.2.1
version: 3.2.1(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react-is@16.13.1)(react@19.1.0)(redux@5.0.1)
rehype-highlight:
specifier: ^7.0.2
version: 7.0.2
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
rehype-sanitize:
specifier: ^6.0.0
version: 6.0.0
rehype-slug:
specifier: ^6.0.0
version: 6.0.0
remark-gfm:
specifier: ^4.0.1
version: 4.0.1
remark-toc:
specifier: ^9.0.0
version: 9.0.0
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
@@ -1016,6 +1025,9 @@ packages:
'@types/node@20.19.21':
resolution: {integrity: sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/react-dom@19.2.2':
resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==}
peerDependencies:
@@ -1027,6 +1039,9 @@ packages:
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@types/ungap__structured-clone@1.2.0':
resolution: {integrity: sha512-ZoaihZNLeZSxESbk9PUAPZOlSpcKx81I1+4emtULDVmBLkYutTcMlCj2K9VNlf9EWODxdO6gkAqEaLorXwZQVA==}
'@types/unist@2.0.11':
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
@@ -1709,6 +1724,10 @@ packages:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
@@ -2007,6 +2026,9 @@ packages:
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
hasBin: true
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -2067,8 +2089,17 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hast-util-is-element@3.0.0:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
hast-util-raw@9.1.0:
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
hast-util-sanitize@5.0.2:
resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==}
@@ -2076,19 +2107,24 @@ packages:
hast-util-to-jsx-runtime@2.3.6:
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
hast-util-to-text@4.0.2:
resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
hast-util-to-parse5@8.0.0:
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
hast-util-to-string@3.0.1:
resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
hast-util-whitespace@3.0.0:
resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
highlight.js@11.11.1:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hastscript@9.0.1:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
human-signals@5.0.0:
resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
engines: {node: '>=16.17.0'}
@@ -2457,9 +2493,6 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
lowlight@3.3.0:
resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@@ -2529,6 +2562,9 @@ packages:
mdast-util-to-string@4.0.0:
resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
mdast-util-toc@7.1.0:
resolution: {integrity: sha512-2TVKotOQzqdY7THOdn2gGzS9d1Sdd66bvxUyw3aNpWfcPXCLYSJCCgfPy30sEtuzkDraJgqF35dzgmz6xlvH/w==}
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -2805,6 +2841,9 @@ packages:
parse-entities@4.0.2:
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
path-data-parser@0.1.0:
resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==}
@@ -2889,6 +2928,11 @@ packages:
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
prism-react-renderer@2.4.1:
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
peerDependencies:
react: '>=16.0.0'
prisma@6.17.1:
resolution: {integrity: sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==}
engines: {node: '>=18.18'}
@@ -2902,6 +2946,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
@@ -2979,12 +3026,15 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'}
rehype-highlight@7.0.2:
resolution: {integrity: sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==}
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
rehype-sanitize@6.0.0:
resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==}
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -2997,6 +3047,9 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
remark-toc@9.0.0:
resolution: {integrity: sha512-KJ9txbo33GjDAV1baHFze7ij4G8c7SGYoY8Kzsm2gzFpbhL/bSoVpMMzGa3vrNDSWASNd/3ppAqL7cP2zD6JIA==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@@ -3319,9 +3372,6 @@ packages:
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
unist-util-find-after@5.0.0:
resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
@@ -3360,6 +3410,9 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -3393,6 +3446,9 @@ packages:
resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==}
engines: {node: 20 || >=22}
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -4194,6 +4250,8 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/prismjs@1.26.5': {}
'@types/react-dom@19.2.2(@types/react@19.2.2)':
dependencies:
'@types/react': 19.2.2
@@ -4205,6 +4263,8 @@ snapshots:
'@types/trusted-types@2.0.7':
optional: true
'@types/ungap__structured-clone@1.2.0': {}
'@types/unist@2.0.11': {}
'@types/unist@3.0.3': {}
@@ -4912,6 +4972,8 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
entities@6.0.1: {}
environment@1.1.0: {}
es-abstract@1.24.0:
@@ -5398,6 +5460,8 @@ snapshots:
nypm: 0.6.2
pathe: 2.0.3
github-slugger@2.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -5445,9 +5509,40 @@ snapshots:
dependencies:
function-bind: 1.1.2
hast-util-is-element@3.0.0:
hast-util-from-parse5@8.0.3:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
devlop: 1.1.0
hastscript: 9.0.1
property-information: 7.1.0
vfile: 6.0.3
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-heading-rank@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw@9.1.0:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
'@ungap/structured-clone': 1.3.0
hast-util-from-parse5: 8.0.3
hast-util-to-parse5: 8.0.0
html-void-elements: 3.0.0
mdast-util-to-hast: 13.2.0
parse5: 7.3.0
unist-util-position: 5.0.0
unist-util-visit: 5.0.0
vfile: 6.0.3
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-sanitize@5.0.2:
dependencies:
@@ -5475,21 +5570,36 @@ snapshots:
transitivePeerDependencies:
- supports-color
hast-util-to-text@4.0.2:
hast-util-to-parse5@8.0.0:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
devlop: 1.1.0
property-information: 6.5.0
space-separated-tokens: 2.0.2
web-namespaces: 2.0.1
zwitch: 2.0.4
hast-util-to-string@3.0.1:
dependencies:
'@types/hast': 3.0.4
'@types/unist': 3.0.3
hast-util-is-element: 3.0.0
unist-util-find-after: 5.0.0
hast-util-whitespace@3.0.0:
dependencies:
'@types/hast': 3.0.4
highlight.js@11.11.1: {}
hastscript@9.0.1:
dependencies:
'@types/hast': 3.0.4
comma-separated-tokens: 2.0.3
hast-util-parse-selector: 4.0.0
property-information: 7.1.0
space-separated-tokens: 2.0.2
html-url-attributes@3.0.1: {}
html-void-elements@3.0.0: {}
human-signals@5.0.0: {}
husky@9.1.7: {}
@@ -5857,12 +5967,6 @@ snapshots:
dependencies:
js-tokens: 4.0.0
lowlight@3.3.0:
dependencies:
'@types/hast': 3.0.4
devlop: 1.1.0
highlight.js: 11.11.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
@@ -6034,6 +6138,16 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
mdast-util-toc@7.1.0:
dependencies:
'@types/mdast': 4.0.4
'@types/ungap__structured-clone': 1.2.0
'@ungap/structured-clone': 1.3.0
github-slugger: 2.0.0
mdast-util-to-string: 4.0.0
unist-util-is: 6.0.0
unist-util-visit: 5.0.0
merge-stream@2.0.0: {}
merge2@1.4.1: {}
@@ -6472,6 +6586,10 @@ snapshots:
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
parse5@7.3.0:
dependencies:
entities: 6.0.1
path-data-parser@0.1.0: {}
path-exists@4.0.0: {}
@@ -6540,6 +6658,12 @@ snapshots:
pretty-format@3.8.0: {}
prism-react-renderer@2.4.1(react@19.1.0):
dependencies:
'@types/prismjs': 1.26.5
clsx: 2.1.1
react: 19.1.0
prisma@6.17.1(typescript@5.9.3):
dependencies:
'@prisma/config': 6.17.1
@@ -6555,6 +6679,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
property-information@6.5.0: {}
property-information@7.1.0: {}
punycode@2.3.1: {}
@@ -6654,12 +6780,10 @@ snapshots:
gopd: 1.2.0
set-function-name: 2.0.2
rehype-highlight@7.0.2:
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-to-text: 4.0.2
lowlight: 3.3.0
unist-util-visit: 5.0.0
hast-util-raw: 9.1.0
vfile: 6.0.3
rehype-sanitize@6.0.0:
@@ -6667,6 +6791,14 @@ snapshots:
'@types/hast': 3.0.4
hast-util-sanitize: 5.0.2
rehype-slug@6.0.0:
dependencies:
'@types/hast': 3.0.4
github-slugger: 2.0.0
hast-util-heading-rank: 3.0.0
hast-util-to-string: 3.0.1
unist-util-visit: 5.0.0
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
@@ -6701,6 +6833,11 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
remark-toc@9.0.0:
dependencies:
'@types/mdast': 4.0.4
mdast-util-toc: 7.1.0
reselect@5.1.1: {}
resolve-from@4.0.0: {}
@@ -7099,11 +7236,6 @@ snapshots:
trough: 2.2.0
vfile: 6.0.3
unist-util-find-after@5.0.0:
dependencies:
'@types/unist': 3.0.3
unist-util-is: 6.0.0
unist-util-is@6.0.0:
dependencies:
'@types/unist': 3.0.3
@@ -7165,6 +7297,11 @@ snapshots:
uuid@8.3.2: {}
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3
vfile: 6.0.3
vfile-message@4.0.3:
dependencies:
'@types/unist': 3.0.3
@@ -7211,6 +7348,8 @@ snapshots:
walk-up-path@4.0.0: {}
web-namespaces@2.0.1: {}
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0

View File

@@ -4,14 +4,274 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import remarkToc from 'remark-toc';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import rehypeSanitize from 'rehype-sanitize';
import { Highlight, themes } from 'prism-react-renderer';
import { Eye, EyeOff, Edit3, X, CheckSquare2 } from 'lucide-react';
import { TagInput } from '@/components/ui/TagInput';
import { TagDisplay } from '@/components/ui/TagDisplay';
import { TaskSelectorWithData } from '@/components/shared/TaskSelectorWithData';
import { MermaidRenderer } from '@/components/ui/MermaidRenderer';
import { Tag, Task } from '@/lib/types';
import type { Components } from 'react-markdown';
// Fonction pour générer les composants Markdown réutilisables
const createMarkdownComponents = (
isPreviewMode: boolean
): Partial<Components> => ({
// Titres avec tailles différentes selon le mode
h1: ({ children }: { children?: React.ReactNode }) => (
<h1
className={
isPreviewMode
? 'text-4xl font-bold text-[var(--foreground)] mb-8 mt-10 first:mt-0 bg-gradient-to-r from-[var(--primary)] to-[var(--accent)] bg-clip-text text-transparent'
: 'text-4xl font-bold text-[var(--foreground)] mb-8 mt-10 first:mt-0 bg-gradient-to-r from-[var(--primary)] to-[var(--accent)] bg-clip-text text-transparent'
}
>
{children}
</h1>
),
h2: ({ children }: { children?: React.ReactNode }) => (
<h2
className={
isPreviewMode
? 'text-3xl font-bold text-[var(--foreground)] mb-6 mt-8 border-b border-[var(--border)]/30 pb-2'
: 'text-3xl font-bold text-[var(--foreground)] mb-6 mt-8 border-b border-[var(--border)]/30 pb-2'
}
>
{children}
</h2>
),
h3: ({ children }: { children?: React.ReactNode }) => (
<h3
className={
isPreviewMode
? 'text-2xl font-semibold text-[var(--foreground)] mb-4 mt-6'
: 'text-2xl font-semibold text-[var(--foreground)] mb-4 mt-6'
}
>
{children}
</h3>
),
h4: ({ children }: { children?: React.ReactNode }) => (
<h4
className={
isPreviewMode
? 'text-xl font-semibold text-[var(--foreground)] mb-3 mt-5'
: 'text-xl font-semibold text-[var(--foreground)] mb-3 mt-5'
}
>
{children}
</h4>
),
h5: ({ children }: { children?: React.ReactNode }) => (
<h5
className={
isPreviewMode
? 'text-lg font-semibold text-[var(--foreground)] mb-2 mt-4'
: 'text-lg font-semibold text-[var(--foreground)] mb-2 mt-4'
}
>
{children}
</h5>
),
h6: ({ children }: { children?: React.ReactNode }) => (
<h6
className={
isPreviewMode
? 'text-base font-semibold text-[var(--foreground)] mb-2 mt-3 text-[var(--muted-foreground)]'
: 'text-base font-semibold text-[var(--foreground)] mb-2 mt-3 text-[var(--muted-foreground)]'
}
>
{children}
</h6>
),
p: ({ children }: { children?: React.ReactNode }) => (
<p className="text-[var(--foreground)] mb-4 leading-relaxed">{children}</p>
),
// Listes avec le même style partout
ul: ({ children }: { children?: React.ReactNode }) => (
<ul className="mb-0 pl-4">{children}</ul>
),
ol: ({ children }: { children?: React.ReactNode }) => (
<ol className="mb-0 pl-4 list-decimal list-inside">{children}</ol>
),
li: ({
children,
className,
}: {
children?: React.ReactNode;
className?: string;
}) => {
const isTaskListItem = className?.includes('task-list-item');
if (isTaskListItem) {
return (
<li className="!m-0 py-0.5 text-[var(--foreground)]">{children}</li>
);
}
return (
<li className="!m-0 py-0.5 text-[var(--foreground)] flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-[var(--primary)] rounded-full mt-2 flex-shrink-0"></span>
<span>{children}</span>
</li>
);
},
blockquote: ({ children }: { children?: React.ReactNode }) => (
<blockquote className="border-l-4 border-[var(--primary)] pl-4 py-2 my-4 bg-[var(--card)]/20 backdrop-blur-sm rounded-r-lg">
<div className="text-[var(--muted-foreground)] italic">{children}</div>
</blockquote>
),
code: (({
inline,
className,
children,
...props
}: {
inline?: boolean;
className?: string;
children?: React.ReactNode;
} & Record<string, unknown>) => {
if (inline) {
return (
<code className="bg-[var(--card)]/60 px-2 py-1 rounded text-[var(--accent)] font-mono text-sm border border-[var(--border)]/40">
{children}
</code>
);
}
return (
<code className={className} {...props}>
{children}
</code>
);
}) as Components['code'],
pre: ({ children }: { children?: React.ReactNode }) => {
let codeElement: string | null = null;
let className = '';
if (React.isValidElement(children)) {
const props = children.props as {
children?: string;
className?: string;
};
codeElement = props?.children || null;
className = props?.className || '';
}
const isMermaid =
className.includes('language-mermaid') ||
(typeof codeElement === 'string' &&
(codeElement.trim().startsWith('graph') ||
codeElement.trim().startsWith('flowchart') ||
codeElement.trim().startsWith('sequenceDiagram') ||
codeElement.trim().startsWith('gantt') ||
codeElement.trim().startsWith('pie') ||
codeElement.trim().startsWith('gitgraph') ||
codeElement.trim().startsWith('journey') ||
codeElement.trim().startsWith('stateDiagram') ||
codeElement.trim().startsWith('classDiagram') ||
codeElement.trim().startsWith('erDiagram') ||
codeElement.trim().startsWith('mindmap') ||
codeElement.trim().startsWith('timeline')));
if (isMermaid && typeof codeElement === 'string') {
return (
<div className="my-6">
<MermaidRenderer chart={codeElement} />
</div>
);
}
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : 'javascript';
return (
<Highlight
theme={themes.vsDark}
code={codeElement || ''}
language={language}
>
{({
className: highlightClassName,
style,
tokens,
getLineProps,
getTokenProps,
}) => {
// Supprimer les lignes vides à la fin
const filteredTokens = tokens.filter((line, i) => {
// Garder toutes les lignes sauf la dernière si elle est vide
if (i === tokens.length - 1) {
return (
line.length > 0 &&
line.some((token) => token.content.trim() !== '')
);
}
return true;
});
return (
<pre
className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm font-mono text-sm mb-6"
style={{
...style,
background: 'color-mix(in srgb, var(--card) 80%, transparent)',
}}
>
<code className={highlightClassName}>
{filteredTokens.map((line, i) => (
<div key={i} {...getLineProps({ line })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token })} />
))}
</div>
))}
</code>
</pre>
);
}}
</Highlight>
);
},
a: ({ children, href }: { children?: React.ReactNode; href?: string }) => (
<a
href={href}
className="text-[var(--primary)] hover:text-[var(--primary)]/80 underline decoration-[var(--primary)]/50 hover:decoration-[var(--primary)] transition-colors"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
table: ({ children }: { children?: React.ReactNode }) => (
<div className="overflow-x-auto my-6">
<table className="min-w-full border border-[var(--border)]/60 rounded-lg overflow-hidden">
{children}
</table>
</div>
),
thead: ({ children }: { children?: React.ReactNode }) => (
<thead className="bg-[var(--card)]/40">{children}</thead>
),
tbody: ({ children }: { children?: React.ReactNode }) => (
<tbody className="divide-y divide-[var(--border)]/60">{children}</tbody>
),
tr: ({ children }: { children?: React.ReactNode }) => (
<tr className="hover:bg-[var(--card)]/20 transition-colors">{children}</tr>
),
th: ({ children }: { children?: React.ReactNode }) => (
<th className="px-4 py-2 text-left text-[var(--foreground)] font-semibold border-b border-[var(--border)]/60">
{children}
</th>
),
td: ({ children }: { children?: React.ReactNode }) => (
<td className="px-4 py-2 text-[var(--foreground)]">{children}</td>
),
hr: () => <hr className="my-8 border-t-2 border-[var(--border)]/30" />,
});
interface MarkdownEditorProps {
value: string;
@@ -444,161 +704,18 @@ export function MarkdownEditor({
<div className="flex-1 overflow-auto p-6 bg-[var(--background)]/50 backdrop-blur-sm">
<div className="prose prose-sm max-w-none prose-headings:text-[var(--foreground)] prose-p:text-[var(--foreground)] prose-strong:text-[var(--foreground)] prose-strong:font-bold prose-em:text-[var(--muted-foreground)] prose-code:text-[var(--accent)] prose-pre:bg-[var(--card)]/60 prose-pre:border prose-pre:border-[var(--border)]/60 prose-blockquote:border-[var(--primary)] prose-blockquote:text-[var(--muted-foreground)] prose-a:text-[var(--primary)] prose-table:border-[var(--border)]/60 prose-th:bg-[var(--card)]/40 prose-th:text-[var(--foreground)] prose-td:text-[var(--foreground)]">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight, rehypeSanitize]}
components={{
// Custom styling for better integration
h1: ({ children }) => (
<h1 className="text-3xl font-bold text-[var(--foreground)] mb-6 mt-8 first:mt-0 bg-gradient-to-r from-[var(--primary)] to-[var(--accent)] bg-clip-text text-transparent">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-2xl font-bold text-[var(--foreground)] mb-4 mt-6 border-b border-[var(--border)]/30 pb-2">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-xl font-semibold text-[var(--foreground)] mb-3 mt-5 flex items-center gap-2">
<span className="w-2 h-2 bg-[var(--primary)] rounded-full"></span>
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-lg font-semibold text-[var(--foreground)] mb-2 mt-4">
{children}
</h4>
),
h5: ({ children }) => (
<h5 className="text-base font-semibold text-[var(--foreground)] mb-2 mt-3">
{children}
</h5>
),
h6: ({ children }) => (
<h6 className="text-sm font-semibold text-[var(--foreground)] mb-2 mt-3 text-[var(--muted-foreground)]">
{children}
</h6>
),
p: ({ children }) => (
<p className="text-[var(--foreground)] mb-4 leading-relaxed">
{children}
</p>
),
ul: ({ children }) => (
<ul className="mb-4 space-y-2">{children}</ul>
),
ol: ({ children }) => (
<ol className="mb-4 space-y-2 list-decimal list-inside">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-[var(--foreground)] flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-[var(--primary)] rounded-full mt-2 flex-shrink-0"></span>
<span>{children}</span>
</li>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-[var(--primary)] pl-4 py-2 my-4 bg-[var(--card)]/20 backdrop-blur-sm rounded-r-lg">
<div className="text-[var(--muted-foreground)] italic">
{children}
</div>
</blockquote>
),
code: ({ children, className }) => {
const isInline = !className;
if (isInline) {
return (
<code className="bg-[var(--card)]/60 px-2 py-1 rounded text-[var(--accent)] font-mono text-sm border border-[var(--border)]/40">
{children}
</code>
);
}
return <code className={className}>{children}</code>;
},
pre: ({ children }) => {
// Check if this is a mermaid code block
let codeElement: string | null = null;
let className = '';
if (React.isValidElement(children)) {
const props = children.props as {
children?: string;
className?: string;
};
codeElement = props?.children || null;
className = props?.className || '';
}
const isMermaid =
className.includes('language-mermaid') ||
(typeof codeElement === 'string' &&
(codeElement.trim().startsWith('graph') ||
codeElement.trim().startsWith('flowchart') ||
codeElement.trim().startsWith('sequenceDiagram') ||
codeElement.trim().startsWith('gantt') ||
codeElement.trim().startsWith('pie') ||
codeElement.trim().startsWith('gitgraph') ||
codeElement.trim().startsWith('journey') ||
codeElement.trim().startsWith('stateDiagram') ||
codeElement.trim().startsWith('classDiagram') ||
codeElement.trim().startsWith('erDiagram') ||
codeElement.trim().startsWith('mindmap') ||
codeElement.trim().startsWith('timeline')));
if (isMermaid && typeof codeElement === 'string') {
return (
<div className="my-6">
<MermaidRenderer chart={codeElement} />
</div>
);
}
return (
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
{children}
</pre>
);
},
a: ({ children, href }) => (
<a
href={href}
className="text-[var(--primary)] hover:text-[var(--primary)]/80 underline decoration-[var(--primary)]/50 hover:decoration-[var(--primary)] transition-colors"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
table: ({ children }) => (
<div className="overflow-x-auto mb-6 rounded-lg border border-[var(--border)]/60">
<table className="min-w-full">{children}</table>
</div>
),
th: ({ children }) => (
<th className="border-b border-[var(--border)]/60 bg-[var(--card)]/40 px-4 py-3 text-left font-semibold text-[var(--foreground)] backdrop-blur-sm">
{children}
</th>
),
td: ({ children }) => (
<td className="border-b border-[var(--border)]/30 px-4 py-3 text-[var(--foreground)]">
{children}
</td>
),
hr: () => (
<hr className="my-8 border-0 h-px bg-gradient-to-r from-transparent via-[var(--border)] to-transparent" />
),
strong: ({ children }) => (
<strong className="font-bold text-[var(--foreground)]">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-[var(--muted-foreground)]">
{children}
</em>
),
}}
remarkPlugins={[
remarkGfm,
[
remarkToc,
{
heading: '(table[ -]of[ -])?contents?|toc|sommaire',
tight: true,
},
],
]}
rehypePlugins={[rehypeRaw, rehypeSlug, rehypeSanitize]}
components={createMarkdownComponents(true)}
>
{value || "*Commencez à écrire pour voir l'aperçu...*"}
</ReactMarkdown>
@@ -670,163 +787,18 @@ export function MarkdownEditor({
<div className="flex-1 overflow-auto p-6 bg-[var(--background)]/50 backdrop-blur-sm">
<div className="prose prose-sm max-w-none prose-headings:text-[var(--foreground)] prose-p:text-[var(--foreground)] prose-strong:text-[var(--foreground)] prose-strong:font-bold prose-em:text-[var(--muted-foreground)] prose-code:text-[var(--accent)] prose-pre:bg-[var(--card)]/60 prose-pre:border prose-pre:border-[var(--border)]/60 prose-blockquote:border-[var(--primary)] prose-blockquote:text-[var(--muted-foreground)] prose-a:text-[var(--primary)] prose-table:border-[var(--border)]/60 prose-th:bg-[var(--card)]/40 prose-th:text-[var(--foreground)] prose-td:text-[var(--foreground)]">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight, rehypeSanitize]}
components={{
// Custom styling for better integration
h1: ({ children }) => (
<h1 className="text-3xl font-bold text-[var(--foreground)] mb-6 mt-8 first:mt-0 bg-gradient-to-r from-[var(--primary)] to-[var(--accent)] bg-clip-text text-transparent">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-2xl font-bold text-[var(--foreground)] mb-4 mt-6 border-b border-[var(--border)]/30 pb-2">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-xl font-semibold text-[var(--foreground)] mb-3 mt-5 flex items-center gap-2">
<span className="w-2 h-2 bg-[var(--primary)] rounded-full"></span>
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="text-lg font-semibold text-[var(--foreground)] mb-2 mt-4">
{children}
</h4>
),
h5: ({ children }) => (
<h5 className="text-base font-semibold text-[var(--foreground)] mb-2 mt-3">
{children}
</h5>
),
h6: ({ children }) => (
<h6 className="text-sm font-semibold text-[var(--foreground)] mb-2 mt-3 text-[var(--muted-foreground)]">
{children}
</h6>
),
p: ({ children }) => (
<p className="text-[var(--foreground)] mb-4 leading-relaxed">
{children}
</p>
),
ul: ({ children }) => (
<ul className="mb-4 space-y-2">{children}</ul>
),
ol: ({ children }) => (
<ol className="mb-4 space-y-2 list-decimal list-inside">
{children}
</ol>
),
li: ({ children }) => (
<li className="text-[var(--foreground)] flex items-start gap-2">
<span className="w-1.5 h-1.5 bg-[var(--primary)] rounded-full mt-2 flex-shrink-0"></span>
<span>{children}</span>
</li>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-[var(--primary)] pl-4 py-2 my-4 bg-[var(--card)]/20 backdrop-blur-sm rounded-r-lg">
<div className="text-[var(--muted-foreground)] italic">
{children}
</div>
</blockquote>
),
code: ({ children, className }) => {
const isInline = !className;
if (isInline) {
return (
<code className="bg-[var(--card)]/60 px-2 py-1 rounded text-[var(--accent)] font-mono text-sm border border-[var(--border)]/40">
{children}
</code>
);
}
return <code className={className}>{children}</code>;
},
pre: ({ children }) => {
// Check if this is a mermaid code block
let codeElement: string | null = null;
let className = '';
if (React.isValidElement(children)) {
const props = children.props as {
children?: string;
className?: string;
};
codeElement = props?.children || null;
className = props?.className || '';
}
const isMermaid =
className.includes('language-mermaid') ||
(typeof codeElement === 'string' &&
(codeElement.trim().startsWith('graph') ||
codeElement.trim().startsWith('flowchart') ||
codeElement
.trim()
.startsWith('sequenceDiagram') ||
codeElement.trim().startsWith('gantt') ||
codeElement.trim().startsWith('pie') ||
codeElement.trim().startsWith('gitgraph') ||
codeElement.trim().startsWith('journey') ||
codeElement.trim().startsWith('stateDiagram') ||
codeElement.trim().startsWith('classDiagram') ||
codeElement.trim().startsWith('erDiagram') ||
codeElement.trim().startsWith('mindmap') ||
codeElement.trim().startsWith('timeline')));
if (isMermaid && typeof codeElement === 'string') {
return (
<div className="my-6">
<MermaidRenderer chart={codeElement} />
</div>
);
}
return (
<pre className="bg-[var(--card)]/60 border border-[var(--border)]/60 rounded-lg p-4 overflow-x-auto backdrop-blur-sm">
{children}
</pre>
);
},
a: ({ children, href }) => (
<a
href={href}
className="text-[var(--primary)] hover:text-[var(--primary)]/80 underline decoration-[var(--primary)]/50 hover:decoration-[var(--primary)] transition-colors"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
table: ({ children }) => (
<div className="overflow-x-auto mb-6 rounded-lg border border-[var(--border)]/60">
<table className="min-w-full">{children}</table>
</div>
),
th: ({ children }) => (
<th className="border-b border-[var(--border)]/60 bg-[var(--card)]/40 px-4 py-3 text-left font-semibold text-[var(--foreground)] backdrop-blur-sm">
{children}
</th>
),
td: ({ children }) => (
<td className="border-b border-[var(--border)]/30 px-4 py-3 text-[var(--foreground)]">
{children}
</td>
),
hr: () => (
<hr className="my-8 border-0 h-px bg-gradient-to-r from-transparent via-[var(--border)] to-transparent" />
),
strong: ({ children }) => (
<strong className="font-bold text-[var(--foreground)]">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-[var(--muted-foreground)]">
{children}
</em>
),
}}
remarkPlugins={[
remarkGfm,
[
remarkToc,
{
heading: '(table[ -]of[ -])?contents?|toc|sommaire',
tight: true,
},
],
]}
rehypePlugins={[rehypeRaw, rehypeSlug, rehypeSanitize]}
components={createMarkdownComponents(false)}
>
{value || "*Commencez à écrire pour voir l'aperçu...*"}
</ReactMarkdown>