Skip to content

Instantly share code, notes, and snippets.

@ebullient
Last active October 28, 2024 01:44
Show Gist options
  • Select an option

  • Save ebullient/ca2533ca1fb0d5d40b77036e1fcec2b5 to your computer and use it in GitHub Desktop.

Select an option

Save ebullient/ca2533ca1fb0d5d40b77036e1fcec2b5 to your computer and use it in GitHub Desktop.

Revamp of https://gist.github.com/ebullient/41d2e9159e32e6a16510836c33d9df0f without Dataview

I'm using JS Engine with CustomJS to replace what Dataview used to do.

Scripts

CustomJS files:

  • _utils.js - general utility functions
  • activity.js - activity charts
  • cmd-all-tasks.js - CustomJS command; create/update a note that embeds Tasks sections from some notes
  • cmd-missing.js - CustomJS command; evaluate contents of vault, and update a note to list missing links and unreferenced files
  • cmd-task-cleanup.js - CustomJS command; evaluate contents of vault. Find tasks completed earlier than this month, and remove their task status (✔️ or 〰️)
  • dated.js - Working with dated notes: daily notes for time blocking, weekly notes for planning, monthly for goals/reflection, years for "dates to remember"
  • priority.js - Functions for working with PARA-esque Projects and Areas, including working with role/priority/status/etc.
  • tasks.js - Functions for working with tasks (specifically, finding the tasks in a file, or collecting a list of tasks completed in a given week)
  • templater.js - Functions that augment templater templates (choosing values for prompts, transporting text

Templates

All templates rely on the Templater plugin + CustomJS

  • [conversation-day.md] - create conversation: prompt to choose file (person or org), create new dated section for notes; embed link to that section in the daily note
  • [conversation-push.md] - push content from the current note to other notes (as a section, a task, or a log item)
  • new-area.md - prompt: name, folder, status, urgent, important, role
  • new-note.md - prompt: name, folder
  • new-project.md - prompt: name, folder, status, urgent, important, role
  • daily.md - daily plan (for use with Day Planner OG)
  • weekly.md - weekly plan (where most actual planning is)
  • weekly-leftovers.md - collects leftover tasks from previous week (based on current file date)
  • monthly.md - monthly goals/reflection
  • yearly.md - not shown, but this is a simple file w/ months as headings
class Utils {
taskPattern = /^([\s>]*- )\[(.)\] (.*)$/;
completedPattern = /.*\((\d{4}-\d{2}-\d{2})\)\s*$/;
dailyNotePattern = /^(\d{4}-\d{2}-\d{2}).md$/;
pathConditionPatterns = [
/^\[.*?\]\((.*?\.md)\)$/, // markdown link
/^\[\[(.*?)\|?([^\]]*)??\]\]$/, // wikilink
/^(.*?\.md)$/ // file path
]
constructor() {
this.app = window.customJS.app;
console.log("loaded Utils");
}
/**
* Cleans a link reference by removing the title and extracting the anchor.
* @param {LinkReference} linkRef Link reference returned from this.app.metadataCache.getFileCache(tfile).links().
* @returns {Object} An object with link display text, the block or heading reference (if any), and plain link.
*/
cleanLinkTarget = (linkRef) => {
let link = linkRef.link;
// remove/drop title: vaultPath#anchor "title" -> vaultPath#anchor
const titlePos = link.indexOf(' "');
if (titlePos >= 0) {
link = link.substring(0, titlePos);
}
// extract anchor and decode spaces: vaultPath#anchor -> anchor and vaultPath
const anchorPos = link.indexOf('#');
const anchor = (anchorPos < 0 ? '' : link.substring(anchorPos + 1).replace(/%20/g, ' ').trim());
link = (anchorPos < 0 ? link : link.substring(0, anchorPos)).replace(/%20/g, ' ').trim();
return {
text: linkRef.displayText,
anchor,
link
}
}
/**
* Create a filter function to evaluate a list of conditions
* - AND/OR logic can be specified at the beginning of the list
* - #tag
* - path/to/file.md, [[wikilink]], [markdown link](path/to/file.md)
* @param {string|Array<string>} conditions
* @returns {tfileFilterFn} Function to apply to evaluate conditions
*/
createConditionFilter = (conditions) => {
if (!Array.isArray(conditions)) {
conditions = [conditions];
}
let logic = "OR";
if (conditions[0].match(/^(AND|OR)$/i)) {
logic = conditions[0].toUpperCase();
conditions = conditions.slice(1);
}
const tags = [];
const paths = [];
conditions.forEach(o => o.startsWith('#')
? tags.push(o)
: paths.push(o));
const files = paths
? paths.map(p => this.stringConditionToTFile(p))
: [];
return (tfile) => {
const tagMatch = tags.length > 0
? this.filterByTag(tfile, tags, logic === "AND")
: false;
const fileMatch = files.length > 0
? this.filterByLinksToFiles(tfile, files, logic === "AND")
: false;
if (logic === "AND") {
// all conditions must be true
return (tags.length === 0 || tagMatch) && (files.length === 0 || fileMatch);
} else {
// Default: any condition can be true
return tagMatch || fileMatch;
}
}
}
/**
* Generates a markdown list item with a markdown-style link for the given file.
* @param {TFile} tfile The file to examine.
* @returns {string} A markdown list item with a markdown-style link for the file.
*/
fileListItem = (tfile) => {
return `- ${this.markdownLink(tfile)}`;
}
/**
* Retrieves a list of file paths for all Markdown files in the vault (used for prompts).
* @returns {Array} A list of file paths for all Markdown files in the vault.
*/
filePaths = () => {
return this.app.vault.getMarkdownFiles()
.map(x => x.path);
}
/**
* Retrieves all tags found in the frontmatter and body of the file without the leading hash.
* @param {TFile} tfile The file to examine.
* @returns {Array} A list of tags found in the frontmatter and body of the file without the leading hash.
* @see removeLeadingHashtag
*/
fileTags = (tfile) => {
const cache = this.app.metadataCache.getFileCache(tfile);
if (!cache) {
return [];
}
const tags = new Set();
if (cache.tags) {
cache.tags
.filter(x => x != null || typeof x === 'string')
.map(x => this.removeLeadingHashtag(x.tag))
.forEach(x => tags.add(x));
}
if (cache.frontmatter?.tags) {
cache.frontmatter.tags
.filter(x => x != null || typeof x === 'string')
.map(x => this.removeLeadingHashtag(x))
.forEach(x => tags.add(x));
}
return [...tags];
}
/**
* Generates a title using either the first alias or the file name (without the .md extension).
* @param {TFile} tfile The file to examine.
* @returns {string} A title using either the first alias or the file name (without the .md extension).
*/
fileTitle = (tfile) => {
const cache = this.app.metadataCache.getFileCache(tfile);
const aliases = cache?.frontmatter?.aliases;
return aliases ? aliases[0] : tfile.name.replace('.md', '');
}
/**
* Retrieves a list of all files that have links to the target file.
* @param {TFile} targetFile The target file to match.
* @returns {Array} A list of all files that have links to the target file.
* @see filesMatchingCondition
* @see filterByLinkToFile
*/
filesLinkedToFile = (targetFile) => {
return this.filesMatchingCondition(tfile => this.filterByLinkToFile(tfile, targetFile));
}
/**
* Retrieves a list of all markdown files (excluding the current file) that match the provided filter function.
* @param {Function} tfileFilterFn Filter function that accepts TFile as a parameter.
* Should return true if the condition is met, and false if not (standard JS array filter behavior).
* @returns {Array} A list of all markdown files (excluding the current file) that match the provided filter function.
*/
filesMatchingCondition = (tfileFilterFn) => {
const current = this.app.workspace.getActiveFile();
return this.app.vault.getMarkdownFiles()
.filter(tfile => tfile !== current)
.filter(tfile => tfileFilterFn(tfile))
.sort(this.sortTFile);
}
/**
* Retrieves a list of all files with the specified tag.
* @param {string|Array} conditions Either a string or an array of strings.
* By default, the array will act as an OR.
* The first element of the array can change that behavior (AND|OR).
* @returns {Array} A list of all files satisfyng specified conditions
* @see createConditionFilter
* @see filesMatchingCondition
*/
filesWithConditions = (conditions) => {
const conditionsFilter = this.createConditionFilter(conditions);
return this.filesMatchingCondition(tfile => conditionsFilter(tfile));
}
/**
* Retrieves a list of all files whose path matches the provided path pattern.
* @param {RegExp} pathPattern The pattern to match against the file path.
* @returns {Array} A list of all files whose path matches the provided path pattern.
* @see filesMatchingCondition
* @see filterByPath
*/
filesWithPath = (pathPattern) => {
return this.filesMatchingCondition(tfile => this.filterByPath(tfile, pathPattern));
}
/**
* Checks if the file has a link to the target file.
* @param {TFile} tfile The file to examine.
* @param {TFile} targetFile The link target to match.
* @returns {boolean} True if the file has a link to the target file.
*/
filterByLinkToFile = (tfile, targetFile) => {
if (tfile.path === targetFile.path) {
return false;
}
const fileCache = this.app.metadataCache.getFileCache(tfile);
if (!fileCache?.links) {
return false;
}
return fileCache.links
.filter(link => !link.link.match(/^(http|mailto|view-source)/))
.map(link => this.cleanLinkTarget(link))
.some(cleanedLink => {
const linkTarget = this.pathToFile(cleanedLink.link, tfile.path);
return targetFile.path === linkTarget?.path;
});
}
/**
* Checks if the file has a link to one of the target files.
* @param {TFile} tfile The file to examine.
* @param {Array<TFile>} targetFiles A list of possible link targets
* @param {boolean} all True if links to all files should be present (AND);
* false (default) if links to any of the files should be present (OR).
* @returns {boolean} True if the file has a link to one of the target files.
*/
filterByLinksToFiles = (tfile, targetFiles, all = false) => {
const fileCache = this.app.metadataCache.getFileCache(tfile);
if (!fileCache?.links) {
return false;
}
const links = fileCache.links
.filter(link => !link.link.match(/^(http|mailto|view-source)/))
.map(link => this.cleanLinkTarget(link))
.map(cleanedLink => this.pathToFile(cleanedLink.link, tfile.path));
return all
? targetFiles.every(t => links.some(linkTarget => t.path === linkTarget?.path))
: targetFiles.some(t => links.some(linkTarget => t.path === linkTarget?.path));
}
/**
* Checks if the file path matches the pattern.
* @param {TFile} tfile The file to examine.
* @param {RegExp} pathPattern The pattern to match against the file path.
* @returns {boolean} True if the file path matches the pattern.
*/
filterByPath = (tfile, pathPattern) => {
return pathPattern.test(tfile.path);
}
/**
* Checks if the required tags are present.
* @param {TFile} tfile The file to examine.
* @param {string|Array} tag A string or array of tag values.
* @param {boolean} all True if all tags should be present (AND);
* false (default) if any of the tags should be present (OR).
* @returns {boolean} True if the required tags are present.
*/
filterByTag = (tfile, tag, all = false) => {
const fileTags = this.fileTags(tfile);
// for an array, use the "all" parameter
if (Array.isArray(tag)) {
const tagRegexes = tag.map(this.tagFilterRegex);
if (all) {
// AND: Every tag should be present in fileTags
return tagRegexes.every(regex => fileTags.some(ftag => regex.test(ftag)));
} else {
// OR: At least one tag should be present in fileTags
return tagRegexes.some(regex => fileTags.some(ftag => regex.test(ftag)));
}
}
// single string: return true if tag is present
const tagRegex = this.tagFilterRegex(tag);
return fileTags.some(ftag => tagRegex.test(ftag));
}
/**
* Creates a markdown list of all files contained within the current folder grouped by subdirectory.
* @param {JSEngine} engine The engine to create markdown.
* @returns {string} A markdown list of all files contained within the current folder grouped by subdirectory.
* @see filesWithPath
* @see index
*/
folderIndex = (engine) => {
const current = this.app.workspace.getActiveFile();
const path = current.parent.path;
const list = this.filesWithPath(new RegExp(`^${path}`));
return this.index(engine, list);
}
/**
* Retrieve a list of folders that are children of the provided
* folder and match the given conditions
* @param {string} folder The initial folder path to filter.
* @param {Function} tfolderFilterFn Filter function that accepts TFolder as a parameter.
* Should return true if the condition is met, and false if not (standard JS array filter behavior).
* @returns {Array} A list of all folders contained within the provided folder
* that match the given conditions.
*/
foldersByCondition = (folder, tfolderFilterFn = (_) => true) => {
let folders = [];
if (!folder || folder === "/") {
folders = this.app.vault.getAllFolders(true)
.filter(tfolder => tfolderFilterFn(tfolder));
} else {
const pathFilter = new RegExp(`^${folder}(\\/|$)`);
folders = this.app.vault.getAllFolders(false)
.filter(tfolder => pathFilter.test(tfolder.path))
.filter(tfolder => tfolderFilterFn(tfolder));
}
return folders.sort((a, b) => a.path.localeCompare(b.path));
}
/**
* Retrieves the frontmatter of a file.
* @param {TFile} tfile The file to examine.
* @returns {Object} The frontmatter or an empty object.
*/
frontmatter = (tfile) => {
const cache = this.app.metadataCache.getFileCache(tfile);
return cache?.frontmatter || {};
}
/**
* Groups elements in a collection by the specified condition.
* @param {Array} collection The collection to group.
* @param {Function} fn The function that defines keys for an object.
* @returns {Object} An object with keys generated by the function and array values that match the key.
*/
groupBy = (collection, fn) => collection
.reduce((accumulator, currentElement, index) => {
// Determine the key for the current element using the provided function
const key = fn(currentElement, index, collection);
// Initialize the array for this key if it doesn't exist
if (!accumulator[key]) {
accumulator[key] = [];
}
// Push the current element into the array for this key
accumulator[key].push(currentElement);
// Return the accumulator for the next iteration
return accumulator;
}, {});
/**
* Groups files by parent path, and then creates a simple sorted list of the files in each group.
* @param {JSEngine} engine The engine to create markdown.
* @param {Array} fileList An array of TFiles to list.
* @returns {string} A markdown list of files grouped by parent path.
*/
index = (engine, fileList) => {
const groups = this.groupBy(fileList, tfile => tfile.parent.path);
const keys = Object.keys(groups).sort();
const result = [];
for (const key of keys) {
const value = groups[key]
.sort((a, b) => {
if (this.isFolderNote(a)) {
return -1;
}
if (this.isFolderNote(b)) {
return 1;
}
return this.sortTFile(a, b);
});
result.push(`\n**${key}**\n`);
for (const v of value) {
const note = this.isFolderNote(v) ? ' <small>(index)</small>' : '';
result.push(`- ${this.markdownLink(v)}${note}`);
}
}
return engine.markdown.create(result.join("\n"));
}
/**
* Checks if the file is a folder note (same name as parent folder or README.md).
* @param {TFile} tfile The file to examine.
* @returns {boolean} True if the file is a folder note.
*/
isFolderNote = (tfile) => {
// allow for GH-style README.md folder notes, too
return tfile.path === `${tfile.parent.path}/${tfile.parent.name}.md`
|| tfile.path === `${tfile.parent.path}/README.md`;
}
/**
* Creates a markdown list of files with the specified tag.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} conditions The conditions to apply.
* @returns {string} A markdown list of files grouped by parent path.
* @see filesMatchingCondition
* @see fileListItem
*/
listFilesMatchingConditions = (engine, condition) => {
const files = this.filesMatchingCondition(condition);
return engine.markdown.create(files
.map(f => this.fileListItem(f))
.join("\n"));
}
/**
* Creates a markdown list of files with the specified tag.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} conditions The conditions to apply.
* @returns {string} A markdown list of files grouped by parent path.
* @see filesWithPath
*/
listFilesWithPath = (engine, pathPattern) => {
const files = this.filesWithPath(pathPattern);
return engine.markdown.create(files
.map(f => this.fileListItem(f))
.join("\n"));
}
/**
* Creates a markdown list of files that link to the current file.
* @param {JSEngine} engine The engine to create markdown.
* @returns {string} A markdown list of files that link to the current file.
* @see filesLinkedToFile
* @see markdownLink
*/
listInboundLinks = (engine) => {
const current = this.app.workspace.getActiveFile();
const files = this.filesLinkedToFile(current);
return engine.markdown.create(files
.map(f => this.fileListItem(f))
.join("\n"));
}
/**
* Converts a name to lower kebab case.
* @param {string} name The name to convert.
* @returns {string} The name converted to lower kebab case.
*/
lowerKebab = (name) => {
return (name || "")
.replace(/([a-z])([A-Z])/g, '$1-$2') // separate on camelCase
.replace(/[\s_]+/g, '-') // replace all spaces and low dash
.replace(/[^0-9a-zA-Z_\-]/g, '') // strip other things
.toLowerCase(); // convert to lower case
}
/**
* Generates a markdown link to the file.
* @param {TFile} tfile The file to examine.
* @returns {string} A markdown link to the file.
* @see markdownLinkPath
* @see fileTitle
*/
markdownLink = (tfile, anchor = '') => {
return `[${this.fileTitle(tfile)}](/${this.markdownLinkPath(tfile, anchor)})`;
}
/**
* Generates a path with spaces replaced by %20.
* @param {TFile} tfile The file to examine.
* @returns {string} The path with spaces replaced by %20.
*/
markdownLinkPath = (tfile, anchor = '') => {
anchor = anchor ? '#' + anchor : '';
return (tfile.path + anchor).replaceAll(' ', '%20') ;
}
/**
* Generates a markdown list item with the first path segment as a small prefix followed by a markdown link to the file.
* @param {string} displayText Display text for link
* @param {string} linkTarget link target
* @returns {string} A markdown list item with a markdown link
*/
markdownListItem = (displayText, link) => {
return `- [${displayText}](${link})`;
}
/**
* Try to resolve the file for the given path
* based on the starting file
* @param {string} path
* @param {string} startPath
* @returns {TFile|null} TFile for the path if it exists
*/
pathToFile = (path, startPath) => {
return this.app.metadataCache.getFirstLinkpathDest(path, startPath);
}
/**
* Removes the leading # from a string if present.
* @param {string} str The string to process.
* @returns {string} The string with the leading # removed, if it was present.
*/
removeLeadingHashtag = (str) => {
if (typeof str !== 'string') {
return str
}
return str.charAt(0) === "#"
? str.substring(1)
: str;
}
/**
* Generates a markdown list item with the first path segment as a small prefix followed by a markdown link to the file.
* @param {TFile} tfile The file to examine.
* @returns {string} A markdown list item with the first path segment as a small prefix followed by a markdown link to the file.
*/
scopedFileListItem = (tfile) => {
const s1 = tfile.path.split('/')[0];
return `- <small>(${s1})</small> ${this.markdownLink(tfile)}`;
}
/**
* Creates a markdown list of files with the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} conditions The conditions to apply.
* @returns {string} A markdown list of files with the first path segment as leading text.
* @see filesMatchingCondition
* @see scopedFileListItem
*/
scopedFilesWithConditions = (engine, conditions) => {
const files = this.filesWithConditions(conditions);
return engine.markdown.create(files
.map(f => this.scopedFileListItem(f))
.join("\n"));
}
/**
* Generates a markdown list item with small scope text
* @param {string} scope Scope of the list item
* @param {string} displayText Display text for link
* @param {string} link Link target
* @returns {string} A markdown list item with small text indicating the scope
*/
scopedListItem = (scope, displayText, link) => {
return `- <small>(${scope})</small> [${displayText}](${link})`;
}
/**
* Create a regex to match a string by segments.
* For example, given a/b, a/b/c would match but a/bd would not
* @param {*} str
*/
segmentFilterRegex = (str) => {
return new RegExp(`^${str}(\\/|$)`);
}
/**
* String condition describing a target file:
* - [[]] current file
* - wikilink, markdown link, or "***.md" path
* @param {string} str
* @returns {TFile|null|undefined} Return file if syntax recognized and file is found;
* return null if syntax recognized and file not found;
* return undefined if syntax not recognized
*/
stringConditionToTFile = (str) => {
const current = this.app.workspace.getActiveFile();
if (str === "[[]]") {
return current;
}
for(const regexp of this.pathConditionPatterns) {
const match = regexp.exec(str);
if (match) {
const result = this.pathToFile(match[1], current.path);
if (result == null) {
console.error("Unable to find file used in condition", str, match[1], "from", current.path);
}
return result;
}
}
console.error("Unknown condition (not a markdown link, wiki link, or markdown file path)", str);
return undefined;
}
/**
* Compares the first segment of the path; if those are equal then compares by file name.
* If a 'sort' field is present in the frontmatter, it is prepended to the filename
* (not to the path).
* This allows for a simple grouping of files by parent folder (see scoped* methods)
* that are alphabetized by name beyond the highest-level folder.
* @param {TFile} a The first file to compare.
* @param {TFile} b The second file to compare.
* @returns {number} A negative number if `a` should come before `b`, a positive number if `a` should come after `b`, or 0 if they are considered equal.
*/
sortTFile = (a, b) => {
const p1 = a.path.split('/')[0].toLowerCase();
const p2 = b.path.split('/')[0].toLowerCase();
const p = p1.localeCompare(p2);
return p === 0
? this.sortTFileByName(a, b)
: p;
}
/**
* Compares by file name.
* If a 'sort' field is present in the frontmatter, it is prepended to the filename
* @param {TFile} a The first file to compare.
* @param {TFile} b The second file to compare.
* @returns {number} A negative number if `a` should come before `b`, a positive number if `a` should come after `b`, or 0 if they are considered equal.
*/
sortTFileByName = (a, b) => {
const sort1 = this.frontmatter(a).sort || '';
const sort2 = this.frontmatter(b).sort || '';
const n1 = sort1 + this.fileTitle(a).toLowerCase();
const n2 = sort2 + this.fileTitle(a).toLowerCase();
return n1.localeCompare(n2);
}
/**
* Looks for daily notes within the chronicles directory.
* If the note is for a day within the provided range, adds its tags
* to the collected list (all tags, not a set).
* @param {moment} begin The beginning date (inclusive).
* @param {moment} end The ending date (inclusive).
* @returns {Array} A list of tags found in daily notes for the date range.
*/
tagsForDates = async (begin, end) => {
return app.vault.getMarkdownFiles()
.filter((f) => f.path.includes("chronicles") && f.name.match(/^\d{4}-\d{2}-\d{2}\.md$/))
.filter((f) => {
const day = moment(f.name.replace('.md', ''));
return day.isSameOrAfter(begin, 'day') && day.isSameOrBefore(end, 'day');
})
.flatMap(element => {
return this.fileTags(element);
});
}
/**
* Create a filter matching nested tags
* @param {String} tag
* @returns Regular expression to match nested/segmented tags
* @see segmentFilterRegex
*/
tagFilterRegex = (tag) => {
const cleanedTag = this.removeLeadingHashtag(tag);
return this.segmentFilterRegex(cleanedTag);
}
}
class Activity {
activities = {
'✨': ['✨', '🍀'],
'🔴': ['🔴'],
'🔵': ['🔵'],
'💚': ['🟢', '🏊', '💃']
};
colors = {
1: '187, 79, 108',
4: '142, 103, 135',
12: '53, 120, 175',
48: '61, 126, 123',
};
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
console.log("loaded Templates");
}
/**
* Counts the tags within the specified date range.
* @param {moment} begin The beginning date (inclusive).
* @param {moment} end The ending date (inclusive).
* @returns {Promise<Object>} An object with activity keys (labels) and
* the count for each activity found between the starting and ending dates.
* @see utils.tagsForDates
*/
countTags = async (begin, end) => {
let count = [0, 0, 0, 0];
const tags = await this.utils.tagsForDates(begin, end);
const keys = Object.keys(this.activities);
tags.forEach(tag => {
for (let i = 0; i < keys.length; i++) {
const parts = tag.split('/');
const value = parts[parts.length - 1];
if (this.activities[keys[i]].includes(value)) {
count[i]++;
continue;
}
}
});
return {
activities: keys,
count
};
}
/**
* Renders a radar chart in the specified container.
* @param {HTMLElement} container The container to render the chart in.
* @param {Array<string>} labels The labels for the radar chart.
* @param {Object} series The data series for the radar chart.
*/
renderRadarChart = (container, labels, series) => {
const chartOptions = {
type: 'radar',
data: {
labels,
datasets: []
},
options: {
scales: {
r: {
min: 0,
max: 7,
angleLines: {
color: '#898989'
},
grid: {
color: '#898989'
}
}
}
}
};
for (const [factor, input] of Object.entries(series)) {
// normalize/average
if (factor > 1) {
for (let i = 0; i < input.length; i++) {
if (input[i] > 0) {
input[i] = input[i] / 7;
}
}
}
chartOptions.data.datasets.push({
label: `${factor} Week(s)`,
data: input,
backgroundColor: `rgba(${this.colors[factor]}, 0.1)`,
borderColor: `rgb(${this.colors[factor]})`,
borderWidth: 2
});
}
const chartData = {
chartOptions: chartOptions,
width: '80%',
};
window.renderChart(chartData, container);
}
/**
* Renders a bar chart in the specified container.
* @param {HTMLElement} container The container to render the chart in.
* @param {Object} input The input data for the bar chart.
* @param {number} factor The factor to normalize the data.
*/
renderBarChart = (container, input, factor) => {
if (factor > 1) {
for (let i = 0; i < input.count.length; i++) {
if (input.count[i] > 0) {
input.count[i] = input.count[i] / 7;
}
}
}
const chartOptions = {
type: 'bar',
data: {
labels: input.activities,
datasets: [{
label: 'Activities',
data: input.count,
backgroundColor: [
'rgb(187, 79, 108)',
'rgb(142, 103, 135)',
'rgb(53, 120, 175)',
'rgb(61, 126, 123)',
'rgb(92, 122, 99)',
'rgb(234, 175, 0)'
],
beginAtZero: true,
borderWidth: 1
}]
},
options: {
animation: false,
events: ['click'],
transitions: {
active: {
animation: {
duration: 0
}
}
},
scales: {
y: {
min: 0,
max: 7,
grid: {
color: 'rgb(183, 183, 183)'
}
}
},
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
}
}
}
};
const chartData = {
chartOptions: chartOptions,
width: '80%'
};
window.renderChart(chartData, container);
}
/**
* Creates a report with charts for the specified date ranges.
* @param {JSEngine} engine The engine to create markdown (not used).
* @returns {Promise<HTMLElement>} A promise that resolves to an HTML element containing the report.
*/
createReport = async (engine) => {
const current = moment();
const monday = moment(current).day(1);
const sunday = moment(current).day(7);
const prev4 = moment(monday).subtract(4, 'weeks');
const prev12 = moment(monday).subtract(12, 'weeks');
const prev48 = moment(monday).subtract(48, 'weeks');
const dataWeek = await this.countTags(monday, sunday);
const last4 = await this.countTags(prev4, monday);
const last12 = await this.countTags(prev12, monday);
const last48 = await this.countTags(prev48, monday);
const container = createEl('div');
let chart = container.createEl('div');
this.renderRadarChart(chart, Object.keys(this.activities),
{
1: dataWeek.count,
4: last4.count,
12: last12.count,
48: last48.count
});
container.createEl('h2').setText('This Week');
chart = container.createEl('div');
this.renderBarChart(chart, dataWeek, 1);
container.createEl('h2').setText('Last 4 weeks (average)');
chart = container.createEl('div');
this.renderBarChart(chart, last4, 4);
container.createEl('h2').setText('Last 12 weeks (average)');
chart = container.createEl('div');
this.renderBarChart(chart, last12, 12);
container.createEl('h2').setText('Last 48 weeks (average)');
chart = container.createEl('div');
this.renderBarChart(chart, last48, 12);
return container;
}
}
class AllTasks {
RENDER_TASKS = /([\s\S]*?<!--\s*ALL TASKS BEGIN\s*-->)[\s\S]*?(<!--\s*ALL TASKS END\s*-->[\s\S]*?)/i;
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
this.targetFile = "all-tasks.md";
// add additional files to ignore here
this.includePaths = [
'demesne/',
'quests/'
]
this.ignoreFiles = [
this.targetFile,
];
}
/**
* Find all "Tasks" sections in the specified paths.
* Replace the TASKS section of of the "All Tasks" file with the
* list of embedded sections sorted by status and priority
* @returns {Promise<void>} A promise that resolves when the operation is complete.
*/
async invoke() {
console.log("Finding all tasks");
const ap = window.customJS.AreaPriority;
const allTasks = this.app.vault.getAbstractFileByPath(this.targetFile);
if (!allTasks) {
console.log(`${this.targetFile} file not found`);
return;
}
// Find all markdown files that are not in the ignore list
const text = this.app.vault.getMarkdownFiles()
.filter(x => this.includePaths.some(p => x.path.startsWith(p)))
.filter(x => !this.ignoreFiles.includes(x.path))
.map((file) => {
const fileCache = this.app.metadataCache.getFileCache(file);
if (!fileCache.headings || !fileCache.frontmatter) {
return null;
}
const taskHeading = fileCache.headings.find(x => x.heading.endsWith("Tasks"));
if (!taskHeading) {
return null;
}
if (fileCache.frontmatter.status == "ignore") {
return null;
}
// 0 1 2
return [file, taskHeading];
})
.filter(x => x != null)
.sort((a, b) => this.sortProjects(a, b))
.map(([file, taskHeading]) => {
const priority = ap.filePriority(file);
const status = ap.fileStatus(file);
const role = ap.fileRole(file);
const title = this.utils.fileTitle(file);
return `\n#### <span class="project-status">[${status}&nbsp;${priority}&nbsp;${role}](${file.path}#${taskHeading.heading})</span> ${title}\n\n![invisible-embed](${file.path}#${taskHeading.heading})\n`;
})
.join("\n");
await this.app.vault.process(allTasks, (source) => {
let match = this.RENDER_TASKS.exec(source);
if (match) {
source = match[1];
source += text;
source += match[2];
}
return source;
});
}
/**
* Sorts projects based on priority, status, group, and name.
* @param {Array} a The first project to compare.
* @param {Array} b The second project to compare.
* @returns {number} A negative number if a should come before b,
* a positive number if a should come after b, or
* 0 if they are considered equal.
*/
sortProjects = (a, b) => {
const ap = window.customJS.AreaPriority;
const fm1 = this.utils.frontmatter(a[0]);
const fm2 = this.utils.frontmatter(b[0]);
return ap.testPriority(fm1, fm2,
() => ap.test(fm1, fm2, ap.status, 'status',
() => this.testGroup(fm1, fm2,
() => ap.testName(a[0], b[0]))));
};
/**
* Compares two files based on their group and a fallback function.
* @param {Object} fm1 The frontmatter of the first file.
* @param {Object} fm2 The frontmatter of the second file.
* @param {Function} fallback The fallback function to use if the group values are equal.
* @returns {number} A negative number if tfile1 should come before tfile2,
* a positive number if tfile1 should come after tfile2, or
* the result of the fallback function if they are considered equal.
*/
testGroup = (fm1, fm2, fallback) => {
let test1 = fm1.group;
let test2 = fm2.group;
if (test1 == test2) {
return fallback();
}
// Handle cases where either test1 or test2 is undefined
if (test1 === undefined) {
return 1; // if test1 is undefined, it moves toward the beginning
} else if (test2 === undefined) {
return -1; // if test2 is undefined, it moves toward the end
}
// Compare the values (as strings) if both are defined
return test1.localeCompare(test2);
}
}
class Missing {
RENDER_MISSING = /([\s\S]*?<!--MISSING BEGIN-->)[\s\S]*?(<!--MISSING END-->[\s\S]*?)/i;
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
this.targetFile = "assets/no-sync/missing.md";
// add additional files to ignore here
this.ignoreFiles = [
this.targetFile,
"${result.lastSession}",
"${result}",
"${file.path}",
"${y[0].file.path}",
"/${p.file.path}",
"assets/templates/periodic-daily.md",
"assets/Publications.bib",
"assets/Readit.bib",
"assets/birthdays.json",
"quests/obsidian-theme-plugins/ebullientworks-theme/links.md",
"quests/obsidian-theme-plugins/ebullientworks-theme/obsidian-theme-test.md"
];
this.ignoreAnchors = [
"callout", "portrait"
]
}
/**
* Converts a file path to a markdown link.
* @param {string} path The file path to convert.
* @returns {string} A markdown link for the specified file path.
*/
pathToMdLink(path) {
return `[${path}](${path.replace(/ /g, '%20')})`;
}
/**
* Find all missing references and update the target file with the results.
* @returns {Promise<void>}
*/
async invoke() {
console.log("Finding lost things");
const missing = this.app.vault.getAbstractFileByPath(this.targetFile);
if (!missing) {
console.log(`${this.targetFile} file not found`);
return;
}
// create a map of not-markdown/not-canvas files that could be referenced
// ignore templates and regex files
const fileMap = {};
this.app.vault.getFiles()
.filter(x => !x.path.endsWith('.canvas'))
.filter(x => !x.path.endsWith('.md'))
.filter(x => !x.path.contains('assets/regex'))
.filter(x => !x.path.contains('assets/templates'))
.filter(x => !x.path.contains('assets/customjs'))
.filter(x => !x.path.contains('assets/enginejs'))
.filter(x => !this.ignoreFiles.includes(x.path))
.forEach((f) => {
fileMap[f.path] = 1;
});
const init = JSON.parse(JSON.stringify(fileMap));
console.log("Initial fileMap:", init);
// Find all markdown files that are not in the ignore list
const files = this.app.vault.getFiles()
.filter(x => x.path.endsWith('.md'))
.filter(x => !this.ignoreFiles.includes(x.path));
console.log("Finding lost things: reading files");
const leaflet = [];
const anchors = [];
const rows = [];
// read all the files and extract the text
const promises = files.map(file => this.app.vault.cachedRead(file)
.then((txt) => this.findReferences(txt, file, leaflet, rows, anchors, fileMap)));
await Promise.all(promises);
console.log("Finding lost things: writing result");
await this.renderMissing(missing, () => {
let result = '\n';
result += '## Missing reference\n';
result += this.renderTable(['Source', 'Target'], rows);
result += '\n';
result += '## Missing heading or block reference\n';
result += this.renderTable(['Source', 'Anchor', 'Target'], anchors);
result += '\n';
result += '## Missing leaflet reference\n';
result += this.renderTable(['Leaflet Source', 'Missing'], leaflet);
result += '\n';
result += '## Unreferenced Things\n';
const keys = Object.keys(fileMap).sort();
keys.filter(x => fileMap[x] != 0)
.filter(x => !x.endsWith('.md'))
.filter(x => {
if (x.contains('excalidraw')) {
const file = this.app.metadataCache.getFirstLinkpathDest(x.replace(/\.(svg|png)/, '.md'), x);
// only excalidraw images where drawing is MIA
return file == null;
}
return true;
}).forEach(x => {
result += `- ${this.pathToMdLink(x)}\n`;
});
return result;
});
console.log("Finding lost things: Done! 🫶 ");
}
/**
* Find references (links) in the text and update the file map.
* @param {string} txt The text to search for references.
* @param {TFile} file The file being processed.
* @param {Array} leaflet The array to store missing leaflet references.
* @param {Array} rows The array to store missing file references.
* @param {Array} anchors The array to store missing anchor references.
* @param {Object} fileMap The map of files to update.
*/
findReferences = (txt, file, leaflet, rows, anchors, fileMap) => {
const fileCache = this.app.metadataCache.getFileCache(file);
if (fileCache.embeds) {
fileCache.embeds.forEach((x) => this.findLink(file, x, rows, anchors, fileMap));
}
if (fileCache.links) {
fileCache.links.forEach((x) => this.findLink(file, x, rows, anchors, fileMap));
}
if (txt.contains('```leaflet')) {
// find all lines matching "image: (path to image)" and extract the image name
[...txt.matchAll(/image: (.*)/g)].forEach((x) => {
const imgName = x[1];
const tgtFile = this.app.metadataCache.getFirstLinkpathDest(imgName, file.path);
if (tgtFile == null) {
// The image this leaflet needs is missing
leaflet.push([this.pathToMdLink(file.path), imgName]);
} else {
// We found the image,
fileMap[tgtFile.path] = 0;
}
});
}
}
/**
* Finds a link in the file and updates the file map.
* @param {TFile} file The file being processed.
* @param {Object} x The link object to process.
* @param {Array} rows The array to store missing file references.
* @param {Array} anchors The array to store missing anchor references.
* @param {Object} fileMap The map of files to update.
* @returns {Promise<void>}
*/
findLink = async (file, x, rows, anchors, fileMap) => {
const now = moment();
let target = x.link;
// remove title: [link text](vaultPath "title") -> [link text](vaultPath)
const titlePos = target.indexOf(' "');
if (titlePos >= 0) {
target = target.substring(0, titlePos);
}
// remove anchor: [link text](vaultPath#anchor) -> [link text](vaultPath)
const anchorPos = x.link.indexOf('#');
const anchor = (anchorPos < 0 ? '' : target.substring(anchorPos + 1).replace(/%20/g, ' ').trim());
target = (anchorPos < 0 ? target : target.substring(0, anchorPos)).replace(/%20/g, ' ').trim();
// ignore external links and ignored files
if (target.startsWith('http')
|| target.startsWith('mailto')
|| target.startsWith('view-source')
|| this.ignoreFiles.includes(target)) {
return;
}
let match = /.*(\d{4}-\d{2}-\d{2})(_week)?\.md/.exec(target);
if (match != null) {
const filedate = moment(match[1]);
if (filedate.isAfter(now)) {
console.log("file for a future date", target);
return;
}
}
match = /.*(\d{4}-\d{2})_month\.md/.exec(target);
if (match != null) {
const filedate = moment(`${match[1]}-01`);
if (filedate.isAfter(now)) {
console.log("file for a future date", target);
return;
}
}
match = /.*(\d{4})\.md/.exec(target);
if (match != null) {
const filedate = moment(`${match[1]}-01-01`);
if (filedate.isAfter(now)) {
console.log("file for a future date", target);
return;
}
}
let tgtFile = file;
if (target) {
// find the target file
tgtFile = this.app.metadataCache.getFirstLinkpathDest(target, file.path);
if (tgtFile == null) {
console.debug(file.path, " has lost ", target);
rows.push([this.pathToMdLink(file.path), target]);
} else {
fileMap[tgtFile.path] = 0;
}
}
if (anchor && tgtFile) {
if (this.ignoreAnchors.includes(anchor)) {
return;
}
const tgtFileCache = this.app.metadataCache.getFileCache(tgtFile);
if (!tgtFileCache) {
console.log("MISSING:", tgtFile.path, "#", anchor, "from", file.path, tgtFileCache);
anchors.push([this.pathToMdLink(file.path), `--`, 'missing cache']);
} else if (anchor.startsWith('^')) {
const blockref = anchor.substring(1);
const tgtBlock = tgtFileCache.blocks ? tgtFileCache.blocks[blockref] : '';
if (!tgtBlock) {
console.log("MISSING:", tgtFile.path, "#^", blockref, "from", file.path, tgtFileCache.blocks);
anchors.push([this.pathToMdLink(file.path), `"#${anchor}"`, `"${x.link}"`]);
}
} else {
const lower = anchor.toLowerCase();
const tgtHeading = tgtFileCache.headings
? tgtFileCache.headings.find(x => lower == x.heading.toLowerCase()
.replace(/[?:]/g, '')
.replace('#', ' '))
: '';
if (!tgtHeading) {
console.log("MISSING:", tgtFile.path, "#", anchor, "from", file.path, tgtFileCache.headings);
anchors.push([this.pathToMdLink(file.path), `"#${anchor}"`, `"${x.link}"`]);
}
}
}
}
/**
* Render the missing references in the specified file.
* @param {TFile} file The file to update.
* @param {Function} renderer The function to generate the missing references content.
* @returns {Promise<void>}
*/
renderMissing = async (file, renderer) => {
await this.app.vault.process(file, (source) => {
let match = this.RENDER_MISSING.exec(source);
if (match) {
source = match[1];
source += renderer();
source += match[2];
}
return source;
});
}
/**
* Create a markdown table with the specified headers and rows.
* @param {Array<string>} headers The headers for the table.
* @param {Array<Array<string>>} rows The rows for the table.
* @returns {string} A markdown table with the specified headers and rows.
*/
renderTable = (headers, rows) => {
let result = '';
result += '|';
headers.forEach((h) => {
result += ` ${h} |`;
});
result += '\n';
result += '|';
headers.forEach(() => {
result += ' --- |';
});
result += '\n';
rows.forEach((r) => {
result += '|';
r.forEach((c) => {
result += ` ${c} |`;
});
result += '\n';
});
return result;
}
}
class TaskCleanup {
anyTaskMark = new RegExp(/^([\s>]*- )(\[(?:x|-)\]) (.*) \((\d{4}-\d{2}-\d{2})\)\s*$/);
app = window.customJS.app;
dailyNote = /^(\d{4}-\d{2}-\d{2}).md$/;
done = new RegExp(/^[\s>]*- (✔️|〰️) .*$/);
list = new RegExp(/^[\s>]*- .*$/);
/**
* Clean up (remove task nature of) old tasks in markdown files.
* @returns {Promise<void>}
*/
async invoke() {
console.log("Cleaning up old tasks");
let monthMoment = moment().startOf('month');
console.log("Cleaning up tasks before", monthMoment.format("(YYYY-MM-"));
// Map each file to the result of a cached read
const promises = this.app.vault.getMarkdownFiles()
.map((file) => {
if (file.name.match(this.dailyNote) || file.path.startsWith("assets")) {
return () => true;
}
const fileCache = this.app.metadataCache.getFileCache(file);
if (!fileCache.headings) {
return () => true;
}
const logHeading = fileCache.headings.find(x => x.heading.endsWith("Log"));
if (logHeading) {
return this.updateTasks(file, logHeading, monthMoment);
}
return () => true;
});
// wait for updates to all relevant files
await Promise.all(promises);
}
/**
* Update tasks in the Log section of the specified file if the file is older than the specified month.
* Relies on marked completed tasks: `- [x] task (YYYY-MM-DD)` or `- [-] task (YYYY-MM-DD)`
* @param {TFile} file The file to update.
* @param {Object} logHeading The log heading object containing position information.
* @param {moment} monthMoment The moment object representing the start of the month.
* @returns {Promise<void>}
*/
updateTasks = async (file, logHeading, monthMoment) => {
await this.app.vault.process(file, (source) => {
const split = source.split("\n");
let i = logHeading.position.start.line + 1;
for (i; i < split.length; i++) {
if (split[i].startsWith("#") || split[i] == "---") {
break;
}
if (split[i].match(this.list)) {
if (!!split[i].match(this.done)) {
break;
}
const taskMatch = this.anyTaskMark.exec(split[i]);
if (taskMatch) {
const mark = taskMatch[2];
const completed = moment(taskMatch[4]);
if (completed.isBefore(monthMoment)) {
if (mark == "[x]") {
split[i] = `${taskMatch[1]} ✔️ ${taskMatch[3]} (${taskMatch[4]})`;
} else {
split[i] = `${taskMatch[1]} 〰️ ~~${taskMatch[3]} (${taskMatch[4]})~~`;
}
}
}
}
}
return split.join("\n");
});
}
}

