DataviewJS 的提取与汇总

DataviewJS 小白手册 提供实践巩固,默认读者已知晓“小白入门 DataviewJS 方法”。
进阶教程:DataviewJS 的翻页与随机

学习关键:

1、分清所用函数属于 JS、Ob 还是 Dv 插件,这样才知道该翻哪个文档、用哪种格式书写。

2、提取与汇总的基础是各种取值操作,通用步骤为“获取数组/对象 - 取出数组项/对象属性”。

  • 数组是有序的,各项用索引标识,如 数组[0]
  • 对象是无序的,各属性用名称标识,如 对象.属性名对象[属性名]

实例代码获取到数组/对象后,往往只取数组的某项/对象的某个属性。

观察代码在取值前如何获取数组/对象,通过开发控制台查看完整的数组/对象,举一反三,尝试取出一些别的值,很快就能学会如何取出自己想要的值。另:

  • moment() 进行时间相关操作需使用 Moment.js 支持的格式。
    自定属性使用 moment() 为防意外,可写 moment(`${ }`) 放花括号里。
  • 也许你在同一文档定义多个相同属性,
    虽然 有时 能借此简便汇总,但长期可能会有意想不到的 问题,原理 见此
    本文不会介绍和使用类似示例,也提醒各位慎用,除非你知道自己在做什么。

折叠三角可点击展开,点击图片或右键 - 打开图片查看大图。

汇总文档

汇总近一周新建文档

要点:

  • .groupBy() 后,需用 rows 来指代每个组。
  • 该实例取得的 p.rows.file.link 是数组,需用 .join() 转为字符串。
代码
let files = dv.pages().filter(p=> Date.now()-p.file.cday.ts < 604800000).groupBy(p=> p.file.cday.ts)
dv.table( ['日期', '文档'], files.map(p=> [`${moment(p.key).format('MMDD')} (${p.rows.length})`, p.rows.file.link.join('\n')] ))
  • JS 方法:.filter()Date.now().groupBy().map().join()
  • Moment.js:moment().format()

注意:.format('MMDD') 为表格呈现的日期格式,参 Moment.js Format 自行修改。

效果:image

相关:Dataviewjs的奇技淫巧 #399

汇总原文

汇总全文及特定标题下内容

代码见原帖:DataviewJS 汇总全文及特定标题下内容并解决图片显示痛点

效果:汇总文本,且正常显示图片。左:汇总全文;右:汇总二级标题“任务一”。

汇总各级标题为大纲 & 汇总各文档各级标题

前者作用于当前活动文档(单文档),后者作用于路径下文档(多文档)。

要点:

  • 通过 Ob API 读取各级标题,通过 JS 方法排版格式。
  • 多文档汇总用 .filter(p=> p) 过滤文档中没有标题的文档。
代码
let header = app.metadataCache.getFileCache(app.workspace.getActiveFile()).headings
dv.paragraph(header.map(p=> `${' '.repeat(p.level*4)}- ${p.heading}\n`).reduce((a, b)=> a+b, ''))  // 格式、输出
let files = dv.pages(`"文件目录"`)
files.map(async p=> {
    let header = app.metadataCache.getFileCache(app.vault.getAbstractFileByPath(p.file.path)).headings.filter(p=> p)
    dv.header(6, p.file.link)
    dv.paragraph(header.map(p=> `${' '.repeat(p.level*4)}- ${p.heading}\n`).reduce((a, b)=> a+b, ''))  // 格式、输出
})
  • JS 方法:.map().repeat().reduce().filter()
  • Ob API:app.metadataCache.getFileCache()app.workspace.getActiveFile()(活动文档)、app.vault.getAbstractFileByPath()(路径下文档)

效果:image 单文档 ↔ 多文档 image

相关:【求助】大纲放到笔记前部Dataviewjs的奇技淫巧 #136

汇总标题下标题
档 1 文本
## 任务一
包括两个任务。
### 扫地
扫地
### 洒水
档 2 文本
## 任务一
包括一个任务。
### 扫地
代码
let term = '任务一'
let files = dv.pages(`"文件目录"`)
let headers = files.map(p=> {
    let header = app.metadataCache.getFileCache(app.vault.getAbstractFileByPath(p.file.path)).headings
    if (!header) return
    let h = []; let b = false
    for (let i of header) {
        if (b && i.level == 2) b = false; if (i.heading == term && i.level == 2) b = true  // 判断是否执行
        if (b && i.level == 3) h.push(i.heading)  // 执行
    }; return h.length == 0 ? false : [p.file.link, h]  // 仅当有结果时,返回 p.file.link 和 h
}); dv.table(['文档', term], headers.filter(p=> p))
  • JS 方法:.map().push().filter()
  • Ob API:app.metadataCache.getFileCache()app.vault.getAbstractFileByPath()

注意:term = ' ' 中的文本为检测条件,判断行的两个 i.level == 2 表明 2 级标题,执行行的 i.level == 3 表明 3 级标题,故此示例代码检测 2 级标题、添加 3 级标题。

效果:也即汇总了文件目录下所有文档中二级标题“任务一”下所有三级标题。

image

相关:Dataviewjs的奇技淫巧 #340

汇总代码块、Callout 等

要点:

  • “汇总标题下标题”衍生代码。
  • 各截取部分间额外 .concat('\n'),满足格式渲染所需空行。
代码
let files = dv.pages(`"文件目录"`)
let code = await Promise.all(files.map(async p=> {
    let c = ( await app.vault.readRaw(p.file.path) ).split('\n')  // 分块
    let a = app.metadataCache.getFileCache(app.vault.getAbstractFileByPath(p.file.path)).sections.filter(p=> p.type == 'code')
    let r = []
    for (let p of a) { r = r.concat(c.slice(p.position.start.line, p.position.end.line+1).concat('\n')) }
    return r.length == 0 ? false : [p.file.link, r.join('\n')]  // 仅当有结果时返回
})); dv.table(['文档', '代码块'], code.filter(p=> p))
  • JS 方法:Promise.all().map().split().filter().concat().slice().join()
  • Ob API:app.metadataCache.getFileCache()app.vault.getAbstractFileByPath()

同理,将 'code' 改为 'callout'(小写),即可汇总 Callout。

效果:image image

彩蛋:汇总块内容

一些短小的内容不想通过井号标题汇总。

要点:想要哪个为第一列,就用哪个 .map( (x, i) => )

示例文本
## A

<优点>
- 优点 1
- 优点 2

<缺点>
缺点 1

## B

<优点>优点

<缺点>缺点
代码
let file = dv.current()
let header = app.metadataCache.getFileCache(app.workspace.getActiveFile()).headings
    .filter(p=> p.level == 2).map(p=> p.heading)  // 表体第一项
let content = ( await app.vault.readRaw(file.file.path) ).split('\n\n')  // 分块
function getT(arr, t) { return arr.filter(p=> p.startsWith(`<${t}>`)).map(p=> p.substring(t.length+2)) }
let bkt = ['优点', '缺点'].map(p=> getT(content, p))  // 表体二三项
dv.table(['名称', '优点', '缺点'], header.map( (x, i) => [x].concat(bkt.map(p=> p[i])) ))
  • JS 方法:.filter().map().split().startsWith().substring().concat()
  • Ob API:app.metadataCache.getFileCache()app.workspace.getActiveFile()

注意:

  • 该实例为汇总当前文档“二级标题、<优点>、<缺点>”,缺少相应内容报错是正常的,需修改为你的格式。
  • .filter(p=> p.level == 2) 限制表体第一项为二级标题,可自行修改。
  • .split('\n\n') 以连续 2 个换行符切片,也即需空段分隔。
    自然,如果不是同一块,比如<优点>后的内容间有空行,也是不适用的。
  • .filter(p=> p.startsWith(`<${title}>`)) 匹配以<块标题>格式开头的项,不明白可以看看示例文本。

效果:image

汇总原文,汇总 YAML

本文是如何汇总的?

1、所有实例放一文件夹下,按分类各自设置 YAML 键 genre
2、读取 genre,格式二级标题;读取各档名,格式三级标题;读取各档全文。自动复制。
3、目录使用“汇总各级标题为大纲”生成。

要点:

  • 汇总档和实例放同一文件夹,读取当前文件 .file.folder,即文件目录;并排除汇总档 .file.path,避免套娃(只是为了偷懒)。
  • .groupBy() 后,需用 rows 来指代每个组。
  • return 会将值又组合成数组,需用 .join() 重新拆开。
    (或许有不需要嵌套的方法,希望知道的人能告诉楼主)
