Open History to see the last commit
1import { textDirection } from "~/textDirection";
2
3type HastNode = {
4 type?: string;
5 tagName?: string;
6 value?: string;
7 properties?: Record<string, unknown>;
8 children?: HastNode[];
9};
10
11// Raw HTML such as `<li>a</li><li>b</li>` has no whitespace between its
12// blocks, so they add their own to keep the words apart.
13const BLOCK_TAGS = new Set([
14 "blockquote",
15 "br",
16 "details",
17 "div",
18 "h1",
19 "h2",
20 "h3",
21 "h4",
22 "h5",
23 "h6",
24 "hr",
25 "li",
26 "ol",
27 "p",
28 "summary",
29 "table",
30 "td",
31 "th",
32 "tr",
33 "ul",
34]);
35
36function proseText(node: HastNode): string {
37 if (node.type === "text") return node.value ?? "";
38 if (node.tagName === "code") return " ";
39 const text = (node.children ?? []).map(proseText).join("");
40 return node.tagName && BLOCK_TAGS.has(node.tagName) ? ` ${text} ` : text;
41}
42
43function isolateInlineCode(node: HastNode): void {
44 if (node.tagName === "pre") return;
45 if (node.tagName === "code") {
46 node.properties = { ...node.properties, dir: "ltr" };
47 return;
48 }
49 node.children?.forEach(isolateInlineCode);
50}
51
52/**
53 * Gives each top-level block the direction of its own words, so an Arabic or
54 * Hebrew reply reads right to left beside English ones. Lines inside a list or
55 * quote follow it, keeping markers and rules on one side. Inline code is left
56 * out of the count and isolated left to right, so its punctuation stays put.
57 */
58export function rehypeTextDirection() {
59 return (tree: HastNode) => {
60 for (const block of tree.children ?? []) {
61 if (block.type !== "element" || block.tagName === "pre") continue;
62 const dir = textDirection(proseText(block));
63 if (dir) block.properties = { ...block.properties, dir };
64 }
65 isolateInlineCode(tree);
66 };
67}