Files
TREK/client/src/components/shared/markdownSanitize.tsx
T
Maurice bf00412863 fix(mobile): Arabic reads in its direction: arrows turn round, Latin names keep their ends, figures stay whole
In the touch shell the back, forward, disclosure, date-range and flow
arrows kept pointing right in Arabic, so Settle up showed the debt the
wrong way round and Vacay's year arrows pointed inward. The shell now
mirrors lucide's direction arrows in right-to-left languages; an icon
that names its own treatment keeps it, and the lightbox arrows stay on
their physical sides.

Latin user text in Arabic rows lost its beginning when cut and moved its
punctuation to the wrong end. Names, titles, addresses and file names take
their direction from their own text (dir=auto) and still line up with the
row; markdown paragraphs, chat messages, poll options and journal stories
are isolated; signed balances, amounts, sizes, rates and leg figures read
as one left-to-right run; URL fields read left to right. Name pairs joined
by an arrow isolate each name and point from the first to the second.

The hero progress and poll result bars fill from the start edge. Arabic
and Hebrew uppercase caption labels grow a step and take the muted ink,
and a todo's due date in a notification is written in the reader's
language instead of as YYYY-MM-DD.
2026-11-07 23:25:16 +01:00

82 lines
2.5 KiB
TypeScript

import type { Components, Options } from 'react-markdown'
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'
type HastNode = { type: string; children?: HastNode[] }
/**
* react-markdown never builds an element out of raw HTML, it shows the markup
* verbatim instead. It does that after the rehype plugins ran, though, so the
* sanitizer gets the raw nodes first and drops them without a trace: a note
* reading "Zimmernummer <TBD>" loses the placeholder on screen while the stored
* text still has it (#2177). Turning raw into text up front keeps it readable
* and still yields no element.
*/
function rehypeRawAsText() {
return (tree: HastNode) => {
const walk = (node: HastNode) => {
if (node.type === 'raw') node.type = 'text'
for (const child of node.children || []) walk(child)
}
walk(tree)
}
}
// remark-gfm already writes footnote ids with the `user-content-` prefix, and
// the hrefs pointing at them carry it too. Clobbering them a second time only
// makes target and reference disagree.
const schema = { ...defaultSchema, clobberPrefix: '' }
export const sanitizedMarkdownPlugins: NonNullable<Options['rehypePlugins']> = [
rehypeRawAsText,
[rehypeSanitize, schema],
]
/**
* A paragraph or heading of user text runs in its own direction: a Latin note
* in the Arabic UI keeps its question mark and full stop at its end (and an
* Arabic one in an English UI), while the block keeps the alignment of the page
* around it. The isolate is inline, so the block itself stays as it was.
*/
export const isolatedTextBlocks: Components = {
p: ({ children }) => (
<p>
<bdi>{children}</bdi>
</p>
),
h1: ({ children }) => (
<h1>
<bdi>{children}</bdi>
</h1>
),
h2: ({ children }) => (
<h2>
<bdi>{children}</bdi>
</h2>
),
h3: ({ children }) => (
<h3>
<bdi>{children}</bdi>
</h3>
),
}
/**
* Links as in markdownLinkComponents (#1629): own tab plus `rel` protection.
* The exception is a footnote or heading link, which points inside the text
* that is already on screen, so opening a tab for it would only reload the trip
* somewhere else.
*/
export const sanitizedMarkdownComponents: Components = {
...isolatedTextBlocks,
a: ({ children, href, ...rest }) =>
href?.startsWith('#') ? (
<a {...rest} href={href}>
{children}
</a>
) : (
<a {...rest} href={href} target="_blank" rel="noopener noreferrer nofollow">
{children}
</a>
),
}