代码
let files = dv.pages(`"${dv.current().file.folder}" & -"${dv.current().file.path}"`)
    .groupBy(p=> p.genre)
let result = await Promise.all(files.map(async p=> {
    let a = await Promise.all(p.rows.map(async p=> {
        let c = ( await app.vault.readRaw(p.file.path) ).split('\n')  // 分块
        let y = app.metadataCache.getFileCache(app.vault.getAbstractFileByPath(p.file.path)).frontmatterPosition
        if (y) c.splice(y.start.line, y.end.line-y.start.line+1)  // 去掉 YAML
        return `### ${p.file.name}\n\n${c.join('\n')}`
    })); return `## ${p.key}\n\n${a.join('\n')}`  // 输出
})); new Notice('copy!'); navigator.clipboard.writeText(result.join('\n'))
  • JS 方法:.groupBy()Promise.all().map().split().splice().join()navigator.clipboard.writeText()
  • Ob API:app.vault.readRaw()app.metadataCache.getFileCache()app.vault.getAbstractFileByPath()

另:

效果:删掉代码块多余的 s,代码执行自动复制,直接粘贴。

20240105_145154

汇总 YAML

图片墙书架点图跳转文档

代码见原帖:YAML 可点击 URL 和 Wiki 链了,分享几个兼适的 DataviewJS 点图跳转文档

要点见 DataviewJS 的翻页与随机 #4“实例 2 格式行”。

效果:

相关:能否给图片增加跳转链接Dataviewjs的奇技淫巧 #374

数据透视表

代码见原帖:DataviewJS 数据透视表

效果:

彩蛋:动态查询日期之间或之前,复制结果 MD 表格

要点:

  • 需用 Number()el3.value 转为数字。提醒:JS 中 '3'+1 将输出 31,很奇妙吧。
  • Moment.js 需在时间加减操作前使用 .clone(),避免影响原值。
  • 其余要点可参 b 站视频 Dataviewjs动态查询 及阅读本话题顶部链接的进阶教程自行研究。
代码
let MSG = document.getElementById('MSG')
dv.span('年月日 '); let el1 = dv.el('input'); el1.style.width = '80px';
dv.span(' 到年月日 '); let el2 = dv.el('input'); el2.style.width = '80px';
dv.span(' 或前 '); let el3 = dv.el('input'); el3.style.width = '30px'; dv.span(' 天 ')
let dep = ()=> { MSG.innerText = '' }; el1.onfocus = dep; el2.onfocus = dep; el3.onfocus = dep
async function extract(files) {
    dv.el('button', '查找').onclick = ()=> { qjt() }; let md_table;
    dv.el('button', '复制结果').onclick = ()=> { new Notice('copy!'); navigator.clipboard.writeText(md_table) }
    let table = dv.el('div')
    function qjt() { table.empty(); let tdata = []; /*清除旧数据*/
        let d1 = moment(el1.value); let d2 = moment(el2.value);
        let d3 = d1.clone().subtract(Number(el3.value)+1, 'd')
        tdata = files.filter(p=> el3.value ? moment(p.file.name).isBetween(d3, d1)
            : moment(p.file.name).isBetween(d1.clone().subtract(1, 'd'), d2.clone().add(1, 'd'))
        ); MSG.innerText = `共 ${tdata.length} 个`;
        tdata = tdata.map(p => [p.file.link, p.起床时间]) /*表体*/
        dv.api.table( ['日期', '起床时间'], tdata, table, dv.component ) /*加表头成表*/
        return md_table = dv.markdownTable( [`日期(${tdata.length})`, '起床时间'], tdata )
    }; qjt()
}; extract(dv.pages(`"文件目录"`)) /*输出*/
  • JS 方法:.getElementById()navigator.clipboard.writeText().clone()Number().filter().map()
  • Moment.js:moment().isBetween().subtract(1, 'd').add(1, 'd')
  • dv.span()dv.el()dv.markdownTable() 属于 Dv 插件。

注意:

  • 需自行在同文档 DvJS 代码块外任意位置写上 <span id='MSG' />,也即效果 GIF 中的 image
    遇报错 (setting 'innerText'),先确认写上,鼠标不放在此 span 上,刷新代码块。
  • 以第 3 栏优先,3 栏都有值同 1、3 栏有值,执行“前几天”查询。
  • 按引入帖要求,读取档名解析为时间,非读取元数据。若想改为读取元数据,可参“汇总近一周新建文档”自行修改。
效果

20240105_161256

相关:请问如何用dataview统计一段日期之内的日记

汇总 TASK

汇总日期前未完成任务

要点:通过 moment().unix() 读取档名解析为秒,进而进行比较。

代码
let files = dv.pages(`"文件目录"`)
    .filter(p=> moment(p.file.name).unix() < moment(dv.current().file.name).unix() && !p.completed)
dv.taskList(files.file.tasks)
  • JS 方法:.filter().split().slice().sort()
  • Moment.js:moment()unix()

注意:

  • 任务本身不存储时间,故无法直接按照任务本身创建时间筛选。通过手动添加内联注释进而筛选为另一种方法,可自行研究。
  • 按引入帖要求,读取档名解析为时间,非读取元数据。若想改为读取元数据,可参“汇总近一周新建文档”自行修改。

效果:仅会汇总文件目录下当前文档日期前的未完成任务。

相关:Dataviewjs的奇技淫巧 #113

彩蛋:横排汇总近三日任务

配合 CSS,得以只需简单的代码,见原帖:Dataview 如何展示最近几天的待办事项 #13

注意:原帖只用到了 Dataview。当然,想用 DvJS 也是可以的。

效果:image

学习 DvJS,不耽于 DvJS。

本话题旨在大家看完后都能写出自己的 DataviewJS,额外需求仍请另建话题。
若出现不显示或报错,先尝试刷新代码块或重启 Ob,并认真阅读各“注意”下的说明。
错误报告请参 论坛发帖指导2023“求助反馈不重不漏自查表”,提供必要信息。提醒:

  • JS 负责生成元素,也即创建内容本身。
  • CSS 负责改变样式,也即改变元素的呈现方式。

所以,一般诸如表格间距过大、表格内容行距过大、表格内容有的换行有的不换行等空白字符和换行显示问题(CSS white-space 属性),等问题,可能需要排查主题或 CSS,而非 DataviewJS。


闲聊:

5 个赞

搜索到你之前的贴子已有成例。(BTW: 同学文案组织得很好,但fold后,就不好找资料了,搜索出的例子,要一个个unfold才找到。)

但我试验下面的代码, dataview版本原来是0.5.55,也是更新到最新,也是YAML里添加了MSG这个属性,但报错误 Cannot set properties of null (setting ‘innerText’):

let MSG = document.getElementById('MSG')
dv.span('年月日 '); 
let el1 = dv.el('input'); 
el1.style.width = '80px';
dv.span(' 到年月日 '); 
let el2 = dv.el('input'); 
el2.style.width = '80px';
dv.span(' 或前 '); 
let el3 = dv.el('input'); 
el3.style.width = '30px'; 
dv.span(' 天 ')
//process.exit();
let dep = ()=> { MSG.innerText = '' }; 
el1.onfocus = dep; 
el2.onfocus = dep;
el3.onfocus = dep;
async function extract(files) {
	dv.el('button', '查找').onclick = ()=> { qjt() }; 
	let md_table;
    dv.el('button', '复制结果').onclick = ()=> { 
		new Notice('copy!'); 
		navigator.clipboard.writeText(md_table) }
    let table = dv.el('div')
    function qjt() { 
	    table.empty(); 
	    let tdata = []; /*清除旧数据*/
        let d1 = moment(el1.value); 
        let d2 = moment(el2.value);
	    let d3 = d1.clone().subtract(Number(el3.value)+1, 'd');
        tdata = files.filter(p=> 
	        el3.value ? moment(p.file.name).isBetween(d3, d1)
            : moment(p.file.name).isBetween(d1.clone().subtract(1, 'd'), d2.clone().add(1, 'd'))
        );
        MSG.innerText = `共 ${tdata.length} 个`;
        tdata = tdata.map(p => [p.file.link, p.起床时间]) /*表体*/
        dv.api.table( ['日期', '起床时间'], tdata, table, dv.component ) /*加表头成表*/
        return md_table = dv.markdownTable( [`日期(${tdata.length})`, '起床时间'], tdata )
    }; 
    qjt()
}; 
extract(dv.pages(`"1 日记"`)) /*输出*/

试着在inner前加个exit,但整个ob黑屏无响应,退出重启都是这样,最后,用写字板改了md内容,才能打开ob,但却把所有第三方插件都disable了。

又一楼代码里的global是dataview的预定义对象吗?