SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG:
I scrolled through a fair amount of this thread looking for a way to list ‘todo’ callouts with a link, but only in a specific folder with a specified tag. I could not find it, so I am posting this for posterity’s sake. Credit to this question thread which allowed me to form a base to build this.
```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')
//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
const regex = />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/
//check for more types of callouts by adding new const regex# where # is a number
const regex2 = />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/
const rows = []
for (const page of pages) {
const file = app.vault.getAbstractFileByPath(page.file.path);
//get file path as string
var checkMe = "" + page.file.path;
// Read the file contents
const contents = await app.vault.read(file)
// Extract the summary via regex
for (const callout of contents.match(new RegExp(regex, 'sg')) || []) {
//check if the file path is in your designated file path.
//change FILEPATH to any keyword, folder name, or file path
//You can also change to exclude a file path by changing "==true" to "!=true"
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex, 's'))
rows.push([match[1], page.file.link])
}
}
//duplicate the for loop for each const regex# you created
//adjust regex to the appropriate name
for (const callout of contents.match(new RegExp(regex2, 'sg')) || []) {
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex2, 's'))
rows.push([match[1], page.file.link])
}
}
}
dv.table(['Term', 'Link'], rows)
```
I am still playing with this, and if I update it, I will edit this post or reply to it.
Hope this helps someone.
SEARCH FOR AND DISPLAY LIST OF CALLOUTS (WITH LINKS TO NOTES) IN A SPECIFIED FOLDER WITH A SPECIFIED TAG (UPDATE):
I went and tried to edit the post I made to include the regex Array, but I was unable to do so.
```dataviewjs
// Replace TAG with whatever tag you wish.
const pages = dv.pages('#TAG')
//Use the regular expression to define what kind of Callout you want to check
//Replace !todo with whatever callout type you wish.
//check for more types of callouts by adding new entries in the array.
//Remember: every key in the Array has to have a comma at the end, except for the last one
const regex = {
0: />\s\[\!todo\]\s(.+?)((\n>\s.*?)*)\n/,
1: />\s\[\!TODO\]\s(.+?)((\n>\s.*?)*)\n/
}
const rows = []
for (const page of pages) {
const file = app.vault.getAbstractFileByPath(page.file.path);
//get file path as string
var checkMe = "" + page.file.path;
// Read the file contents
const contents = await app.vault.read(file)
//cycle through the callout type array.
//Be sure to change the 'i<2' to be one more than your highest array key.
for(var i = 0; i < 2; i++){
// Extract the summary via regex
for (const callout of contents.match(new RegExp(regex[i], 'sg')) || []) {
//check if the file path is in your designated file path.
//change FILEPATH to any keyword, folder name, or file path
//You can also change to exclude a file path by changing "==true" to "!=true"
if(checkMe.includes('FILEPATH') == true){
const match = callout.match(new RegExp(regex[i], 's'))
rows.push([match[1], page.file.link])
}
}
}
}
dv.table(['Term', 'Link'], rows);
```
Not the OP, but the following works in a local diagnostic (see screenshot below).
Person File:
---
birthday: 2021-08-17
tags: family
---
Birthdays File
---
searchterm: '#family'
duration: 1 year
---
```dataviewjs
var start = moment().startOf('day');
var end = moment(start).add(dv.current().duration);
var dateformat = "YYYY-MM-DD";
if (dv.current().dateformat) { dateformat = dv.current().dateformat; }
// info text above table, {0}=duration, {1}=start date, {2}=end date
// parameters can be left out, or the string empty
var infotext = "Upcoming birthdays for {0} from now ({1} – {2})<br><br>";
//=============
function nextBirthday(birthday) {
// Get person’s next birthday on or after "start"
// returns a moment
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
var bdayNext = moment(bday).year(start.year());
if (bdayNext.isBefore(start, 'day')) {
bdayNext.add(1, "year");
}
return bdayNext;
}
function turns(birthday) {
// Get the age in years a person will turn to on their next birthday
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
return nextBirthday(birthday).diff(bday, 'years');
}
function showBirthday(birthday) {
// Determine if this birthday is in the range to be shown
// including the start date, excluding the end date
// because that comes from a duration calculation
// for use with "where", returns true or false
if (birthday) {
// need to "unparse" because DV has already converted YAML birthday to DateTime object
// shouldn’t harm if already a string
var bday = moment(birthday.toString());
var bdayNext = nextBirthday(birthday);
if (bdayNext.isBetween(start, end, 'day', '[)')) {
return true;
} else {
return false;
}
} else {
return false;
}
}
function sortByNextBirthday(a, b) {
// comparator function for "sort"
if (nextBirthday(a).isBefore(nextBirthday(b))) {
return -1;
}
if (nextBirthday(a).isAfter(nextBirthday(b))) {
return 1;
}
// they’re equal
return 0;
}
//======================================================================
dv.paragraph(infotext.format(moment.duration(dv.current().duration.toString()).humanize(), start.format(dateformat), end.format(dateformat)));
dv.table(
["Name", "Birthday", "Turns"],
dv.pages(dv.current().searchterm)
// use a function to see if this birthday is in range to be shown
.where(p => showBirthday(p.birthday))
// use a comparator function to sort by next birthday
.sort(p => p.birthday, 'asc', sortByNextBirthday)
.map(p => [
p.file.link,
p.birthday ? nextBirthday(p.birthday).format(dateformat) : '–',
turns(p.birthday)
])
);
```
thanks for your work which works fine on my desktop device. Unfortunately it’s not working on my mobile (iOS) device. I added the same script folder as on my desktop but I get the following error:
I found the solution. By default Obsidian does not sync all file types within your Vault. You need to explicitly allow the synchronization of unknown files in the sync settings on all devices. After a restart of Obsidian everything works fine.
Maybe you could add this as hint to the customJS plugin description.
Hey, folks. I’m sure I’m missing something obvious: I’m working with Metadata Menu and in a lookup field, I want to query only the files that link to the page the field is on. How would I limit them in dv.pages() ? Thank you!
Does anyone have an example of creating a normal HTML table with dataviewjs? Preferably not using literal HTML tag strings? I’m having trouble getting createElement working?
I modified the original GTD script by @AlanG to perform GTD on Daily notes. More details here
tasksTodayDaily.js
/*
* ---------------------
* CONFIGURATION OPTIONS
* ---------------------
*
* Add the folders, tags, and headings which should be excluded.
* Tasks found on pages with these tags or paths will not appear in the Tasks page.
* Headings will to exclude tasks that fall under a heading with this name.
*/
const globalExclude = {
folders: [
'Extras'
],
tags: [
'#exclude-master-tasklist',
// '#completed'
],
headings: [
'🌱 Daily Habits'
]
}
/*
* When displaying tasks from a project, tasks that fall under headings with these
* names won't have these headings displayed when showing the project info
*/
const hideSubsectionName = [
'Todo',
'Tasks'
]
/*
* ----------------------------
* END OF CONFIGURATION OPTIONS
* ----------------------------
*/
// Set up the variables to store the tasks and projects
const tasks = []
const noNextAction = []
const excludeItems = ['', ...globalExclude.tags]
globalExclude.folders.forEach(folder => excludeItems.push(`"${folder}"`))
const globalExcludeString = excludeItems.join(' AND -')
// console.log("globalExcludeString", globalExcludeString)
// Define groups for Daily tasklist page
/*
For the daily view we have so far these groups:
* Tasks started today
* Tasks completed today
* Pending tasks carrying over from previous days
* Tasks due today, which already exists but doesn't properly work
*/
const Groups = {
Started: 0,
Completed: 1,
Pending: 2,
Due: 3,
Normal: 4
}
const self = dv.current();
const path = require('path');
/**
isStartedToday()
Shows only tasks that were started on the day of the Daily note
CRITERIA
--------
* Task text must not be empty
* Task must NOT be completed today
* Task header path corresponds to today's daily note and is not completed
* Task with plus-sign emoji and done emoji must not show as Started but Completed
* Completed tasks have four ways of marking:
* Done emoji with date with dashes
* Done emoji with date with no dashes
* The `completion `property` and a date with dashes
* The completion circle filled with a check mark
@param {task} t - An Obsidian task
@returns {boolean} true if matching all conditions, false otherwise
*/
function isStartedToday(t) {
let taskTextNotEmpty = t.text.length > 0;
// let notCompletedTodayDaily = self.file.path == t.header.path && !t.completed
let taskInDailyNotCompleted = self.file.path == t.header.path && !t.completed
let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
let DateNoDashes = moment(self.file.name).format('YYYYMMDD')
// let containsDateNoDashes = t.text.match(self.file.name)
// && typeof(self.file.name) == 'number'
let matchPlusEmojiDateDashes = t.text.match('➕' + DateWithDashes) // started today
let containsDateWithDashes = t.text.match(DateWithDashes)
let containsDateCompletionEmojiDashes = t.text.match('✅ ' + DateWithDashes) // completed
let containsDateCompletionEmojiNoDashes = t.text.match('✅ ' + DateNoDashes) // completed
let containsDateCompletionColonColon = t.text.match('completion:: ' + DateWithDashes) // completed
return (
// containsDateNoDashes
// || containsDateWithDashes
matchPlusEmojiDateDashes
&& !containsDateCompletionEmojiDashes
&& !containsDateCompletionEmojiNoDashes
&& !containsDateCompletionColonColon
&& taskInDailyNotCompleted
&& taskTextNotEmpty
)
}
/**
isCompletedToday()
Shows only tasks that were completed on the day of the Daily note
CRITERIA
--------
* Task text must not be empty
* Task must be completed
* Task header path corresponds to today's daily note
* Completed tasks have four ways of marking:
* Done emoji with date with dashes: '✅ ' + DateWithDashes
* Done emoji with date with no dashes: '✅ ' + DateNoDashes
* `completion` property and date with dashes: 'completion:: ' + DateWithDashes
* The completion circle filled with a check mark: t.completed
@param {task} t - An Obsidian task
@returns {boolean} true if matching all conditions, false otherwise
*/
function isCompletedToday(t) {
let taskTextNotEmpty = t.text.length > 0;
// capture tasks that are completed today
let completedTodayDaily = self.file.path == t.header.path && t.completed
// date in two formats
let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
let DateNoDashes = moment(self.file.name).format('YYYYMMDD')
// capture tasks which text contains the current date, which could be start or completion
let containsDateNoDashes = t.text.match(self.file.name)
let containsDateWithDashes = t.text.match(DateWithDashes)
// boolean statements
let containsDateCompletionEmojiDashes = t.text.match('✅ ' + DateWithDashes)
let containsDateCompletionEmojiNoDashes = t.text.match('✅ ' + DateNoDashes)
let containsDateCompletionColonColon = t.text.match('completion:: ' + DateWithDashes)
return (
containsDateCompletionEmojiDashes
|| containsDateCompletionColonColon
|| containsDateCompletionEmojiNoDashes
|| completedTodayDaily
&& taskTextNotEmpty
)
}
/**
isOlderThanThisDailyNote()
Pending tasks.
Shows only older tasks that were not yet completed at the date of the Daily note
CRITERIA
--------
* Task text must not be empty
* Task must NOT be completed
* Task header path corresponds to today's daily note
* Task must be older than current daily note
* Task contains a `due` date property
@param {task} t - An Obsidian task
@returns {boolean} true if matching all conditions, false otherwise
*/
function isOlderThanThisDailyNote(t) {
let basename = path.parse(t.header.path).name
let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
let mDailyDate = moment(self.file.name, 'YYYYMMDD', true); // Note
let mAnyTaskDate = moment(basename, 'YYYYMMDD', true); // task anywehere else
// < 0 does not include pending task if they were created today
// <= 0 includes pending task if they were created today
let isOlderNote = mAnyTaskDate.diff(mDailyDate) < 0 // task date - note date
let containsDateDueColonColon = t.text.match('due:: ' + DateWithDashes)
return (
isOlderNote
&& !t.completed
&& !containsDateDueColonColon
)
}
/**
tasksDueToday()
Shows only tasks that are marked due today in the Daily note
CRITERIA
--------
* Task text must not be empty
* Task must NOT be completed
* Task header path corresponds to today's daily note
* Task must be older than current daily note
@param {task} t - An Obsidian task
@returns {boolean} true if matching all conditions, false otherwise
*/
function tasksDueToday(t) {
let taskTextNotEmpty = t.text.length > 0;
let containsDateDueColonColon = false;
let includesDateDueColonColon = false
let DateWithDashes = moment(self.file.name).format('YYYY-MM-DD')
// includesDateDueColonColon = t.text.includes(DateWithDashes)
if (t.due) {
// console.log(DateWithDashes)
containsDateDueColonColon = t.text.match('due:: ' + DateWithDashes)
// console.log(containsDateDueColonColon)
}
return (
taskTextNotEmpty
&& t.due
&& !t.completed
//&& includesDateDueColonColon
&& containsDateDueColonColon
)
}
/**
* Take a task and the page it's from, and return a formatted element for the tasks array
* @param {*} task
* @param {*} page
* @returns {object}
*/
function generateTaskElement(task, page) {
// console.log(task)
let group = Groups.Normal
// if (task.tags.includes('#someday')) {
if (isStartedToday(task)) {
group = Groups.Started
} else if (isCompletedToday(task)) {
group = Groups.Completed
} else if (isOlderThanThisDailyNote(task)) {
group = Groups.Pending
} else if (tasksDueToday(task)) {
group = Groups.Due
}
return {
task: task,
date: (page.created ? page.created.ts || moment(page.created).valueOf() : null) || page.ctime.ts,
group: group
}
}
/*
* Process tasks
*/
dv.pages("-#exclude" + globalExcludeString)
.where(p => p.file.tasks.length && !p['kanban-plugin'] && !p['exclude_master_tasklist']).file
.forEach(page => {
page.tasks
.where(t =>
t.text && // where the task has text (is not blank)
// !t.completed && // and not completed // 1063
// t.completed && // 1004
!t.tags.includes('#exclude') && // and not excluded
(!t.header.subpath || !t.header.subpath.includes('exclude')) &&
!globalExclude.headings.includes(t.header.subpath) // and the heading is not excluded
)
.forEach(task => tasks.push(generateTaskElement(task, page)))
})
// Sort tasks into groups, then ascending by created time
tasks.sort((a, b) => a.group - b.group || a.date - b.date)
// console.log(tasks)
/**
* Output a formatted task list
* @param {number|null} group - Filter for tasks in a particular group, or null for all tasks
* @param {string|null} header - The text header for the task list
*/
function taskList(group, header) {
const list = isNaN(group) ? tasks : tasks.filter(x => x.group === group)
if (list.length) {
if (header) dv.header(2, header)
// dv.taskList(list.map(x => x.task), false)
dv.taskList(list.map(x => x.task), true)
return list.length
} else return 0
}
let todayWithDashes = "*" + moment(self.file.name).format('YYYY-MM-DD') + "*"
// Output the task list
let nStarted = taskList(Groups.Started, 'Started today: ' + todayWithDashes)
let nCompleted = taskList(Groups.Completed, 'Completed today: ' + todayWithDashes)
let nPending = taskList(Groups.Pending, 'Still Pending Tasks Today: ' + todayWithDashes)
let nDue = taskList(Groups.Due, 'Due Today: ' + todayWithDashes)
if (nStarted == 0 && nCompleted == 0 && nPending == 0 && nDue == 0) {
dv.header(2, "No tasks to report")
}