tailormade-eu_obsidian-task.../main.js

604 lines
20 KiB
JavaScript
Raw Normal View History

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => TaskExportPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian3 = require("obsidian");
// src/types.ts
var DEFAULT_SETTINGS = {
outputPath: "outstanding_tasks.csv",
customersFolder: "Customers",
autoExport: false,
exportOnSave: true,
exportOnModify: false,
showNotifications: true,
compressLevels: false,
includeHeader: true,
debounceDelay: 3,
delimiter: ","
};
// src/settings.ts
var import_obsidian = require("obsidian");
var TaskExportSettingTab = class extends import_obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new import_obsidian.Setting(containerEl).setName("Task Export Tool Settings").setHeading();
new import_obsidian.Setting(containerEl).setName("Output path").setDesc("CSV file location (relative to vault root)").addText((text) => text.setPlaceholder("outstanding_tasks.csv").setValue(this.plugin.settings.outputPath).onChange((value) => {
this.plugin.settings.outputPath = value || "outstanding_tasks.csv";
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Customers folder").setDesc("Root folder containing customer files").addText((text) => text.setPlaceholder("Customers").setValue(this.plugin.settings.customersFolder).onChange((value) => {
this.plugin.settings.customersFolder = value || "Customers";
void this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
new import_obsidian.Setting(containerEl).setName("Auto export").setDesc("Automatically export tasks when files change").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoExport).onChange((value) => {
this.plugin.settings.autoExport = value;
void this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
new import_obsidian.Setting(containerEl).setName("Export on save").setDesc("Trigger export when files are saved").addToggle((toggle) => toggle.setValue(this.plugin.settings.exportOnSave).onChange((value) => {
this.plugin.settings.exportOnSave = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Export on modify").setDesc("Trigger export when files are modified (more frequent)").addToggle((toggle) => toggle.setValue(this.plugin.settings.exportOnModify).onChange((value) => {
this.plugin.settings.exportOnModify = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Show notifications").setDesc("Display notifications on export completion").addToggle((toggle) => toggle.setValue(this.plugin.settings.showNotifications).onChange((value) => {
this.plugin.settings.showNotifications = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Compress levels").setDesc("Remove empty hierarchy columns from CSV output").addToggle((toggle) => toggle.setValue(this.plugin.settings.compressLevels).onChange((value) => {
this.plugin.settings.compressLevels = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Include header").setDesc("Include CSV header row in output").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeHeader).onChange((value) => {
this.plugin.settings.includeHeader = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("CSV delimiter").setDesc("Choose delimiter for CSV output. Comma is standard, semicolon is common in Europe.").addDropdown((dropdown) => dropdown.addOption(",", "Comma (,)").addOption(";", "Semicolon (;)").setValue(this.plugin.settings.delimiter).onChange((value) => {
this.plugin.settings.delimiter = value;
void this.plugin.saveSettings();
}));
new import_obsidian.Setting(containerEl).setName("Debounce delay").setDesc("Wait time after changes before exporting (1-30 seconds)").addSlider((slider) => slider.setLimits(1, 30, 1).setValue(this.plugin.settings.debounceDelay).setDynamicTooltip().onChange((value) => {
this.plugin.settings.debounceDelay = value;
void this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
}
};
// src/exporter.ts
var import_obsidian2 = require("obsidian");
// src/parser.ts
var _MarkdownParser = class {
/**
* Parses a markdown file and extracts all outstanding tasks.
*/
parseFile(content, customerName, projectName) {
const tasks = [];
const lines = content.split("\n");
const headers = [];
const parentTaskStack = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const headerMatch = line.match(_MarkdownParser.HEADER_REGEX);
if (headerMatch) {
const headerLevel = headerMatch[1].length - 1;
const headerText = headerMatch[2].trim();
while (headers.length <= headerLevel) {
headers.push("");
}
headers[headerLevel] = headerText;
for (let j = headerLevel + 1; j < headers.length; j++) {
headers[j] = "";
}
parentTaskStack.length = 0;
continue;
}
if (_MarkdownParser.COMPLETED_TASK_REGEX.test(line)) {
continue;
}
const taskMatch = line.match(_MarkdownParser.TASK_REGEX);
if (taskMatch) {
const indent = taskMatch[1].length;
const taskText = taskMatch[3].trim();
if (_MarkdownParser.CHECKMARK_REGEX.test(taskText)) {
continue;
}
const hasSubTasks = this.hasSubTasks(lines, i + 1, indent);
if (hasSubTasks) {
while (parentTaskStack.length > 0 && parentTaskStack[parentTaskStack.length - 1].indentLevel >= indent) {
parentTaskStack.pop();
}
parentTaskStack.push({ indentLevel: indent, taskText });
} else {
while (parentTaskStack.length > 0 && parentTaskStack[parentTaskStack.length - 1].indentLevel >= indent) {
parentTaskStack.pop();
}
const task = {
customerName,
projectName,
task: taskText,
levels: []
};
const levels = [];
levels.push(...headers);
if (parentTaskStack.length > 0) {
const parents = [...parentTaskStack].reverse();
for (const parent of parents) {
levels.push(parent.taskText);
}
}
task.levels = levels;
tasks.push(task);
}
}
}
return tasks;
}
/**
* Checks if a task has sub-tasks by looking at following lines.
*/
hasSubTasks(lines, startIndex, currentIndent) {
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) {
continue;
}
if (_MarkdownParser.HEADER_REGEX.test(line)) {
return false;
}
const taskMatch = line.match(_MarkdownParser.TASK_REGEX);
if (taskMatch) {
const indent = taskMatch[1].length;
if (indent > currentIndent) {
return true;
}
if (indent <= currentIndent) {
return false;
}
}
}
return false;
}
};
var MarkdownParser = _MarkdownParser;
MarkdownParser.HEADER_REGEX = /^(#{2,})\s+(.+)$/;
MarkdownParser.TASK_REGEX = /^(\s*)-\s+\[([ ])\]\s+(.+)$/;
MarkdownParser.COMPLETED_TASK_REGEX = /^(\s*)-\s+\[(x|X)\]/;
MarkdownParser.CHECKMARK_REGEX = /✅\s+\d{4}-\d{2}-\d{2}/;
// src/csv-writer.ts
var CsvWriter = class {
constructor(delimiter = ",") {
this.delimiter = delimiter;
}
/**
* Generates CSV content from tasks with UTF-8 BOM encoding.
*/
generateCsv(tasks, compressLevels = false, includeHeader = true) {
if (tasks.length === 0) {
throw new Error("No tasks to export");
}
const maxLevelDepth = compressLevels ? this.determineMaxCompressedLevelDepth(tasks) : this.determineMaxLevelDepth(tasks);
let csv = "";
if (includeHeader) {
csv += `CustomerName${this.delimiter}ProjectName`;
for (let i = 1; i <= maxLevelDepth; i++) {
csv += `${this.delimiter}Level${i}`;
}
csv += `${this.delimiter}Task
`;
}
for (const task of tasks) {
csv += this.escapeField(task.customerName);
csv += this.delimiter;
csv += this.escapeField(task.projectName);
if (compressLevels) {
const nonEmptyLevels = task.levels.filter((l) => l !== "");
for (const level of nonEmptyLevels) {
csv += this.delimiter;
csv += this.escapeField(level);
}
} else {
for (let i = 0; i < maxLevelDepth; i++) {
csv += this.delimiter;
if (i < task.levels.length) {
csv += this.escapeField(task.levels[i]);
}
}
}
csv += this.delimiter;
csv += this.escapeField(task.task);
csv += "\n";
}
return "\uFEFF" + csv;
}
/**
* Determines the maximum level depth across all tasks (includes empty slots).
*/
determineMaxLevelDepth(tasks) {
let maxDepth = 0;
for (const task of tasks) {
if (task.levels.length > maxDepth) {
maxDepth = task.levels.length;
}
}
return maxDepth;
}
/**
* Determines the maximum number of non-empty levels across all tasks.
*/
determineMaxCompressedLevelDepth(tasks) {
let maxDepth = 0;
for (const task of tasks) {
const nonEmptyCount = task.levels.filter((l) => l !== "").length;
if (nonEmptyCount > maxDepth) {
maxDepth = nonEmptyCount;
}
}
return maxDepth;
}
/**
* Escapes a CSV field according to RFC 4180.
* - Wraps in quotes if contains delimiter, newline, or quote
* - Doubles any quotes inside the field
*/
escapeField(field) {
if (!field) {
return "";
}
const needsQuotes = field.includes(this.delimiter) || field.includes('"') || field.includes("\n") || field.includes("\r");
if (needsQuotes) {
field = field.replace(/"/g, '""');
return `"${field}"`;
}
return field;
}
};
// src/exporter.ts
var TaskExporter = class {
constructor(app) {
this.app = app;
this.parser = new MarkdownParser();
this.csvWriter = new CsvWriter();
}
/**
* Extracts all outstanding tasks from the Customers directory.
*/
async extractTasks(customersFolder, verbose = false) {
const allTasks = [];
const normalizedPath = (0, import_obsidian2.normalizePath)(customersFolder);
const folder = this.app.vault.getAbstractFileByPath(normalizedPath);
if (!folder) {
console.error(`Customers directory not found: ${normalizedPath}`);
return allTasks;
}
const allFiles = this.app.vault.getMarkdownFiles();
const customerFiles = allFiles.filter(
(file) => file.path.startsWith(normalizedPath + "/")
);
for (const file of customerFiles) {
const { customerName, projectName } = this.extractCustomerAndProject(file, normalizedPath);
if (customerName.startsWith(".")) {
continue;
}
if (verbose) {
console.debug(`Processing: ${customerName} / ${projectName}`);
}
try {
const content = await this.app.vault.read(file);
const tasks = this.parser.parseFile(content, customerName, projectName);
if (tasks.length > 0) {
allTasks.push(...tasks);
if (verbose) {
console.debug(` Found ${tasks.length} task(s)`);
}
}
} catch (ex) {
console.warn(`Failed to process file ${file.path}:`, ex);
}
}
return allTasks;
}
/**
* Exports tasks to CSV file.
*/
async exportToFile(tasks, outputPath, compressLevels = false, includeHeader = true, delimiter = ",") {
const csvWriter = new CsvWriter(delimiter);
const csv = csvWriter.generateCsv(tasks, compressLevels, includeHeader);
const normalizedPath = (0, import_obsidian2.normalizePath)(outputPath);
await this.app.vault.adapter.write(normalizedPath, csv);
}
/**
* Extracts customer name and project name from file path.
* Customer name is the first folder under customersFolder.
* Project name is the filename without extension.
*/
extractCustomerAndProject(file, customersFolder) {
let relativePath = file.path.substring(customersFolder.length + 1);
const parts = relativePath.split("/");
const customerName = parts[0];
const projectName = file.basename;
return { customerName, projectName };
}
};
// src/file-watcher.ts
var FileWatcher = class {
constructor(callback, debounceDelay, customersFolder) {
this.debounceTimer = null;
this.isEnabled = false;
this.callback = callback;
this.debounceDelay = debounceDelay * 1e3;
this.customersFolder = customersFolder;
}
/**
* Updates the debounce delay.
*/
setDebounceDelay(seconds) {
this.debounceDelay = seconds * 1e3;
}
/**
* Updates the customers folder path.
*/
setCustomersFolder(folder) {
this.customersFolder = folder;
}
/**
* Triggers the debounced callback.
*/
trigger() {
if (!this.isEnabled) {
return;
}
if (this.debounceTimer !== null) {
window.clearTimeout(this.debounceTimer);
}
this.debounceTimer = window.setTimeout(() => {
this.callback();
this.debounceTimer = null;
}, this.debounceDelay);
}
/**
* Checks if a file should trigger an export.
*/
shouldTrigger(file) {
if (!this.isEnabled || !file) {
return false;
}
if (file.extension !== "md") {
return false;
}
if (!file.path.startsWith(this.customersFolder + "/")) {
return false;
}
return true;
}
/**
* Enables the file watcher.
*/
enable() {
this.isEnabled = true;
}
/**
* Disables the file watcher and cancels any pending callbacks.
*/
disable() {
this.isEnabled = false;
if (this.debounceTimer !== null) {
window.clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
}
/**
* Returns whether the watcher is enabled.
*/
isWatcherEnabled() {
return this.isEnabled;
}
};
// src/main.ts
var TaskExportPlugin = class extends import_obsidian3.Plugin {
constructor() {
super(...arguments);
this.lastExportTime = null;
this.exportInProgress = false;
}
async onload() {
await this.loadSettings();
this.exporter = new TaskExporter(this.app);
this.fileWatcher = new FileWatcher(
() => this.autoExport(),
this.settings.debounceDelay,
this.settings.customersFolder
);
this.addRibbonIcon("file-text", "Export Outstanding Tasks", async () => {
await this.exportTasks();
});
this.addCommand({
id: "export-tasks",
name: "Export Outstanding Tasks",
callback: async () => {
await this.exportTasks();
}
});
this.addCommand({
id: "toggle-auto-export",
name: "Toggle Auto-Export",
callback: async () => {
this.settings.autoExport = !this.settings.autoExport;
await this.saveSettings();
this.updateFileWatcher();
const status = this.settings.autoExport ? "enabled" : "disabled";
new import_obsidian3.Notice(`Auto-export ${status}`);
}
});
this.addCommand({
id: "open-settings",
name: "Open Export Settings",
callback: () => {
this.app.setting.open();
this.app.setting.openTabById(this.manifest.id);
}
});
this.addSettingTab(new TaskExportSettingTab(this.app, this));
this.statusBarItem = this.addStatusBarItem();
this.updateStatusBar();
this.registerFileEvents();
this.updateFileWatcher();
console.debug("Task Export Plugin loaded");
}
onunload() {
this.fileWatcher.disable();
console.debug("Task Export Plugin unloaded");
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
/**
* Registers file modification events.
*/
registerFileEvents() {
this.registerEvent(
this.app.vault.on("modify", (file) => {
if (file instanceof import_obsidian3.TFile && this.fileWatcher.shouldTrigger(file)) {
if (this.settings.exportOnModify || this.settings.exportOnSave) {
this.fileWatcher.trigger();
}
}
})
);
}
/**
* Updates the file watcher based on current settings.
*/
updateFileWatcher() {
if (this.settings.autoExport) {
this.fileWatcher.enable();
this.fileWatcher.setDebounceDelay(this.settings.debounceDelay);
this.fileWatcher.setCustomersFolder(this.settings.customersFolder);
} else {
this.fileWatcher.disable();
}
}
/**
* Performs automatic export (called by file watcher).
*/
async autoExport() {
try {
await this.exportTasks(false);
} catch (error) {
console.error("Auto-export failed:", error);
}
}
/**
* Exports tasks to CSV.
*/
async exportTasks(showNotice = true) {
if (this.exportInProgress) {
if (showNotice) {
new import_obsidian3.Notice("Export already in progress...");
}
return;
}
this.exportInProgress = true;
try {
const tasks = await this.exporter.extractTasks(this.settings.customersFolder, false);
if (tasks.length === 0) {
if (showNotice && this.settings.showNotifications) {
new import_obsidian3.Notice("No outstanding tasks found");
}
return;
}
await this.exporter.exportToFile(
tasks,
this.settings.outputPath,
this.settings.compressLevels,
this.settings.includeHeader,
this.settings.delimiter
);
this.lastExportTime = new Date();
this.updateStatusBar();
if (showNotice && this.settings.showNotifications) {
new import_obsidian3.Notice(`Exported ${tasks.length} task(s) to ${this.settings.outputPath}`);
}
} catch (error) {
console.error("Export failed:", error);
if (showNotice && this.settings.showNotifications) {
new import_obsidian3.Notice(`Export failed: ${error.message}`);
}
} finally {
this.exportInProgress = false;
}
}
/**
* Updates the status bar with last export time.
*/
updateStatusBar() {
if (!this.lastExportTime) {
this.statusBarItem.setText("");
return;
}
const now = new Date();
const diff = now.getTime() - this.lastExportTime.getTime();
const minutes = Math.floor(diff / 6e4);
let timeText = "";
if (minutes < 1) {
timeText = "just now";
} else if (minutes === 1) {
timeText = "1 minute ago";
} else if (minutes < 60) {
timeText = `${minutes} minutes ago`;
} else {
const hours = Math.floor(minutes / 60);
if (hours === 1) {
timeText = "1 hour ago";
} else {
timeText = `${hours} hours ago`;
}
}
this.statusBarItem.setText(`\u2705 Last export: ${timeText}`);
window.setTimeout(() => this.updateStatusBar(), 6e4);
}
};