mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: Allow custom emoji in TOC (#11275)
This commit is contained in:
@@ -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,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>
|
||||
|
||||
@@ -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}</>;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user