astro-tips
Version:
🎨 Beautiful, customizable tip and notice blocks for Astro - 16 built-in styles with auto dark mode support and built-in compression
367 lines (340 loc) • 10.5 kB
JavaScript
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import fs from 'fs';
import remarkDirective from 'remark-directive';
import { visit } from 'unist-util-visit';
const __dirname = dirname(fileURLToPath(import.meta.url));
// 支持的 tip 类型 - 从 defaultConfig 动态生成
const TIP_VARIANTS = new Set([
'warning', 'danger', 'tip', 'mention', 'recommend', 'note',
'info', 'success', 'error', 'bug', 'quote', 'important',
'example', 'question', 'answer', 'caution'
]);
// 页面级提示框使用跟踪
const pageUsagTracker = new Set();
function isTipVariant(variant) {
return TIP_VARIANTS.has(variant);
}
// 生成 mdast 节点的辅助函数
function createTipNode(type, attrs = {}, children = []) {
return {
type: type,
data: {
hName: type,
hProperties: attrs
},
children
};
}
// 默认配置 - 包含图标和颜色
const defaultConfig = {
warning: {
icon: '⚠️',
style: {
border: '#ffb100',
light: { background: '#fff8e6' },
dark: { background: '#3d371f' }
}
},
danger: {
icon: '⛔',
style: {
border: '#ff4545',
light: { background: '#ffeded' },
dark: { background: '#3d2222' }
}
},
tip: {
icon: '💡',
style: {
border: '#409eff',
light: { background: '#e6f4ff' },
dark: { background: '#1f2f3d' }
}
},
mention: {
icon: '💬',
style: {
border: '#b45fff',
light: { background: '#f3e6ff' },
dark: { background: '#2f1f3d' }
}
},
recommend: {
icon: '👍',
style: {
border: '#67c23a',
light: { background: '#e6ffe6' },
dark: { background: '#1f3d1f' }
}
},
note: {
icon: '📝',
style: {
border: '#9e9e9e',
light: { background: '#f5f5f5' },
dark: { background: '#363636' }
}
},
info: {
icon: 'ℹ️',
style: {
border: '#03a9f4',
light: { background: '#e3f2fd' },
dark: { background: '#1f313d' }
}
},
success: {
icon: '✅',
style: {
border: '#4caf50',
light: { background: '#e8f5e9' },
dark: { background: '#1f3d24' }
}
},
error: {
icon: '❌',
style: {
border: '#f44336',
light: { background: '#ffebee' },
dark: { background: '#3d1f22' }
}
},
bug: {
icon: '🐛',
style: {
border: '#e91e63',
light: { background: '#fce4ec' },
dark: { background: '#3d1f2a' }
}
},
quote: {
icon: '💬',
style: {
border: '#607d8b',
light: { background: '#eceff1' },
dark: { background: '#1f292d' }
}
},
important: {
icon: '❗',
style: {
border: '#ff5722',
light: { background: '#fbe9e7' },
dark: { background: '#3d241f' }
}
},
example: {
icon: '📖',
style: {
border: '#ff9800',
light: { background: '#fff3e0' },
dark: { background: '#3d311f' }
}
},
question: {
icon: '❓',
style: {
border: '#2196f3',
light: { background: '#e3f2fd' },
dark: { background: '#1f2937' }
}
},
answer: {
icon: '💡',
style: {
border: '#8bc34a',
light: { background: '#f1f8e9' },
dark: { background: '#273318' }
}
},
caution: {
icon: '⚠️',
style: {
border: '#ff9800',
light: { background: '#fff3e0' },
dark: { background: '#3d311f' }
}
}
};
// 检查一个节点是否是指令 - 只允许容器指令(:::语法)
function isDirective(node) {
return node.type === 'containerDirective';
}
// 基于 AST 的 remark 插件处理提示框语法
function remarkTips(options = {}) {
const tipsConfig = { ...defaultConfig, ...options };
return function transformer(tree, file) {
let hasTips = false;
try {
visit(tree, (node, index, parent) => {
try {
if (!parent || index === undefined || !isDirective(node)) {
return;
}
// 只处理 containerDirective(:::tip:::)
if (node.type !== 'containerDirective') {
return;
}
const variant = node.name;
if (!isTipVariant(variant)) {
console.warn(`[astro-tips] Warning: Unknown tip variant "${variant}", skipping...`);
return;
}
const config = tipsConfig[variant];
if (!config) {
console.warn(`[astro-tips] Warning: No configuration found for tip variant "${variant}", skipping...`);
return;
}
hasTips = true;
// 创建提示框 HTML 结构
const tipBox = createTipNode(
'div',
{
class: `astro-tips-layout astro-tips-${variant} tips-style-${variant}`,
'data-type': variant
},
[
createTipNode(
'div',
{ class: 'icon' },
[{ type: 'text', value: config.icon }]
),
createTipNode(
'div',
{ class: 'content' },
node.children
)
]
);
parent.children[index] = tipBox;
} catch (error) {
console.error(`[astro-tips] Error processing tip node "${node.name}":`, error.message);
}
});
// 记录这个文件使用了提示框
if (hasTips && file?.path) {
pageUsagTracker.add(file.path);
}
} catch (error) {
console.error('[astro-tips] Error during AST transformation:', error.message);
}
};
}
function astroTips(options = {}) {
return {
name: 'astro-tips',
hooks: {
'astro:config:setup': ({ updateConfig, addWatchFile, injectScript }) => {
try {
// 提取配置选项
const {
minifyCSS = true,
minifyJS = true,
...tipTypes
} = options;
// 验证配置选项
if (typeof minifyCSS !== 'boolean') {
console.warn('[astro-tips] Warning: minifyCSS should be a boolean, using default value (true)');
}
if (typeof minifyJS !== 'boolean') {
console.warn('[astro-tips] Warning: minifyJS should be a boolean, using default value (true)');
}
// 最终的提示框类型配置
const finalTipsConfig = { ...defaultConfig, ...tipTypes };
// 验证自定义提示框类型
Object.keys(tipTypes).forEach(typeName => {
if (!tipTypes[typeName] || typeof tipTypes[typeName] !== 'object') {
console.warn(`[astro-tips] Warning: Invalid configuration for tip type "${typeName}", skipping...`);
delete finalTipsConfig[typeName];
return;
}
const config = tipTypes[typeName];
if (!config.icon || typeof config.icon !== 'string') {
console.warn(`[astro-tips] Warning: Tip type "${typeName}" is missing a valid icon, using default`);
finalTipsConfig[typeName].icon = '💡';
}
});
// 添加 CSS 文件监听
const cssFilePath = resolve(__dirname, 'styles/tips.css');
addWatchFile(cssFilePath);
// 读取基础 CSS 文件
let baseCss = '';
try {
baseCss = fs.readFileSync(cssFilePath, 'utf8');
} catch (error) {
console.error('[astro-tips] Error: Failed to read CSS file:', error.message);
// 使用最小CSS作为后备
baseCss = `
.astro-tips-layout {
margin: 0.5rem 0;
padding: 0.5rem 1.5rem;
border-radius: 8px;
border: 1px solid;
border-left-width: 6px;
display: flex;
align-items: center;
}
.astro-tips-layout .icon { margin-right: 1rem; }
.astro-tips-layout .content { flex: 1; }
`;
}
// 生成动态样式变量
let dynamicCss = '';
try {
Object.keys(finalTipsConfig).forEach(type => {
const style = finalTipsConfig[type].style || {};
dynamicCss += `
.astro-tips-${type}.tips-style-${type} {
--tips-light-bg: ${style.light?.background || '#fff'};
--tips-dark-bg: ${style.dark?.background || '#333'};
--tips-border: ${style.border || '#000'};
}`;
});
} catch (error) {
console.error('[astro-tips] Error generating dynamic CSS:', error.message);
}
const fullCss = baseCss + dynamicCss;
// 利用 Astro 的内置脚本注入,Vite 会自动处理压缩
try {
injectScript('head-inline', `
(function(){
if (!document.getElementById('astro-tips-styles')) {
const style = document.createElement('style');
style.id = 'astro-tips-styles';
style.textContent = ${JSON.stringify(fullCss)};
document.head.appendChild(style);
}
})();`);
} catch (error) {
console.error('[astro-tips] Error injecting styles:', error.message);
}
// 配置 markdown 处理
try {
updateConfig({
markdown: {
remarkPlugins: [
remarkDirective,
[remarkTips, finalTipsConfig]
]
},
// 应用用户的压缩配置
vite: {
build: {
cssMinify: minifyCSS, // 用户可配置的 CSS 压缩
minify: minifyJS, // 用户可配置的 JS 压缩
}
}
});
} catch (error) {
console.error('[astro-tips] Error updating Astro config:', error.message);
}
} catch (error) {
console.error('[astro-tips] Fatal error during setup:', error.message);
console.error('[astro-tips] Plugin will be disabled to prevent build failure');
}
}
}
};
}
export default astroTips;