2025-11-16 15:03:25 +00:00
/ *
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 ( ) ;
2025-11-17 09:07:40 +00:00
new import _obsidian . Setting ( containerEl ) . setName ( "Output path" ) . setDesc ( "CSV file location (relative to vault root)" ) . addText ( ( text ) => text . setPlaceholder ( "tasks.csv" ) . setValue ( this . plugin . settings . outputPath ) . onChange ( ( value ) => {
this . plugin . settings . outputPath = value || "tasks.csv" ;
2025-11-16 15:03:25 +00:00
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 ( ) ;
} ) ) ;
2025-11-17 09:07:40 +00:00
new import _obsidian . Setting ( containerEl ) . setName ( "CSV delimiter" ) . setDesc ( "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 ) => {
2025-11-16 15:03:25 +00:00
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 (
2025-11-17 06:39:15 +00:00
( ) => void this . autoExport ( ) ,
2025-11-16 15:03:25 +00:00
this . settings . debounceDelay ,
this . settings . customersFolder
) ;
2025-11-17 07:43:50 +00:00
this . addRibbonIcon ( "file-text" , "Export outstanding tasks" , async ( ) => {
2025-11-16 15:03:25 +00:00
await this . exportTasks ( ) ;
} ) ;
this . addCommand ( {
id : "export-tasks" ,
2025-11-17 07:43:50 +00:00
name : "Export outstanding tasks" ,
2025-11-16 15:03:25 +00:00
callback : async ( ) => {
await this . exportTasks ( ) ;
}
} ) ;
this . addCommand ( {
id : "toggle-auto-export" ,
2025-11-17 07:43:50 +00:00
name : "Toggle auto-export" ,
2025-11-16 15:03:25 +00:00
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" ,
2025-11-17 07:43:50 +00:00
name : "Open export settings" ,
2025-11-16 15:03:25 +00:00
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 ( ` \u 2705 Last export: ${ timeText } ` ) ;
window . setTimeout ( ( ) => this . updateStatusBar ( ) , 6e4 ) ;
}
} ;