Skip to content

Commit

Permalink
feat: ✨ custom emoji handler w/ reactnode n fallback supoort
Browse files Browse the repository at this point in the history
  • Loading branch information
themashcodee committed Jul 11, 2024
1 parent 14721fc commit 8edac58
Show file tree
Hide file tree
Showing 20 changed files with 241 additions and 26 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion src/components/blocks/rich_text/rich_text_section_emoji.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<span className="slack_blocks_to_jsx__rich_text_section_element_emoji">{custom_emoji}</span>
);
}
}

return (
<span className="slack_blocks_to_jsx__rich_text_section_element_emoji">
{hooks.emoji ? hooks.emoji(name) : parseEmojis(`:${name}:`)}
{parseEmojis(`:${name}:`)}
</span>
);
};
20 changes: 4 additions & 16 deletions src/components/composition_objects/text_object.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(/&gt;/g, "> ").replace(/&lt;/g, "<");
const parsed = text.replace(/&gt;/g, "> ").replace(/&lt;/g, "<");

if (type === "plain_text")
return (
<div className={className}>
{markdown_parser(emoji_parsed, { markdown: false, users, channels, hooks })}
{markdown_parser(parsed, { markdown: false, users, channels, hooks })}
</div>
);

return (
<div className={className}>
{markdown_parser(emoji_parsed, { markdown: true, users, channels, hooks })}
{markdown_parser(parsed, { markdown: true, users, channels, hooks })}
</div>
);
};
2 changes: 1 addition & 1 deletion src/store/useGlobalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/utils/emojis/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1905,4 +1905,5 @@ export const missing_emojis: Record<string, string> = {
part_alternation_mark: "&#x303D-FE0F;",
congratulations: "&#x3297-FE0F;",
secret: "&#x3299-FE0F;",
thumbsup: "&#x1F44D;",
};
2 changes: 2 additions & 0 deletions src/utils/markdown_parser/elements/paragraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
SlackBroadcast,
SlackChannelMention,
SlackDate,
SlackEmoji,
SlackUserGroupMention,
SlackUserMention,
Strong,
Expand Down Expand Up @@ -38,6 +39,7 @@ export const Paragraph = (props: Props) => {
if (subelement.type === "slack_broadcast")
return <SlackBroadcast key={i} element={subelement} />;
if (subelement.type === "slack_date") return <SlackDate key={i} element={subelement} />;
if (subelement.type === "slack_emoji") return <SlackEmoji key={i} element={subelement} />;

return null;
})}
Expand Down
8 changes: 3 additions & 5 deletions src/utils/markdown_parser/parser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SlackUserGroupMentionTokenizer,
SlackBroadcastTokenizer,
SlackDateTokenizer,
SlackEmojiTokenizer,
} from "./tokenizers";
import { ReactNode } from "react";

Expand All @@ -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;
Expand All @@ -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;

