Files
docmd-io--docmd/packages/parser/src/features/buttons.ts
T
wehub-resource-sync 6db8fca185
docmd CI verification / verify (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:31:55 +08:00

106 lines
3.3 KiB
TypeScript

/**
* --------------------------------------------------------------------
* docmd : the zero-config documentation engine.
*
* @package @docmd/core (and ecosystem)
* @website https://docmd.io
* @repository https://github.com/docmd-io/docmd
* @license MIT
* @copyright Copyright (c) 2025-present docmd.io
*
* [docmd-source] - Please do not remove this header.
* --------------------------------------------------------------------
*/
import { renderIcon } from '../utils/icon-renderer.js';
import { resolveHref } from '../utils/normalize-href.js';
function buttonRule(state, startLine, endLine, silent) {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
const lineContent = state.src.slice(start, max).trim();
// Regex matches: ::: button "Text" Link [color] or :::button (spaceless)
const match = lineContent.match(/^:::\s*button\s+(?:["'](.*?)["']|(\S+))\s+(.*)$/);
if (!match) return false;
if (silent) return true;
// Extract Data
// Group 1 is quoted text, Group 2 is single-word text
let text = match[1] || match[2] || 'Button';
// Replace underscores only if it was a single word (legacy support)
if (match[2]) text = text.replace(/_/g, ' ');
const rest = match[3].trim();
// Parse Link and Options
// We look for the first string as the link, rest as options
const parts = rest.split(/\s+/);
const rawLink = parts[0];
let color = '';
let icon = '';
// Look for options in remaining parts
for (let i = 1; i < parts.length; i++) {
if (parts[i].startsWith('color:')) {
color = parts[i].replace('color:', '');
} else if (parts[i].startsWith('icon:')) {
icon = parts[i].replace('icon:', '');
}
}
// Resolve link through the unified normaliser
const result = resolveHref(rawLink);
let href = result.href;
// Clean URL depth adjustment for non-index pages
if (!result.isRaw && !result.isExternal && !href.startsWith('#')) {
let hashPart = '';
let pathPart = href;
const hashIdx = href.indexOf('#');
if (hashIdx >= 0) {
hashPart = href.substring(hashIdx);
pathPart = href.substring(0, hashIdx);
}
const isProtocol = pathPart.match(/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i);
if (!isProtocol && !pathPart.startsWith('/') && state.env && state.env.isIndex === false) {
if (pathPart.startsWith('./')) {
pathPart = '../' + pathPart.substring(2);
} else if (pathPart !== '') {
pathPart = '../' + pathPart;
}
}
href = pathPart + hashPart;
}
// Generate Token
const token = state.push('html_inline', '', 0);
let styleAttr = '';
if (color) {
// Basic validation to prevent CSS injection if needed, or allow flexibility
styleAttr = ` style="background-color: ${color}; border-color: ${color}; color: #fff;"`;
}
const targetAttr = result.isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
let iconHtml = '';
if (icon) {
iconHtml = renderIcon(icon, { class: 'button-icon' });
}
token.content = `<a href="${href}" class="docmd-button"${styleAttr}${targetAttr}>${iconHtml}${state.md.renderInline(text)}</a>`;
state.line = startLine + 1;
return true;
}
export default {
name: 'buttons',
setup(md) {
md.block.ruler.before('paragraph', 'docmd_button', buttonRule, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
}
};