Merge pull request #2521 from lbryio/smart-links
Extend markdown support for LBRY urls
This commit is contained in:
commit
081d86097b
14 changed files with 465 additions and 25 deletions
|
@ -65,6 +65,7 @@
|
|||
"@lbry/components": "^2.7.2",
|
||||
"@reach/rect": "^0.2.1",
|
||||
"@reach/tabs": "^0.1.5",
|
||||
"@reach/tooltip": "^0.2.1",
|
||||
"@types/three": "^0.93.1",
|
||||
"async-exit-hook": "^2.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
|
@ -163,6 +164,7 @@
|
|||
"redux-persist-transform-filter": "0.0.16",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"remark": "^9.0.0",
|
||||
"remark-attr": "^0.8.3",
|
||||
"remark-emoji": "^2.0.1",
|
||||
"remark-react": "^4.0.3",
|
||||
"render-media": "^3.1.0",
|
||||
|
@ -170,11 +172,13 @@
|
|||
"sass-loader": "^7.1.0",
|
||||
"semver": "^5.3.0",
|
||||
"stream-to-blob-url": "^2.1.1",
|
||||
"strip-markdown": "^3.0.3",
|
||||
"style-loader": "^0.23.1",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"three": "^0.93.0",
|
||||
"three-full": "^17.1.0",
|
||||
"tree-kill": "^1.1.0",
|
||||
"unist-util-visit": "^1.4.1",
|
||||
"video.js": "^7.2.2",
|
||||
"villain": "btzr-io/Villain",
|
||||
"wavesurfer.js": "^2.2.1",
|
||||
|
|
|
@ -7,7 +7,7 @@ import { formatLbryUriForWeb } from 'util/uri';
|
|||
import { OutboundLink } from 'react-ga';
|
||||
|
||||
type Props = {
|
||||
onClick: ?(any) => any,
|
||||
id: ?string,
|
||||
href: ?string,
|
||||
title: ?string,
|
||||
label: ?string,
|
||||
|
@ -24,6 +24,11 @@ type Props = {
|
|||
iconSize?: number,
|
||||
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
||||
activeClass?: string,
|
||||
innerRef: ?any,
|
||||
// Events
|
||||
onClick: ?(any) => any,
|
||||
onMouseEnter: ?(any) => any,
|
||||
onMouseLeave: ?(any) => any,
|
||||
};
|
||||
|
||||
class Button extends React.PureComponent<Props> {
|
||||
|
@ -33,7 +38,11 @@ class Button extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
innerRef,
|
||||
href,
|
||||
title,
|
||||
label,
|
||||
|
@ -102,6 +111,7 @@ class Button extends React.PureComponent<Props> {
|
|||
|
||||
return path ? (
|
||||
<NavLink
|
||||
id={id}
|
||||
exact
|
||||
to={path}
|
||||
title={title}
|
||||
|
@ -112,13 +122,17 @@ class Button extends React.PureComponent<Props> {
|
|||
onClick();
|
||||
}
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={combinedClassName}
|
||||
activeClassName={activeClass}
|
||||
innerRef={innerRef}
|
||||
>
|
||||
{content}
|
||||
</NavLink>
|
||||
) : (
|
||||
<button
|
||||
id={id}
|
||||
title={title}
|
||||
aria-label={description || label || title}
|
||||
className={combinedClassName}
|
||||
|
|
26
src/ui/component/claimLink/index.js
Normal file
26
src/ui/component/claimLink/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { doResolveUri, makeSelectTitleForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
|
||||
|
||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||
|
||||
import ClaimLink from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
uri: props.uri,
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(ClaimLink);
|
87
src/ui/component/claimLink/view.jsx
Normal file
87
src/ui/component/claimLink/view.jsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import PreviewLink from 'component/previewLink';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
title: ?string,
|
||||
claim: StreamClaim,
|
||||
children: React.Node,
|
||||
className: ?string,
|
||||
autoEmbed: ?boolean,
|
||||
description: ?string,
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
blackListedOutpoints: Array<{
|
||||
txid: string,
|
||||
nout: number,
|
||||
}>,
|
||||
};
|
||||
|
||||
class ClaimLink extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
href: null,
|
||||
link: false,
|
||||
title: null,
|
||||
thumbnail: null,
|
||||
autoEmbed: false,
|
||||
description: null,
|
||||
isResolvingUri: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
isClaimBlackListed() {
|
||||
const { claim, blackListedOutpoints } = this.props;
|
||||
|
||||
if (claim && blackListedOutpoints) {
|
||||
let blackListed = false;
|
||||
|
||||
for (let i = 0; i < blackListedOutpoints.length; i += 1) {
|
||||
const outpoint = blackListedOutpoints[i];
|
||||
if (outpoint.txid === claim.txid && outpoint.nout === claim.nout) {
|
||||
blackListed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return blackListed;
|
||||
}
|
||||
}
|
||||
|
||||
resolve = (props: Props) => {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { uri, claim, title, className, autoEmbed, children, isResolvingUri } = this.props;
|
||||
const isUnresolved = (!isResolvingUri && !claim) || !claim;
|
||||
const isBlacklisted = this.isClaimBlackListed();
|
||||
|
||||
if (isBlacklisted || isUnresolved) {
|
||||
return <span>{children}</span>;
|
||||
}
|
||||
|
||||
const { name: claimName } = claim;
|
||||
const showPreview = autoEmbed === true && !isUnresolved;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button label={children} title={title || claimName} button={'link'} navigate={uri} className={className} />
|
||||
{showPreview && <PreviewLink uri={uri} />}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ClaimLink;
|
|
@ -2,11 +2,12 @@
|
|||
import type { ElementRef } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import Toggle from 'react-toggle';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import MarkdownPreview from 'component/common/markdown-preview-internal';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import remark from 'remark';
|
||||
import reactRenderer from 'remark-react';
|
||||
import remarkAttr from 'remark-attr';
|
||||
import remarkStrip from 'strip-markdown';
|
||||
import remarkEmoji from 'remark-emoji';
|
||||
import reactRenderer from 'remark-react';
|
||||
import ExternalLink from 'component/externalLink';
|
||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
|
||||
type MarkdownProps = {
|
||||
content: ?string,
|
||||
promptLinks?: boolean,
|
||||
type SimpleTextProps = {
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
type SimpleLinkProps = {
|
||||
|
@ -17,6 +19,16 @@ type SimpleLinkProps = {
|
|||
children?: React.Node,
|
||||
};
|
||||
|
||||
type MarkdownProps = {
|
||||
strip?: boolean,
|
||||
content: ?string,
|
||||
promptLinks?: boolean,
|
||||
};
|
||||
|
||||
const SimpleText = (props: SimpleTextProps) => {
|
||||
return <span>{props.children}</span>;
|
||||
};
|
||||
|
||||
const SimpleLink = (props: SimpleLinkProps) => {
|
||||
const { href, title, children } = props;
|
||||
return (
|
||||
|
@ -31,19 +43,54 @@ const schema = { ...defaultSchema };
|
|||
|
||||
// Extend sanitation schema to support lbry protocol
|
||||
schema.protocols.href.push('lbry');
|
||||
schema.attributes.a.push('embed');
|
||||
|
||||
const MarkdownPreview = (props: MarkdownProps) => {
|
||||
const { content, promptLinks } = props;
|
||||
const remarkOptions = {
|
||||
const { content, strip, promptLinks } = props;
|
||||
|
||||
const remarkOptions: Object = {
|
||||
sanitize: schema,
|
||||
fragment: React.Fragment,
|
||||
remarkReactComponents: {
|
||||
a: promptLinks ? ExternalLink : SimpleLink,
|
||||
// Workaraund of remarkOptions.Fragment
|
||||
div: React.Fragment,
|
||||
},
|
||||
};
|
||||
|
||||
const remarkAttrOpts = {
|
||||
scope: 'extended',
|
||||
elements: ['link'],
|
||||
extend: { link: ['embed'] },
|
||||
defaultValue: true,
|
||||
};
|
||||
|
||||
// Strip all content and just render text
|
||||
if (strip) {
|
||||
// Remove new lines and extra space
|
||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
||||
return (
|
||||
<span className="markdown-preview">
|
||||
{
|
||||
remark()
|
||||
.use(remarkStrip)
|
||||
.use(reactRenderer, remarkOptions)
|
||||
.processSync(content).contents
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="markdown-preview">
|
||||
{
|
||||
remark()
|
||||
.use(remarkAttr, remarkAttrOpts)
|
||||
// Remark plugins for lbry urls
|
||||
// Note: The order is important
|
||||
.use(formatedLinks)
|
||||
.use(inlineLinks)
|
||||
// Emojis
|
||||
.use(remarkEmoji)
|
||||
.use(reactRenderer, remarkOptions)
|
||||
.processSync(content).contents
|
||||
|
|
|
@ -2,14 +2,24 @@
|
|||
import * as React from 'react';
|
||||
|
||||
type Props = {
|
||||
text: ?string,
|
||||
text?: ?string,
|
||||
lines: number,
|
||||
showTooltip?: boolean,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const TruncatedText = (props: Props) => (
|
||||
<span title={props.text} className="truncated-text" style={{ WebkitLineClamp: props.lines }}>
|
||||
{props.text}
|
||||
</span>
|
||||
);
|
||||
const TruncatedText = (props: Props) => {
|
||||
const { text, children, lines, showTooltip } = props;
|
||||
const tooltip = showTooltip ? children || text : '';
|
||||
return (
|
||||
<span title={tooltip} className="truncated-text" style={{ WebkitLineClamp: lines }}>
|
||||
{children || text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
TruncatedText.defaultProps = {
|
||||
showTooltip: true,
|
||||
};
|
||||
|
||||
export default TruncatedText;
|
||||
|
|
|
@ -4,10 +4,12 @@ import * as ICONS from 'constants/icons';
|
|||
import * as React from 'react';
|
||||
import { isURIValid } from 'lbry-redux';
|
||||
import Button from 'component/button';
|
||||
import ClaimLink from 'component/claimLink';
|
||||
|
||||
type Props = {
|
||||
href: string,
|
||||
title?: string,
|
||||
embed?: boolean,
|
||||
children: React.Node,
|
||||
openModal: (id: string, { uri: string }) => void,
|
||||
};
|
||||
|
@ -16,18 +18,16 @@ class ExternalLink extends React.PureComponent<Props> {
|
|||
static defaultProps = {
|
||||
href: null,
|
||||
title: null,
|
||||
embed: false,
|
||||
};
|
||||
|
||||
createLink() {
|
||||
const { href, title, children, openModal } = this.props;
|
||||
|
||||
const { href, title, embed, children, openModal } = this.props;
|
||||
// Regex for url protocol
|
||||
const protocolRegex = new RegExp('^(https?|lbry|mailto)+:', 'i');
|
||||
const protocol = href ? protocolRegex.exec(href) : null;
|
||||
|
||||
// Return plain text if no valid url
|
||||
let element = <span>{children}</span>;
|
||||
|
||||
// Return external link if protocol is http or https
|
||||
if (protocol && (protocol[0] === 'http:' || protocol[0] === 'https:' || protocol[0] === 'mailto:')) {
|
||||
element = (
|
||||
|
@ -41,10 +41,13 @@ class ExternalLink extends React.PureComponent<Props> {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Return local link if protocol is lbry uri
|
||||
if (protocol && protocol[0] === 'lbry:' && isURIValid(href)) {
|
||||
element = <Button button="link" title={title || href} label={children} navigate={href} />;
|
||||
element = (
|
||||
<ClaimLink uri={href} autoEmbed={embed}>
|
||||
{children}
|
||||
</ClaimLink>
|
||||
);
|
||||
}
|
||||
|
||||
return element;
|
||||
|
|
37
src/ui/component/previewLink/index.js
Normal file
37
src/ui/component/previewLink/index.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
doResolveUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectTitleForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectIsUriResolving,
|
||||
makeSelectMetadataItemForUri,
|
||||
} from 'lbry-redux';
|
||||
|
||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||
|
||||
import PreviewLink from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
return {
|
||||
uri: props.uri,
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
|
||||
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(PreviewLink);
|
58
src/ui/component/previewLink/view.jsx
Normal file
58
src/ui/component/previewLink/view.jsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
title: ?string,
|
||||
thumbnail: ?string,
|
||||
description: ?string,
|
||||
history: { push: string => void },
|
||||
};
|
||||
|
||||
class PreviewLink extends React.PureComponent<Props> {
|
||||
handleClick = () => {
|
||||
const { uri, history } = this.props;
|
||||
history.push(formatLbryUriForWeb(uri));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { uri, title, description, thumbnail } = this.props;
|
||||
const placeholder = 'static/img/placeholder.png';
|
||||
|
||||
const thumbnailStyle = {
|
||||
backgroundImage: `url(${thumbnail || placeholder})`,
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={'preview-link'} role="button" onClick={this.handleClick}>
|
||||
<span className={'file-list__item'}>
|
||||
<span style={thumbnailStyle} className={'preview-link__thumbnail media__thumb'} />
|
||||
<span className={'file-list__item-metadata'}>
|
||||
<span className={'file-list__item-info'}>
|
||||
<span className={'file-list__item-title'}>
|
||||
<TruncatedText text={title} lines={1} />
|
||||
</span>
|
||||
</span>
|
||||
<span className={'preview-link__description media__subtext'}>
|
||||
<UriIndicator uri={uri} link />
|
||||
</span>
|
||||
<span className={'file-list__item-properties'}>
|
||||
<span className={'preview-link__description media__subtext'}>
|
||||
<TruncatedText lines={2} showTooltip={false}>
|
||||
<MarkdownPreview content={description} promptLinks strip />
|
||||
</TruncatedText>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(PreviewLink);
|
|
@ -34,8 +34,8 @@
|
|||
}
|
||||
|
||||
.editor-preview.editor-preview-active {
|
||||
background-color: $lbry-gray-5;
|
||||
color: $lbry-black;
|
||||
background-color: $lbry-black;
|
||||
color: $lbry-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,4 +120,23 @@
|
|||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview-link {
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
background-color: rgba($lbry-teal-5, 0.1);
|
||||
border-left: 0.5rem solid $lbry-teal-5;
|
||||
display: block;
|
||||
align-items: center;
|
||||
width: 40rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.preview-link__thumbnail {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.preview-link__description {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
111
src/ui/util/remark-lbry.js
Normal file
111
src/ui/util/remark-lbry.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { parseURI } from 'lbry-redux';
|
||||
import visit from 'unist-util-visit';
|
||||
|
||||
const protocol = 'lbry://';
|
||||
const locateURI = (value, fromIndex) => value.indexOf(protocol, fromIndex);
|
||||
const locateMention = (value, fromIndex) => value.indexOf('@', fromIndex);
|
||||
|
||||
// Generate a valid markdown link
|
||||
const createURI = (text, uri, embed = false) => ({
|
||||
type: 'link',
|
||||
url: (uri.startsWith(protocol) ? '' : protocol) + uri,
|
||||
data: {
|
||||
// Custom attribute
|
||||
hProperties: { embed },
|
||||
},
|
||||
children: [{ type: 'text', value: text }],
|
||||
});
|
||||
|
||||
const validateURI = (match, eat) => {
|
||||
if (match) {
|
||||
try {
|
||||
const text = match[0];
|
||||
const uri = parseURI(text);
|
||||
const isValid = uri && uri.claimName;
|
||||
const isChannel = uri.isChannel && !uri.path;
|
||||
|
||||
if (isValid) {
|
||||
// Create channel link
|
||||
if (isChannel) {
|
||||
return eat(text)(createURI(uri.claimName, text, false));
|
||||
}
|
||||
// Create claim link
|
||||
return eat(text)(createURI(text, text, true));
|
||||
}
|
||||
} catch (err) {
|
||||
// Silent errors: console.error(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Generate a markdown link from channel name
|
||||
function tokenizeMention(eat, value, silent) {
|
||||
const match = /^@+[a-zA-Z0-9-#:/]+/.exec(value);
|
||||
return validateURI(match, eat);
|
||||
}
|
||||
|
||||
// Generate a markdown link from lbry url
|
||||
function tokenizeURI(eat, value, silent) {
|
||||
const match = /^(lbry:\/\/)+[a-zA-Z0-9-@#:/]+/.exec(value);
|
||||
return validateURI(match, eat);
|
||||
}
|
||||
|
||||
// Configure tokenizer for lbry urls
|
||||
tokenizeURI.locator = locateURI;
|
||||
tokenizeURI.notInList = true;
|
||||
tokenizeURI.notInLink = true;
|
||||
tokenizeURI.notInBlock = true;
|
||||
|
||||
// Configure tokenizer for lbry channels
|
||||
tokenizeMention.locator = locateMention;
|
||||
tokenizeMention.notInList = true;
|
||||
tokenizeMention.notInLink = true;
|
||||
tokenizeMention.notInBlock = true;
|
||||
|
||||
const visitor = (node, index, parent) => {
|
||||
if (node.type === 'link' && parent && parent.type === 'paragraph') {
|
||||
try {
|
||||
const uri = parseURI(node.url);
|
||||
const isValid = uri && uri.claimName;
|
||||
const isChannel = uri.isChannel && !uri.path;
|
||||
if (isValid && !isChannel) {
|
||||
if (!node.data || !node.data.hProperties) {
|
||||
// Create new node data
|
||||
node.data = {
|
||||
hProperties: { embed: true },
|
||||
};
|
||||
} else if (node.data.hProperties) {
|
||||
// Don't overwrite current attributes
|
||||
node.data.hProperties = {
|
||||
embed: true,
|
||||
...node.data.hProperties,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Silent errors: console.error(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// transform
|
||||
const transform = tree => {
|
||||
visit(tree, ['link'], visitor);
|
||||
};
|
||||
|
||||
export const formatedLinks = () => transform;
|
||||
|
||||
// Main module
|
||||
export function inlineLinks() {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||
const methods = Parser.prototype.inlineMethods;
|
||||
|
||||
// Add an inline tokenizer (defined in the following example).
|
||||
tokenizers.uri = tokenizeURI;
|
||||
tokenizers.mention = tokenizeMention;
|
||||
|
||||
// Run it just before `text`.
|
||||
methods.splice(methods.indexOf('text'), 0, 'uri');
|
||||
methods.splice(methods.indexOf('text'), 0, 'mention');
|
||||
}
|
25
yarn.lock
25
yarn.lock
|
@ -5498,6 +5498,11 @@ html-comment-regex@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7"
|
||||
integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==
|
||||
|
||||
html-element-attributes@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/html-element-attributes/-/html-element-attributes-2.1.0.tgz#ff674b8716526b8a9ee1d8e454466eda56d6d0cc"
|
||||
integrity sha512-uWNlZuzM3MfRvZLAHWUzwwU6He2NvX9szpXwjG7+hoVqZzY3e0vc6mlujiSnJ/i9zVFZDhBDN33Pm9HNEzcyPg==
|
||||
|
||||
html-entities@^1.2.0, html-entities@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
|
||||
|
@ -7114,6 +7119,11 @@ math-expression-evaluator@^1.2.14:
|
|||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
||||
integrity sha1-3oGf282E3M2PrlnGrreWFbnSZqw=
|
||||
|
||||
md-attr-parser@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/md-attr-parser/-/md-attr-parser-1.2.1.tgz#1043f6451c33ed3e392b40504df08010f4d03e3d"
|
||||
integrity sha512-dZqt2L4Q7FUcx6ZcuownAxa74Y7d5jcsHRB2MIgQ0vT10Pa+/0Som6hhJ+jgAjP3vnFtrd4aO+ZMc5K7QVfbiQ==
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
|
||||
|
@ -9841,6 +9851,14 @@ relateurl@0.2.x:
|
|||
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
|
||||
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
|
||||
|
||||
remark-attr@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/remark-attr/-/remark-attr-0.8.3.tgz#0263045acb958e1556b0c6955b5d66a4b36d4d92"
|
||||
integrity sha512-JMP6rmLwhj5VmDRG99kGr+HNXQ6MmAs+UojeAHGMQFiIitEmJZBkmgOBsWadxu/RTACD4XnMuJXY1QWEFdw31A==
|
||||
dependencies:
|
||||
html-element-attributes "^2.0.0"
|
||||
md-attr-parser "^1.2.1"
|
||||
|
||||
remark-emoji@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-2.0.2.tgz#49c134021132c192ee4cceed1988ec9b8ced7eb8"
|
||||
|
@ -10912,6 +10930,11 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
||||
strip-markdown@^3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/strip-markdown/-/strip-markdown-3.0.3.tgz#93b4526abe32a1d69e5ca943f4d9ba25c01a4d48"
|
||||
integrity sha512-G2DSM9wy3PWxY3miAibWpsTqZgXLXgRoq0yVyaVs9O7FDGEwPO6pmSE8CyzHhU88Z2w1dkFqFmWUklFNsQKwqg==
|
||||
|
||||
style-loader@^0.23.1:
|
||||
version "0.23.1"
|
||||
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
|
||||
|
@ -11564,7 +11587,7 @@ unist-util-visit-parents@^2.0.0:
|
|||
dependencies:
|
||||
unist-util-is "^2.1.2"
|
||||
|
||||
unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0:
|
||||
unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0, unist-util-visit@^1.4.0, unist-util-visit@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
|
||||
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
|
||||
|
|
Loading…
Reference in a new issue