Expand Down
2 changes: 2 additions & 0 deletions src/utils/markdown_parser/sub_elements/delete.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,6 +18,7 @@ export const Delete = (props: Props) => {
if (child.type === "slack_date") return <SlackDate key={i} element={child} />;
if (child.type === "strong") return <Strong key={i} element={child} />;
if (child.type === "emphasis") return <Emphasis key={i} element={child} />;
if (child.type === "slack_emoji") return <SlackEmoji key={i} element={child} />;

return <Text key={i} element={child} />;
})}
Expand Down
2 changes: 2 additions & 0 deletions src/utils/markdown_parser/sub_elements/emphasis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,6 +29,7 @@ export const Emphasis = (props: Props) => {
if (child.type === "slack_broadcast") return <SlackBroadcast key={i} element={child} />;
if (child.type === "slack_date") return <SlackDate key={i} element={child} />;
if (child.type === "strong") return <Strong key={i} element={child} />;
if (child.type === "slack_emoji") return <SlackEmoji key={i} element={child} />;

return <Text key={i} element={child} />;
})}
Expand Down
1 change: 1 addition & 0 deletions src/utils/markdown_parser/sub_elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
3 changes: 3 additions & 0 deletions src/utils/markdown_parser/sub_elements/link.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,6 +18,8 @@ export const Link = (props: Props) => {
if (child.type === "delete") return <Delete key={i} element={child} />;
if (child.type === "emphasis") return <Emphasis key={i} element={child} />;
if (child.type === "strong") return <Strong key={i} element={child} />;
if (child.type === "slack_emoji") return <SlackEmoji key={i} element={child} />;

return <Text key={i} element={child} />;
})}
</a>
Expand Down
19 changes: 19 additions & 0 deletions src/utils/markdown_parser/sub_elements/slack_emoji.tsx
Original file line number Diff line number Diff line change
@@ -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 <span className="slack_emoji">{parseEmojis(`:${element.value}:`)}</span>;
};
2 changes: 2 additions & 0 deletions src/utils/markdown_parser/sub_elements/strong.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,6 +29,7 @@ export const Strong = (props: Props) => {
if (child.type === "slack_broadcast") return <SlackBroadcast key={i} element={child} />;
if (child.type === "slack_date") return <SlackDate key={i} element={child} />;
if (child.type === "emphasis") return <Emphasis key={i} element={child} />;
if (child.type === "slack_emoji") return <SlackEmoji key={i} element={child} />;

return <Text element={child} key={i} />;
})}
Expand Down
1 change: 1 addition & 0 deletions src/utils/markdown_parser/tokenizers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
1 change: 1 addition & 0 deletions src/utils/markdown_parser/tokenizers/slack_emoji/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./tokenizer";
85 changes: 85 additions & 0 deletions src/utils/markdown_parser/tokenizers/slack_emoji/match.ts
Original file line number Diff line number Diff line change
@@ -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<T, IDelimiter, IToken, IThis> = function (api) {
return { findDelimiter, processSingleDelimiter };

function* findDelimiter(): IResultOfFindDelimiters<IDelimiter> {
const nodePoints: ReadonlyArray<INodePoint> = 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<INodePoint>,
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<T, IToken> {
const token: IToken = {
nodeType: SlackEmojiType,
startIndex: delimiter.startIndex,
endIndex: delimiter.endIndex,
thickness: delimiter.thickness,
};
return [token];
}
};
26 changes: 26 additions & 0 deletions src/utils/markdown_parser/tokenizers/slack_emoji/parse.ts
Original file line number Diff line number Diff line change
@@ -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<T, IToken, INode, IThis> = function (api) {
return {
parse: (tokens) =>
tokens.map((token) => {
const nodePoints: ReadonlyArray<INodePoint> = 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;
}),
};
};
32 changes: 32 additions & 0 deletions src/utils/markdown_parser/tokenizers/slack_emoji/tokenizer.ts
Original file line number Diff line number Diff line change
@@ -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<T, IDelimiter, IToken, INode, IThis>
implements IInlineTokenizer<T, IDelimiter, IToken, INode, IThis>
{
constructor(props: ITokenizerProps = {}) {
super({
name: SlackEmojiType,
priority: props.priority || TokenizerPriority.ATOMIC,
});
}

public override readonly match: IMatchInlineHookCreator<T, IDelimiter, IToken, IThis> = match;
public override readonly parse: IParseInlineHookCreator<T, IToken, INode, IThis> = parse;
}
23 changes: 23 additions & 0 deletions src/utils/markdown_parser/tokenizers/slack_emoji/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof SlackEmojiType>;

export interface IToken extends IPartialInlineToken<T> {
thickness: number;
}

export interface IDelimiter extends ITokenDelimiter {
type: "full" | "both";
thickness: number;
}

export type IThis = ITokenizer;
export type ITokenizerProps = Partial<IBaseInlineTokenizerProps>;
Loading

0 comments on commit 8edac58

Please sign in to comment.