Last active
July 8, 2019 13:48
-
-
Save brookback/6a44b6a4cead55e17c1664a29b1f26b0 to your computer and use it in GitHub Desktop.
A React component which supports Markdown, emojis, and @mentions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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