/* 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("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( () => void 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); } };