Skip to content

Instantly share code, notes, and snippets.

@brookback
Last active July 8, 2019 13:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brookback/6a44b6a4cead55e17c1664a29b1f26b0 to your computer and use it in GitHub Desktop.
Save brookback/6a44b6a4cead55e17c1664a29b1f26b0 to your computer and use it in GitHub Desktop.
A React component which supports Markdown, emojis, and @mentions.
import * as React from 'react';
import * as unified from 'unified';
import { useContext } from 'react';
import classNames from '../libs/classNames';
import mentions from './remark-mentions';
import { StateContext } from '..';
import { traverseMentions } from '../traverse-mentions';
import { User } from '../types';
import { createComponentFromProcessor } from 'remark-react-component/build/es6';
import {
emojis,
addEmojiClasses,
} from 'remark-react-component/build/es6/emojis';
const markdown = require('remark-parse');
interface Props {
text: string;
className?: string;
}
enum ClassNames {
Emoji = 'emoji',
EmojiLarge = 'emoji-large',
Mention = 'mention',
MentionCurrentUser = 'mention--you',
}
const pipeline = unified()
// This is the Markdown parser
.use(markdown)
// Parse @mentions and :emojis: to AST nodes
.use(emojis)
.use(mentions);
// Build a renderer component based on the Unified pipeline above
const Renderer = createComponentFromProcessor(pipeline);
const findUserFromMention = (users: User[], mention: string) =>
users ? users.find((u) => u.username === mention) : null;
/**
* A general text component which renders nice things from a single
* text string.
*
* Currently supports:
*
* - Markdown
* - Emojis (shortcodes and unicode)
* - @mentions
*/
const Text: React.FunctionComponent<Props> = (props) => {
const ctx = useContext(StateContext);
return (
<Renderer
text={props.text}
className={classNames('markdown-content', props.className)}
// Here we can attach "transformers" of the AST. With them, we can
// spruce up the nodes a bit, such as attaching custom class names and title
// attributes.
transformers={[
addEmojiClasses({
className: ClassNames.Emoji,
classNameForOnlyEmojis: ClassNames.EmojiLarge,
}),
traverseMentions({
title: (mention) => {
const user =
ctx.state.users &&
findUserFromMention(ctx.state.users, mention);
if (!user) {
return '';
}
return user._id === ctx.state.userID
? 'This is you!'
: user.profile.fullname;
},
className: (mention) => {
const user =
ctx.state.users &&
findUserFromMention(ctx.state.users, mention);
if (!user) {
return '';
}
return user._id === ctx.state.userID
? `${ClassNames.Mention} ${
ClassNames.MentionCurrentUser
}`
: ClassNames.Mention;
},
}),
]}
/>
);
};
// tslint:disable-next-line no-object-mutation
Text.displayName = 'Text';
// tslint:disable-next-line no-object-mutation
Renderer.displayName = 'Renderer';
export default React.memo(Text);
// tslint:disable no-object-mutation no-this
import { Processor } from 'unified';
import { Tokenizer, isRemarkParser } from './remark';
// This module is a tokenizer for at-mentions for Remark
interface Options {
mentionSymbol?: string;
}
export default function mentions(this: Processor, opts: Options = {}): void {
const parser = this.Parser;
if (!isRemarkParser(parser)) {
throw new Error('Missing parser to attach to');
}
const tokenizers = parser.prototype.inlineTokenizers;
const methods = parser.prototype.inlineMethods;
tokenizers.mention = mkTokenizer(opts);
methods.splice(methods.indexOf('text'), 0, 'mention');
}
const mkTokenizer = (opts: Options) => {
const { mentionSymbol = '@' } = opts;
const re = new RegExp(`^${mentionSymbol}(\\w+)`);
const tokenizer: Tokenizer = (eat, value, silent) => {
const match = re.exec(value);
if (!match) {
return;
}
if (silent) {
return true;
}
return eat(match[0])({
type: 'mention',
data: {
mentionedString: match[1],
hName: 'span',
},
children: [
{
type: 'text',
value: match[0],
},
],
});
};
tokenizer.locator = (value: string, fromIndex: number) =>
value.indexOf(mentionSymbol, fromIndex);
return tokenizer;
};
import { Node } from 'unist';
// Types and utils for Remark
export type Locator = (value: string, fromIndex: number) => number;
export type Add = (node: Node) => Node;
export type Eat = (value: string) => Add;
export interface Tokenizer {
(eat: Eat, value: string, silent?: boolean): Node | boolean | undefined;
locator: Locator;
}
export const isRemarkParser = (parser: any) =>
Boolean(parser && parser.prototype && parser.prototype.inlineTokenizers);
import { Node, Data } from 'unist';
const visit = require('unist-util-visit');
interface MentionData extends Data {
mentionedString: string;
hProperties: any;
}
// This is a Remark *transformer*, i.e. it works off an unist AST.
type PredicateFunction = (mention: string) => string;
interface Options {
className?: string | PredicateFunction;
title?: string | PredicateFunction;
}
/** Attaches a given class name and title attribute on a mention node given a predicate. */
export const traverseMentions = (opts: Options) => (tree: Node) => {
visit(tree, 'mention', (node: Node) => {
// tslint:disable-next-line no-let
let { className = 'mention--you', title } = opts;
if (node.data) {
const data = node.data as MentionData;
const { mentionedString } = data;
if (className && typeof className !== 'string') {
className = className(mentionedString);
}
if (title && typeof title !== 'string') {
title = title(mentionedString);
}
// tslint:disable-next-line no-object-mutation
data.hProperties = {
...data.hProperties,
title,
className,
};
}
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment