使用DV创建动态更新的笔记目录大纲(TOC)

本帖的想法来自 DataviewJS 的提取与汇总 - 经验分享 - Obsidian 中文论坛 以及 使用 Dataview 读取文件中所有标题 - 经验分享 - Obsidian 中文论坛
感谢前人的经验,也特别感谢 @PlayerMiller 的代码示例

效果

其中,目录 里的链接是使用 Dataview JS 脚本自动生成的,会随着笔记内容自动更新。

和其他方案的对比:

  1. Automatic Table of Content 等插件相比,使用了大部分人都在用的 Dataview 插件,减少额外的插件安装
  2. Table of Contents 插件相比,优势是可以自动实时更新,劣势是并非实际的 Markdown 文本,无法脱离插件使用

使用方法

将 dataviewjs 代码粘贴进笔记,开启 Dataview 插件的脚本渲染功能的情况下,直接会生效。
生成的目录本质是 [[#标题]] 形式的链接,可以直接点击跳转。

代码

第一版:纯列表形式
image

```dataviewjs
const startHeadinglevel = 2;
const file = app.workspace.getActiveFile();
const { headings } = app.metadataCache.getFileCache(file);

// 全列表的形式
const raws = headings.map( p => {
    let repeatCount = Math.max((p.level - startHeadinglevel) * 4, 0);
    let spacesPrefix = ' '.repeat( repeatCount + 4 );
    let listSign = repeatCount > 0 ? '- ' : '';
    let linkText = `[[#${p.heading}]]`;
    let headingList = (p.level < startHeadinglevel) ? `- ${linkText}` : `${spacesPrefix}- ${linkText}`;
    return headingList;
  }
)

let result = raws.join('\n');
// 添加行距
dv.container.style.lineHeight = "1.5em";
dv.paragraph(result)
```

第二种:一级标题单独呈现
image

```dataviewjs
// 尝试另一种形式,个人更喜欢让一级列表特殊一点

const startHeadinglevel = 2;
const file = app.workspace.getActiveFile();
const { headings } = app.metadataCache.getFileCache(file);

let output = '';
const additionalAttr = {attr: {style: 'margin-block-start: 0 !important; margin-block-end: 0 !important;'}};

// 整个 dv 容器的样式调整
dv.container.style.marginBlockStart = "0em";

headings.forEach ( h => {
    if (h.level < startHeadinglevel) {
        // 先输出之前的子列表内容
        if (output) dv.paragraph(output, additionalAttr);

        // 将一级标题输出成单独的段落
        output = `[[#${h.heading}]]\n`;
        dv.paragraph(output);
        output = '';
    } else {
        output += `${' '.repeat((h.level - startHeadinglevel) * 4)}- [[#${h.heading}]]\n`;
    }
})

// 把最后的残余内容输出出来
if (output) dv.paragraph(output, additionalAttr);

```

脚本要点

  • 最初的难点就是 「怎么用 Dataview 输出有层级的列表」,因为直接加空格会导致文本内容被识别成「代码」而非「嵌套列表」
    • 查看 @PlayerMiller 的代码发现可以通过 将所有文本一起输出成 paragraph 的方式渲染成正确的列表
    • 但是对第二版来说,没法让作为「非列表」的一级标题很好地共同渲染,所以分段进行单独渲染
  • 为了调整行间距等样式,使用 dv.container.style 直接给 dv 容器指定样式
  • 针对列表的段落,用 dv.paragraph(output, {attr: {style: 'margin-block-start: 0 !important; margin-block-end: 0 !important;'}} ) 单独指定样式
4 个赞

收到,感谢,和自带的笔记大纲有什么区别呢

  1. 位置不一样(独立窗口 VS 笔记上方)
  2. 可定制性(可以自定义显示的内容,比如还可以加上序号,etc)

别的大差不差,主要就是提供展示目录+跳转到标题的功能。

你好,使用了你的代码,我的三级和四级标题以及以下的标题会显示错误,一级和二级就没问题
image

好的,我明天测试一下

我调整一下 lever=4 就可以显示了。只不过调整后,二级和三级标题同级了,但是也挺满意的,谢谢啦

后面尝试之后,默认level=2 的话,标题一定要有一级标题出现才行,不然缩进有问题,同理 level=3 的话,文章中要有二级标题才行,这是 level=2,但是没有一级标题的显示效果

还有个问题请教下,那个间距我调了好像没效果,不知道是不是和我的 css 或插件起冲突了


const startHeadingLevel = 2;
const file = app.workspace.getActiveFile();
const { headings } = app.metadataCache.getFileCache(file);

if (headings) {
    const fragment = document.createDocumentFragment();
    let currentList = null;
    let currentLevel = 0;

    dv.container.style.marginBlockStart = "0em";

    headings.forEach(h => {
        if (h.level < startHeadingLevel) {
            // 对于一级标题,创建一个单独的段落
            const p = dv.el('p', '');
            p.appendChild(dv.el('a', h.heading, {href: '#' + h.heading, cls: 'internal-link'}));
            fragment.appendChild(p);
            currentList = null;
            currentLevel = 0;
        } else {
            const listItem = dv.el('li', '');
            listItem.appendChild(dv.el('a', h.heading, {href: '#' + h.heading, cls: 'internal-link'}));

            if (!currentList || h.level > currentLevel) {
                const newList = dv.el('ul', '', {
                    attr: {
                        style: 'margin-block-start: 0; margin-block-end: 0; line-height: 1.5em;'
                    }
                });

                if (currentList) {
                    currentList.lastChild.appendChild(newList);
                } else {
                    fragment.appendChild(newList);
                }
                currentList = newList;
            } else if (h.level < currentLevel) {
                for (let i = 0; i < currentLevel - h.level; i++) {
                    currentList = currentList.parentNode.parentNode;
                }
            }

            currentList.appendChild(listItem);
            currentLevel = h.level;
        }
    });

    dv.container.appendChild(fragment);
}

换了一个代码方案,试试这个呢?

以及间距没效果的话,按 Ctrl+Shift+I 然后点左上角那个按钮:
image

选择列表,看一下是不是有什么属性把它覆盖了

我的文章只有三级和四级标题,这个代码也出错,将 level=3 就正常了哈哈哈,我的需求已经满足了,感谢大佬,换了代码间距也正常了

嗯嗯,好的 :+1:t2: