the browser-facing portion of osu!
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
2// See the LICENCE file in the repository root for full licence text.
3
4import { timestampDecorator, transformLinkUri } from 'markdown/renderers';
5import React from 'react';
6import ReactMarkdown from 'react-markdown';
7import rehypeTruncate from 'rehype-truncate';
8import autolink from 'remark-plugins/autolink';
9import disableConstructs, { DisabledType } from 'remark-plugins/disable-constructs';
10import { maxMessagePreviewLength, propsFromHref } from 'utils/beatmapset-discussion-helper';
11import { presence } from 'utils/string';
12
13const components = Object.freeze({
14 a: linkRenderer,
15 code: textRenderer,
16 em: textRenderer,
17 img: imageRenderer,
18 p: textRenderer,
19 pre: textRenderer,
20 strong: textRenderer,
21});
22
23interface Props {
24 markdown: string;
25 maxLength?: number;
26 type?: DisabledType;
27}
28
29function imageRenderer(astProps: JSX.IntrinsicElements['img']) {
30 // render something besides image url.
31 return <>{presence(astProps.alt) ?? '[image]'}</>;
32}
33
34export function linkRenderer(astProps: JSX.IntrinsicElements['a']) {
35 const props = propsFromHref(astProps.href);
36
37 return props.children != null ? <a {...props} /> : <>{astProps.children}</>;
38}
39
40function textRenderer(astProps: JSX.IntrinsicElements[keyof JSX.IntrinsicElements]) {
41 return <>{timestampDecorator(astProps.children)}</>;
42}
43
44export default class PlainTextPreview extends React.Component<Props> {
45 render() {
46 return (
47 <ReactMarkdown
48 className='plain-text-preview'
49 components={components}
50 rehypePlugins={[[rehypeTruncate, { maxChars: this.props.maxLength ?? maxMessagePreviewLength }]]}
51 remarkPlugins={[autolink, [disableConstructs, { type: this.props.type }]]}
52 transformLinkUri={transformLinkUri}
53 unwrapDisallowed
54 >
55 {this.props.markdown}
56 </ReactMarkdown>
57 );
58 }
59}