From 8edac589110b4a7085e0d1f3324465a07b78e746 Mon Sep 17 00:00:00 2001 From: themashcodee Date: Fri, 12 Jul 2024 00:06:03 +0530 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20custom=20emoji=20handler=20?= =?UTF-8?q?w/=20reactnode=20n=20fallback=20supoort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- .../rich_text/rich_text_section_emoji.tsx | 11 ++- .../composition_objects/text_object.tsx | 20 +---- src/store/useGlobalData.ts | 2 +- src/utils/emojis/list.ts | 1 + .../markdown_parser/elements/paragraph.tsx | 2 + src/utils/markdown_parser/parser.tsx | 8 +- .../markdown_parser/sub_elements/delete.tsx | 2 + .../markdown_parser/sub_elements/emphasis.tsx | 2 + .../markdown_parser/sub_elements/index.ts | 1 + .../markdown_parser/sub_elements/link.tsx | 3 + .../sub_elements/slack_emoji.tsx | 19 +++++ .../markdown_parser/sub_elements/strong.tsx | 2 + src/utils/markdown_parser/tokenizers/index.ts | 1 + .../tokenizers/slack_emoji/index.ts | 1 + .../tokenizers/slack_emoji/match.ts | 85 +++++++++++++++++++ .../tokenizers/slack_emoji/parse.ts | 26 ++++++ .../tokenizers/slack_emoji/tokenizer.ts | 32 +++++++ .../tokenizers/slack_emoji/types.ts | 23 +++++ src/utils/markdown_parser/types.ts | 24 +++++- 20 files changed, 241 insertions(+), 26 deletions(-) create mode 100644 src/utils/markdown_parser/sub_elements/slack_emoji.tsx create mode 100644 src/utils/markdown_parser/tokenizers/slack_emoji/index.ts create mode 100644 src/utils/markdown_parser/tokenizers/slack_emoji/match.ts create mode 100644 src/utils/markdown_parser/tokenizers/slack_emoji/parse.ts create mode 100644 src/utils/markdown_parser/tokenizers/slack_emoji/tokenizer.ts create mode 100644 src/utils/markdown_parser/tokenizers/slack_emoji/types.ts diff --git a/package.json b/package.json index 7b24c27..1ee8841 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "repository": "https://github.com/themashcodee/slack-blocks-to-jsx.git", "license": "MIT", - "version": "0.3.6", + "version": "0.3.7", "main": "dist/index.js", "module": "dist/index.mjs", "types": "dist/index.d.ts", diff --git a/src/components/blocks/rich_text/rich_text_section_emoji.tsx b/src/components/blocks/rich_text/rich_text_section_emoji.tsx index db4d1b1..bb881e7 100644 --- a/src/components/blocks/rich_text/rich_text_section_emoji.tsx +++ b/src/components/blocks/rich_text/rich_text_section_emoji.tsx @@ -8,9 +8,18 @@ export const RichTextSectionEmoji = (props: Props) => { const { name } = props; const { hooks } = useGlobalData(); + if (hooks.emoji) { + const custom_emoji = hooks.emoji(name); + if (custom_emoji !== "fallback") { + return ( + {custom_emoji} + ); + } + } + return ( - {hooks.emoji ? hooks.emoji(name) : parseEmojis(`:${name}:`)} + {parseEmojis(`:${name}:`)} ); }; diff --git a/src/components/composition_objects/text_object.tsx b/src/components/composition_objects/text_object.tsx index 7860fbe..388eb76 100644 --- a/src/components/composition_objects/text_object.tsx +++ b/src/components/composition_objects/text_object.tsx @@ -1,6 +1,6 @@ import { useGlobalData } from "../../store"; import type { TextObject as TextObjectType } from "../../types"; -import { markdown_parser, parseEmojis } from "../../utils"; +import { markdown_parser } from "../../utils"; type TextObjectProps = { data: TextObjectType; @@ -14,30 +14,18 @@ export const TextObject = (props: TextObjectProps) => { // TODO: HANDLE VERBATIM - let emoji_parsed = - type == "mrkdwn" - ? hooks.emoji - ? hooks.emoji(text) - : parseEmojis(text) - : emoji === false - ? text - : hooks.emoji - ? hooks.emoji(text) - : parseEmojis(text); - - // BASIC MARKDOWN PARSING - emoji_parsed = emoji_parsed.replace(/>/g, "> ").replace(/</g, "<"); + const parsed = text.replace(/>/g, "> ").replace(/</g, "<"); if (type === "plain_text") return (
- {markdown_parser(emoji_parsed, { markdown: false, users, channels, hooks })} + {markdown_parser(parsed, { markdown: false, users, channels, hooks })}
); return (
- {markdown_parser(emoji_parsed, { markdown: true, users, channels, hooks })} + {markdown_parser(parsed, { markdown: true, users, channels, hooks })}
); }; diff --git a/src/store/useGlobalData.ts b/src/store/useGlobalData.ts index c26b0bc..71c8362 100644 --- a/src/store/useGlobalData.ts +++ b/src/store/useGlobalData.ts @@ -23,7 +23,7 @@ type Hooks = { atChannel?: () => ReactNode; atEveryone?: () => ReactNode; atHere?: () => ReactNode; - emoji?: (emoji_text: string) => string; + emoji?: (name: string) => ReactNode | "fallback"; date?: (data: { timestamp: string; format: string; diff --git a/src/utils/emojis/list.ts b/src/utils/emojis/list.ts index af74d85..73f948c 100644 --- a/src/utils/emojis/list.ts +++ b/src/utils/emojis/list.ts @@ -1905,4 +1905,5 @@ export const missing_emojis: Record = { part_alternation_mark: "〽-FE0F;", congratulations: "㊗-FE0F;", secret: "㊙-FE0F;", + thumbsup: "👍", }; diff --git a/src/utils/markdown_parser/elements/paragraph.tsx b/src/utils/markdown_parser/elements/paragraph.tsx index d97d492..a33a0ab 100644 --- a/src/utils/markdown_parser/elements/paragraph.tsx +++ b/src/utils/markdown_parser/elements/paragraph.tsx @@ -6,6 +6,7 @@ import { SlackBroadcast, SlackChannelMention, SlackDate, + SlackEmoji, SlackUserGroupMention, SlackUserMention, Strong, @@ -38,6 +39,7 @@ export const Paragraph = (props: Props) => { if (subelement.type === "slack_broadcast") return ; if (subelement.type === "slack_date") return ; + if (subelement.type === "slack_emoji") return ; return null; })} diff --git a/src/utils/markdown_parser/parser.tsx b/src/utils/markdown_parser/parser.tsx index 504c154..462ee26 100644 --- a/src/utils/markdown_parser/parser.tsx +++ b/src/utils/markdown_parser/parser.tsx @@ -8,6 +8,7 @@ import { SlackUserGroupMentionTokenizer, SlackBroadcastTokenizer, SlackDateTokenizer, + SlackEmojiTokenizer, } from "./tokenizers"; import { ReactNode } from "react"; @@ -17,7 +18,8 @@ const parser = new YozoraParser() .useTokenizer(new SlackChannelMentionTokenizer()) .useTokenizer(new SlackUserGroupMentionTokenizer()) .useTokenizer(new SlackBroadcastTokenizer()) - .useTokenizer(new SlackDateTokenizer()); + .useTokenizer(new SlackDateTokenizer()) + .useTokenizer(new SlackEmojiTokenizer()); type Options = { markdown: boolean; @@ -26,10 +28,6 @@ type Options = { hooks: GlobalStore["hooks"]; }; -// #region HELPER CODE -// TODO: HANDLE DATE PARSING ...(hooks.date && { date: hooks.date }), -// #endregion - export const markdown_parser = (markdown: string, options: Options): ReactNode => { if (!markdown) return null; diff --git a/src/utils/markdown_parser/sub_elements/delete.tsx b/src/utils/markdown_parser/sub_elements/delete.tsx index 85f0ed8..1289b14 100644 --- a/src/utils/markdown_parser/sub_elements/delete.tsx +++ b/src/utils/markdown_parser/sub_elements/delete.tsx @@ -1,6 +1,7 @@ import { DeleteSubElement } from "../types"; import { Emphasis } from "./emphasis"; import { SlackDate } from "./slack_date"; +import { SlackEmoji } from "./slack_emoji"; import { Strong } from "./strong"; import { Text } from "./text"; @@ -17,6 +18,7 @@ export const Delete = (props: Props) => { if (child.type === "slack_date") return ; if (child.type === "strong") return ; if (child.type === "emphasis") return ; + if (child.type === "slack_emoji") return ; return ; })} diff --git a/src/utils/markdown_parser/sub_elements/emphasis.tsx b/src/utils/markdown_parser/sub_elements/emphasis.tsx index 81b9284..b91f7b8 100644 --- a/src/utils/markdown_parser/sub_elements/emphasis.tsx +++ b/src/utils/markdown_parser/sub_elements/emphasis.tsx @@ -3,6 +3,7 @@ import { Delete } from "./delete"; import { SlackBroadcast } from "./slack_broadcast"; import { SlackChannelMention } from "./slack_channel_mention"; import { SlackDate } from "./slack_date"; +import { SlackEmoji } from "./slack_emoji"; import { SlackUserGroupMention } from "./slack_user_group_mention"; import { SlackUserMention } from "./slack_user_mention"; import { Strong } from "./strong"; @@ -28,6 +29,7 @@ export const Emphasis = (props: Props) => { if (child.type === "slack_broadcast") return ; if (child.type === "slack_date") return ; if (child.type === "strong") return ; + if (child.type === "slack_emoji") return ; return ; })} diff --git a/src/utils/markdown_parser/sub_elements/index.ts b/src/utils/markdown_parser/sub_elements/index.ts index 8f2b83b..a0ce7bd 100644 --- a/src/utils/markdown_parser/sub_elements/index.ts +++ b/src/utils/markdown_parser/sub_elements/index.ts @@ -9,3 +9,4 @@ export * from "./slack_channel_mention"; export * from "./slack_user_group_mention"; export * from "./slack_broadcast"; export * from "./slack_date"; +export * from "./slack_emoji"; diff --git a/src/utils/markdown_parser/sub_elements/link.tsx b/src/utils/markdown_parser/sub_elements/link.tsx index cd2d9f9..8ae2848 100644 --- a/src/utils/markdown_parser/sub_elements/link.tsx +++ b/src/utils/markdown_parser/sub_elements/link.tsx @@ -1,6 +1,7 @@ import { LinkSubElement } from "../types"; import { Delete } from "./delete"; import { Emphasis } from "./emphasis"; +import { SlackEmoji } from "./slack_emoji"; import { Strong } from "./strong"; import { Text } from "./text"; @@ -17,6 +18,8 @@ export const Link = (props: Props) => { if (child.type === "delete") return ; if (child.type === "emphasis") return ; if (child.type === "strong") return ; + if (child.type === "slack_emoji") return ; + return ; })} diff --git a/src/utils/markdown_parser/sub_elements/slack_emoji.tsx b/src/utils/markdown_parser/sub_elements/slack_emoji.tsx new file mode 100644 index 0000000..d7573b5 --- /dev/null +++ b/src/utils/markdown_parser/sub_elements/slack_emoji.tsx @@ -0,0 +1,19 @@ +import { useGlobalData } from "../../../store"; +import { parseEmojis } from "../../emojis"; +import { SlackEmojiSubElement } from "../types"; + +type Props = { + element: SlackEmojiSubElement; +}; + +export const SlackEmoji = (props: Props) => { + const { element } = props; + const { hooks } = useGlobalData(); + + if (hooks.emoji) { + const custom_emoji = hooks.emoji(element.value); + if (custom_emoji !== "fallback") return <>{custom_emoji}; + } + + return {parseEmojis(`:${element.value}:`)}; +}; diff --git a/src/utils/markdown_parser/sub_elements/strong.tsx b/src/utils/markdown_parser/sub_elements/strong.tsx index 36c534c..7d4169b 100644 --- a/src/utils/markdown_parser/sub_elements/strong.tsx +++ b/src/utils/markdown_parser/sub_elements/strong.tsx @@ -4,6 +4,7 @@ import { Emphasis } from "./emphasis"; import { SlackBroadcast } from "./slack_broadcast"; import { SlackChannelMention } from "./slack_channel_mention"; import { SlackDate } from "./slack_date"; +import { SlackEmoji } from "./slack_emoji"; import { SlackUserGroupMention } from "./slack_user_group_mention"; import { SlackUserMention } from "./slack_user_mention"; import { Text } from "./text"; @@ -28,6 +29,7 @@ export const Strong = (props: Props) => { if (child.type === "slack_broadcast") return ; if (child.type === "slack_date") return ; if (child.type === "emphasis") return ; + if (child.type === "slack_emoji") return ; return ; })} diff --git a/src/utils/markdown_parser/tokenizers/index.ts b/src/utils/markdown_parser/tokenizers/index.ts index 95b0b81..ceae84b 100644 --- a/src/utils/markdown_parser/tokenizers/index.ts +++ b/src/utils/markdown_parser/tokenizers/index.ts @@ -3,3 +3,4 @@ export * from "./slack_channel_mention"; export * from "./slack_user_group_mention"; export * from "./slack_broadcast"; export * from "./slack_date"; +export * from "./slack_emoji"; diff --git a/src/utils/markdown_parser/tokenizers/slack_emoji/index.ts b/src/utils/markdown_parser/tokenizers/slack_emoji/index.ts new file mode 100644 index 0000000..816278d --- /dev/null +++ b/src/utils/markdown_parser/tokenizers/slack_emoji/index.ts @@ -0,0 +1 @@ +export * from "./tokenizer"; diff --git a/src/utils/markdown_parser/tokenizers/slack_emoji/match.ts b/src/utils/markdown_parser/tokenizers/slack_emoji/match.ts new file mode 100644 index 0000000..24b3cfc --- /dev/null +++ b/src/utils/markdown_parser/tokenizers/slack_emoji/match.ts @@ -0,0 +1,85 @@ +import type { INodePoint } from "@yozora/character"; +import { AsciiCodePoint } from "@yozora/character"; +import type { + IMatchInlineHookCreator, + IResultOfFindDelimiters, + IResultOfProcessSingleDelimiter, +} from "@yozora/core-tokenizer"; +import { SlackEmojiType, type IDelimiter, type IThis, type IToken, type T } from "./types"; + +export const match: IMatchInlineHookCreator = function (api) { + return { findDelimiter, processSingleDelimiter }; + + function* findDelimiter(): IResultOfFindDelimiters { + const nodePoints: ReadonlyArray = api.getNodePoints(); + const blockStartIndex: number = api.getBlockStartIndex(); + const blockEndIndex: number = api.getBlockEndIndex(); + + const potentialDelimiters: IDelimiter[] = []; + + for (let i = blockStartIndex; i < blockEndIndex; ++i) { + if (nodePoints[i]?.codePoint === AsciiCodePoint.COLON) { + const endIndex = findEndDelimiter(nodePoints, i, blockEndIndex); + if (endIndex !== -1) { + potentialDelimiters.push({ + type: "both", + startIndex: i, + endIndex: endIndex + 1, + thickness: endIndex + 1 - i, + }); + i = endIndex; // Skip past the matched emoji + } + } + } + + let pIndex = 0; + let lastEndIndex = -1; + let currentDelimiter: IDelimiter | null = null; + while (pIndex < potentialDelimiters.length) { + const [startIndex, endIndex] = yield currentDelimiter; + + if (lastEndIndex === endIndex) { + if (currentDelimiter == null || currentDelimiter.startIndex >= startIndex) continue; + } + lastEndIndex = endIndex; + + for (; pIndex < potentialDelimiters.length; ++pIndex) { + const delimiter = potentialDelimiters[pIndex]!; + if (delimiter.startIndex >= startIndex) { + currentDelimiter = { + type: "full", + startIndex: delimiter.startIndex, + endIndex: delimiter.endIndex, + thickness: delimiter.thickness, + }; + break; + } + } + } + } + + function findEndDelimiter( + nodePoints: ReadonlyArray, + startIndex: number, + blockEndIndex: number, + ): number { + for (let i = startIndex + 1; i < blockEndIndex; ++i) { + if (nodePoints[i]?.codePoint === AsciiCodePoint.COLON) { + return i; + } + } + return -1; // Not found + } + + function processSingleDelimiter( + delimiter: IDelimiter, + ): IResultOfProcessSingleDelimiter { + const token: IToken = { + nodeType: SlackEmojiType, + startIndex: delimiter.startIndex, + endIndex: delimiter.endIndex, + thickness: delimiter.thickness, + }; + return [token]; + } +}; diff --git a/src/utils/markdown_parser/tokenizers/slack_emoji/parse.ts b/src/utils/markdown_parser/tokenizers/slack_emoji/parse.ts new file mode 100644 index 0000000..434b850 --- /dev/null +++ b/src/utils/markdown_parser/tokenizers/slack_emoji/parse.ts @@ -0,0 +1,26 @@ +import type { INodePoint } from "@yozora/character"; +import { calcStringFromNodePoints } from "@yozora/character"; +import type { IParseInlineHookCreator } from "@yozora/core-tokenizer"; +import { SlackEmojiType, type INode, type IThis, type IToken, type T } from "./types"; + +export const parse: IParseInlineHookCreator = function (api) { + return { + parse: (tokens) => + tokens.map((token) => { + const nodePoints: ReadonlyArray = api.getNodePoints(); + const fullString = calcStringFromNodePoints(nodePoints, token.startIndex, token.endIndex); + + const emojiPattern = /^:(\w+):$/; + const match = emojiPattern.exec(fullString); + + let value = fullString; + + if (match) value = match[1] as string; + + const node: INode = api.shouldReservePosition + ? { type: SlackEmojiType, position: api.calcPosition(token), value } + : { type: SlackEmojiType, value }; + return node; + }), + }; +}; diff --git a/src/utils/markdown_parser/tokenizers/slack_emoji/tokenizer.ts b/src/utils/markdown_parser/tokenizers/slack_emoji/tokenizer.ts new file mode 100644 index 0000000..438e517 --- /dev/null +++ b/src/utils/markdown_parser/tokenizers/slack_emoji/tokenizer.ts @@ -0,0 +1,32 @@ +import type { + IInlineTokenizer, + IMatchInlineHookCreator, + IParseInlineHookCreator, +} from "@yozora/core-tokenizer"; +import { BaseInlineTokenizer, TokenizerPriority } from "@yozora/core-tokenizer"; +import { match } from "./match"; +import { parse } from "./parse"; +import { + SlackEmojiType, + type IDelimiter, + type INode, + type IThis, + type IToken, + type ITokenizerProps, + type T, +} from "./types"; + +export class SlackEmojiTokenizer + extends BaseInlineTokenizer + implements IInlineTokenizer +{ + constructor(props: ITokenizerProps = {}) { + super({ + name: SlackEmojiType, + priority: props.priority || TokenizerPriority.ATOMIC, + }); + } + + public override readonly match: IMatchInlineHookCreator = match; + public override readonly parse: IParseInlineHookCreator = parse; +} diff --git a/src/utils/markdown_parser/tokenizers/slack_emoji/types.ts b/src/utils/markdown_parser/tokenizers/slack_emoji/types.ts new file mode 100644 index 0000000..e1f6c1d --- /dev/null +++ b/src/utils/markdown_parser/tokenizers/slack_emoji/types.ts @@ -0,0 +1,23 @@ +import { Literal } from "@yozora/ast"; +import type { + IBaseInlineTokenizerProps, + IPartialInlineToken, + ITokenDelimiter, + ITokenizer, +} from "@yozora/core-tokenizer"; + +export const SlackEmojiType = "slack_emoji"; +export type T = typeof SlackEmojiType; +export type INode = Literal; + +export interface IToken extends IPartialInlineToken { + thickness: number; +} + +export interface IDelimiter extends ITokenDelimiter { + type: "full" | "both"; + thickness: number; +} + +export type IThis = ITokenizer; +export type ITokenizerProps = Partial; diff --git a/src/utils/markdown_parser/types.ts b/src/utils/markdown_parser/types.ts index 47cf2bc..ed86026 100644 --- a/src/utils/markdown_parser/types.ts +++ b/src/utils/markdown_parser/types.ts @@ -1,3 +1,8 @@ +export type SlackEmojiSubElement = { + type: "slack_emoji"; + value: string; +}; + export type SlackDateSubElement = { type: "slack_date"; value: { @@ -49,6 +54,7 @@ export type EmphasisSubElement = { | SlackBroadcastSubElement | SlackDateSubElement | StrongSubElement + | SlackEmojiSubElement )[]; }; @@ -63,18 +69,31 @@ export type StrongSubElement = { | SlackBroadcastSubElement | SlackDateSubElement | EmphasisSubElement + | SlackEmojiSubElement )[]; }; export type DeleteSubElement = { type: "delete"; - children: (TextSubElement | SlackDateSubElement | EmphasisSubElement | StrongSubElement)[]; + children: ( + | TextSubElement + | SlackDateSubElement + | EmphasisSubElement + | StrongSubElement + | SlackEmojiSubElement + )[]; }; export type LinkSubElement = { type: "link"; url: "http://www.example.com"; - children: (TextSubElement | EmphasisSubElement | StrongSubElement | DeleteSubElement)[]; + children: ( + | TextSubElement + | EmphasisSubElement + | StrongSubElement + | DeleteSubElement + | SlackEmojiSubElement + )[]; }; export type ParagraphElement = { @@ -91,6 +110,7 @@ export type ParagraphElement = { | SlackUserGroupMentionSubElement | SlackBroadcastSubElement | SlackDateSubElement + | SlackEmojiSubElement )[]; };