<%* const { Templates } = await window.cJS(); tR += await Templates.createConversation(tp); %>

<%* const { Templates } = await window.cJS(); await Templates.pushText(tp); %>

<%* const { Dated } = await window.cJS(); const result = Dated.daily(tp.file.title); await tp.file.move(result.dailyFile); const today = result.dates.day.isoWeekday(); -%><% result.header %> Goals for today

  • .

<%* if (1 <= today && today <= 5 ) { -%> %% agenda %%

Day Planner

%%

  • 🎉 Focused for the entire time block
  • 🎠 Got a little distracted %%

Morning

  • 07:30 Reflection / Planning
  • 08:00 Start : GH Notifications / Email
  • 08:45 BREAK / chat
  • 09:00 Start :
  • 09:45 BREAK / chat
  • 10:00 Start :
  • 10:45 BREAK / Sudo
  • 11:00 Start :
  • 11:45 Lunch

After Lunch

  • 12:30 Meditation
  • 12:45 BREAK / chat
  • 13:00 Start :
  • 13:45 BREAK / chat

Afternoon

  • 14:00 Start :
  • 14:45 BREAK / chat
  • 15:00 Start :
  • 15:45 BREAK / chat
  • 16:00 Start :
  • 16:45 BREAK / chat

Wrap up

  • 17:00 Preview tomorrow's Agenda
  • 17:30 Reflection
  • 18:00 END

<%* } else { -%> %% %% <%* } -%>

Log

%% done %%

class Dated {
constructor() {
this.app = window.customJS.app;
this.birthdayFile = this.app.vault.adapter.basePath + "/assets/birthdays.json";
}
/**
* Parse the date from a filename and calculate related dates.
* @param {string} filename The filename to parse.
* @returns {Object} An object containing:
* the parsed date, next workday, next workday name, last Monday, this Monday, and next Monday.
*/
parseDate = (filename) => {
const titledate = filename.replace("_week", '');
const day = moment(titledate);
const dayOfWeek = day.isoWeekday();
let theMonday = moment(day).day(1);
let nextWorkDay = moment(day).add(1, 'd');
let nextWorkDayName = 'tomorrow';
if (dayOfWeek === 0 || dayOfWeek === 7) {
theMonday = moment(day).add(-1, 'week').day(1);
} else if (dayOfWeek > 4) {
nextWorkDay = moment(theMonday).add(1, 'week');
nextWorkDayName = 'Monday';
}
return {
day: day,
nextWorkDay: nextWorkDay,
nextWorkDayName: nextWorkDayName,
lastMonday: moment(theMonday).add(-1, 'week'),
monday: theMonday,
nextMonday: moment(theMonday).add(1, 'week')
}
}
/**
* Create the file path for a specific day of the week.
* @param {moment} monday The Monday of the week.
* @param {number} dayOfWeek The day of the week (1 for Monday, 7 for Sunday).
* @returns {string} The file path for the specified day of the week.
*/
dayOfWeekFile = (monday, dayOfWeek) => {
return this.dailyFile(moment(monday).day(dayOfWeek));
}
/**
* Create the file path for a specific date.
* @param {moment} target The date to generate the file path for.
* @returns {string} The file path for the specified date.
*/
dailyFile = (target) => {
return target.format("[/chronicles]/YYYY/YYYY-MM-DD[.md]");
}
/**
* Create the file path for a specific Monday.
* @param {moment} monday The Monday to generate the file path for.
* @returns {string} The file path for the specified Monday.
*/
weeklyFile = (monday) => {
return monday.format("[/chronicles]/YYYY/YYYY-MM-DD[_week.md]");
}
/**
* Create the file path for a specific month.
* @param {moment} target The date to generate the file path for.
* @returns {string} The file path for the specified month.
*/
monthlyFile = (target) => {
return target.format("[/chronicles]/YYYY/YYYY-MM[_month.md]");
}
/**
* Create the file path for a specific year.
* @param {moment} target The date to generate the file path for.
* @returns {string} The file path for the specified year.
*/
yearlyFile = (target) => {
return target.format("[/chronicles]/YYYY/YYYY[.md]");
}
/**
* Create information for the daily note template for a specific date.
* @param {string} filename The filename to parse.
* @returns {Object} An object containing the dates, header, and daily file path.
*/
daily = (filename) => {
const dates = this.parseDate(filename);
const header = '# My Day\n'
+ dates.day.format("dddd, MMMM DD, YYYY")
+ ' .... [' + dates.nextWorkDayName + '](' + this.dailyFile(dates.nextWorkDay) + ') \n'
+ 'Week of [' + dates.monday.format("MMMM DD") + '](' + this.weeklyFile(dates.monday) + ') \n';
return {
dates,
header,
dailyFile: this.dailyFile(dates.day).replace('.md', '')
}
}
/**
* Create information for the weekly note template for a specific date.
* @param {string} filename The filename to parse.
* @returns {Object} An object containing the dates, header, log, weekly projects, activity, upcoming, week file path, last week file path, month name, monthly reflection, and weekly reflection.
*/
weekly = (filename) => {
const dates = this.parseDate(filename);
const weekFile = this.weeklyFile(dates.monday);
const thisMonthFile = this.monthlyFile(dates.monday);
const lastWeekFile = this.weeklyFile(dates.lastMonday);
const lastMonthFile = this.monthlyFile(dates.lastMonday);
let monthlyReflection = '';
let upcoming = `> ![Upcoming](${this.yearlyFile(dates.monday)}#${dates.monday.format("MMMM")})\n`;
var header = `# Week of ${dates.monday.format("MMM D")}\n`
+ `[< ${dates.lastMonday.format("MMM D")}](${lastWeekFile}) --`
+ ` [Mo](${this.dayOfWeekFile(dates.monday, 1)})`
+ ` [Tu](${this.dayOfWeekFile(dates.monday, 2)})`
+ ` [We](${this.dayOfWeekFile(dates.monday, 3)})`
+ ` [Th](${this.dayOfWeekFile(dates.monday, 4)})`
+ ` [Fr](${this.dayOfWeekFile(dates.monday, 5)})`
+ ` -- [${dates.nextMonday.format("MMM D")} >](${this.weeklyFile(dates.nextMonday)}) \n`
+ `Goals for [${dates.monday.format("MMMM")}](${thisMonthFile})`;
if (dates.monday.month() !== dates.nextMonday.month()) {
header += `, [${dates.nextMonday.format("MMMM")}](${this.monthlyFile(dates.nextMonday)})`;
upcoming += `\n> ![Upcoming](${dates.nextMonday.format("YYYY")}.md#${dates.nextMonday.format("MMMM")})`
monthlyReflection =
`- [ ] [Reflect on last month](${lastMonthFile})\n`
+ `- [ ] [Goals for this month](${this.monthlyFile(dates.nextMonday)})`;
} else if (dates.monday.month() !== dates.lastMonday.month()) {
monthlyReflection =
`- [ ] [Reflect on last month](${lastMonthFile})\n`
+ `- [ ] [Goals for this month](${thisMonthFile})`;
}
const log =
`### Log ${this.dayOfWeekFile(dates.monday, 1)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 1)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 2)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 2)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 3)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 3)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 4)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 4)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 5)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 5)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 6)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 6)}#Log)\n\n`
+ `### Log ${this.dayOfWeekFile(dates.monday, 7)}\n`
+ `![invisible-embed](${this.dayOfWeekFile(dates.monday, 7)}#Log)\n\n`;
const weeklyProjects =
`js-engine
const { Tasks } = await window.cJS();
return await Tasks.thisWeekTasks(engine);`
return {
dates,
header,
log,
weeklyProjects,
upcoming,
weekFile: weekFile.replace('.md', ''),
lastWeekFile,
monthName: `${dates.monday.format("MMMM")}`,
monthlyReflection: monthlyReflection,
weeklyReflection: `${lastMonthFile}#${dates.lastMonday.format("YYYY-MM-DD")}`,
}
}
/**
* Parse the date from a monthly filename and calculate related dates.
* @param {string} fileName The filename to parse.
* @returns {Object} An object containing the month year, month, month file path, year, last month, last month file path, next month, next month file path, and first Monday of the month.
*/
monthlyDates = (fileName) => {
const dateString = fileName.replace('.md', '').replace('_month', '-01');
const date = moment(dateString);
const lastMonth = moment(date).add(-1, 'month');
const nextMonth = moment(date).add(1, 'month');
let firstMonday = moment(date).startOf('month').day("Monday");
if (firstMonday.date() > 7) {
// We might be at the end of the previous month. So
// find the next Monday.. the first Monday *in* the month
firstMonday.add(7, 'd');
}
return {
monthYear: date.format("MMMM YYYY"),
month: date.format("MMMM"),
monthFile: this.monthlyFile(date).replace('.md', ''),
year: date.format("YYYY"),
lastMonth: lastMonth.format("MMMM"),
lastMonthFile: this.monthlyFile(lastMonth),
nextMonth: nextMonth.format("MMMM"),
nextMonthFile: this.monthlyFile(nextMonth),
firstMonday: firstMonday
}
}
/**
* Create information for the monthly note template for a specific date.
* @param {string} filename The filename to parse.
* @returns {Object} An object containing the dates, year embed, and header.
*/
monthly = (filename) => {
const dates = this.monthlyDates(filename);
const header = `# Goals for ${dates.monthYear}\n`
+ `[< ${dates.lastMonth}](${dates.lastMonthFile}) -- [${dates.nextMonth} >](${dates.nextMonthFile})`;
return {
dates: dates,
yearEmbed: `![${dates.month}](${this.yearlyFile(dates.firstMonday)}#${dates.month})`,
header: header
}
}
/**
* Create information for the yearly note template for a specific date.
* @param {string} filename The filename to parse.
* @returns {Object} An object containing the year, year file path, header, birthdays, and year by week.
*/
yearly = (filename) => {
const dateString = filename.replace('.md', '') + '-01-01';
const date = moment(dateString);
const year = date.format("YYYY");
const yearFile = this.yearlyFile(year);
const lastYear = moment(date).add(-1, 'year');
const lastYearFile = this.yearlyFile(lastYear);
const nextYear = moment(date).add(1, 'year');
const nextYearFile = this.yearlyFile(nextYear);
const header = `# Overview of ${year}\n`
+ `[< ${lastYear.format("YYYY")}](${lastYearFile}) -- [${nextYear.format("YYYY")} >](${nextYearFile})`;
const birthdays = {};
const contents = this.app.vault.adapter.fs.readFileSync(this.birthdayFile, "utf8");
const dates = JSON.parse(contents);
for (const [MM, value] of Object.entries(dates)) {
let list = '';
value.forEach(v => {
if (v.year) {
const diff = year - v.year;
list += `- ${v.date}: ${v.text} (${diff})\n`;
} else {
list += `- ${v.date}: ${v.text}\n`;
}
});
birthdays[MM] = list;
}
const yearByWeek =
`js-engine
const { Utils } = await window.cJS();
return Utils.listFilesWithPath(engine, /chronicles\/${year}\/${year}-\d{2}-\d{2}_week\.md/);`
return {
year,
yearFile: yearFile.replace('.md', ''),
header,
birthdays,
yearByWeek
}
}
/**
* Create month information for the yearly note template.
* @param {number} year The year to generate the file path for.
* @param {number} i The month index (0 for January, 11 for December).
* @returns {Object} An object containing the month name and month file path.
*/
monthOfYear = (year, i) => {
let month = moment([year, i, 1]);
return {
month: month.format("MMMM"),
monthFile: this.monthlyFile(month, month)
}
}
/**
* Filter lines containing leftover/unfinished tasks.
* @param {string} line The line to check.
* @returns {boolean} True if the line contains a leftover task, false otherwise.
*/
filterLeftoverTasks = (line) => {
return line.includes('- [ ] ')
|| line.includes('- [>] ')
|| line.includes('- [/] ')
|| line.includes('- [R] ');
}
}

<%* const { Dated } = await window.cJS(); const result = Dated.monthly(tp.file.title); await tp.file.move(result.dates.monthFile); -%><% result.header %>

%% What are your goals for this month? What practical actions will you be taking to achieve them? S = Specific (What EXACTLY do you want to accomplish?)
M = Measurable (How will you measure success?)
A = Attainable (Is it within your reach?)
R = Resonant (Do you feel driven to accomplish it?)
T = Thrilling (Thrilling?) %%

  • Focus: %% one thing to focus on this month %%
  • Habit: %% one habit to focus on this month %%

<% result.yearEmbed %>

🤓 Weekly review

<%* const monday = result.dates.firstMonday; var month = monday.month(); while(month === monday.month()) { var weekStart = monday.format("YYYY-MM-DD"); var weekFile = Dated.weeklyFile(monday, monday); var tyiwFile = Dated.tyiwFile(monday); %>

<% weekStart %>

🎉 Big wins

🎯 How far did I get on my goals?

👩‍🎓 What worked? What didn't?

✨ How should I tweak my strategy next week?

<%* monday.add(7,'d'); } -%>

Reflection

🎉 This month's wins

  1. .
  2. .
  3. .

🙌 Insights gained

  1. .
  2. .
  3. .
Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 14 column 15
---
<%* const { AreaPriority, Templates, Utils } = await window.customJS;
const title = await tp.system.prompt("Enter Name"); 
const lower = Utils.lowerKebab(title); 
const folder = await Templates.chooseFolder(tp, tp.file.folder(true));
console.log(title, lower, folder);
await tp.file.move(`${folder}/${lower}`); 

const status = await AreaPriority.chooseStatus(tp);
const urgent = await AreaPriority.chooseUrgent(tp);
const important = await AreaPriority.chooseImportant(tp);
const role = await AreaPriority.chooseRole(tp);
console.log(status, urgent, important ,role);

tR += `aliases: ["${title}"]`;
%>
type: area
important: <% important %>
urgent: <% urgent %>
status: <% status %>
role: <%role %>
---

<% title %>

%% What? Description %%

%% How does this align with my passion and interest? %%

const { AreaPriority } = await window.cJS();
return AreaPriority.relatedAreas(engine);
const { AreaPriority } = await window.cJS();
return AreaPriority.relatedProjects(engine);

Tasks

  • Define tasks #gtd/next

❧ Percolator

%% ideas in flight %%

❧ Resources

%% links, contacts %%

Log

Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 9 column 15
---
<%* const { Templates, Utils } = await window.cJS();
const title = await tp.system.prompt("Enter Name"); 
const lower = Utils.lowerKebab(title); 
const current = tp.file.folder(true);
const folder = await Templates.chooseFolder(tp, current);
console.log("pre-move", title, lower, folder);
await tp.file.move(`/${folder}/${lower}`);

tR += `aliases: ["${title}"]`;
%>
---

<% title %>

Error in user YAML: (<unknown>): mapping values are not allowed in this context at line 13 column 15
---
<%* const { Templates, AreaPriority, Utils } = await window.cJS();
const title = await tp.system.prompt("Enter Name"); 
const lower = Utils.lowerKebab(title); 
const folder = await Templates.chooseFolder(tp, tp.file.folder(true));
console.log(title, lower, folder);
await tp.file.move(`${folder}/${lower}`); 

const status = await AreaPriority.chooseStatus(tp);
const urgent = await AreaPriority.chooseUrgent(tp);
const important = await AreaPriority.chooseImportant(tp);
const role = await AreaPriority.chooseRole(tp);
console.log(status, urgent, important ,role);
tR += `aliases: ["${title}"]`;
%>
type: quest
important: <% important %>
urgent: <% urgent %>
status: <% status %>
role: <%role %>
---

<% title %>

  • What: %% synopsis %%
  • Who: %% collaboration %%
  • When: %% timeline%%
  • Why: %% internal or external motivation %%

%% How does this align with my passion and interest? %%

Summary

%% how far along is this? where are we? %%

Tasks

  • Define tasks

❧ Percolator

%% ideas in flight %%

❧ Resources

%% links, contacts %%

Log

class AreaPriority {
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
this.urgent = ['yes', 'no'];
this.important = ['yes', 'no'];
this.unknown = '??';
const urgent = '\u23f0'; // ⏰
const important = '!!'; // ‼️
const one = '\u0031\ufe0f\u20e3';
const two = '\u0032\ufe0f\u20e3';
const three = '\u0033\ufe0f\u20e3';
const four = '\u0034\ufe0f\u20e3';
this.priorityVisual = [this.unknown, `${one}${important}${urgent}`, `${two}${important}`, `${three}${urgent}`, `${four}`];
this.status = ['active', 'ongoing', 'brainstorming', 'blocked', 'inactive', 'complete', 'ignore'];
this.statusVisual = {
'active': '\ud83c\udfca\u200d\u2640\ufe0f', // 🏊‍♀️
'blocked': '🧱',
'brainstorming': '🧠',
'ongoing': '\ud83d\udd1b', // 🔛
'inactive': '\ud83d\udca4', // 💤
'complete': '\ud83c\udfc1', // '🏁',
'ignore': '\ud83e\udee3' // 🫣
}
this.role = ['owner', 'collaborator', 'observer'];
this.roleVisual = {
'owner': '🖐',
'collaborator': '🤝',
'observer': '👀',
}
console.log("loaded AreaPriority");
}
/**
* Create a markdown list of all projects matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {boolean} archived Whether to include archived projects.
* @param {string|Array} [orOther=''] Additional conditions to apply.
* @returns {string} A markdown list of all projects matching the specified conditions.
* @see isArchived
* @see isProject
* @see priorityFilesMatchingCondition
* @see showPriority
* @see showRole
* @see showStatus
* @see utils.filterByConditions
* @see utils.markdownLink
*/
allProjects = (engine, archived, orOther = '') => {
const list = this.priorityFilesMatchingCondition(
(tfile) => this.isProject(tfile) && this.isArchived(tfile, archived))
.map((tfile) => `- ${this.showPriority(tfile)}${this.showStatus(tfile)}${this.showRole(tfile)} ${this.utils.markdownLink(tfile)}`);
return engine.markdown.create(list.join("\n"));
}
/**
* Templater prompt with suggester to choose whether a task is urgent.
* @param {Tp} tp The templater plugin instance.
* @returns {Promise<string>} The user's choice of urgency.
*/
chooseUrgent = async (tp) => {
return await tp.system.suggester(['urgent', 'not urgent'], this.urgent);
}
/**
* Templater prompt with suggester to choose whether a task is important.
* @param {Tp} tp The templater plugin instance.
* @returns {Promise<string>} The user's choice of importance.
*/
chooseImportant = async (tp) => {
return await tp.system.suggester(['important', 'not important'], this.important);
}
/**
* Templater prompt with suggester to choose a status for a task.
* @param {Tp} tp The templater plugin instance.
* @returns {Promise<string>} The user's choice of status.
*/
chooseStatus = async (tp) => {
return await tp.system.suggester(this.status, this.status);
}
/**
* Templater prompt with suggester to choose a role for a task.
* @param {Tp} tp The templater plugin instance.
* @returns {Promise<string>} The user's choice of role.
*/
chooseRole = async (tp) => {
return await tp.system.suggester(this.role, this.role);
}
/**
* Retrieves the priority visual representation for a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The priority visual representation for the file.
* @see priorityVisual
* @see utils.frontmatter
*/
filePriority = (tfile) => {
const fm = this.utils.frontmatter(tfile);
return this.priorityVisual[this.priority(fm)];
}
/**
* Retrieves the role visual representation for a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The role visual representation for the file.
* @see roleVisual
* @see utils.frontmatter
*/
fileRole = (tfile) => {
const fm = this.utils.frontmatter(tfile);
return fm.role
? this.roleVisual[fm.role]
: this.unknown;
}
/**
* Retrieves the status visual representation for a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The status visual representation for the file.
* @see statusVisual
* @see utils.frontmatter
*/
fileStatus = (tfile) => {
const fm = this.utils.frontmatter(tfile);
if (fm.status && fm.status.match(/(completed|closed|done)/)) {
fm.status = 'complete';
}
return fm.status
? this.statusVisual[fm.status]
: this.unknown;
}
/**
* Test if a file is archived.
* @param {TFile} tfile The file to examine.
* @param {boolean} archived Archived status to test
* @returns {boolean} True if the file's archive state matches the specified status, false otherwise.
*/
isArchived = (tfile, archived) => {
return tfile.path.includes("archives") === archived;
}
/**
* Determines if a file is an area.
* @param {TFile} tfile The file to examine.
* @returns {boolean} True if the file is an area, false otherwise.
*/
isArea = (tfile) => {
const type = this.utils.frontmatter(tfile).type;
return type && type === "area";
}
/**
* Determines if a file is a project.
* @param {TFile} tfile The file to examine.
* @returns {boolean} True if the file is a project, false otherwise.
*/
isProject = (tfile) => {
const type = this.utils.frontmatter(tfile).type;
return type && type.match(/(project|quest)/);
}
/**
* Determines if a file is a project or area.
* @param {TFile} tfile The file to examine.
* @returns {boolean} True if the file is a project or area, false otherwise.
*/
isProjectArea = (tfile) => {
const type = this.utils.frontmatter(tfile).type;
return type && type.match(/(project|quest|area)/);
}
/**
* Create an index of other related items (not projects or areas)
* within the same folder as the current file or matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} conditions Additional conditions to apply.
* @returns {string} An index of other related items.
* @see utils.filterByPath
* @see utils.filterByConditions
*/
otherRelatedItemsIndex = (engine, conditions) => {
const current = this.app.workspace.getActiveFile();
const pathRegex = this.utils.segmentFilterRegex(current.parent.path);
const list = this.utils.filesMatchingCondition((tfile) => {
return this.isProjectArea(tfile)
? false
: (this.utils.filterByPath(tfile, pathRegex) || this.utils.filterByConditions(tfile, conditions));
});
return this.utils.index(engine, list);
}
/**
* Determine the priority level of a file based on its frontmatter.
* @param {Object} fm The frontmatter of the file.
* @returns {number} The priority level of the file.
*/
priority = (fm) => {
if (fm.important == 'yes') {
return fm.urgent == 'yes' ? 1 : 2;
}
return fm.urgent == 'yes' ? 3 : 4;
}
/**
* Retrieve files matching a specified condition and sorts them by priority.
* @param {Function} tfileFilterFn The filter function to apply to files.
* @returns {Array<TFile>} An array of files matching the specified condition, sorted by priority.
* @see sortProjects
*/
priorityFilesMatchingCondition = (tfileFilterFn) => {
const current = this.app.workspace.getActiveFile();
return this.app.vault.getMarkdownFiles()
.filter(tfile => tfile !== current)
.filter(tfile => tfileFilterFn(tfile))
.sort(this.sortProjects);
}
/**
* Create a markdown list of related areas matching the specified conditions.
* Will constrain to archived areas if the current file is archived.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of related areas matching the specified conditions.
* @see relatedAreasList
*/
relatedAreas = (engine, conditions = '') => {
const current = this.app.workspace.getActiveFile();
return this.relatedAreasList(engine, current.path.contains("archives"), conditions);
}
/**
* Create a markdown list of active related areas matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of active related areas matching the specified conditions.
* @see relatedAreasList
*/
relatedAreasActive = (engine, conditions = '') => {
return this.relatedAreasList(engine, false, conditions);
}
/**
* Create a markdown list of archived related areas matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of archived related areas matching the specified conditions.
* @see relatedAreasList
*/
relatedAreasArchived = (engine, conditions = '') => {
return this.relatedAreasList(engine, true, conditions);
}
/**
* Create a markdown list of related areas matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {boolean} archived Whether to include archived areas.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of related areas matching the specified conditions.
* @see isArchived
* @see isArea
* @see priorityFilesMatchingCondition
* @see showRole
* @see utils.filterByConditions
* @see utils.filterByPath
* @see utils.markdownLink
*/
relatedAreasList = (engine, archived, conditions = '') => {
const current = this.app.workspace.getActiveFile();
const pathRegex = this.utils.segmentFilterRegex(current.parent.path);
const list = this.priorityFilesMatchingCondition((tfile) => {
const areaIncluded = this.isArea(tfile) && this.isArchived(tfile, archived)
const inFolder = this.utils.filterByPath(tfile, pathRegex);
return conditions
? areaIncluded && (inFolder || this.utils.filterByConditions(tfile, conditions))
: areaIncluded && inFolder;
}).map((tfile) => `- ${this.showRole(tfile)} ${this.utils.markdownLink(tfile)}`);
return engine.markdown.create(list.join("\n"));
}
/**
* Create a markdown list of related projects matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of related projects matching the specified conditions.
* @see relatedProjectsList
*/
relatedProjects = (engine, conditions = '') => {
const current = this.app.workspace.getActiveFile();
return this.relatedProjectsList(engine, current.path.contains("archives"), conditions);
}
/**
* Create a markdown list of active related projects matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of active related projects matching the specified conditions.
* @see relatedProjectsList
*/
relatedProjectsActive = (engine, conditions = '') => {
return this.relatedProjectsList(engine, false, conditions);
}
/**
* Create a markdown list of archived related projects matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of archived related projects matching the specified conditions.
* @see relatedProjectsList
*/
relatedProjectsArchived = (engine, conditions = '') => {
return this.relatedProjectsList(engine, true, conditions);
}
/**
* Create a markdown list of related projects matching the specified conditions.
* @param {JSEngine} engine The engine to create markdown.
* @param {boolean} archived Whether to include archived projects.
* @param {string|Array} [conditions=''] Additional conditions to apply.
* @returns {string} A markdown list of related projects matching the specified conditions.
* @see isArchived
* @see isProject
* @see priorityFilesMatchingCondition
* @see showPriority
* @see showRole
* @see showStatus
* @see utils.filterByConditions
* @see utils.filterByPath
* @see utils.markdownLink
*/
relatedProjectsList = (engine, archived, conditions = '') => {
const current = this.app.workspace.getActiveFile();
const pathRegex = this.utils.segmentFilterRegex(current.parent.path);
const list = this.priorityFilesMatchingCondition((tfile) => {
const projectIncluded = this.isProject(tfile) && this.isArchived(tfile, archived)
const inFolder = this.utils.filterByPath(tfile, pathRegex);
return conditions
? projectIncluded && (inFolder || this.utils.filterByConditions(tfile, conditions))
: projectIncluded && inFolder;
}).map((tfile) => `- ${this.showPriority(tfile)}${this.showStatus(tfile)}${this.showRole(tfile)} ${this.utils.markdownLink(tfile)}`);
return engine.markdown.create(list.join("\n"));
}
/**
* Generates the HTML for displaying the priority of a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The HTML for displaying the priority of the file.
* @see priorityVisual
*/
showPriority = (tfile) => {
const fm = this.utils.frontmatter(tfile);
return `<span class="ap-priority">${this.priorityVisual[this.priority(fm)]}</span>`
}
/**
* Generates the HTML for displaying the role of a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The HTML for displaying the role of the file.
* @see roleVisual
*/
showRole = (tfile) => {
const fm = this.utils.frontmatter(tfile);
const role = fm.role
? this.roleVisual[fm.role]
: this.unknown;
return `<span class="ap-role">${role}</span>`
}
/**
* Generates the HTML for displaying the status of a file.
* @param {TFile} tfile The file to examine.
* @returns {string} The HTML for displaying the status of the file.
* @see statusVisual
*/
showStatus = (tfile) => {
const fm = this.utils.frontmatter(tfile);
if (fm.status && fm.status.match(/(completed|closed|done)/)) {
fm.status = 'complete';
}
const status = fm.status
? this.statusVisual[fm.status]
: this.unknown;
return `<span class="ap-status">${status}</span>`
}
/**
* Sorts projects based on priority, status, role, and name.
* @param {TFile} tfile1 The first file to compare.
* @param {TFile} tfile2 The second file to compare.
* @returns {number} A negative number if tfile1 should come before tfile2,
* a positive number if tfile1 should come after tfile2,
* or 0 if they are considered equal.
* @see test
* @see testName
* @see testPriority
*/
sortProjects = (tfile1, tfile2) => {
const fm1 = this.utils.frontmatter(tfile1);
const fm2 = this.utils.frontmatter(tfile2);
return this.testPriority(fm1, fm2,
() => this.test(fm1, fm2, this.status, 'status',
() => this.test(fm1, fm2, this.role, 'role',
() => this.testName(tfile1, tfile2))));
}
/**
* Compares two files based on a specified field and fallback function.
* @param {Object} fm1 The frontmatter of the first file.
* @param {Object} fm2 The frontmatter of the second file.
* @param {Array} values The array of possible values for the field.
* @param {string} field The field to compare.
* @param {Function} fallback The fallback function to use if the field values are equal.
* @returns {number} A negative number if fm1 should come before fm2,
* a positive number if fm1 should come after fm2,
* or the result of the fallback function if they are considered equal.
*/
test = (fm1, fm2, values, field, fallback) => {
let test1 = values.indexOf(fm1[field]);
let test2 = values.indexOf(fm2[field]);
if (test1 == test2) {
return fallback();
}
return test1 - test2;
}
/**
* Compares two files based on priority and a fallback function.
* @param {Object} fm1 The frontmatter of the first file.
* @param {Object} fm2 The frontmatter of the second file.
* @param {Function} fallback The fallback function to use if the priority values are equal.
* @returns {number} A negative number if fm1 should come before fm2,
* a positive number if fm1 should come after fm2,
* or the result of the fallback function if they are considered equal.
*/
testPriority = (fm1, fm2, fallback) => {
let test1 = this.priority(fm1);
let test2 = this.priority(fm2)
if (test1 == test2) {
return fallback();
}
return test1 - test2;
}
/**
* Compares two files based on their names.
* @param {TFile} tfile1 The first file to compare.
* @param {TFile} tfile2 The second file to compare.
* @returns {number} A negative number if tfile1 should come before tfile2,
* a positive number if tfile1 should come after tfile2,
* or 0 if they are considered equal.
*/
testName = (tfile1, tfile2) => {
return tfile1.name.localeCompare(tfile2.name);
}
}
class Tasks {
taskPattern = /^([\s>]*- )\[(.)\] (.*)$/;
completedPattern = /\((\d{4}-\d{2}-\d{2})\)/;
dailyNotePattern = /^(\d{4}-\d{2}-\d{2}).md$/;
taskPaths = [
'demesne',
'quests'
]
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
console.log("loaded Tasks");
}
/**
* Retrieve tasks from the current active file.
* @returns {Promise<Array>} A promise that resolves to an array of tasks from the current active file.
*/
currentFileTasks = async () => {
const tfile = this.app.workspace.getActiveFile();
return tfile
? this.fileTasks(tfile)
: [];
}
/**
* Retrieve tasks from a specified file.
* @param {TFile} tfile The file to examine.
* @returns {Promise<Array>} A promise that resolves to an array of tasks from the specified file.
*/
fileTasks = async (tfile) => {
const tasks = [];
const content = await this.app.vault.cachedRead(tfile);
const split = content?.split("\n");
if (split) {
split.forEach(l => {
const taskMatch = this.taskPattern.exec(l);
if (taskMatch) {
tasks.push([tfile, taskMatch[2], taskMatch[3]]);
}
});
}
return tasks;
}
/**
* Retrieve tasks for the week derived from the name of the current active file
* @param {JSEngine} engine The engine to create markdown.
* @returns {Promise<string>} A promise that resolves to a markdown list of tasks for the current week.
*/
thisWeekTasks = async (engine) => {
const tfile = this.app.workspace.getActiveFile();
const titledate = tfile.name.replace("_week.md", '');
const begin = moment(titledate).day(1).add(-1, 'd');
const end = moment(begin).add(7, 'd');
console.log(
begin.format("YYYY-MM-DD"),
end.format("YYYY-MM-DD"),
engine);
const files = this.app.vault.getMarkdownFiles();
const list = [];
for(const file of files) {
if (this.taskPaths.some(x => file.path.contains(x))) {
const tasks = await this.fileTasks(file);
tasks
.filter(task => task[1].match(/[x-]/))
.filter(task => {
const completed = this.completedPattern.exec(task[2]);
return completed
? moment(completed[1]).isBetween(begin, end)
: false;
})
.forEach(task => {
const link = this.utils.markdownLink(task[0]);
list.push(`- *${link}*: ${task[2]}`);
});
}
}
return engine.markdown.create(list.join("\n"));
}
}
class Templates {
headerPush = ['Section', 'Log item', 'Tasks item'];
itemPush = ['Log item', 'Tasks item'];
dated = /^.*?(\d{4}-\d{2}-\d{2}).*$/;
constructor() {
this.app = window.customJS.app;
this.utils = window.customJS.Utils;
console.log("loaded Templates");
}
/**
* Add text to a specified section in a file.
* @param {Templater} tp The templater plugin instance.
* @param {string} choice The file path to add text to.
* @param {string} addThis The text to add.
* @param {string} [section='Log'] The section to add the text to.
* @returns {Promise<void>}
*/
addToSection = async (tp, choice, addThis, section = "Log") => {
const file = tp.file.find_tfile(choice);
const fileCache = this.app.metadataCache.getCache(choice);
const headings = fileCache.headings
.filter(x => x.level >= 2)
.filter(x => x.heading.contains(section));
if (headings[0]) {
await this.app.vault.process(file, (content) => {
const split = content.split("\n");
split.splice(headings[0].position.start.line + 1, 0, addThis);
return split.join("\n");
});
}
}
/**
* Templater prompt with suggester to choose a file from the vault.
* @param {Templater} tp The templater plugin instance.
* @returns {Promise<string>} The chosen file path.
*/
chooseFile = async (tp) => {
const files = this.utils.filePaths();
return await tp.system.suggester(files, files);
}
/**
* Templater prompt with suggester to choose a folder from the vault.
* @param {Templater} tp The templater plugin instance.
* @param {string} folder The initial folder path to filter.
* @returns {Promise<string>} The chosen folder path or a user-entered folder path.
*/
chooseFolder = async (tp, folder) => {
const folders = this.utils.foldersByCondition(folder,
(tfolder) =>!tfolder.path.startsWith("assets"))
.map(f => f.path);
console.log(folders);
folders.unshift('--');
const choice = await tp.system.suggester(folders, folders);
if (choice) {
if (choice == '--') {
return await tp.system.prompt("Enter folder path");
}
return choice;
}
warn("No choice selected. Using 'athenaeum'");
return 'athenaeum';
}
/**
* Create a conversation entry for the specified day:
* - Create a new dated section in the relevant file
* - Add a link to the conversation in the daily log, and embed that section
* @param {Templater} tp The templater plugin instance.
* @returns {Promise<string>} The markdown content for the conversation entry.
*/
createConversation = async (tp) => {
let result = "";
const day = moment(tp.file.title).format("YYYY-MM-DD");
const files = Object.entries(this.app.vault.adapter.files)
.filter(x => x[1].type === "file")
.filter(x => x[0].startsWith('chronicles/conversations'))
.map(x => x[0]);
files.sort();
const choice = await tp.system.suggester(files, files);
if (choice) {
const file = tp.file.find_tfile(choice);
const fileCache = this.app.metadataCache.getFileCache(file);
let title = fileCache.frontmatter && fileCache.frontmatter.aliases
? fileCache.frontmatter.aliases[0]
: file.basename;
result = `\n- [**${title}**](${file.path}#${day})\n`;
result += ` ![${day}](${file.path}#${day})\n`;
const headings = fileCache.headings
.filter(x => x.level == 2);
if (!headings || headings.length == 0) {
await this.app.vault.process(file, (content) => {
return content + `\n\n## ${day}\n`;
});
} else if (headings[0].heading != day) {
await this.app.vault.process(file, (content) => {
const split = content.split("\n");
split.splice(headings[0].position.start.line, 0, `## ${day}\n\n`);
return split.join("\n");
});
}
}
return result;
}
/**
* Find the current line in the active file and extract relevant information.
* @param {Templater} tp The templater plugin instance.
* @returns {Promise<Object>} An object containing the title, path, heading, and text of the current line.
*/
findLine = async (tp) => {
let line = undefined;
const split = tp.file.content.split("\n");
const file = tp.file.find_tfile(tp.file.title);
const fileCache = this.app.metadataCache.getFileCache(file);
const title = fileCache.frontmatter && fileCache.frontmatter.aliases
? fileCache.frontmatter.aliases[0]
: file.basename;
const view = this.app.workspace.getActiveViewOfType(window.customJS.obsidian.MarkdownView);
if (view) {
const cursor = view.editor.getCursor("from").line;
line = split[cursor];
}
let heading = undefined;
let text = undefined;
if (line && line.match(/^\s*- .*/)) {
text = line.replace(/^\s*- (?:\[.\] )?(.*)/, "$1").trim();
} else {
if (!line || !line.startsWith('#')) {
const headings = fileCache.headings
.filter(x => x.level == 2);
line = split[headings[0]?.position.start.line];
}
heading = line.replace(/#+ /, "").trim();
}
return {
title,
path: tp.file.path(true),
heading,
text
}
}
/**
* Prompt the user to choose a file and push text to it.
* @param {Templater} tp The templater plugin instance.
* @returns {Promise<void>}
*/
pushText = async (tp) => {
const choice = await this.chooseFile(tp);
if (choice) {
const { title, heading, path, text } = await this.findLine(tp);
if (heading) {
await this.doPushHeader(tp, choice, title, heading, path);
} else {
await this.doPushText(tp, choice, title, path, text);
}
}
}
/**
* Push Header link to the specified file.
* - Templater prompt with suggester to choose the kind of text to push (Section, Log item, Tasks item)
* @param {Templater} tp The templater plugin instance.
* @param {string} choice The file path to push the header to.
* @param {string} title The title of the header.
* @param {string} heading The heading text.
* @param {string} path The path of the file containing the heading.
* @returns {Promise<void>}
*/
doPushHeader = async (tp, choice, title, heading, path) => {
const type = await tp.system.suggester(this.headerPush, this.headerPush);
const date = heading.replace(/^.*?(\d{4}-\d{2}-\d{2}).*$/, `$1`);
const interesting = heading.replace(/\s*\d{4}-\d{2}-\d{2}\s*/, '');
const pretty = path.contains("conversations") ? `**${title}**` : `_${title}_`;
const linkText = path == choice ? '⤴' : `${pretty}`;
const lineText = interesting ? `: ${interesting}` : '';
console.log("PUSH HEADER", `"${path}"`, `"${title}"`, `"${heading}"`, `"${date}"`, `"${interesting}"`);
const anchor = heading
.replace(/\s+/g, ' ')
.replace(/:/g, '')
.replace(/ /g, '%20');
switch (type) {
case 'Section': {
// Create a new section with the heading and title
let addThis = `## ${heading} ${title}\n`;
addThis += `![invisible-embed](${path}#${anchor})\n\n`;
const file = tp.file.find_tfile(choice);
const fileCache = this.app.metadataCache.getCache(choice);
const headings = fileCache.headings
.filter(x => x.level == 2);
await this.app.vault.process(file, (content) => {
const split = content.split("\n");
if (headings && headings[0]) {
split.splice(headings[0].position.start.line, 0, addThis);
} else {
split.push("");
split.push(addThis);
}
return split.join("\n");
});
break;
}
case 'Tasks item': {
// Create a new task
const addThis = `- [ ] [${linkText}](${path}#${anchor})${lineText}\n`;
this.addToSection(tp, choice, addThis, 'Tasks');
break;
}
default: { // Log section
const isDaily = choice.match(this.dated);
const isWeekly = choice.endsWith("_week.md");
const task = !isDaily || isWeekly ? '[x] ' : '';
const completed = task ? ` (${date})` : '';
const addThis = `- ${task}[${linkText}](${path}#${anchor})${lineText}${completed}`;
this.addToSection(tp, choice, addThis);
break;
}
}
}
/**
* Push text to a specified file.
* @param {Templater} tp The templater plugin instance.
* @param {string} choice The file path to push the text to.
* @param {string} title The title of the text.
* @param {string} path The path of the file containing the text.
* @param {string} text The text to push.
* @returns {Promise<void>}
*/
doPushText = async (tp, choice, title, path, text) => {
const type = await tp.system.suggester(this.itemPush, this.itemPush);
const fromDaily = path.match(this.dated);
const isDaily = choice.match(this.dated);
const lineDate = text.match(this.dated);
const pretty = path.contains("conversations") ? `**${title}**` : `_${title}_`;
const date = lineDate
? lineDate[1]
: (fromDaily ? fromDaily[1] : window.moment().format('YYYY-MM-DD'));
console.log("PUSH TEXT", `"${path}"`, `"${text}"`, `"${date}"`);
switch (type) {
case 'Tasks item': { // Tasks section
const from = isDaily ? '' : ` from [${pretty}](${path})`;
const addThis = `- [ ] ${text}${from}\n`;
this.addToSection(tp, choice, addThis, 'Tasks');
break;
}
default: { // Log section
const from = isDaily ? '' : `[${pretty}](${path}): `;
const isWeekly = choice.endsWith("_week.md");
const task = (!isDaily || isWeekly) ? '[x] ' : '';
const completed = task && !lineDate ? ` (${date})` : '';
const addThis = `- ${task}${from}${text}${completed}`;
this.addToSection(tp, choice, addThis);
break;
}
}
}
}

<%* const { Dated } = await window.cJS(); const result = Dated.weekly(tp.file.title);

var incompleteTasks = '';
const lastWeek = await tp.file.find_tfile(result.lastWeekFile);
if(lastWeek) { 
    const content = await app.vault.cachedRead(lastWeek); 
    incompleteTasks = content.split('\n')
                .filter(Dated.filterLeftoverTasks)
                .join('\n'); 
}
if(incompleteTasks) {%>

Leftovers <% incompleteTasks %> <%*}%>

<%* const { Dated } = await window.cJS(); const result = Dated.weekly(tp.file.title); await tp.file.move(result.weekFile); const monday = result.dates.monday.format("YYYY-MM-DD"); -%> <% result.header %> %%

  • Reflect on last week
  • Review percolator (priority, next actions, etc)
  • File any Inbox items from last week <%* if(result.monthlyReflection) {-%> <% result.monthlyReflection %> <%*}-%> %%

Goals / Focus

Habit:
Goal for the week:
I am excited about:

Priorities:

  1. .
  2. .
  3. .

[!tldr] Upcoming <% result.upcoming %>

Tasks

Commonhaus

Red Hat

Other

  • Run qk to update git repos (anduin, erebor)
  • updates on erebor
  • updates on anduin
  • updates on esgaroth

<% tp.file.include(tp.file.find_tfile("assets/templates/weekly-leftovers.md")) %>


Project items completed this week:


<% result.log %>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment