Skip to content

Instantly share code, notes, and snippets.

@Moyf
Last active April 4, 2025 18:34
Show Gist options
  • Save Moyf/22fe074d9a95d40d93a46f01ce27e005 to your computer and use it in GitHub Desktop.
Save Moyf/22fe074d9a95d40d93a46f01ce27e005 to your computer and use it in GitHub Desktop.
[NTB×TP] Share some useful scripts that I use with NTB 😇

(1) ReOrder Tasks

This action will reorder the tasks near the cursor (or select Chinese version) based on the current cursor position, and put the completed tasks at the bottom.

(2) Cancel Task

This is a utility function done by the Tasks plugin, the current Tasks plugin does not provide the function of "cancel the task (and add a cancellation date)", so I implemented one.

(3) Toggle Callout Plus

https://github.com/user-attachments/assets/8bf72925-9808-44ec-aaa0-bfb41b44be47

This is an enhancement to OB's built-in "Toggle Callout" feature:

  1. You can set whether to collapse or not
  2. You can customize the default callout type and/or default title behavior (whether use first line as title)
  3. You can choose the cursor position after the package
  4. Just select a few characters (or even none, just place the edit Cursor!), which automatically selects consecutive lines around it (unless you encounter a blank line or header)

Reference

https://github.com/chrisgurney/obsidian-note-toolbar/wiki/Executing-scripts

Most of the scripts are wrote in association with Cursor

OK, to be honest, mostly of them are WROTEN BY Cursor 😂

