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({ try: () => 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) as { [id: string]: number | undefined } }), catch: () => ({}) as { [id: string]: number | undefined }, }) 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'; /** 展开状态持久化 key */ const STORAGE_KEY = 'oceanpress-doctree-open'; /** 默认展开层级(首次访问且无当前路径命中时) */ const DEFAULT_OPEN_LEVEL = 1; // 文档树数据 const docTreeData = ${jsCode}; /** svg 图标,与思源 material icon.js 中的 symbol id 对应 */ const ICONS = { arrow: '', folder: '', file: '', title: '', menu: '' }; /** 获取当前页面对应的树节点 hpath(去除 .html / index.html 后缀并解码中文) */ function getCurrentPath() { let p = window.location.pathname; if (/(^|\\/)index\\.html$/.test(p)) { /** 站点根的 index.html:hpath 为 /index(思源文档树的根节点) */ p = p.replace(/index\\.html$/, 'index'); } else if (p === '/' || p === '') { /** 以目录形式访问站点根:同样映射到 /index */ p = '/index'; } else { /** 普通文档页:去掉 .html 后缀 */ p = p.replace(/\\.html$/, ''); } try { p = decodeURIComponent(p); } catch (e) { /* 已是明文则保留原样 */ } return p; } /** 读取持久化的展开节点集合 */ function loadOpenState() { try { const raw = localStorage.getItem(STORAGE_KEY); return new Set(raw ? JSON.parse(raw) : []); } catch (e) { return new Set(); } } /** 持久化展开节点集合 */ function saveOpenState(set) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(set))); } catch (e) { /* 忽略存储异常(隐私模式等) */ } } /** 判断节点是否应展开:包含当前路径的祖先必展开;其余看用户操作记录;都没有则按默认层级 */ function shouldOpen(node, currentPath, openSet) { if (currentPath === node.hpath || (currentPath && currentPath.startsWith(node.hpath + '/'))) { return true; } if (openSet.size > 0 || currentPath) return openSet.has(node.hpath); return (node.hpath.match(/\\//g) || []).length < DEFAULT_OPEN_LEVEL; } /** HTML 转义 */ function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } /** 递归生成 ul/li 嵌套列表 HTML */ function generateHTMLTree(nodes, currentPath, openSet, isChild) { let html = '