mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor(canvas): consolidate Canvas parsing into core CanvasParser
Major refactoring to centralize Canvas parsing functionality and improve timeline features: - Consolidate Canvas parsing logic into CanvasParser class - Remove separate CanvasEntry module (functionality moved to CanvasParser) - Add static helper methods for Canvas JSON parsing and node operations - Enhance timeline sidebar with duration badges for time ranges - Fix timezone handling using local time instead of UTC for consistency - Update Canvas task updater to use centralized parser - Clean up settings UI removing obsolete indexing options - Add comprehensive parser refactoring documentation This improves code organization, reduces duplication, and enhances the timeline view with better time range visualization including duration badges.
This commit is contained in:
parent
ec032f0eae
commit
52573bfcd0
16 changed files with 485 additions and 131 deletions
262
docs/parser-refactoring-plan.md
Normal file
262
docs/parser-refactoring-plan.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Parser Refactoring Plan
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### 1. Parser Duplication Issues
|
||||
|
||||
#### Canvas Parser Chain (3 layers)
|
||||
```
|
||||
src/parsers/canvas-parser.ts (redirect)
|
||||
→ src/dataflow/core/CanvasParser.ts (implementation)
|
||||
→ src/dataflow/parsers/CanvasEntry.ts (entry point)
|
||||
```
|
||||
|
||||
**Current Usage:**
|
||||
- Tests import from `src/parsers/canvas-parser.ts` (2 test files)
|
||||
- Worker imports directly from `src/dataflow/core/CanvasParser.ts`
|
||||
- Orchestrator uses `parseCanvas()` from `CanvasEntry.ts`
|
||||
|
||||
#### TaskParserConfig Duplication
|
||||
- **Old**: `src/types/task.d.ts:267` - Simple interface with regex patterns
|
||||
- **New**: `src/types/TaskParserConfig.ts:12` - Comprehensive with 60+ fields
|
||||
- **Active Usage**: All 13 imports use the new TaskParserConfig.ts
|
||||
|
||||
#### Parser Implementation Overlap
|
||||
- **MarkdownTaskParser**: Core implementation (1700+ lines)
|
||||
- **ConfigurableTaskParser**: Extends MarkdownTaskParser with time parsing
|
||||
- **FileMetadataTaskParser**: Separate implementation for metadata
|
||||
- **TaskParsingService**: High-level orchestrator
|
||||
|
||||
### 2. Dependency Analysis
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Orchestrator] --> B[CanvasEntry::parseCanvas]
|
||||
B --> C[CanvasParser::parseCanvasFile]
|
||||
C --> D[ConfigurableTaskParser]
|
||||
|
||||
E[Tests] --> F[canvas-parser.ts redirect]
|
||||
F --> C
|
||||
|
||||
G[Worker] --> C
|
||||
|
||||
H[TaskParsingService] --> D
|
||||
H --> I[FileMetadataTaskParser]
|
||||
```
|
||||
|
||||
## Refactoring Steps
|
||||
|
||||
### Phase 1: Immediate Cleanup (Low Risk)
|
||||
|
||||
#### 1.1 Remove Canvas Parser Redirect
|
||||
**Files to modify:**
|
||||
- Delete: `src/parsers/canvas-parser.ts`
|
||||
- Update imports in:
|
||||
- `src/__tests__/CanvasParser.test.ts`
|
||||
- `src/__tests__/CanvasIntegration.test.ts`
|
||||
|
||||
**Change:**
|
||||
```typescript
|
||||
// Before
|
||||
import { CanvasParser } from '../parsers/canvas-parser';
|
||||
|
||||
// After
|
||||
import { CanvasParser } from '../dataflow/core/CanvasParser';
|
||||
```
|
||||
|
||||
#### 1.2 Remove Old TaskParserConfig
|
||||
**Files to modify:**
|
||||
- Remove interface from `src/types/task.d.ts:267-284`
|
||||
- No import changes needed (all use the new one)
|
||||
|
||||
### Phase 2: Consolidate Canvas Parsing (Medium Risk)
|
||||
|
||||
#### 2.1 Merge CanvasEntry into CanvasParser
|
||||
**Goal:** Single entry point for Canvas parsing
|
||||
|
||||
**New structure:**
|
||||
```typescript
|
||||
// src/dataflow/core/CanvasParser.ts
|
||||
export class CanvasParser {
|
||||
// Existing implementation
|
||||
|
||||
// Add entry function from CanvasEntry
|
||||
static async parseCanvas(
|
||||
filePath: string,
|
||||
fileContent: string,
|
||||
plugin: TaskProgressBarPlugin
|
||||
): Promise<TaskResult> {
|
||||
// Merge logic from CanvasEntry
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Update Canvas Updater
|
||||
**Goal:** Share parsing logic with CanvasParser
|
||||
|
||||
**Files to modify:**
|
||||
- `src/parsers/canvas-task-updater.ts`
|
||||
- Import Canvas types from CanvasParser
|
||||
- Reuse node parsing logic
|
||||
|
||||
### Phase 3: Unified Parser Architecture (High Impact)
|
||||
|
||||
#### 3.1 Create Unified Parser Registry
|
||||
```typescript
|
||||
// src/parsing/ParserRegistry.ts
|
||||
export class ParserRegistry {
|
||||
private parsers: Map<string, BaseParser>;
|
||||
|
||||
register(extension: string, parser: BaseParser) { }
|
||||
parse(file: TFile, config: TaskParserConfig): Promise<TaskResult> { }
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Standardize Parser Interface
|
||||
```typescript
|
||||
// src/parsing/BaseParser.ts
|
||||
export interface BaseParser {
|
||||
parse(content: string, config: TaskParserConfig): Promise<ParseResult>;
|
||||
update(content: string, updates: TaskUpdate[]): Promise<string>;
|
||||
supports(file: TFile): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Implement Specific Parsers
|
||||
```
|
||||
src/parsing/
|
||||
├── BaseParser.ts
|
||||
├── ParserRegistry.ts
|
||||
├── parsers/
|
||||
│ ├── MarkdownParser.ts
|
||||
│ ├── CanvasParser.ts
|
||||
│ └── ICSParser.ts
|
||||
└── updaters/
|
||||
├── MarkdownUpdater.ts
|
||||
└── CanvasUpdater.ts
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Step-by-Step Execution
|
||||
|
||||
1. **Week 1**: Phase 1 - Immediate Cleanup
|
||||
- Remove redirects and duplicates
|
||||
- Run full test suite
|
||||
- Monitor for issues
|
||||
|
||||
2. **Week 2**: Phase 2.1 - Canvas Consolidation
|
||||
- Merge CanvasEntry into CanvasParser
|
||||
- Update Orchestrator imports
|
||||
- Test Canvas functionality
|
||||
|
||||
3. **Week 3**: Phase 2.2 - Updater Integration
|
||||
- Refactor Canvas updater
|
||||
- Share parsing logic
|
||||
- Test update operations
|
||||
|
||||
4. **Week 4+**: Phase 3 - Architecture (Optional)
|
||||
- Implement if needed for new features
|
||||
- Gradual migration
|
||||
- Maintain backward compatibility
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
- [ ] Canvas parsing tests pass
|
||||
- [ ] Markdown parsing tests pass
|
||||
- [ ] Task parsing service tests pass
|
||||
- [ ] Worker tests pass
|
||||
|
||||
### Integration Tests
|
||||
- [ ] Canvas file operations
|
||||
- [ ] Task updates in Canvas
|
||||
- [ ] Cross-file task references
|
||||
- [ ] Performance benchmarks
|
||||
|
||||
### Manual Testing
|
||||
- [ ] Create tasks in Canvas
|
||||
- [ ] Update task status in Canvas
|
||||
- [ ] Parse complex Canvas files
|
||||
- [ ] Verify no UI regressions
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Low Risk Changes
|
||||
- Removing redirect files
|
||||
- Removing unused interfaces
|
||||
- Import path updates
|
||||
|
||||
### Medium Risk Changes
|
||||
- Merging CanvasEntry logic
|
||||
- Sharing parsing code between parser/updater
|
||||
|
||||
### High Risk Changes
|
||||
- Full architecture refactor
|
||||
- Parser registry implementation
|
||||
- Breaking API changes
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Code Reduction**: ~200 lines removed from duplicates
|
||||
2. **Performance**: No regression in parsing speed
|
||||
3. **Maintainability**: Single source of truth for each parser
|
||||
4. **Test Coverage**: All tests passing
|
||||
5. **Bug Count**: No new parsing issues reported
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
1. Revert commits in reverse order
|
||||
2. Restore redirect files temporarily
|
||||
3. Document specific failure points
|
||||
4. Create targeted fixes
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### Phase 1: Immediate Cleanup ✅ COMPLETED
|
||||
- ✅ Removed canvas-parser.ts redirect file
|
||||
- ✅ Updated test imports to use dataflow/core/CanvasParser
|
||||
- ✅ Removed old TaskParserConfig from task.d.ts
|
||||
- ✅ All tests passing
|
||||
- ✅ TypeScript compilation clean
|
||||
- ✅ Build successful
|
||||
|
||||
### Phase 2.1: Canvas Consolidation ✅ COMPLETED
|
||||
- ✅ Added static parseCanvas method to CanvasParser
|
||||
- ✅ Removed CanvasEntry.ts file
|
||||
- ✅ Updated Orchestrator to use CanvasParser.parseCanvas()
|
||||
- ✅ Updated WorkerOrchestrator dynamic imports
|
||||
- ✅ Reduced Canvas parsing layers from 3 to 2
|
||||
|
||||
### Phase 2.2: Canvas Updater Integration ✅ COMPLETED
|
||||
- ✅ Added utility methods to CanvasParser:
|
||||
- parseCanvasJSON() - Safe JSON parsing
|
||||
- findTextNode() - Find node by ID
|
||||
- getTextNodes() - Get all text nodes
|
||||
- ✅ Refactored canvas-task-updater to use shared utilities
|
||||
- ✅ Eliminated duplicate Canvas JSON parsing logic
|
||||
- ✅ All Canvas functionality tested and working
|
||||
|
||||
## Results Summary
|
||||
|
||||
### Code Improvements
|
||||
- **Lines Removed**: ~60 lines (redundant code)
|
||||
- **Files Deleted**: 2 (canvas-parser.ts, CanvasEntry.ts)
|
||||
- **Architecture**: Simplified from 3-layer to 2-layer Canvas parsing
|
||||
- **Code Reuse**: Shared utilities between parser and updater
|
||||
|
||||
### Quality Metrics
|
||||
- ✅ **TypeScript**: No compilation errors
|
||||
- ✅ **Build**: Successful (3.3MB bundle)
|
||||
- ✅ **Tests**: Canvas-specific tests passing
|
||||
- ✅ **Backward Compatibility**: Maintained through static methods
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. ✅ Document current state
|
||||
2. ✅ Remove canvas-parser.ts redirect
|
||||
3. ✅ Remove old TaskParserConfig interface
|
||||
4. ✅ Test changes
|
||||
5. ✅ Proceed with Phase 2
|
||||
6. ⬜ Consider Phase 3 (Unified Parser Architecture) for future major refactor
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
import { isSupportedFile, getFileType, SupportedFileType } from '../utils/file/file-type-detector';
|
||||
import { CanvasParser } from '../parsers/canvas-parser';
|
||||
import { CanvasParser } from '../dataflow/core/CanvasParser';
|
||||
import { getConfig } from '../common/task-parser-config';
|
||||
|
||||
// Mock TFile for testing
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Tests for Canvas file parsing functionality
|
||||
*/
|
||||
|
||||
import { CanvasParser } from '../parsers/canvas-parser';
|
||||
import { CanvasParser } from '../dataflow/core/CanvasParser';
|
||||
import { CanvasData, CanvasTextData } from '../types/canvas';
|
||||
import { getConfig } from '../common/task-parser-config';
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export function createFileSourceSettings(
|
|||
containerEl: HTMLElement,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
): void {
|
||||
const config = plugin.settings.fileSource;
|
||||
const config = plugin.settings?.fileSource;
|
||||
|
||||
// Main FileSource enable/disable toggle
|
||||
createEnableToggle(containerEl, plugin, config);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { TaskProgressBarSettingTab } from "../../setting";
|
|||
import { t } from "../../translations/helper";
|
||||
import { SingleFolderSuggest } from "../AutoComplete";
|
||||
import { ConfirmModal } from "../ConfirmModal";
|
||||
import { createFileSourceSettings } from "./FileSourceSettings";
|
||||
|
||||
/**
|
||||
* Renders the Index Settings tab that consolidates all indexing-related settings
|
||||
|
|
@ -26,26 +27,17 @@ export function renderIndexSettingsTab(
|
|||
// SECTION 0: Core Architecture Configuration
|
||||
// ========================================
|
||||
new Setting(containerEl)
|
||||
.setName(t("Core Architecture"))
|
||||
.setDesc(t("Configure the core data processing architecture."))
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable Dataflow Architecture"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Use the modern Dataflow architecture for improved performance and reliability. This is the recommended setting for all users.",
|
||||
),
|
||||
)
|
||||
.setName(t("Enable Indexer"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(settingTab.plugin.settings.dataflowEnabled ?? true);
|
||||
toggle.onChange(async (value) => {
|
||||
settingTab.plugin.settings.dataflowEnabled = value;
|
||||
await settingTab.plugin.saveSettings();
|
||||
settingTab.plugin.settings.enableIndexer = value;
|
||||
settingTab.applySettingsUpdate();
|
||||
settingTab.display(); // Refresh settings display
|
||||
|
||||
// Show restart notice
|
||||
const restartNotice = new Notice(
|
||||
t("Please restart Obsidian for the architecture change to take effect."),
|
||||
new Notice(
|
||||
t("Please restart Obsidian for the Indexer change to take effect."),
|
||||
8000
|
||||
);
|
||||
});
|
||||
|
|
@ -59,26 +51,6 @@ export function renderIndexSettingsTab(
|
|||
.setDesc(t("Configure how tasks are parsed from markdown content."))
|
||||
.setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Enable Task Genius indexer"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Enable the Task Genius indexer to scan and index tasks from your entire vault. This is required for task views and search functionality.",
|
||||
),
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(settingTab.plugin.settings.enableIndexer);
|
||||
toggle.onChange((value) => {
|
||||
settingTab.plugin.settings.enableIndexer = value;
|
||||
if (!value) {
|
||||
// If indexer is disabled, also disable views
|
||||
settingTab.plugin.settings.enableView = false;
|
||||
}
|
||||
settingTab.applySettingsUpdate();
|
||||
settingTab.display(); // Refresh settings display
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Prefer metadata format of task"))
|
||||
.setDesc(
|
||||
|
|
@ -441,8 +413,8 @@ export function renderIndexSettingsTab(
|
|||
t("Configure how files can be recognized and treated as tasks with various strategies."),
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
// File Task settings have been migrated to File Filter tab for central management.
|
||||
|
||||
createFileSourceSettings(containerEl, settingTab.plugin);
|
||||
|
||||
// ========================================
|
||||
// SECTION 3: Performance Settings
|
||||
|
|
|
|||
|
|
@ -490,8 +490,9 @@ export class TimelineSidebarView extends ItemView {
|
|||
}
|
||||
|
||||
// Create enhanced datetime by combining date and time component
|
||||
// Use local time (setHours) instead of UTC to match the parsed time components
|
||||
const enhancedDateTime = new Date(date);
|
||||
enhancedDateTime.setUTCHours(
|
||||
enhancedDateTime.setHours(
|
||||
relevantTimeComponent.hour,
|
||||
relevantTimeComponent.minute,
|
||||
relevantTimeComponent.second || 0,
|
||||
|
|
@ -506,7 +507,8 @@ export class TimelineSidebarView extends ItemView {
|
|||
// create the end time from the range partner
|
||||
if (relevantTimeComponent.isRange && !relevantEndTime && relevantTimeComponent.rangePartner) {
|
||||
const endDateTime = new Date(date);
|
||||
endDateTime.setUTCHours(
|
||||
// Use local time (setHours) instead of UTC to match the parsed time components
|
||||
endDateTime.setHours(
|
||||
relevantTimeComponent.rangePartner.hour,
|
||||
relevantTimeComponent.rangePartner.minute,
|
||||
relevantTimeComponent.rangePartner.second || 0,
|
||||
|
|
@ -678,21 +680,69 @@ export class TimelineSidebarView extends ItemView {
|
|||
if (event.timeInfo?.timeComponent) {
|
||||
// Use parsed time component for accurate display
|
||||
const { timeComponent, isRange, endTime } = event.timeInfo;
|
||||
|
||||
|
||||
if (isRange && endTime) {
|
||||
// Display time range
|
||||
const startTimeStr = this.formatTimeComponent(timeComponent);
|
||||
const endTimeStr = moment(endTime).format("HH:mm");
|
||||
timeEl.setText(`${startTimeStr}-${endTimeStr}`);
|
||||
timeEl.addClass("timeline-event-time-range");
|
||||
// Add duration badge attribute for CSS ::after to render
|
||||
try {
|
||||
const start = event.timeInfo?.primaryTime;
|
||||
if (start && endTime.getTime() > start.getTime()) {
|
||||
const minutes = Math.round((endTime.getTime() - start.getTime()) / 60000);
|
||||
const duration = minutes >= 60
|
||||
? `${Math.floor(minutes / 60)}h${minutes % 60 ? ` ${minutes % 60}m` : ''}`
|
||||
: `${minutes}m`;
|
||||
timeEl.setAttribute("data-duration", duration);
|
||||
}
|
||||
} catch (_) {}
|
||||
} else {
|
||||
// Display single time
|
||||
timeEl.setText(this.formatTimeComponent(timeComponent));
|
||||
timeEl.addClass("timeline-event-time-single");
|
||||
}
|
||||
} else {
|
||||
// Fallback to default time display
|
||||
timeEl.setText(moment(event.time).format("HH:mm"));
|
||||
// Try to parse time directly from content as a fallback to avoid 00:00 mismatches
|
||||
const content = event.content || "";
|
||||
// Detect time range first (e.g., 15:00-16:00)
|
||||
const rangeRegex = /([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?\s*[-~~]\s*([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?/;
|
||||
const rangeMatch = content.match(rangeRegex);
|
||||
if (rangeMatch) {
|
||||
const start = `${rangeMatch[1].padStart(2, '0')}:${rangeMatch[2]}${rangeMatch[3] ? `:${rangeMatch[3]}` : ''}`;
|
||||
const end = `${rangeMatch[4].padStart(2, '0')}:${rangeMatch[5]}${rangeMatch[6] ? `:${rangeMatch[6]}` : ''}`;
|
||||
timeEl.setText(`${start}-${end}`);
|
||||
timeEl.addClass("timeline-event-time-range");
|
||||
return;
|
||||
}
|
||||
// Detect 12-hour format (e.g., 3:30 PM)
|
||||
const pattern12h = /(1[0-2]|0?[1-9]):([0-5]\d)(?::([0-5]\d))?\s*(AM|PM|am|pm)/;
|
||||
const m12 = content.match(pattern12h);
|
||||
if (m12) {
|
||||
let hour = parseInt(m12[1], 10);
|
||||
const minute = m12[2];
|
||||
const second = m12[3];
|
||||
const period = m12[4].toUpperCase();
|
||||
if (period === 'PM' && hour !== 12) hour += 12;
|
||||
if (period === 'AM' && hour === 12) hour = 0;
|
||||
const display = `${hour.toString().padStart(2, '0')}:${minute}${second ? `:${second}` : ''}`;
|
||||
timeEl.setText(display);
|
||||
timeEl.addClass("timeline-event-time-single");
|
||||
return;
|
||||
}
|
||||
// Detect 24-hour single time (e.g., 15:00)
|
||||
const pattern24h = /([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?/;
|
||||
const m24 = content.match(pattern24h);
|
||||
if (m24) {
|
||||
const display = `${m24[1].padStart(2, '0')}:${m24[2]}${m24[3] ? `:${m24[3]}` : ''}`;
|
||||
timeEl.setText(display);
|
||||
timeEl.addClass("timeline-event-time-single");
|
||||
return;
|
||||
}
|
||||
// Fallback to default time display - prefer enhanced primaryTime when available
|
||||
const fallbackTime = event.timeInfo?.primaryTime || event.time;
|
||||
timeEl.setText(moment(fallbackTime).format("HH:mm"));
|
||||
timeEl.addClass("timeline-event-time-default");
|
||||
}
|
||||
}
|
||||
|
|
@ -769,9 +819,19 @@ export class TimelineSidebarView extends ItemView {
|
|||
return true;
|
||||
}
|
||||
|
||||
// Heuristic: detect explicit time patterns in the content (e.g., "15:00", "3:30 PM")
|
||||
if (event.content) {
|
||||
const timePattern24h = /(^|[^0-9])([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?/;
|
||||
const timePattern12h = /(^|\s)(1[0-2]|0?[1-9]):([0-5]\d)(?::([0-5]\d))?\s*(AM|PM|am|pm)/;
|
||||
if (timePattern24h.test(event.content) || timePattern12h.test(event.content)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the original time has non-zero hours/minutes (not just midnight)
|
||||
// Use local time (getHours) to check for specific time
|
||||
const time = event.timeInfo?.primaryTime || event.time;
|
||||
return time.getUTCHours() !== 0 || time.getUTCMinutes() !== 0 || time.getUTCSeconds() !== 0;
|
||||
return time.getHours() !== 0 || time.getMinutes() !== 0 || time.getSeconds() !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -942,21 +1002,21 @@ export class TimelineSidebarView extends ItemView {
|
|||
|
||||
// Create a section for date-only events
|
||||
const dateOnlySection = containerEl.createDiv("timeline-date-only-section");
|
||||
|
||||
|
||||
const sectionHeaderEl = dateOnlySection.createDiv("timeline-date-only-header");
|
||||
const headerTimeEl = sectionHeaderEl.createDiv("timeline-event-time timeline-event-time-date-only");
|
||||
headerTimeEl.setText("All day");
|
||||
|
||||
|
||||
const headerTextEl = sectionHeaderEl.createDiv("timeline-date-only-title");
|
||||
headerTextEl.setText(`${events.length} all-day event${events.length > 1 ? 's' : ''}`);
|
||||
|
||||
// Render each date-only event
|
||||
// Render each date-only event (hide individual time labels)
|
||||
events.forEach((event) => {
|
||||
this.renderEvent(dateOnlySection, event);
|
||||
this.renderEvent(dateOnlySection, event, false);
|
||||
});
|
||||
}
|
||||
|
||||
private renderEvent(containerEl: HTMLElement, event: EnhancedTimelineEvent): void {
|
||||
private renderEvent(containerEl: HTMLElement, event: EnhancedTimelineEvent, showTime: boolean = true): void {
|
||||
const eventEl = containerEl.createDiv("timeline-event");
|
||||
eventEl.setAttribute("data-event-id", event.id);
|
||||
|
||||
|
|
@ -965,8 +1025,10 @@ export class TimelineSidebarView extends ItemView {
|
|||
}
|
||||
|
||||
// Event time - use enhanced time information if available
|
||||
const timeEl = eventEl.createDiv("timeline-event-time");
|
||||
this.renderEventTime(timeEl, event);
|
||||
if (showTime) {
|
||||
const timeEl = eventEl.createDiv("timeline-event-time");
|
||||
this.renderEventTime(timeEl, event);
|
||||
}
|
||||
|
||||
// Event content
|
||||
const contentEl = eventEl.createDiv("timeline-event-content");
|
||||
|
|
|
|||
|
|
@ -17,8 +17,7 @@ import { ProjectDataWorkerManager } from "./workers/ProjectDataWorkerManager";
|
|||
import { FileFilterManager } from "../managers/file-filter-manager";
|
||||
|
||||
// Parser imports
|
||||
import { parseMarkdown } from "./parsers/MarkdownEntry";
|
||||
import { parseCanvas } from "./parsers/CanvasEntry";
|
||||
import { CanvasParser } from "./core/CanvasParser";
|
||||
import { parseFileMeta } from "./parsers/FileMetaEntry";
|
||||
import { ConfigurableTaskParser } from "./core/ConfigurableTaskParser";
|
||||
import { MetadataParseMode } from "../types/TaskParserConfig";
|
||||
|
|
@ -717,8 +716,8 @@ export class DataflowOrchestrator {
|
|||
const fileMetaTasks = await parseFileMeta(this.plugin, file.path);
|
||||
tasks.push(...fileMetaTasks);
|
||||
} else if (extension === "canvas") {
|
||||
// Parse canvas tasks
|
||||
const canvasTasks = await parseCanvas(this.plugin, file);
|
||||
// Parse canvas tasks using the static method
|
||||
const canvasTasks = await CanvasParser.parseCanvas(this.plugin, file);
|
||||
tasks.push(...canvasTasks);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
} from "../../types/canvas";
|
||||
import { MarkdownTaskParser } from "./ConfigurableTaskParser";
|
||||
import { TaskParserConfig } from "../../types/TaskParserConfig";
|
||||
import { getConfig } from "../../common/task-parser-config";
|
||||
import type TaskProgressBarPlugin from "../../index";
|
||||
|
||||
/**
|
||||
* Default options for canvas parsing
|
||||
|
|
@ -259,4 +261,62 @@ export class CanvasParser {
|
|||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Canvas JSON content safely
|
||||
* @param canvasContent Raw Canvas file content
|
||||
* @returns Parsed CanvasData or null if parsing fails
|
||||
*/
|
||||
public static parseCanvasJSON(canvasContent: string): CanvasData | null {
|
||||
try {
|
||||
return JSON.parse(canvasContent);
|
||||
} catch (error) {
|
||||
console.error("Error parsing Canvas JSON:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a text node by ID in Canvas data
|
||||
* @param canvasData Parsed Canvas data
|
||||
* @param nodeId The node ID to find
|
||||
* @returns The text node or null if not found
|
||||
*/
|
||||
public static findTextNode(canvasData: CanvasData, nodeId: string): CanvasTextData | null {
|
||||
const node = canvasData.nodes.find(
|
||||
(n): n is CanvasTextData => n.type === "text" && n.id === nodeId
|
||||
);
|
||||
return node || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all text nodes from Canvas data
|
||||
* @param canvasData Parsed Canvas data
|
||||
* @returns Array of text nodes
|
||||
*/
|
||||
public static getTextNodes(canvasData: CanvasData): CanvasTextData[] {
|
||||
return canvasData.nodes.filter(
|
||||
(node): node is CanvasTextData => node.type === "text"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method for parsing Canvas files with plugin context
|
||||
* This replaces the separate parseCanvas function from CanvasEntry
|
||||
* @param plugin - The TaskProgressBarPlugin instance
|
||||
* @param file - File object with path property
|
||||
* @param content - Optional file content (if not provided, will be read from vault)
|
||||
* @returns Array of parsed tasks
|
||||
*/
|
||||
public static async parseCanvas(
|
||||
plugin: TaskProgressBarPlugin,
|
||||
file: { path: string },
|
||||
content?: string
|
||||
): Promise<Task[]> {
|
||||
const config = getConfig(plugin.settings.preferMetadataFormat, plugin);
|
||||
const parser = new CanvasParser(config);
|
||||
const filePath = file.path;
|
||||
const text = content ?? await plugin.app.vault.cachedRead(file as any);
|
||||
return parser.parseCanvasFile(text, filePath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { CanvasParser } from "../../dataflow/core/CanvasParser";
|
||||
import type { Task } from "../../types/task";
|
||||
import { getConfig } from "../../common/task-parser-config";
|
||||
import TaskProgressBarPlugin from "../../index";
|
||||
|
||||
// This entry requires plugin to provide config like original code did
|
||||
export async function parseCanvas(plugin: TaskProgressBarPlugin, file: { path: string }, content?: string): Promise<Task[]> {
|
||||
const config = getConfig(plugin.settings.preferMetadataFormat, plugin);
|
||||
const parser = new CanvasParser(config);
|
||||
const filePath = file.path;
|
||||
const text = content ?? await plugin.app.vault.cachedRead(file as any);
|
||||
return parser.parseCanvasFile(text, filePath);
|
||||
}
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
// Legacy re-export shim for tests and old imports
|
||||
export { CanvasParser } from "../dataflow/core/CanvasParser";
|
||||
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
* Canvas task updater for modifying tasks within Canvas files
|
||||
*/
|
||||
|
||||
import { TFile, Vault } from "obsidian";
|
||||
import { Vault } from "obsidian";
|
||||
import { Task, CanvasTaskMetadata } from "../types/task";
|
||||
import { CanvasData, CanvasTextData } from "../types/canvas";
|
||||
import type TaskProgressBarPlugin from "../index";
|
||||
import { MetadataFormat } from "../utils/task/task-operations";
|
||||
import { Events, emit } from "../dataflow/events/Events";
|
||||
import { CanvasParser } from "../dataflow/core/CanvasParser";
|
||||
|
||||
/**
|
||||
* Result of a Canvas task update operation
|
||||
|
|
@ -43,14 +43,13 @@ export class CanvasTaskUpdater {
|
|||
|
||||
// Read the Canvas file content
|
||||
const content = await this.vault.read(file);
|
||||
let canvasData: CanvasData;
|
||||
|
||||
try {
|
||||
canvasData = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
// Use CanvasParser utility to parse JSON
|
||||
const canvasData = CanvasParser.parseCanvasJSON(content);
|
||||
if (!canvasData) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse Canvas JSON: ${parseError.message}`,
|
||||
error: "Failed to parse Canvas JSON",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -63,10 +62,8 @@ export class CanvasTaskUpdater {
|
|||
};
|
||||
}
|
||||
|
||||
const textNode = canvasData.nodes.find(
|
||||
(node): node is CanvasTextData =>
|
||||
node.type === "text" && node.id === nodeId
|
||||
);
|
||||
// Use CanvasParser utility to find the text node
|
||||
const textNode = CanvasParser.findTextNode(canvasData, nodeId);
|
||||
|
||||
if (!textNode) {
|
||||
return {
|
||||
|
|
@ -682,14 +679,13 @@ export class CanvasTaskUpdater {
|
|||
|
||||
// Read the Canvas file content
|
||||
const content = await this.vault.read(file);
|
||||
let canvasData: CanvasData;
|
||||
|
||||
try {
|
||||
canvasData = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
// Use CanvasParser utility to parse JSON
|
||||
const canvasData = CanvasParser.parseCanvasJSON(content);
|
||||
if (!canvasData) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse Canvas JSON: ${parseError.message}`,
|
||||
error: "Failed to parse Canvas JSON",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -702,10 +698,8 @@ export class CanvasTaskUpdater {
|
|||
};
|
||||
}
|
||||
|
||||
const textNode = canvasData.nodes.find(
|
||||
(node): node is CanvasTextData =>
|
||||
node.type === "text" && node.id === nodeId
|
||||
);
|
||||
// Use CanvasParser utility to find the text node
|
||||
const textNode = CanvasParser.findTextNode(canvasData, nodeId);
|
||||
|
||||
if (!textNode) {
|
||||
return {
|
||||
|
|
@ -844,14 +838,13 @@ export class CanvasTaskUpdater {
|
|||
|
||||
// Read the Canvas file content
|
||||
const content = await this.vault.read(file);
|
||||
let canvasData: CanvasData;
|
||||
|
||||
try {
|
||||
canvasData = JSON.parse(content);
|
||||
} catch (parseError) {
|
||||
// Use CanvasParser utility to parse JSON
|
||||
const canvasData = CanvasParser.parseCanvasJSON(content);
|
||||
if (!canvasData) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse Canvas JSON: ${parseError.message}`,
|
||||
error: "Failed to parse Canvas JSON",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -194,4 +194,15 @@
|
|||
max-width: 1000px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.time-parsing-example {
|
||||
padding: var(--size-2-2);
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
margin-top: var(--size-2-2);
|
||||
margin-bottom: var(--size-2-2);
|
||||
padding-left: var(--size-4-2);
|
||||
padding-right: var(--size-4-2);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
|
@ -401,7 +401,7 @@
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.task-genius-settings .settings-tab-section-header .header-button {
|
||||
|
|
|
|||
|
|
@ -215,18 +215,26 @@ div[data-type^="tg-timeline-sidebar-view"] .timeline-event-time.timeline-event-t
|
|||
div[data-type^="tg-timeline-sidebar-view"] .timeline-event-time.timeline-event-time-range::after {
|
||||
content: "⏱";
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: -3px;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
font-size: 10px;
|
||||
background-color: var(--background-primary);
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
border-radius: 999px;
|
||||
min-width: 18px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
padding: 0 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--interactive-accent);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* When duration is provided, show it instead of the icon */
|
||||
div[data-type^="tg-timeline-sidebar-view"] .timeline-event-time.timeline-event-time-range[data-duration]::after {
|
||||
content: attr(data-duration);
|
||||
}
|
||||
|
||||
div[data-type^="tg-timeline-sidebar-view"] .timeline-event-time.timeline-event-time-default {
|
||||
|
|
@ -295,6 +303,10 @@ div[data-type^="tg-timeline-sidebar-view"] .timeline-date-only-section {
|
|||
border-top: 2px dashed var(--background-modifier-border);
|
||||
}
|
||||
|
||||
|
||||
/* Hide per-event time labels inside All day section as safety net */
|
||||
div[data-type^="tg-timeline-sidebar-view"] .timeline-date-only-section .timeline-event .timeline-event-time { display: none !important; }
|
||||
|
||||
div[data-type^="tg-timeline-sidebar-view"] .timeline-date-only-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
19
src/types/task.d.ts
vendored
19
src/types/task.d.ts
vendored
|
|
@ -263,25 +263,6 @@ export interface SortingCriteria {
|
|||
direction: "asc" | "desc";
|
||||
}
|
||||
|
||||
/** Task parsing configuration */
|
||||
export interface TaskParserConfig {
|
||||
/** Regular expression to match task items */
|
||||
taskRegex: RegExp;
|
||||
/** Start date format for parsing */
|
||||
startDateFormat?: string;
|
||||
/** Due date format for parsing */
|
||||
dueDateFormat?: string;
|
||||
/** Scheduled date format for parsing */
|
||||
scheduledDateFormat?: string;
|
||||
/** Project tag prefix */
|
||||
projectPrefix?: string;
|
||||
/** Context tag prefix */
|
||||
contextPrefix?: string;
|
||||
/** Task priority markers */
|
||||
priorityMarkers?: Record<string, number>;
|
||||
/** Prefer metadata format */
|
||||
preferMetadataFormat?: "dataview" | "tasks";
|
||||
}
|
||||
|
||||
/** Task indexer interface */
|
||||
export interface TaskIndexer extends Component {
|
||||
|
|
|
|||
33
styles.css
33
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue