125 lines
3.3 KiB
JavaScript
125 lines
3.3 KiB
JavaScript
const linkTagPattern = /\{%\s*link\s+([\s\S]*?)\s*%\}/g;
|
|
const markdownLinkPattern = /^\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function splitLinkArgs(value) {
|
|
const parts = [];
|
|
let current = '';
|
|
let bracketDepth = 0;
|
|
let parenDepth = 0;
|
|
|
|
for (const char of value) {
|
|
if (char === '[') bracketDepth += 1;
|
|
if (char === ']' && bracketDepth > 0) bracketDepth -= 1;
|
|
if (char === '(') parenDepth += 1;
|
|
if (char === ')' && parenDepth > 0) parenDepth -= 1;
|
|
|
|
if (char === ',' && bracketDepth === 0 && parenDepth === 0 && parts.length < 2) {
|
|
parts.push(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
|
|
parts.push(current.trim());
|
|
return parts;
|
|
}
|
|
|
|
function normalizeUrl(value) {
|
|
const trimmed = value.trim();
|
|
const markdownLink = trimmed.match(markdownLinkPattern);
|
|
return markdownLink ? markdownLink[1] : trimmed;
|
|
}
|
|
|
|
function getHostname(url) {
|
|
try {
|
|
return new URL(url).hostname.replace(/^www\./, '');
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function renderLinkCard(rawValue) {
|
|
const [title, source, rawUrl] = splitLinkArgs(rawValue);
|
|
const url = normalizeUrl(rawUrl ?? '');
|
|
|
|
if (!title || !url) return `{% link ${rawValue} %}`;
|
|
|
|
const host = getHostname(url);
|
|
const label = source?.split('@')[0]?.trim() || host || 'Link';
|
|
const initial = label.slice(0, 1).toUpperCase();
|
|
|
|
return `<a class="hexo-link-card" href="${escapeHtml(url)}" target="_blank" rel="external nofollow noopener noreferrer">
|
|
<span class="hexo-link-card__icon" aria-hidden="true">${escapeHtml(initial)}</span>
|
|
<span class="hexo-link-card__body">
|
|
<strong>${escapeHtml(title)}</strong>
|
|
<span>${escapeHtml(label)}${host ? ` · ${escapeHtml(host)}` : ''}</span>
|
|
</span>
|
|
</a>`;
|
|
}
|
|
|
|
function transformText(value) {
|
|
let matched = false;
|
|
const transformed = value.replace(linkTagPattern, (_, rawValue) => {
|
|
matched = true;
|
|
return renderLinkCard(rawValue);
|
|
});
|
|
|
|
return matched ? transformed : value;
|
|
}
|
|
|
|
function inlineToSource(node) {
|
|
if (node.type === 'text' || node.type === 'inlineCode') return node.value ?? '';
|
|
|
|
if (node.type === 'link') {
|
|
const text = Array.isArray(node.children) ? node.children.map(inlineToSource).join('') : node.url;
|
|
return `[${text}](${node.url})`;
|
|
}
|
|
|
|
if (Array.isArray(node.children)) return node.children.map(inlineToSource).join('');
|
|
return '';
|
|
}
|
|
|
|
function visit(node) {
|
|
if (!node || typeof node !== 'object') return;
|
|
|
|
if (node.type === 'paragraph' && Array.isArray(node.children)) {
|
|
const source = node.children.map(inlineToSource).join('');
|
|
const transformed = transformText(source);
|
|
|
|
if (transformed !== source) {
|
|
node.type = 'html';
|
|
node.value = transformed;
|
|
delete node.children;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (node.type === 'text' && typeof node.value === 'string') {
|
|
const transformed = transformText(node.value);
|
|
|
|
if (transformed !== node.value) {
|
|
node.type = 'html';
|
|
node.value = transformed;
|
|
}
|
|
}
|
|
|
|
if (Array.isArray(node.children)) {
|
|
for (const child of node.children) visit(child);
|
|
}
|
|
}
|
|
|
|
export default function remarkHexoLinkCards() {
|
|
return (tree) => {
|
|
visit(tree);
|
|
};
|
|
}
|