import { Effect } from 'effect' import { EffectConfigDep } from './EffectDep.ts' import type { DB_block } from './siyuan_type.ts' import { allDocBlock_by_bookId } from './cache.ts' import { API } from './siyuan_api.ts' import { tempConfig } from './config.ts' export const renderDocTreeJsPath = `/__oceanpress/docTree.js` /** 生成文档树 JS 文件 */ export function renderDocTree() { return Effect.gen(function* () { const config = yield* EffectConfigDep const Doc_blocks: DB_block[] = yield* Effect.tryPromise(() => allDocBlock_by_bookId(config.notebook.id), ) /** 获取文档树排序信息 */ const sortJSON: { [id: string]: number | undefined } = yield* Effect.tryPromise(() => API.file_getFile({ path: `/data/${config.notebook.id}/.siyuan/sort.json`, }).then((r) => { // 1. 将 ArrayBuffer 转为字符串 const decoder = new TextDecoder('utf-8') const jsonString = decoder.decode(r as ArrayBuffer) // 2. 解析字符串为 JSON 对象 return JSON.parse(jsonString) }), ) const docs = Doc_blocks.map((el) => ({ id: el.id, /** 类似 '/record/cssFlex' */ hpath: el.hpath, title: el.content, sort: sortJSON[el.id], })) const tree = buildTree(docs) // 生成 JS 代码 const jsCode = generateJSTree(tree) return ` // OceanPress DocTree - 动态加载的文档树 (function() { 'use strict'; // 文档树数据 const docTreeData = ${jsCode}; // 渲染函数 function renderDocTree(containerId, options = {}) { const container = document.getElementById(containerId); if (!container) { console.error('Container not found:', containerId); return; } const currentPath = options.currentPath || window.location.pathname.replace(/\\.html$/, ''); // 生成 HTML const html = generateHTMLTree(docTreeData, currentPath); container.innerHTML = html; // 加载样式 loadStyles(); // 初始化交互 initInteractions(container, currentPath); } // 检查是否为单一路径(只有单个子节点的文件夹) function isSinglePath(node) { if (!node.children || node.children.length !== 1) { return false; } let current = node; while (current.children && current.children.length === 1) { current = current.children[0]; } // 如果最终节点没有子节点,说明是单一路径 return !current.children || current.children.length === 0; } // 获取单一路径的所有节点 function getSinglePathNodes(node) { const pathNodes = [node]; let current = node; while (current.children && current.children.length === 1) { current = current.children[0]; pathNodes.push(current); } return pathNodes; } // 生成面包屑路径 function generateBreadcrumb(pathNodes, currentPath) { const lastNode = pathNodes[pathNodes.length - 1]; const isCurrent = lastNode.hpath === currentPath; let breadcrumbHtml = ''; return breadcrumbHtml; } // 生成 HTML 树 function generateHTMLTree(nodes, currentPath, level = 0) { let html = ''; for (const node of nodes) { const isCurrent = node.hpath === currentPath; const isActive = isCurrent || (currentPath && node.hpath && currentPath.startsWith(node.hpath)); // 检查是否为单一路径,如果是则显示为面包屑 if (isSinglePath(node)) { const pathNodes = getSinglePathNodes(node); html += generateBreadcrumb(pathNodes, currentPath); continue; } if (node.children && node.children.length > 0) { // 有子节点时使用 details/summary const isExpanded = isActive ? 'open' : ''; html += \`
\${node.title}
\${generateHTMLTree(node.children, currentPath, level + 1)}
\`; } else { // 没有子节点的普通项目 html += \`
\${node.title}
\`; } } return html; } // 加载样式 function loadStyles() { if (document.getElementById('oceanpress-doctree-styles')) return; const link = document.createElement('link'); link.id = 'oceanpress-doctree-styles'; link.rel = 'stylesheet'; link.type = 'text/css'; link.href = '${tempConfig.cdn.siyuanPrefix}appearance/docTree.css'; document.head.appendChild(link); } // 初始化交互 function initInteractions(container, currentPath) { // 为当前页面项添加高亮样式 const currentItems = container.querySelectorAll('.current'); currentItems.forEach(item => { // 如果是面包屑路径中的当前项,需要特殊处理 if (item.classList.contains('breadcrumb-part')) { item.style.fontWeight = 'bold'; item.style.color = 'var(--oceanpress-sidebar-current-border)'; } else { // 使用 CSS 变量而不是硬编码颜色 item.style.backgroundColor = 'var(--oceanpress-sidebar-current-bg)'; item.style.borderLeft = '3px solid var(--oceanpress-sidebar-current-border)'; item.style.paddingLeft = '7px'; } }); // 确保当前页面的所有父文件夹都展开 const currentElements = container.querySelectorAll('.current'); currentElements.forEach(currentElement => { // 向上遍历所有父级 details 元素并展开 let parent = currentElement.parentElement; while (parent) { if (parent.tagName === 'DETAILS') { parent.setAttribute('open', 'open'); } parent = parent.parentElement; } }); // 自动滚动到当前页面 const firstCurrent = container.querySelector('.current'); if (firstCurrent) { setTimeout(() => { firstCurrent.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); } // 监听主题变化事件,更新高亮样式 window.addEventListener('oceanpress-theme-changed', function(e) { currentItems.forEach(item => { if (item.classList.contains('breadcrumb-part')) { item.style.fontWeight = 'bold'; item.style.color = 'var(--oceanpress-sidebar-current-border)'; } else { item.style.backgroundColor = 'var(--oceanpress-sidebar-current-bg)'; item.style.borderLeft = '3px solid var(--oceanpress-sidebar-current-border)'; } }); }); } // 暴露到全局 window.OceanPressDocTree = { render: renderDocTree, data: docTreeData }; // 自动渲染(如果容器存在) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('oceanpress-doctree')) { renderDocTree('oceanpress-doctree'); } }); } else { if (document.getElementById('oceanpress-doctree')) { renderDocTree('oceanpress-doctree'); } } })(); ` }) } /** 生成 JavaScript 格式的文档树数据 */ function generateJSTree(nodes: DocNode[]): string { return JSON.stringify(nodes, null, 2); } interface DocNode { id: string hpath: string title: string sort: number | undefined children?: DocNode[] } function buildTree(docs: DocNode[]): DocNode[] { // 1. 创建根节点和路径映射 const root: DocNode[] = [] const pathMap: Record = {} // 2. 先按 hpath 排序,确保父节点先处理 docs.sort((a, b) => a.hpath.localeCompare(b.hpath)) // 3. 构建树结构 for (const doc of docs) { const pathParts = doc.hpath.split('/').filter((part) => part !== '') let currentPath = '' let parentNode: DocNode | undefined = undefined // 逐级查找或创建父节点 for (let i = 0; i < pathParts.length - 1; i++) { currentPath += '/' + pathParts[i] if (!pathMap[currentPath]) { // 创建虚拟父节点 pathMap[currentPath] = { id: 'virtual_' + currentPath, hpath: currentPath, title: pathParts[i], sort: undefined, children: [], } // 添加到父节点的children中 if (parentNode) { parentNode.children = parentNode.children || [] parentNode.children.push(pathMap[currentPath]) } else { root.push(pathMap[currentPath]) } } parentNode = pathMap[currentPath] } // 添加当前节点 if (parentNode) { parentNode.children = parentNode.children || [] parentNode.children.push(doc) } else { root.push(doc) } pathMap[doc.hpath] = doc } // 4. 递归排序 function sortNodes(nodes: DocNode[]): DocNode[] { return nodes .map((node) => { if (node.children) { node.children = sortNodes(node.children) } return node }) .sort((a, b) => { // 有sort值的优先按sort排序,没有sort值的按title排序 if (a.sort !== undefined && b.sort !== undefined) { return a.sort - b.sort } else if (a.sort !== undefined) { return -1 } else if (b.sort !== undefined) { return 1 } else { return (a.title || '').localeCompare(b.title || '') } }) } return sortNodes(root) }