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:
Quorafind 2025-08-24 22:46:16 +08:00
parent ec032f0eae
commit 52573bfcd0
16 changed files with 485 additions and 131 deletions

View 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

View file

@ -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

View file

@ -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';

View file

@ -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);

View file

@ -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

View file

@ -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");

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -1,3 +0,0 @@
// Legacy re-export shim for tests and old imports
export { CanvasParser } from "../dataflow/core/CanvasParser";

View file

@ -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",
};
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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
View file

@ -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 {

File diff suppressed because one or more lines are too long