> [!info] 这是标注的标题
> 这是一个标注块。
> 它支持 **Markdown**、[[内部链接|内部链接]] 和 [[插入文件|嵌入]]!
> ![[Engelbart.jpg]]
然而,当这些标注内容发布到Hexo博客时,无法正常显示。虽然Hexo本身支持类似的标注语法:
{% note class_name %} Content (md partial supported) {% endnote %}
但为了保持Obsidian的编辑体验,我们需要一个插件来解决这个问题。
解决方案
在Hexo博客的scripts目录下创建hexo-obsidian-tabs.js文件,并将以下内容复制到文件中:
'use strict';
/**
* Hexo Obsidian Tabs Plugin
* Support Obsidian Tabs plugin syntax rendering
*/
const { marked } = require('marked');
// Match the HTML output of tabs code blocks after markdown rendering
// Hexo converts ```tabs to <pre><code class="language-tabs">...</code></pre>
const TABS_HTML_REGEX = /<pre[^>]*>\s*<code[^>]*class="[^"]*language-tabs[^"]*"[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/g;
// Match tab header in HTML content
const TAB_HEADER_REGEX = /tab:\s*([^\n]+)/;
// Match code blocks within tab content
const CODE_BLOCK_REGEX = /```(\w*)\n([\s\S]*?)```/g;
function parseTabsHtml(content) {
return content.replace(TABS_HTML_REGEX, (match, codeContent) => {
// Decode HTML entities
let decodedContent = codeContent
.replace(///g, '/')
.replace(/'/g, "'")
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/&/g, '&');
// First, restore code blocks that were encoded
// Convert ` back to ` for code blocks
decodedContent = decodedContent.replace(/`/g, '`');
// Split by tab headers
const lines = decodedContent.split('\n');
const tabs = [];
let currentTab = null;
let currentContent = [];
for (const line of lines) {
const tabMatch = line.match(/^tab:\s*(.+)$/);
if (tabMatch) {
// Save previous tab
if (currentTab) {
tabs.push({
title: currentTab,
content: currentContent.join('\n').trim()
});
}
// Start new tab
currentTab = tabMatch[1].trim();
currentContent = [];
} else if (currentTab) {
// Collect current tab content
currentContent.push(line);
}
}
// Save last tab
if (currentTab) {
tabs.push({
title: currentTab,
content: currentContent.join('\n').trim()
});
}
if (tabs.length === 0) {
return match;
}
// Generate unique ID
const tabsId = 'obsidian-tabs-' + Math.random().toString(36).substr(2, 9);
// Build HTML
let html = `<div class="obsidian-tabs" id="${tabsId}">`;
// Tab buttons
html += '<div class="obsidian-tabs-nav">';
tabs.forEach((tab, index) => {
const activeClass = index === 0 ? ' active' : '';
html += `<button class="obsidian-tab-btn${activeClass}" data-tab="${tabsId}-tab-${index}">${escapeHtml(tab.title)}</button>`;
});
html += '</div>';
// Tab content - render markdown for each tab
html += '<div class="obsidian-tabs-content">';
tabs.forEach((tab, index) => {
const activeClass = index === 0 ? ' active' : '';
// Use hexo's markdown renderer
const renderedContent = hexo.render.renderSync({ text: tab.content, engine: 'markdown' });
html += `<div class="obsidian-tab-panel${activeClass}" id="${tabsId}-tab-${index}">${renderedContent}</div>`;
});
html += '</div>';
html += '</div>';
return html;
});
}
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Register filter - process after markdown rendering
hexo.extend.filter.register('after_post_render', function(data) {
if (!data.content) return data;
data.content = parseTabsHtml(data.content);
return data;
}, 1); // Lower priority to run after markdown rendering
// Inject CSS
hexo.extend.injector.register('head_end', `
<style>
.obsidian-tabs {
margin: 1.5em 0;
border: 1px solid var(--tab-border-color, #e0e0e0);
border-radius: 8px;
overflow: hidden;
background: var(--tab-bg, #fafafa);
}
.obsidian-tabs-nav {
display: flex;
background: var(--tab-nav-bg, #f0f0f0);
border-bottom: 1px solid var(--tab-border-color, #e0e0e0);
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.obsidian-tabs-nav::-webkit-scrollbar {
display: none;
}
.obsidian-tab-btn {
padding: 12px 20px;
border: none;
background: transparent;
color: var(--tab-text-color, #666);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.obsidian-tab-btn:hover {
color: var(--tab-hover-color, #333);
background: var(--tab-hover-bg, rgba(0,0,0,0.03));
}
.obsidian-tab-btn.active {
color: var(--tab-active-color, #448aff);
border-bottom-color: var(--tab-active-color, #448aff);
background: var(--tab-bg, #fafafa);
}
.obsidian-tabs-content {
padding: 20px;
background: var(--tab-bg, #fafafa);
}
.obsidian-tab-panel {
display: none;
animation: fadeIn 0.3s ease;
}
.obsidian-tab-panel.active {
display: block;
}
.obsidian-tab-panel > *:first-child {
margin-top: 0;
}
.obsidian-tab-panel > *:last-child {
margin-bottom: 0;
}
.obsidian-tab-panel pre {
margin: 1em 0;
border-radius: 6px;
}
.obsidian-tab-panel code {
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
/* Fix code block background color inside tabs */
.obsidian-tab-panel pre code {
background: transparent !important;
}
.obsidian-tab-panel .highlight {
background: transparent !important;
}
.obsidian-tab-panel .highlight pre {
background: var(--code-bg, #f5f5f5) !important;
}
.obsidian-tab-panel figure.highlight {
background: var(--code-bg, #f5f5f5) !important;
}
.obsidian-tab-panel td.gutter {
background: var(--code-gutter-bg, #f0f0f0) !important;
}
.obsidian-tab-panel td.code {
background: var(--code-bg, #f5f5f5) !important;
}
@media (prefers-color-scheme: dark) {
.obsidian-tab-panel .highlight pre,
.obsidian-tab-panel figure.highlight {
--code-bg: #2d2d2d;
--code-gutter-bg: #252525;
}
}
@media (prefers-color-scheme: dark) {
.obsidian-tabs {
--tab-bg: #1e1e1e;
--tab-nav-bg: #252526;
--tab-border-color: #333;
--tab-text-color: #999;
--tab-hover-color: #ccc;
--tab-hover-bg: rgba(255,255,255,0.05);
--tab-active-color: #58a6ff;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
`);
// Inject JavaScript
hexo.extend.injector.register('body_end', `
<script>
(function() {
document.querySelectorAll('.obsidian-tab-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var tabsContainer = this.closest('.obsidian-tabs');
var tabId = this.getAttribute('data-tab');
tabsContainer.querySelectorAll('.obsidian-tab-btn').forEach(function(b) {
b.classList.remove('active');
});
tabsContainer.querySelectorAll('.obsidian-tab-panel').forEach(function(p) {
p.classList.remove('active');
});
this.classList.add('active');
document.getElementById(tabId).classList.add('active');
});
});
})();
</script>
`);
console.log('[hexo-obsidian-tabs] Plugin loaded successfully!');
使用说明
- 将上述代码保存为
hexo-obsidian-tabs.js文件 - 将该文件放置在Hexo博客的
scripts目录下 - 重启Hexo服务器,插件将自动加载
该插件会自动处理Obsidian标注语法,并将其转换为Hexo可识别的格式,确保内容在博客上正确显示。