fix: Allow custom emoji in TOC (#11275)

This commit is contained in:
Tom Moor
2026-01-26 19:04:08 -05:00
committed by GitHub
parent e9ed1ba5d1
commit 126e876f0c
4 changed files with 82 additions and 4 deletions
+3 -2
View File
@@ -3,6 +3,7 @@ import { TableOfContentsIcon } from "outline-icons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { EmojiText } from "@shared/components/EmojiText";
import { createAction, createActionGroup } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
import Button from "~/components/Button";
@@ -26,7 +27,7 @@ function TableOfContentsMenu() {
createAction({
name: (
<HeadingWrapper $level={heading.level - minHeading}>
{t(heading.title)}
<EmojiText>{heading.title}</EmojiText>
</HeadingWrapper>
),
section: ActiveDocumentSection,
@@ -38,7 +39,7 @@ function TableOfContentsMenu() {
),
})
),
[t, headings, minHeading]
[headings, minHeading]
);
const actions = useMemo(() => {
+4 -1
View File
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import breakpoint from "styled-components-breakpoint";
import { EmojiText } from "@shared/components/EmojiText";
import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper";
import { depths, hideScrollbars, s } from "@shared/styles";
import { useDocumentContext } from "~/components/DocumentContext";
@@ -80,7 +81,9 @@ function Contents() {
level={heading.level - headingAdjustment}
active={activeSlug === heading.id}
>
<Link href={`#${heading.id}`}>{heading.title}</Link>
<Link href={`#${heading.id}`}>
<EmojiText>{heading.title}</EmojiText>
</Link>
</ListItem>
))}
</List>
+66
View File
@@ -0,0 +1,66 @@
import { isUUID } from "validator";
import { getEmojiFromName } from "../editor/lib/emoji";
import { CustomEmoji } from "./CustomEmoji";
type Props = {
/** The text to render, which may contain emoji shortcodes like :smile: or :uuid: */
children: string;
/** Size of rendered custom emojis */
emojiSize?: number | string;
};
// Matches emoji shortcodes like :smile: or :550e8400-e29b-41d4-a716-446655440000:
const emojiShortcodeRegex = /:([a-zA-Z0-9_-]+):/g;
/**
* Renders text with embedded emoji shortcodes. Standard emoji shortcodes like
* :smile: are converted to native emoji characters, while UUID shortcodes are
* rendered as custom emoji images.
*
* @param props.children - the text to render, which may contain emoji shortcodes.
* @param props.emojiSize - size of rendered custom emojis.
* @returns a React element with text and inline emojis.
*/
export function EmojiText({ children, emojiSize = "1em" }: Props) {
const parts: (string | JSX.Element)[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
let key = 0;
while ((match = emojiShortcodeRegex.exec(children)) !== null) {
// Add text before the match
if (match.index > lastIndex) {
parts.push(children.slice(lastIndex, match.index));
}
const shortcode = match[1];
if (isUUID(shortcode)) {
// Custom emoji - render as image
parts.push(
<CustomEmoji key={key++} value={shortcode} size={emojiSize} />
);
} else {
// Standard emoji - convert to native character
const emoji = getEmojiFromName(shortcode);
if (emoji !== "?") {
parts.push(emoji);
} else {
// Unknown shortcode, keep original text
parts.push(match[0]);
}
}
lastIndex = match.index + match[0].length;
}
// Add remaining text after last match
if (lastIndex < children.length) {
parts.push(children.slice(lastIndex));
}
// Reset regex state for next call
emojiShortcodeRegex.lastIndex = 0;
return <>{parts}</>;
}
+9 -1
View File
@@ -63,7 +63,15 @@ export default class Emoji extends Extension {
getEmojiFromName(name),
];
},
leafText: (node) => getEmojiFromName(node.attrs["data-name"]),
leafText: (node) => {
const name = node.attrs["data-name"];
// Custom emojis are stored as UUIDs, preserve the shortcode format
// so they can be rendered by EmojiText component
if (isUUID(name)) {
return `:${name}:`;
}
return getEmojiFromName(name);
},
};
}