Remplace react-markdown par markdown-to-jsx

La version utilisée de react-markdown n'était pas compatible avec
ViteJS. J'ai tenté la mise à jour vers la v7 qui est publiée sous forme
de ES Module, ce qui nécessitait d'intégrer plusieurs changements d'API.
En m'y attelant j'ai réalisé que la motivation première de
react-markdown était de ne surtout pas utiliser
`dangerouslySetInnerHTML`, ce qui est utile pour les cas d'usages où le
markdown n'est pas digne de confiance (message d'utilisateurs par
exemple). Cette contrainte oblige à alourdir sensiblement la quantité de
JavaScript à charger et à évaluer.

Anisi dans certains markdown que l'on affiche, on utilise la balise HTML
`<sup>`, qui n'est pas parsée nativement pas react-markdown. Comme on ne
peut pas faire de `dangerouslySetInnerHTML` il faut intégrer un parseur
HTML complet qui rajout 60kb, juste pour quelques occurences de `<sup>`
dans les pages nouveautés.

Dans notre cas d'usage reparser tout le html en Javascript, n'est pas
utile. markdown-to-jsx semble plus adapté et beaucoup plus léger. Par
ailleurs le paquet est 5 fois plus utilisé que react-markdown :
https://www.npmtrends.com/react-markdown-vs-markdown-to-jsx
pull/1967/head
Maxime Quandalle 2021-12-29 14:46:14 +01:00 committed by Maxime Quandalle
parent e6e2ec2c9e
commit 9ad8b0f186
16 changed files with 65 additions and 67 deletions

View File

@ -63,6 +63,7 @@
"algoliasearch": "^4.10.2",
"fuse.js": "^6.4.6",
"iframe-resizer": "^4.1.1",
"markdown-to-jsx": "^7.1.5",
"modele-social": "^0.5.0",
"publicodes": "^1.0.0-beta.25",
"publicodes-react": "^1.0.0-beta.25",
@ -75,7 +76,6 @@
"react-i18next": "^11.0.0",
"react-instantsearch": "^6.11.2",
"react-instantsearch-dom": "^6.11.2",
"react-markdown": "^4.1.0",
"react-redux": "^7.0.3",
"react-router-dom": "^5.1.1",
"react-signature-pad-wrapper": "^1.2.11",

View File

@ -130,7 +130,7 @@ function ActivitéMixte() {
</Checkbox>
</Trans>
<ButtonHelp type="aide" title={rule.title} light>
<Markdown source={rule.rawNode.description} />
<Markdown>{rule.rawNode.description ?? ''}</Markdown>
</ButtonHelp>
</StyledActivitéMixteContainer>
)

View File

