Skip to content

Latest commit

 

History

History
910 lines (736 loc) · 26.5 KB

File metadata and controls

910 lines (736 loc) · 26.5 KB
layout default
title Chapter 4: Advanced Features
nav_order 4
has_children false
parent Obsidian Outliner Plugin

Chapter 4: Advanced Features

Welcome to Chapter 4: Advanced Features. In this part of Obsidian Outliner Plugin: Deep Dive Tutorial, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.

Performance optimization and advanced functionality for large-scale outline management

🎯 Learning Objectives

By the end of this chapter, you'll understand:

  • Performance optimization techniques for large outlines
  • Memory management and garbage collection strategies
  • Advanced user interface patterns and interactions
  • Plugin extensibility and customization options
  • Real-world deployment and maintenance considerations

⚡ Performance Optimization

Efficient Rendering for Large Documents

// Virtualized outline rendering
class VirtualizedOutlineRenderer {
  private visibleRange: { start: number; end: number } = { start: 0, end: 50 };
  private lineHeight: number = 24;
  private containerHeight: number = 600;
  private scrollTop: number = 0;

  constructor(private editor: Editor, private tree: OutlineTree) {}

  // Calculate visible range based on scroll position
  updateVisibleRange(scrollTop: number): void {
    this.scrollTop = scrollTop;
    const startLine = Math.floor(scrollTop / this.lineHeight);
    const visibleLines = Math.ceil(this.containerHeight / this.lineHeight);

    this.visibleRange = {
      start: Math.max(0, startLine - 10), // Buffer for smooth scrolling
      end: startLine + visibleLines + 10
    };
  }

  // Render only visible nodes
  renderVisibleNodes(): HTMLElement {
    const container = document.createElement('div');
    container.className = 'outline-virtual-container';

    const visibleNodes = this.getVisibleNodes();
    const totalHeight = this.tree.getTotalLineCount() * this.lineHeight;

    container.style.height = `${totalHeight}px`;

    let currentTop = this.visibleRange.start * this.lineHeight;

    for (const node of visibleNodes) {
      const nodeElement = this.renderNode(node);
      nodeElement.style.position = 'absolute';
      nodeElement.style.top = `${currentTop}px`;
      nodeElement.style.width = '100%';

      container.appendChild(nodeElement);
      currentTop += this.lineHeight;
    }

    return container;
  }

  private getVisibleNodes(): OutlineNode[] {
    const visibleNodes: OutlineNode[] = [];
    let currentLine = 0;

    const traverser = new TreeTraverser();
    traverser.traversePreOrder(this.tree.root, (node) => {
      if (currentLine >= this.visibleRange.start && currentLine <= this.visibleRange.end) {
        visibleNodes.push(node);
      }
      currentLine++;

      // Stop traversal if we've passed the visible range
      if (currentLine > this.visibleRange.end) {
        return true; // Signal to stop traversal
      }

      return false;
    });

    return visibleNodes;
  }

  private renderNode(node: OutlineNode): HTMLElement {
    const element = document.createElement('div');
    element.className = `outline-node outline-level-${node.level}`;

    if (node.collapsed) {
      element.classList.add('collapsed');
    }

    // Indentation
    const indentElement = document.createElement('span');
    indentElement.className = 'outline-indent';
    indentElement.style.width = `${node.level * 20}px`;
    element.appendChild(indentElement);

    // Bullet point
    const bulletElement = document.createElement('span');
    bulletElement.className = 'outline-bullet';
    bulletElement.textContent = node.level === 0 ? '•' : '◦';
    element.appendChild(bulletElement);

    // Content
    const contentElement = document.createElement('span');
    contentElement.className = 'outline-content';
    contentElement.textContent = node.content;
    element.appendChild(contentElement);

    // Collapse/expand button
    if (node.children.length > 0) {
      const toggleElement = document.createElement('span');
      toggleElement.className = 'outline-toggle';
      toggleElement.textContent = node.collapsed ? '▶' : '▼';
      toggleElement.onclick = () => this.toggleNode(node);
      element.appendChild(toggleElement);
    }

    return element;
  }

  private toggleNode(node: OutlineNode): void {
    node.collapsed = !node.collapsed;
    this.updateRendering();
  }

  private updateRendering(): void {
    // Trigger re-render of visible area
    const container = this.renderVisibleNodes();
    // Replace existing container
  }
}

Memory Management

// Memory-efficient tree operations
class MemoryOptimizedTreeOperations {
  private nodePool: Map<string, OutlineNode> = new Map();
  private weakRefs: WeakMap<OutlineNode, WeakRef<OutlineNode>> = new WeakMap();

  // Object pooling for node creation
  createNode(content: string, level: number): OutlineNode {
    const key = `${level}:${content.substring(0, 50)}`; // Simple cache key

    if (this.nodePool.has(key)) {
      const node = this.nodePool.get(key)!;
      this.nodePool.delete(key); // Remove from pool
      node.content = content; // Update content
      node.level = level;
      return node;
    }

    return new OutlineNode(content, level);
  }

  // Return node to pool for reuse
  releaseNode(node: OutlineNode): void {
    // Clear node state
    node.parent = null;
    node.children = [];
    node.collapsed = false;
    node.metadata = {};

    const key = `${node.level}:${node.content.substring(0, 50)}`;
    this.nodePool.set(key, node);
  }

  // Weak references for large trees
  createWeakReference(node: OutlineNode): void {
    this.weakRefs.set(node, new WeakRef(node));
  }

  // Garbage collection helper
  performGarbageCollection(): void {
    // Clean up weak references
    for (const [node, weakRef] of this.weakRefs) {
      if (!weakRef.deref()) {
        this.weakRefs.delete(node);
      }
    }

    // Limit pool size
    if (this.nodePool.size > 1000) {
      const keys = Array.from(this.nodePool.keys()).slice(0, 500);
      keys.forEach(key => this.nodePool.delete(key));
    }
  }

  // Streaming parser for very large documents
  async parseLargeDocument(content: string, chunkSize: number = 10000): Promise<OutlineNode> {
    const root = new OutlineNode('root', -1);
    let currentPath: OutlineNode[] = [root];
    let currentLevel = -1;
    let processedChars = 0;

    while (processedChars < content.length) {
      const chunk = content.substring(processedChars, processedChars + chunkSize);
      const lines = chunk.split('\n');

      // Process chunk
      for (const line of lines) {
        if (!line.trim()) continue;

        const listInfo = ListParser.getListInfo(line);
        if (!listInfo) continue;

        const level = Math.floor(listInfo.indent / 2);
        const node = this.createNode(listInfo.content, level);

        // Find correct parent
        while (currentPath.length > level + 1) {
          currentPath.pop();
        }

        const parent = currentPath[currentPath.length - 1];
        parent.children.push(node);
        node.parent = parent;

        // Update path
        currentPath = currentPath.slice(0, level + 1);
        currentPath.push(node);
      }

      processedChars += chunkSize;

      // Yield control to avoid blocking
      await new Promise(resolve => setImmediate(resolve));

      // Periodic garbage collection
      if (processedChars % 50000 === 0) {
        this.performGarbageCollection();
      }
    }

    return root;
  }
}

🎨 Advanced User Interface Patterns

Custom Views and Panels

// Custom outline view
class OutlineView extends ItemView {
  private tree: OutlineTree;
  private renderer: VirtualizedOutlineRenderer;

  constructor(leaf: WorkspaceLeaf, private plugin: OutlinerPlugin) {
    super(leaf);
    this.tree = new OutlineTree('');
  }

  getViewType(): string {
    return 'outline-view';
  }

  getDisplayText(): string {
    return 'Outline View';
  }

  async onOpen(): Promise<void> {
    // Create view container
    const container = this.containerEl;
    container.empty();
    container.addClass('outline-view-container');

    // Create toolbar
    const toolbar = container.createDiv('outline-toolbar');

    // Search input
    const searchInput = toolbar.createEl('input', {
      type: 'text',
      placeholder: 'Search outline...'
    });
    searchInput.addEventListener('input', (e) => {
      this.filterOutline((e.target as HTMLInputElement).value);
    });

    // View mode toggle
    const viewModeSelect = toolbar.createEl('select');
    viewModeSelect.createEl('option', { text: 'Tree View', value: 'tree' });
    viewModeSelect.createEl('option', { text: 'List View', value: 'list' });
    viewModeSelect.createEl('option', { text: 'Mind Map', value: 'mindmap' });
    viewModeSelect.addEventListener('change', (e) => {
      this.changeViewMode((e.target as HTMLSelectElement).value);
    });

    // Outline container
    const outlineContainer = container.createDiv('outline-content');
    this.renderer = new VirtualizedOutlineRenderer(outlineContainer);

    // Load current file's outline
    await this.loadCurrentFileOutline();
  }

