@fedify/markdown-it-mention
Version:
A markdown-it plugin that parses and renders Mastodon-style @mentions.
107 lines (106 loc) • 3.79 kB
JavaScript
import { isLinkClose, isLinkOpen } from "./html.js";
import { toBareHandle } from "./label.js";
/**
* A markdown-it plugin to parse and render Mastodon-style mentions.
*/
export const mention = (md, options) => {
md.core.ruler.after("inline", "mention", (state) => parseMention(state, options));
md.renderer.rules.mention = renderMention;
};
function parseMention(state, options) {
for (const blockToken of state.tokens) {
if (blockToken.type !== "inline")
continue;
if (blockToken.children == null)
continue;
let linkDepth = 0;
let htmlLinkDepth = 0;
blockToken.children = blockToken.children.flatMap((token) => {
if (token.type === "link_open") {
linkDepth++;
}
else if (token.type === "link_close") {
linkDepth--;
}
else if (token.type === "html_inline") {
if (isLinkOpen(token.content)) {
htmlLinkDepth++;
}
else if (isLinkClose(token.content)) {
htmlLinkDepth--;
}
}
if (linkDepth > 0 || htmlLinkDepth > 0 || token.type !== "text") {
return [token];
}
return splitTokens(token, state, options);
});
}
}
const MENTION_PATTERN = /@[\p{L}\p{N}._-]+(@(?:[\p{L}\p{N}][\p{L}\p{N}_-]*\.)+[\p{L}\p{N}]{2,})?/giu;
function splitTokens(token, state, options) {
const { content, level } = token;
const tokens = [];
let pos = 0;
for (const match of content.matchAll(MENTION_PATTERN)) {
if (match.index == null)
continue;
let handle = match[0];
if (match[1] == null) {
const localDomain = options?.localDomain == null
? null
: options.localDomain(handle, state.env);
if (localDomain == null)
continue;
handle += `@${localDomain}`;
}
if (match.index > pos) {
const token = new state.Token("text", "", 0);
token.content = content.substring(pos, match.index);
token.level = level;
tokens.push(token);
}
const href = options?.link?.(handle, state.env);
if (href == null && options?.link != null) {
const token = new state.Token("text", "", 0);
token.content = match[0];
token.level = level;
tokens.push(token);
pos = match.index + match[0].length;
continue;
}
const token = new state.Token("mention", "", 0);
token.content = options?.label?.(handle, state.env) ??
toBareHandle(handle, state.env);
token.level = level;
const attrs = options?.linkAttributes?.(handle, state.env) ?? {};
attrs.href = href ?? `acct:${handle}`;
token.attrs = Object.entries(attrs);
token.info = handle;
tokens.push(token);
pos = match.index + match[0].length;
}
if (pos < content.length) {
const token = new state.Token("text", "", 0);
token.content = content.substring(pos);
token.level = level;
tokens.push(token);
}
return tokens;
}
function renderMention(tokens, idx, opts,
// deno-lint-ignore no-explicit-any
env, self) {
if (tokens.length <= idx)
return "";
const token = tokens[idx];
if (token.type !== "mention")
return self.renderToken(tokens, idx, opts);
if (typeof env === "object" && env !== null) {
if (!("mentions" in env)) {
env.mentions = [];
}
env.mentions.push(token.info);
}
return `<a ${self.renderAttrs(token)}>${token.content}</a>`;
}