@ -67,7 +67,7 @@ export default function Notifications() {
<Notification className="notification" key={dottedName}>
<Emoji emoji={sévérité == 'avertissement' ? '⚠️' : '💁🏻'} />
<NotificationContent className="notificationText">
<Markdown source={résumé ?? description} />{' '}
<Markdown>{résumé ?? description ?? ''}</Markdown>{' '}
{résumé && (
<RuleLink dottedName={dottedName as DottedName}>
<Trans>En savoir plus</Trans>

View File

@ -111,7 +111,7 @@ function RadioChoice({
</Radio>{' '}
{node.rawNode.description && (
<ButtonHelp type="info" light title={node.title}>
<Markdown source={node.rawNode.description} />
<Markdown>{node.rawNode.description ?? ''}</Markdown>
</ButtonHelp>
)}
</span>

View File

@ -28,7 +28,7 @@ export function ExplicableRule({
title={rule.title}
light={light}
>
<Markdown source={rule.rawNode.description} />
<Markdown>{rule.rawNode.description}</Markdown>
</ButtonHelp>
)
}

View File

@ -23,7 +23,7 @@ export default function CotisationsForfaitaires() {
<Value expression="dirigeant . indépendant . cotisations et contributions . début activité" />
</Intro>
<Markdown source={rule.rawNode.description} />
<Markdown>{rule.rawNode.description ?? ''}</Markdown>
{rule.rawNode.références && (
<>
<Spacing lg />

View File

@ -14,7 +14,7 @@ export default function CotisationsRégularisation() {
<FromBottom>
<div>
<H3 as="h2">{rule.title}</H3>
<Markdown source={rule.rawNode.description} />
<Markdown>{rule.rawNode.description ?? ''}</Markdown>
{rule.rawNode.références && (
<>

View File

@ -80,7 +80,7 @@ export function CheckItem({
`}
>
{typeof explanations === 'string' ? (
<Markdown source={explanations} />
<Markdown>{explanations}</Markdown>
) : (
explanations
)}

View File

@ -4,7 +4,7 @@ import { Link } from 'DesignSystem/typography/link'
import { Li, Ul } from 'DesignSystem/typography/list'
import { Body } from 'DesignSystem/typography/paragraphs'
import React, { useContext, useEffect } from 'react'
import ReactMarkdown, { ReactMarkdownProps } from 'react-markdown'
import MarkdownToJsx from 'markdown-to-jsx'
import { useLocation } from 'react-router-dom'
import { SiteNameContext } from '../../Provider'
import Emoji from './Emoji'
@ -12,7 +12,6 @@ import Emoji from './Emoji'
const internalURLs = {
'mon-entreprise.urssaf.fr': 'mon-entreprise',
'mycompanyinfrance.urssaf.fr': 'infrance',
'publi.codes': 'publicodes',
} as const
export function LinkRenderer({
@ -33,14 +32,6 @@ export function LinkRenderer({
)
}
if (href && !href.startsWith('http')) {
return (
<Link to={href} {...otherProps}>
{children}
</Link>
)
}
// Convert absolute links that reload the full app into in-app links handled
// by react-router.
for (const domain of Object.keys(internalURLs)) {
@ -67,8 +58,7 @@ const TextRenderer = ({ children }: { children: string }) => (
<Emoji emoji={children} />
)
type MarkdownProps = ReactMarkdownProps & {
source: string | undefined
type MarkdownProps = React.ComponentProps<typeof MarkdownToJsx> & {
className?: string
}
@ -100,29 +90,34 @@ const CodeBlock = ({
)
export const Markdown = ({
source,
className = '',
renderers = {},
children,
components = {},
...otherProps
}: MarkdownProps) => (
<ReactMarkdown
transformLinkUri={(src) => src}
source={source}
className={`markdown ${className}`}
renderers={{
link: LinkRenderer,
paragraph: Body,
text: TextRenderer,
code: CodeBlock,
list: Ul,
strong: Strong,
listItem: Li,
heading: Heading,
emphasis: ({ children }) => <small>{children}</small>,
...renderers,
}}
<MarkdownToJsx
{...otherProps}
/>
options={{
...otherProps.options,
overrides: {
h1: H1,
h2: H2,
h3: H3,
h4: H4,
h5: H5,
h6: H6,
p: Body,
strong: Strong,
a: LinkRenderer,
ul: Ul,
li: Li,
code: CodeBlock,
span: TextRenderer,
...components,
},
}}
>
{children}
</MarkdownToJsx>
)
export const MarkdownWithAnchorLinks = ({

View File

@ -54,7 +54,7 @@ export default function Budget() {
<H1>
Budget <Emoji emoji="💶" />
</H1>
<Markdown source={intro} />
<Markdown>{intro}</Markdown>
<H2>Budget consommé</H2>
<Grid item xs={6} sm={4}>
<Select
@ -72,7 +72,7 @@ export default function Budget() {
</Select>
</Grid>
<Markdown source={ressources[selectedYear]} />
<Markdown>{ressources[selectedYear]}</Markdown>
{selectedYear !== '2019' && (
<>
<div
@ -194,7 +194,7 @@ export default function Budget() {
</tfoot>
</RessourcesAllocationTable>
</div>
<Markdown source={ressourcesDescription} />
<Markdown>{ressourcesDescription}</Markdown>
</>
)}
<MoreInfosOnUs />

View File

@ -74,7 +74,7 @@ export default function ResultatsSimples() {
</H3>
{r.rawNode.description && (
<Markdown source={r.rawNode.description} />
<Markdown>{r.rawNode.description}</Markdown>
)}
</FromTop>
)
@ -113,7 +113,7 @@ export default function ResultatsSimples() {
</Intro>
{r.rawNode.description && (
<Markdown source={r.rawNode.description} />
<Markdown>{r.rawNode.description}</Markdown>
)}
<Intro>
<RuleLink dottedName={r.dottedName}>

View File

@ -80,7 +80,7 @@ export default function ResultatsParFormulaire() {
{r.rawNode.description && (
<div className="ui__ notice">
<Markdown source={r.rawNode.description} />
<Markdown>{r.rawNode.description}</Markdown>
</div>
)}
<p className="ui__ lead" css="margin-bottom: 1rem;">
@ -137,7 +137,7 @@ function DeclarationForm({ dottedName }: { dottedName: DottedName }) {
{node.rawNode.résumé}
{node.rawNode.description && (
<ButtonHelp title={node.title} type="info">
<Markdown source={node.rawNode.description} />
<Markdown>{node.rawNode.description}</Markdown>
</ButtonHelp>
)}
</small>

View File

@ -121,7 +121,7 @@ function FormulairePublicodes() {
{type === 'groupe' ? (
<Grid item xs={12}>
{title && <HeaderComponent>{title}</HeaderComponent>}
{description && <Markdown source={description} />}
{description && <Markdown>{description}</Markdown>}
</Grid>
) : type === 'notification' ? (
<WhenApplicable dottedName={dottedName as DottedName}>
@ -148,13 +148,11 @@ function FormulairePublicodes() {
`}
>
{' '}
<Markdown
source={
typeof question !== 'string'
? (engine.evaluate(question) as any).nodeValue
: question
}
/>
<Markdown>
{typeof question !== 'string'
? (engine.evaluate(question) as any).nodeValue
: question}
</Markdown>
</div>
)}
@ -164,7 +162,7 @@ function FormulairePublicodes() {
onChange={(value) => onChange(dottedName, value)}
/>
{question && type === undefined && description && (
<Markdown source={description} />
<Markdown>{description}</Markdown>
)}
</Grid>
)}

View File

@ -107,12 +107,10 @@ export default function Nouveautés() {
</Sidebar>
</Grid>
<Grid item xs={12} lg={9}>
<SimulationGoals>
<MarkdownWithAnchorLinks
source={data[selectedRelease].description}
escapeHtml={false}
renderers={{ text: TextRenderer }}
/>
<MainBlock>
<MarkdownWithAnchorLinks renderers={{ text: TextRenderer }}>
{data[selectedRelease].description}
</MarkdownWithAnchorLinks>
<NavigationButtons>
{selectedRelease + 1 < data.length ? (
@ -128,7 +126,7 @@ export default function Nouveautés() {
</Link>
)}
</NavigationButtons>
</SimulationGoals>
</MainBlock>
</Grid>
</Grid>
<MoreInfosOnUs />
@ -182,9 +180,7 @@ const Sidebar = styled.ul`
}
`
const SmallScreenSelect = styled.select``
const SimulationGoals = styled.div`
const MainBlock = styled.div`
flex: 1;
> h1:first-child,

View File

@ -69,7 +69,7 @@ export default function Activité({
<H1>
<Emoji emoji={activité.icônes} /> {activité.titre}
</H1>
<Markdown source={activité.explication} />
<Markdown>{activité.explication}</Markdown>
{activité.plateformes && (
<SmallBody>
<Emoji emoji={'📱 '} />

View File

@ -6031,6 +6031,11 @@ markdown-escapes@^1.0.0:
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
markdown-to-jsx@^7.1.5:
version "7.1.5"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.5.tgz#caf72ad8a8c34a2bb692c4d17e44aabbe4eb19fd"
integrity sha512-YQEMMMCX3PYOWtUAQu8Fmz5/sH09s17eyQnDubwaAo8sWmnRTT1og96EFv1vL59l4nWfmtF3L91pqkuheVqRlA==
matcher-collection@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29"
@ -6798,7 +6803,11 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
<<<<<<< HEAD
react-markdown@^4.1.0:
=======
react-markdown@^4.3.1:
>>>>>>> d96c166a1 (Remplace react-markdown par markdown-to-jsx)
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-4.3.1.tgz#39f0633b94a027445b86c9811142d05381300f2f"
integrity sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==