  async loadCurrentFileOutline(): Promise<void> {
    const activeFile = this.app.workspace.getActiveFile();
    if (!activeFile) return;

    const content = await this.app.vault.read(activeFile);
    this.tree = new OutlineTree(content);
    this.renderer.updateTree(this.tree);
  }

  filterOutline(query: string): void {
    if (!query) {
      this.renderer.showAll();
      return;
    }

    const matchingNodes = this.tree.searchByContent(query);
    this.renderer.showOnly(matchingNodes);
  }

  changeViewMode(mode: string): void {
    switch (mode) {
      case 'tree':
        this.renderer.setMode('tree');
        break;
      case 'list':
        this.renderer.setMode('list');
        break;
      case 'mindmap':
        this.renderer.setMode('mindmap');
        break;
    }
  }

  // Register the view type
  static register(plugin: OutlinerPlugin): void {
    plugin.registerView(
      'outline-view',
      (leaf) => new OutlineView(leaf, plugin)
    );

    // Add command to open view
    plugin.addCommand({
      id: 'open-outline-view',
      name: 'Open Outline View',
      callback: () => {
        plugin.app.workspace.getLeaf(true).setViewState({
          type: 'outline-view'
        });
      }
    });
  }
}

Interactive Mind Map Visualization

// Mind map visualization
class MindMapRenderer {
  private svg: SVGSVGElement;
  private nodes: Map<string, SVGElement> = new Map();
  private edges: Map<string, SVGElement> = new Map();

  constructor(container: HTMLElement) {
    this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    this.svg.classList.add('mind-map-svg');
    container.appendChild(this.svg);
  }

  renderTree(tree: OutlineTree): void {
    this.clear();
    this.layoutTree(tree.root);
    this.renderNodes();
    this.renderEdges();
  }

  private layoutTree(root: OutlineNode): void {
    // Calculate positions using tree layout algorithm
    this.calculatePositions(root, { x: 400, y: 300 }, 0);
  }

  private calculatePositions(node: OutlineNode, parentPos: { x: number; y: number }, angle: number): void {
    // Store node position
    node.metadata.position = parentPos;

    const children = node.children;
    const angleStep = (Math.PI * 2) / Math.max(children.length, 1);

    children.forEach((child, index) => {
      const childAngle = angle + (index * angleStep) - (Math.PI / 2);
      const distance = 100 + (node.level * 50);
      const childPos = {
        x: parentPos.x + Math.cos(childAngle) * distance,
        y: parentPos.y + Math.sin(childAngle) * distance
      };

      this.calculatePositions(child, childPos, childAngle);
    });
  }

  private renderNodes(): void {
    // Implementation for rendering nodes as circles/rectangles
  }

  private renderEdges(): void {
    // Implementation for rendering connecting lines
  }

  private clear(): void {
    while (this.svg.firstChild) {
      this.svg.removeChild(this.svg.firstChild);
    }
    this.nodes.clear();
    this.edges.clear();
  }
}

🔌 Plugin Extensibility

Hook System for Extensions

// Plugin hooks system
interface PluginHooks {
  onNodeCreate?: (node: OutlineNode) => void;
  onNodeUpdate?: (node: OutlineNode, oldContent: string) => void;
  onNodeDelete?: (node: OutlineNode) => void;
  onTreeChange?: (tree: OutlineTree) => void;
  onCommandExecute?: (command: string, args: any[]) => void;
  onViewRender?: (view: OutlineView) => void;
}

class HookManager {
  private hooks: PluginHooks[] = [];

  registerHooks(hooks: PluginHooks): void {
    this.hooks.push(hooks);
  }

  unregisterHooks(hooks: PluginHooks): void {
    const index = this.hooks.indexOf(hooks);
    if (index > -1) {
      this.hooks.splice(index, 1);
    }
  }

  async triggerHook(hookName: keyof PluginHooks, ...args: any[]): Promise<void> {
    for (const hooks of this.hooks) {
      const hook = hooks[hookName];
      if (hook) {
        try {
          await hook(...args);
        } catch (error) {
          console.error(`Hook ${hookName} failed:`, error);
        }
      }
    }
  }
}

// Example extension
class TaskManagementExtension {
  private hooks: PluginHooks;

  constructor(hookManager: HookManager) {
    this.hooks = {
      onNodeCreate: this.onNodeCreate.bind(this),
      onNodeUpdate: this.onNodeUpdate.bind(this)
    };

    hookManager.registerHooks(this.hooks);
  }

  private async onNodeCreate(node: OutlineNode): Promise<void> {
    // Add task metadata if content looks like a task
    if (node.content.match(/^(TODO|FIXME|NOTE)/i)) {
      node.metadata.isTask = true;
      node.metadata.created = new Date();
      node.metadata.status = 'pending';
    }
  }

  private async onNodeUpdate(node: OutlineNode, oldContent: string): Promise<void> {
    // Update task status based on content changes
    if (node.metadata.isTask) {
      if (node.content.includes('[x]') || node.content.includes('[X]')) {
        node.metadata.status = 'completed';
        node.metadata.completedAt = new Date();
      } else if (node.content.includes('[ ]')) {
        node.metadata.status = 'pending';
      }
    }
  }

  destroy(): void {
    // Cleanup when extension is disabled
  }
}

Settings and Configuration

// Plugin settings
interface OutlinerSettings {
  defaultIndentation: number;
  autoSave: boolean;
  showLineNumbers: boolean;
  enableKeyboardShortcuts: boolean;
  maxUndoSteps: number;
  theme: 'light' | 'dark' | 'auto';
  fontSize: number;
  showCompletedTasks: boolean;
  defaultViewMode: 'tree' | 'list' | 'mindmap';
  exportFormats: string[];
  backupEnabled: boolean;
  backupInterval: number;
}

const DEFAULT_SETTINGS: OutlinerSettings = {
  defaultIndentation: 2,
  autoSave: true,
  showLineNumbers: false,
  enableKeyboardShortcuts: true,
  maxUndoSteps: 50,
  theme: 'auto',
  fontSize: 14,
  showCompletedTasks: true,
  defaultViewMode: 'tree',
  exportFormats: ['markdown', 'json', 'html'],
  backupEnabled: true,
  backupInterval: 300000 // 5 minutes
};

// Settings tab
class OutlinerSettingTab extends PluginSettingTab {
  plugin: OutlinerPlugin;

  constructor(app: App, plugin: OutlinerPlugin) {
    super(app, plugin);
    this.plugin = plugin;
  }

  display(): void {
    const { containerEl } = this;
    containerEl.empty();

    containerEl.createEl('h2', { text: 'Outliner Plugin Settings' });

    // General settings
    new Setting(containerEl)
      .setName('Default Indentation')
      .setDesc('Number of spaces for each indentation level')
      .addSlider(slider => slider
        .setLimits(2, 8, 2)
        .setValue(this.plugin.settings.defaultIndentation)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.defaultIndentation = value;
          await this.plugin.saveSettings();
        }));

    new Setting(containerEl)
      .setName('Auto Save')
      .setDesc('Automatically save changes')
      .addToggle(toggle => toggle
        .setValue(this.plugin.settings.autoSave)
        .onChange(async (value) => {
          this.plugin.settings.autoSave = value;
          await this.plugin.saveSettings();
        }));

    // View settings
    containerEl.createEl('h3', { text: 'View Settings' });

    new Setting(containerEl)
      .setName('Default View Mode')
      .setDesc('Default outline visualization mode')
      .addDropdown(dropdown => dropdown
        .addOption('tree', 'Tree View')
        .addOption('list', 'List View')
        .addOption('mindmap', 'Mind Map')
        .setValue(this.plugin.settings.defaultViewMode)
        .onChange(async (value) => {
          this.plugin.settings.defaultViewMode = value;
          await this.plugin.saveSettings();
        }));

    // Advanced settings
    containerEl.createEl('h3', { text: 'Advanced Settings' });

    new Setting(containerEl)
      .setName('Backup Interval')
      .setDesc('How often to backup outline data (in minutes)')
      .addSlider(slider => slider
        .setLimits(1, 60, 1)
        .setValue(this.plugin.settings.backupInterval / 60000)
        .setDynamicTooltip()
        .onChange(async (value) => {
          this.plugin.settings.backupInterval = value * 60000;
          await this.plugin.saveSettings();
        }));
  }
}

🚀 Production Deployment

Plugin Packaging and Distribution

// Plugin build and packaging
class PluginBuilder {
  private plugin: OutlinerPlugin;

  constructor(plugin: OutlinerPlugin) {
    this.plugin = plugin;
  }

  async build(): Promise<void> {
    // Clean dist directory
    await this.cleanDist();

    // Compile TypeScript
    await this.compileTypeScript();

    // Bundle dependencies
    await this.bundleDependencies();

    // Minify for production
    if (process.env.NODE_ENV === 'production') {
      await this.minifyCode();
    }

    // Generate manifest
    await this.generateManifest();

    // Create release archive
    await this.createReleaseArchive();
  }

  private async cleanDist(): Promise<void> {
    const distPath = path.join(__dirname, 'dist');
    await fs.rm(distPath, { recursive: true, force: true });
    await fs.mkdir(distPath, { recursive: true });
  }

  private async compileTypeScript(): Promise<void> {
    const tsConfig = {
      compilerOptions: {
        target: 'ES2020',
        module: 'ESNext',
        lib: ['DOM', 'ES2020'],
        declaration: true,
        outDir: './dist',
        rootDir: './src',
        strict: true,
        esModuleInterop: true,
        skipLibCheck: true,
        forceConsistentCasingInFileNames: true
      },
      include: ['src/**/*'],
      exclude: ['node_modules', 'dist']
    };

    // Use TypeScript compiler API
    const program = ts.createProgram(['src/main.ts'], tsConfig.compilerOptions);
    const emitResult = program.emit();

    if (emitResult.emitSkipped) {
      throw new Error('TypeScript compilation failed');
    }
  }

  private async generateManifest(): Promise<void> {
    const manifest = {
      id: 'obsidian-outliner',
      name: 'Outliner',
      version: this.plugin.manifest.version,
      minAppVersion: '1.0.0',
      description: 'Work with your lists like in Workflowy or Roam Research.',
      author: 'Viacheslav Slinko',
      authorUrl: 'https://github.com/vslinko',
      isDesktopOnly: false,
      fundingUrl: 'https://github.com/sponsors/vslinko'
    };

    await fs.writeFile(
      path.join(__dirname, 'dist', 'manifest.json'),
      JSON.stringify(manifest, null, 2)
    );
  }

  private async createReleaseArchive(): Promise<void> {
    const archive = archiver('zip', { zlib: { level: 9 } });
    const output = fs.createWriteStream('dist/release.zip');

    archive.pipe(output);
    archive.directory('dist/', false);
    await archive.finalize();
  }
}

📊 Monitoring and Analytics

Usage Analytics

// Plugin usage analytics
class AnalyticsManager {
  private events: PluginEvent[] = [];
  private sessionId: string;
  private userId: string | null = null;

  constructor(private plugin: OutlinerPlugin) {
    this.sessionId = this.generateSessionId();
    this.loadUserId();
  }

  trackEvent(eventType: string, properties: Record<string, any> = {}): void {
    const event: PluginEvent = {
      sessionId: this.sessionId,
      userId: this.userId,
      eventType,
      timestamp: Date.now(),
      properties,
      pluginVersion: this.plugin.manifest.version,
      obsidianVersion: (this.plugin.app as any).version
    };

    this.events.push(event);

    // Send to analytics service (if enabled)
    if (this.plugin.settings.analyticsEnabled) {
      this.sendEvent(event);
    }
  }

  private async sendEvent(event: PluginEvent): Promise<void> {
    try {
      // Send to analytics endpoint
      await fetch('https://analytics.example.com/track', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event)
      });
    } catch (error) {
      // Silently fail if analytics service is unavailable
      console.debug('Analytics send failed:', error);
    }
  }

  getAnalyticsSummary(): AnalyticsSummary {
    const summary: AnalyticsSummary = {
      totalSessions: 1,
      totalEvents: this.events.length,
      eventTypes: {},
      averageSessionDuration: 0,
      mostUsedFeatures: []
    };

    // Calculate event type distribution
    for (const event of this.events) {
      summary.eventTypes[event.eventType] =
        (summary.eventTypes[event.eventType] || 0) + 1;
    }

    return summary;
  }

  private generateSessionId(): string {
    return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private loadUserId(): void {
    // Generate or load user ID (anonymized)
    const stored = localStorage.getItem('outliner-user-id');
    if (stored) {
      this.userId = stored;
    } else {
      this.userId = `user_${Math.random().toString(36).substr(2, 9)}`;
      localStorage.setItem('outliner-user-id', this.userId);
    }
  }
}

interface PluginEvent {
  sessionId: string;
  userId: string | null;
  eventType: string;
  timestamp: number;
  properties: Record<string, any>;
  pluginVersion: string;
  obsidianVersion: string;
}

interface AnalyticsSummary {
  totalSessions: number;
  totalEvents: number;
  eventTypes: Record<string, number>;
  averageSessionDuration: number;
  mostUsedFeatures: string[];
}

🧪 Hands-On Exercise

Estimated Time: 75 minutes

  1. Performance Optimization:

    • Implement virtualized rendering for large outlines
    • Add memory management and garbage collection
    • Profile and optimize tree operations
  2. Advanced UI Features:

    • Create a custom outline view panel
    • Implement mind map visualization
    • Add drag-and-drop reordering
  3. Plugin Extensions:

    • Build a hook system for extensions
    • Create a sample extension (e.g., task management)
    • Add comprehensive settings management
  4. Production Deployment:

    • Set up automated building and packaging
    • Implement analytics and monitoring
    • Create release management workflow

🎉 Congratulations! You've completed the comprehensive Obsidian Outliner Plugin Architecture Deep Dive tutorial. You now have the knowledge to build sophisticated plugins that handle complex text editing, tree structures, and advanced user interactions.

🎯 What You've Learned

  1. Plugin Architecture: Deep understanding of Obsidian's plugin system and API boundaries
  2. Advanced Text Editing: Complex editor behaviors, keyboard shortcuts, and state management
  3. Tree Data Structures: Efficient hierarchical content management and algorithms
  4. Performance Optimization: Memory management, virtualized rendering, and scalability
  5. User Experience: Advanced UI patterns, custom views, and interactive visualizations
  6. Plugin Development: Professional development practices, testing, and deployment

🚀 Next Steps

  • Build Your Own Plugin: Apply these patterns to create custom Obsidian functionality
  • Contribute to Existing Plugins: Improve the Outliner plugin or similar projects
  • Explore Advanced Topics: Study CodeMirror extensions, WebAssembly integrations, and native modules

Happy plugin development! 🚀

What Problem Does This Solve?

Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for node, void, plugin so behavior stays predictable as complexity grows.

In practical terms, this chapter helps you avoid three common failures:

  • coupling core logic too tightly to one implementation path
  • missing the handoff boundaries between setup, execution, and validation
  • shipping changes without clear rollback or observability strategy

After working through this chapter, you should be able to reason about Chapter 4: Advanced Features as an operating subsystem inside Obsidian Outliner Plugin: Deep Dive Tutorial, with explicit contracts for inputs, state transitions, and outputs.

Use the implementation notes around content, OutlineNode, outline as your checklist when adapting these patterns to your own repository.

How it Works Under the Hood

Under the hood, Chapter 4: Advanced Features usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for node.
  2. Input normalization: shape incoming data so void receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through plugin.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. Operational telemetry: emit logs/metrics needed for debugging and performance tuning.

When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.

Source Walkthrough

Use the following upstream sources to verify implementation details while reading this chapter:

  • Obsidian Outliner Why it matters: authoritative reference on Obsidian Outliner (github.com).

Suggested trace strategy:

  • search upstream code for node and void to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections