2f4dedfba2
* Add Channel Mention selection ability * Fix mentioned user name being smaller than other text * Improve logic for locating a mention * Fix mentioning with enter on livestream * Fix breaking for invalid URI query * Handle punctuation after mention * Fix name display and appeareance * Use canonical url * Fix missing search
167 lines
4.3 KiB
JavaScript
167 lines
4.3 KiB
JavaScript
import { parseURI } from 'lbry-redux';
|
|
import visit from 'unist-util-visit';
|
|
|
|
const protocol = 'lbry://';
|
|
const uriRegex = /(lbry:\/\/)[^\s"]*[^)]/g;
|
|
const punctuationMarks = [',', '.', '!', '?', ':', ';', '-', ']', ')', '}'];
|
|
|
|
const mentionToken = '@';
|
|
// const mentionTokenCode = 64; // @
|
|
const mentionRegex = /@[^\s()"]*/gm;
|
|
|
|
const invalidRegex = /[-_.+=?!@#$%^&*:;,{}<>\w/\\]/;
|
|
|
|
function handlePunctuation(value) {
|
|
const modifierIndex =
|
|
(value.indexOf(':') >= 0 && value.indexOf(':')) || (value.indexOf('#') >= 0 && value.indexOf('#'));
|
|
|
|
let punctuationIndex;
|
|
punctuationMarks.some((p) => {
|
|
if (modifierIndex) {
|
|
punctuationIndex = value.indexOf(p, modifierIndex + 1) >= 0 && value.indexOf(p, modifierIndex + 1);
|
|
}
|
|
return punctuationIndex;
|
|
});
|
|
|
|
return punctuationIndex ? value.substring(0, punctuationIndex) : value;
|
|
}
|
|
|
|
// Find channel mention
|
|
function locateMention(value, fromIndex) {
|
|
const index = value.indexOf(mentionToken, fromIndex);
|
|
|
|
// Skip invalid mention
|
|
if (index > 0 && invalidRegex.test(value.charAt(index - 1))) {
|
|
return locateMention(value, index + 1);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
// Find claim url
|
|
function locateURI(value, fromIndex) {
|
|
var index = value.indexOf(protocol, fromIndex);
|
|
|
|
// Skip invalid uri
|
|
if (index > 0 && invalidRegex.test(value.charAt(index - 1))) {
|
|
return locateMention(value, index + 1);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
// 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 newText = handlePunctuation(text);
|
|
const uri = parseURI(newText);
|
|
const isValid = uri && uri.claimName;
|
|
const isChannel = uri.isChannel && uri.path === uri.claimName;
|
|
|
|
if (isValid) {
|
|
// Create channel link
|
|
if (isChannel) {
|
|
return eat(newText)(createURI(uri.claimName, newText, false));
|
|
}
|
|
// Create claim link
|
|
return eat(newText)(createURI(newText, newText, true));
|
|
}
|
|
} catch (err) {
|
|
// Silent errors: console.error(err)
|
|
}
|
|
}
|
|
};
|
|
|
|
// Generate a markdown link from channel name
|
|
function tokenizeMention(eat, value, silent) {
|
|
if (silent) {
|
|
return true;
|
|
}
|
|
|
|
const match = value.match(mentionRegex);
|
|
|
|
return validateURI(match, eat, self);
|
|
}
|
|
|
|
// Generate a markdown link from lbry url
|
|
function tokenizeURI(eat, value, silent) {
|
|
if (silent) {
|
|
return true;
|
|
}
|
|
|
|
const match = value.match(uriRegex);
|
|
|
|
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 === uri.claimName;
|
|
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');
|
|
}
|