<%*
// 辅助函数
const isListLine = (line) => /^[\s]*-/.test(line); // 检查是否为列表行
const isTaskLine = (line) => /^[\s]*- \[[ x-]\]/.test(line);
const isTaskDone = (line) => /^[\s]*- \[[x-]\]/.test(line);
const getIndentLevel = (line) => line.search(/\S|$/) / 2;
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;
const cursor = editor.getCursor();
const currentLine = editor.getLine(cursor.line);
// 如果当前行不是列表行,直接返回
if (!isListLine(currentLine)) {
return;
}
// 找到所有连续列表行的起始位置和结束位置
let startLine = cursor.line;
let endLine = cursor.line;
// 向上查找连续的列表
while (startLine > 0) {
const prevLine = editor.getLine(startLine - 1);
if (!isListLine(prevLine)) break;
startLine--;
}
// 向下查找连续的列表
while (endLine < editor.lineCount() - 1) {
const nextLine = editor.getLine(endLine + 1);
if (!isListLine(nextLine)) break;
endLine++;
}
// 获取整个列表块的文本
const from = { line: startLine, ch: 0 };
const to = { line: endLine, ch: editor.getLine(endLine).length };
const selectedText = editor.getRange(from, to);
// 如果没有有效内容,直接返回
if (!selectedText.trim()) return;
function reorganizeTasks(inputText) {
const lines = inputText.trim().split('\n');
// 构建任务树
function buildTaskTree(lines) {
const root = { children: [], level: -1 };
const stack = [root];
for (const line of lines) {
const level = getIndentLevel(line);
const task = {
text: line,
isDone: isTaskDone(line),
isTask: isTaskLine(line),
level: level,
children: []
};
// 找到正确的父节点
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
stack.pop();
}
// 添加到父节点的子节点列表
stack[stack.length - 1].children.push(task);
// 将当前任务作为可能的父节点
stack.push(task);
}
return root.children;
}
// 对每个层级的任务进行排序
function sortTasksAtLevel(tasks) {
// 先将任务和非任务分开
const taskItems = tasks.filter(t => t.isTask);
const nonTaskItems = tasks.filter(t => !t.isTask);
// 对任务项进行排序
taskItems.sort((a, b) => {
if (a.isDone === b.isDone) return 0;
return a.isDone ? 1 : -1;
});
// 合并回原数组
tasks.splice(0, tasks.length, ...nonTaskItems, ...taskItems);
// 递归对子任务排序
for (const task of tasks) {
if (task.children.length > 0) {
sortTasksAtLevel(task.children);
}
}
return tasks;
}
// 将任务树转换回文本
function taskTreeToText(tasks, level = 0) {
const result = [];
for (const task of tasks) {
result.push(task.text);
if (task.children.length > 0) {
result.push(...taskTreeToText(task.children, level + 1));
}
}
return result;
}
const taskTree = buildTaskTree(lines);
const sortedTasks = sortTasksAtLevel(taskTree);
return taskTreeToText(sortedTasks).join('\n');
}
// 处理任务并替换内容
const reorganizedTasks = reorganizeTasks(selectedText);
editor.replaceRange(reorganizedTasks, from, to);
-%>
<%*
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;
// 获取当前行
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
// 检查是否为任务行
if (!/^[\s]*- \[[ x-]\]/.test(line)) return;
// 获取今天的日期
const today = new Date();
const dateStr = today.toISOString().split('T')[0].replace(/-/g, '-');
// 构建日期标记
const dateMark = ` ❌ ${dateStr}`;
// 替换复选框状态并添加日期标记
let newLine = line;
// 如果行尾已经有日期标记,先移除它
newLine = newLine.replace(/\s*[✅❌]\s*\d{4}-\d{2}-\d{2}\s*$/, '');
// 替换复选框状态
newLine = newLine.replace(/\[[ x-]\]/, '[-]');
// 添加新的日期标记
newLine = newLine + dateMark;
// 替换当前行
editor.replaceRange(
newLine,
{ line: cursor.line, ch: 0 },
{ line: cursor.line, ch: line.length }
);
-%>
<%*
// 【配置选项】
const CONFIG = {
calloutType: "info", // Callout 类型
collapsed: false, // 是否折叠
useFirstLineAsTitle: false, // 是否使用第一行作为标题
cursorPosition: "type" // 可选值: "end", "type", "title"
};
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;
// 【拓展范围】
// 判断是否为标题行
const isHeading = (line) => /^#{1,6}\s+/.test(line);
// 判断是否为空行或标题行
const shouldStop = (line) => !line.trim() || isHeading(line);
// 获取当前选中的内容和范围
let selection = editor.getSelection();
let from, to;
if (!selection) {
// 如果没有选中文本,从光标所在行开始扩展
const cursor = editor.getCursor();
// 如果光标在标题行上,直接返回
if (isHeading(editor.getLine(cursor.line))) return;
// 设置初始范围为光标所在行的开始到结束
from = { line: cursor.line, ch: 0 };
to = { line: cursor.line, ch: editor.getLine(cursor.line).length };
} else {
// 如果有选中文本,获取选择范围
from = editor.getCursor('from');
to = editor.getCursor('to');
}
// 向上扩展选择范围直到遇到空行或标题行
let line = from.line;
while (line > 0) {
const prevLine = editor.getLine(line - 1);
if (shouldStop(prevLine)) break;
line--;
}
from.line = line;
from.ch = 0;
// 向下扩展选择范围直到遇到空行或标题行
line = to.line;
while (line < editor.lineCount() - 1) {
const nextLine = editor.getLine(line + 1);
if (shouldStop(nextLine)) break;
line++;
}
to.line = line;
to.ch = editor.getLine(line).length;
// 更新选择范围
editor.setSelection(from, to);
// 获取扩展后的选中内容
const selectedText = editor.getRange(from, to);
// 如果没有有效内容,直接返回
if (!selectedText.trim()) return;
// 将内容按行分割
const lines = selectedText.split('\n');
let firstLine = lines[0];
// 定义正则表达式,匹配Markdown标题(#、##、### 等)和无序列表(-、*、+)
const markdownPattern = /^(#{1,6}\s+|- |\* |\+ )/;
// 如果第一行匹配正则表达式,则去除相应的前缀
if (markdownPattern.test(firstLine)) {
firstLine = firstLine.replace(markdownPattern, '');
}
// 构建 Callout 标记
const calloutMark = `> [!${CONFIG.calloutType}]${CONFIG.collapsed ? '- ' : ' '}`;
// 根据配置决定是否添加标题
const titleLine = CONFIG.useFirstLineAsTitle ? ` ${firstLine}` : '';
// 确定要处理的内容行
const contentLines = CONFIG.useFirstLineAsTitle ? lines.slice(1) : lines;
// 检查前后是否有空行
const prevLine = from.line > 0 ? editor.getLine(from.line - 1) : '';
const nextLine = to.line < editor.lineCount() - 1 ? editor.getLine(to.line + 1) : '';
const hasLeadingNewline = !prevLine.trim();
const hasTrailingNewline = !nextLine.trim();
// 将内容行添加引用前缀 (移除末尾可能的换行符)
const formattedContent = contentLines.map(line => '> ' + line).join('\n');
// 组合最终内容
const finalContent = `${hasLeadingNewline ? '' : '\n'}${calloutMark}${titleLine}\n${formattedContent}${hasTrailingNewline ? '' : '\n'}`;
// 替换选中的内容
editor.replaceRange(finalContent, from, to);
// 计算新的光标位置
switch (CONFIG.cursorPosition) {
case "type": {
// 选中 Callout 类型
const typeStartLine = from.line + (hasLeadingNewline ? 0 : 1);
const typeStartCh = editor.getLine(typeStartLine).indexOf(CONFIG.calloutType);
editor.setSelection(
{ line: typeStartLine, ch: typeStartCh },
{ line: typeStartLine, ch: typeStartCh + CONFIG.calloutType.length }
);
break;
}
case "title": {
// 将光标放在标题行的末尾
const titleLine = from.line + (hasLeadingNewline ? 0 : 1);
const lineText = editor.getLine(titleLine);
editor.setCursor({
line: titleLine,
ch: lineText.length
});
break;
}
case "end":
default: {
// 找到最后一个引用行并定位到末尾
const newLines = finalContent.split('\n');
const lastContentLine = newLines.findLastIndex(line => line.startsWith('> '));
if (lastContentLine !== -1) {
const targetLine = from.line + lastContentLine;
const targetText = editor.getLine(targetLine);
editor.setCursor({
line: targetLine,
ch: targetText.length
});
}
break;
}
}
/*
// 第二版
// 获取编辑器实例
const editor = app.workspace.activeEditor.editor;
// 获取当前选中的内容和范围
let selection = editor.getSelection();
let from, to;
if (!selection) {
// 如果没有选中文本,从光标所在行开始扩展
const cursor = editor.getCursor();
// 设置初始范围为光标所在行的开始到结束
from = { line: cursor.line, ch: 0 };
to = { line: cursor.line, ch: editor.getLine(cursor.line).length };
} else {
// 如果有选中文本,获取选择范围
from = editor.getCursor('from');
to = editor.getCursor('to');
}
// 向上扩展选择范围直到遇到空行
let line = from.line;
while (line > 0) {
const prevLine = editor.getLine(line - 1);
if (!prevLine.trim()) break;
line--;
}
from.line = line;
from.ch = 0;
// 向下扩展选择范围直到遇到空行
line = to.line;
while (line < editor.lineCount() - 1) {
const nextLine = editor.getLine(line + 1);
if (!nextLine.trim()) break;
line++;
}
to.line = line;
to.ch = editor.getLine(line).length;
// 更新选择范围
editor.setSelection(from, to);
// 获取扩展后的选中内容
const selectedText = editor.getRange(from, to);
// 如果没有有效内容,直接返回
if (!selectedText.trim()) return;
// 将内容按行分割
const lines = selectedText.split('\n');
// 获取第一行
let firstLine = lines[0];
// 定义正则表达式,匹配Markdown标题(#、##、### 等)和无序列表(-、*、+)
const markdownPattern = /^(#{1,6}\s+|- |\* |\+ )/;
// 如果第一行匹配正则表达式,则去除相应的前缀
if (markdownPattern.test(firstLine)) {
firstLine = firstLine.replace(markdownPattern, '');
}
// 将除第一行以外的每一行前添加 "> " 进行引用
const formattedContent = lines.slice(1).map(line => '> ' + line).join('\n') + '\n';
// 在formattedContent之前加上 "> [!info]- \n",并保留第一行
const finalContent = '\n> [!info]- ' + firstLine + '\n' + formattedContent;
// 替换选中的内容
editor.replaceRange(finalContent, from, to);
*/
/*
// 原始版本
// 来自:[「QuickAdd」使用callout一键折叠所选 - 经验分享 - Obsidian 中文论坛](https://forum-zh.obsidian.md/t/topic/42073)
// 获取选中的内容
let selectedText = app.workspace.activeEditor.getSelection();
// 将内容按行分割
const lines = selectedText.split('\n');
// 获取第一行
let firstLine = lines[0];
// 定义正则表达式,匹配Markdown标题(#、##、### 等)和无序列表(-、*、+)
const markdownPattern = /^(#{1,6}\s+|- |\* |\+ )/;
// 如果第一行匹配正则表达式,则去除相应的前缀
if (markdownPattern.test(firstLine)) {
firstLine = firstLine.replace(markdownPattern, ''); // 去掉匹配的部分
}
// 将除第一行以外的每一行前添加 "> " 进行引用
const formattedContent = lines.slice(1).map(line => '> ' + line).join('\n') + '\n';
// 在formattedContent之前加上 "> [!info]- \n",并保留第一行
const finalContent = '\n> [!info]- ' + firstLine + '\n' + formattedContent;
// 替换选中的内容
app.workspace.activeEditor.editor.replaceSelection(finalContent);
*/
-%>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment