Overview

Scimax VS Code is designed with extensibility in mind. The architecture separates core parsing logic (npm library extractable) from VS Code-specific adapters, enabling both internal extension and external plugin development.

This document describes the extension points available for adding new functionality.

Architecture Layers

  ┌────────────────────────────────────────────────────────────┐
  │                   VS Code Extension                         │
  │  ┌─────────────────────────────────────────────────────────┐│
  │  │              Plugin Adapter Layer                        ││
  │  │  • src/adapters/linkFollowAdapter.ts                    ││
  │  │  • VS Code-specific actions (commands, UI)              ││
  │  └─────────────────────────────────────────────────────────┘│
  │                              │                              │
  │                              ▼                              │
  │  ┌─────────────────────────────────────────────────────────┐│
  │  │              Core Library (npm extractable)              ││
  │  │  • src/parser/ - Parsing, export, link resolution       ││
  │  │  • No VS Code dependencies                              ││
  │  └─────────────────────────────────────────────────────────┘│
  └────────────────────────────────────────────────────────────┘

Extension Points

Custom Exporters

Add custom export formats using Handlebars templates - no code required.

Directory Structure

Each exporter is a subdirectory containing manifest.json, placed in one of the searched locations:

LocationNotes
{scimax.directory}/exporters/Default ~/scimax/exporters/
~/.scimax/exporters/Always searched
$XDG_CONFIG_HOME/scimax/exporters/Only when XDG_CONFIG_HOME is set
<workspace>/.scimax/exporters/Per-project exporters
scimax.export.customExporterPathsAny extra directories you list (~ is expanded)

A plain exporters/ directory at the root of a project is not one of them -- it has to be .scimax/exporters/, or added to scimax.export.customExporterPaths.

~/.scimax/exporters/
├── my-journal/
│   ├── manifest.json      # Configuration
│   ├── template.tex       # Handlebars template
│   └── partials/          # Optional partial templates
│       └── header.tex

Run Scimax: Reload Custom Exporters after adding or editing one; exporters are loaded when the extension starts.

When an Exporter Does Not Appear

An exporter that fails to load is reported rather than silently skipped: Scimax: Show Custom Exporter Problems lists each directory that could not be read, with the reason, and opens its manifest.json. The export dispatcher also shows a [c !] entry when something failed. The usual causes are:

  • Invalid JSON in manifest.json -- most often a trailing comma: "documentClass": "elsarticle", followed by }. Comments and trailing commas are tolerated (with a warning), but anything else JSON rejects will keep the exporter from loading.

  • Missing template file -- the template path is relative to the exporter directory.

  • A missing required field: id, name, parent, outputFormat, template.

  • The directory is not in a searched location (see the table above).

manifest.json

{
  "id": "my-journal",
  "name": "My Journal Format",
  "description": "Custom journal article format",
  "parent": "latex",
  "outputFormat": "pdf",
  "template": "template.tex",
  "orgTemplate": "template.org",
  "keywords": {
    "JOURNAL": {
      "required": true,
      "description": "Target journal name"
    },
    "VOLUME": {
      "default": "1"
    }
  },
  "latexOptions": {
    "documentClass": "article",
    "packages": ["hyperref", "graphicx"]
  }
}

The Org Header an Exporter Needs

An exporter that declares keywords needs those keywords in the document (#+TO:, #+FROM:, #+SUBJECT: for a memo). You do not have to remember them:

  • Scimax: Insert Custom Exporter Header inserts the header for an exporter you pick, with defaults filled in and required keywords left as <<<PLACEHOLDER>>>.

  • The same header appears in the template pickers (Scimax: Insert Template / Insert Org Template) under the Custom Exporters category, named after the exporter.

  • Exporting with a required keyword missing warns and offers to insert the header, instead of quietly writing [NOT FOUND: to] into the output.

Ship a real skeleton with an exporter by adding an orgTemplate to the manifest -- a .org file in the exporter directory:

{
  "id": "cmu-memo",
  "template": "template.tex",
  "orgTemplate": "template.org"
}

That file may use the template conventions: {{author}}, {{date}}, ${1:tab stops}, $0 for the final cursor position, and <<<PLACEHOLDER>>> for text to replace by hand. Without orgTemplate, the header is generated from the keywords in the manifest.

Running a Custom Exporter

HowNotes
C-c C-e, then [c 1], [c 2], ...Loaded exporters appear in the export dispatcher
C-c C-e, then [c c]Opens the custom exporter picker
Scimax: Export with Custom ExporterThe same picker from the palette
#+LATEX_CLASS routingSee below - the normal LaTeX/PDF exports use the exporter

Automatic Routing by LaTeX Class

You do not have to remember to run the custom exporter by hand. Map a #+LATEX_CLASS value to an exporter id and the normal LaTeX/PDF exports (C-c C-e l l, l p, l o) use that exporter automatically:

{
  "scimax.export.latexClassExporters": {
    "cmu-memo": "cmu-memo",
    "cmu-ms-report": "cmu-ms-report"
  }
}

With that setting, a document starting with

#+LATEX_CLASS: cmu-memo
#+TO: Selection Committee
#+FROM: John Kitchin
#+SUBJECT: Recommendation

exports through the cmu-memo exporter on C-c C-e l o, no custom command needed. The class name is matched case-insensitively, and the mapped value may be any exporter id (the class and the exporter do not have to share a name).

A class that names a loaded exporter routes to it even with no mapping. There is no cmu-memo.cls for LaTeX to load -- the exporter's template is what supplies \documentclass -- so an unmapped #+LATEX_CLASS: cmu-memo would otherwise fail the compile with File `cmu-memo.cls' not found. Map the class to none if you really do have a class file of that name and want the built-in backend.

Per-document overrides use the #+EXPORTER keyword, which needs no configuration at all and is what the inserted header uses:

KeywordEffect
#+EXPORTER: cmu-memoUse this exporter, whatever #+LATEX_CLASS says
#+EXPORTER: noneForce the built-in LaTeX backend for this file

Notes:

  • Routing applies to l l (writes the rendered .tex), l p (compiles the PDF), and l o (compiles and opens the PDF). Body-only export is never routed, since a template needs the full document.

  • The CLI follows the same rules: scimax export file.org --format pdf routes through the mapped exporter. --exporter <id> still wins over routing.

  • If the mapped exporter is not installed, you get a warning and the built-in LaTeX export runs instead. Run Scimax: Reload Custom Exporters after adding a new exporter directory.

Template Helpers

HelperDescription
{{latex value}}Escape for LaTeX
{{html value}}Escape for HTML
{{default field "fallback"}}Default value if empty
{{required field "name"}}Mark field as required
{{join items ", "}}Join array with separator
{{#ifeq a b}}...{{/ifeq}}Conditional equality
{{today}}Current date
{{year}}Current year
{{upper text}}Uppercase
{{lower text}}Lowercase
{{{body}}}Raw body content (triple braces)
{{{latexHeaders}}}Document #+LATEX_HEADER: lines

Document #+LATEX_HEADER: Lines

A document's #+LATEX_HEADER: and #+LATEX_HEADER_EXTRA: lines are collected into the latexHeaders variable, so a letter can add packages the template does not know about:

#+LATEX_CLASS: cmu-memo
#+LATEX_HEADER: \usepackage{setspace}
#+LATEX_HEADER: \onehalfspacing

Place {{{latexHeaders}}} in the template wherever the headers belong. A LaTeX template that does not mention latexHeaders gets them inserted automatically just before \begin{document}, so older templates keep working.

Export Backends (Programmatic)

For complex export needs, implement the full ExportBackend interface.

Location: src/parser/orgExport.ts

interface ExportBackend {
  name: string;
  exportDocument(doc: OrgDocumentNode, options?: ExportOptions): string;
  exportElement(element: OrgElement, state: ExportState): string;
  exportObject(object: OrgObject, state: ExportState): string;
  exportAffiliatedKeywords?(affiliated: AffiliatedKeywords, state: ExportState): string;
}

See implementations:

  • src/parser/orgExportHtml.ts - HTML backend

  • src/parser/orgExportLatex.ts - LaTeX backend

  • src/parser/orgExportIpynb.ts - Jupyter notebook backend

Babel Executors

Add new language execution engines for source blocks.

Location: src/adapters/babelExecutorAdapter.ts

LanguageExecutor Interface

interface LanguageExecutor {
  /** Language names this executor handles (e.g., ['ruby', 'rb']) */
  languages: string[];

  /** Execute code and return result */
  execute(code: string, context: ExecutionContext): Promise<ExecutionResult>;

  /** Initialize a persistent session (optional) */
  initSession?(sessionName: string, context: ExecutionContext): Promise<void>;

  /** Close a session (optional) */
  closeSession?(sessionName: string): Promise<void>;

  /** Check if the runtime is available */
  isAvailable(): Promise<boolean>;
}

interface ExecutionContext {
  cwd?: string;                    // Working directory
  env?: Record<string, string>;    // Environment variables
  session?: string;                // Session name for persistence
  variables?: Record<string, unknown>; // From :var arguments
  timeout?: number;                // Execution timeout
}

interface ExecutionResult {
  success: boolean;
  stdout: string;
  stderr: string;
  executionTime: number;  // milliseconds
}

Registering a Custom Executor

import { registerBabelExecutor, LanguageExecutor } from './adapters/babelExecutorAdapter';

const rubyExecutor: LanguageExecutor = {
  languages: ['ruby', 'rb'],

  async execute(code, context) {
    // Spawn ruby process, capture output
    const startTime = Date.now();
    // ... execution logic ...
    return {
      success: true,
      stdout: output,
      stderr: '',
      executionTime: Date.now() - startTime
    };
  },

  async isAvailable() {
    // Check if ruby is installed
    return true;
  }
};

// Register returns a Disposable for cleanup
const disposable = registerBabelExecutor(rubyExecutor);
context.subscriptions.push(disposable);

Using the Helper Functions

For simple command-based executors, use the helper:

import { createSimpleExecutor, registerBabelExecutor } from './adapters/babelExecutorAdapter';

// Create an executor that writes code to a temp file and runs a command
const rubyExecutor = createSimpleExecutor({
  languages: ['ruby', 'rb'],
  command: 'ruby',
  extension: '.rb',
  args: [],  // Optional: arguments before the file
  timeout: 30000,  // Optional: default timeout
  transformCode: (code, ctx) => code,  // Optional: transform code
});

context.subscriptions.push(registerBabelExecutor(rubyExecutor));

Built-in Executors

LanguageAliasesNotes
pythonpython3, pySupports sessions
javascriptjs, node, nodejsVia Node.js
typescripttsVia tsx, ts-node, or bun
shellsh, bash
juliajl
Rr, Rscript

Jupyter kernels are also supported via jupyter-python, jupyter-julia, etc.

Block Export Handlers

Define how custom special blocks (#+BEGIN_X...#+END_X) render in exports.

Location: src/adapters/blockExportAdapter.ts

BlockExportHandler Interface

interface BlockExportHandler {
  /** Block type name (case-insensitive, e.g., 'sidebar') */
  blockType: string;

  /** Optional description for documentation */
  description?: string;

  /**
   * Export the block content for a specific backend
   * @param content The processed content inside the block
   * @param backend The export backend ('html', 'latex', 'text')
   * @param context Additional context about the block
   * @returns The exported string, or undefined to use default handling
   */
  export(
    content: string,
    backend: 'html' | 'latex' | 'text',
    context: BlockExportContext
  ): string | undefined;
}

interface BlockExportContext {
  blockType: string;
  parameters?: string;  // From #+BEGIN_foo :param value
  filePath?: string;
  affiliated?: {
    name?: string;
    caption?: string;
    [key: string]: string | undefined;
  };
}

Registering a Custom Block Handler

import { registerBlockExport, BlockExportHandler } from './adapters/blockExportAdapter';

const calloutHandler: BlockExportHandler = {
  blockType: 'callout',
  description: 'Custom callout blocks',

  export(content, backend, context) {
    const title = context.parameters?.trim() || 'Note';

    switch (backend) {
      case 'html':
        return `<div class="callout">\n<strong>${title}</strong>\n${content}</div>`;
      case 'latex':
        return `\\begin{tcolorbox}[title=${title}]\n${content}\\end{tcolorbox}`;
      default:
        return undefined;  // Use default handling for 'text' backend
    }
  }
};

const disposable = registerBlockExport(calloutHandler);
context.subscriptions.push(disposable);

Built-in Block Types

TypeDescriptionHTML Class
warningWarning/alert admonitionorg-warning
noteInformational noteorg-note
tipTip/hint admonitionorg-tip
importantImportant noticeorg-important
cautionCaution noticeorg-caution
sidebarSidebar contentorg-sidebar
detailsCollapsible details (HTML only)<details>

Block Highlight Configuration

Define visual highlighting (decorations) for custom blocks in the editor.

Location: src/adapters/blockHighlightAdapter.ts

BlockHighlightConfig Interface

interface BlockHighlightConfig {
  /** Block type to highlight (case-insensitive) */
  blockType: string;

  /** Background color for the block (CSS color or theme variable) */
  backgroundColor?: string;

  /** Color for the #+BEGIN/#+END lines */
  headerColor?: string;

  /** Border color (left border is common) */
  borderColor?: string;

  /** Whether content should be italic */
  italic?: boolean;
}

Registering Block Highlights

import { registerBlockHighlight } from './adapters/blockHighlightAdapter';

const disposable = registerBlockHighlight({
  blockType: 'callout',
  backgroundColor: 'rgba(100, 149, 237, 0.1)',
  borderColor: 'rgba(100, 149, 237, 0.5)',
  headerColor: 'cornflowerblue',
});

context.subscriptions.push(disposable);

Built-in Highlight Configs

Block TypeBackground ColorBorder Color
warningrgba(255, 193, 7, 0.1)rgba(255, 193, 7, 0.5)
notergba(33, 150, 243, 0.1)rgba(33, 150, 243, 0.5)
tiprgba(76, 175, 80, 0.1)rgba(76, 175, 80, 0.5)
importantrgba(244, 67, 54, 0.1)rgba(244, 67, 54, 0.5)
cautionrgba(255, 152, 0, 0.1)rgba(255, 152, 0, 0.5)
sidebarrgba(158, 158, 158, 0.1)rgba(158, 158, 158, 0.5)
detailsrgba(103, 58, 183, 0.1)rgba(103, 58, 183, 0.5)

Export Hooks

Lightweight callbacks that run at specific points during export, allowing customization without implementing a full ExportBackend.

Location: src/adapters/exportHooksAdapter.ts

ExportHook Interface

interface ExportHook {
  /** Unique identifier for this hook */
  id: string;

  /** Optional description */
  description?: string;

  /** Priority for ordering (higher runs first, default 0) */
  priority?: number;

  /**
   * Pre-export hook - runs before export starts
   * Can modify export options
   */
  preExport?: (context: PreExportContext) => ExportOptions | undefined;

  /**
   * Post-export hook - runs after export completes
   * Can transform the final output
   */
  postExport?: (output: string, context: PostExportContext) => string | undefined;

  /**
   * Element filter - runs for each element during export
   * Can override the rendered output for specific elements
   */
  elementFilter?: (rendered: string, context: ElementFilterContext) => string | undefined;
}

interface PreExportContext {
  document: OrgDocumentNode;
  options: ExportOptions;
  backend: string;  // 'html', 'latex', etc.
  filePath?: string;
}

interface PostExportContext {
  backend: string;
  options: ExportOptions;
  filePath?: string;
}

interface ElementFilterContext {
  element: OrgElement;
  backend: string;
  options: ExportOptions;
  parent?: OrgElement;
}

Registering Export Hooks

import { registerExportHook } from './adapters/exportHooksAdapter';

// Add a company header/footer to HTML exports
const disposable = registerExportHook({
  id: 'company-branding',
  description: 'Add company header and footer',

  postExport: (output, context) => {
    if (context.backend === 'html') {
      return `<!-- Company Header -->\n${output}\n<!-- Company Footer -->`;
    }
    return undefined; // Don't modify other backends
  }
});

context.subscriptions.push(disposable);

Use Cases

Hook TypeUse Case
preExportInject metadata, set default options, add custom macros
postExportAdd headers/footers, wrap in template, minify, post-process
elementFilterCustom handling for specific elements, transform rendering

Helper Functions

createWrapperHook

Creates a simple hook that wraps output with before/after content.

import { createWrapperHook, registerExportHook } from './adapters/exportHooksAdapter';

// Add copyright notice to HTML/LaTeX exports
const hook = createWrapperHook('copyright', {
  backend: ['html', 'latex'],  // Only these backends
  before: '',
  after: '\n<!-- Copyright 2024 MyCompany -->',
});

context.subscriptions.push(registerExportHook(hook));

createElementReplacerHook

Creates a hook that modifies specific element types.

import { createElementReplacerHook, registerExportHook } from './adapters/exportHooksAdapter';

// Wrap all paragraphs in a custom div
const hook = createElementReplacerHook('para-wrapper', {
  elementType: 'paragraph',
  backend: 'html',
  replace: (rendered) => `<div class="custom-para">${rendered}</div>`,
});

context.subscriptions.push(registerExportHook(hook));

Hook Execution Order

  1. Hooks are sorted by priority (higher numbers run first)

  2. Default priority is 0

  3. Multiple hooks chain together - each receives the output of the previous

  4. Return undefined from a hook to skip modification and pass through

Error Handling

Hooks that throw errors are caught and logged. The export continues with the unmodified content, so a failing hook won't break the entire export.

Database Extension Points

Extension points for extracting custom data during indexing, running custom queries, and enriching graph visualizations. These enable building knowledge graphs, custom search algorithms, and entity extraction systems.

IndexerAdapter - Custom Content Extraction

Hook into the file indexing pipeline to extract entities, relationships, or custom metadata from files as they are indexed.

Location: src/adapters/indexerAdapter.ts

IndexerAdapter Interface

interface IndexerAdapter {
  /** Unique identifier */
  id: string;

  /** Human-readable description */
  description?: string;

  /** Priority for ordering (higher first) */
  priority?: number;

  /** File types to process (['org', 'md']). Empty = all. */
  fileTypes?: string[];

  /** Extract custom data during indexing */
  extract(
    content: string,
    ast: OrgDocumentNode | undefined,
    context: IndexContext
  ): Promise<ExtractedData[]>;

  /** Called when file is removed (cleanup) */
  onFileRemoved?(filePath: string, fileId: number, db: unknown): Promise<void>;
}

interface IndexContext {
  filePath: string;
  fileId: number;
  fileType: string;
  mtime: number;
  db: unknown;  // Database client for custom queries
}

Extracted Data Types

// Entity - a named concept, person, project, etc.
interface ExtractedEntity {
  type: 'entity';
  category: string;  // 'person', 'concept', 'project'
  name: string;
  properties?: Record<string, unknown>;
  lineNumber?: number;
}

// Relationship between entities or files
interface ExtractedRelationship {
  type: 'relationship';
  source: string;
  target: string;
  relation: string;  // 'references', 'is_part_of', 'related_to'
  weight?: number;
  properties?: Record<string, unknown>;
}

// Custom metadata about a file
interface ExtractedMetadata {
  type: 'metadata';
  key: string;
  value: unknown;
}

Example: Entity Extractor

import { registerIndexer, IndexerAdapter } from './adapters/indexerAdapter';

const entityExtractor: IndexerAdapter = {
  id: 'knowledge-graph-entities',
  description: 'Extract named entities for knowledge graph',
  fileTypes: ['org', 'md'],

  async extract(content, ast, context) {
    const entities: ExtractedEntity[] = [];

    // Example: Extract #+CONCEPT: definitions
    const conceptMatches = content.matchAll(/^#\+CONCEPT:\s*(.+)$/gm);
    for (const match of conceptMatches) {
      entities.push({
        type: 'entity',
        category: 'concept',
        name: match[1].trim()
      });
    }

    return entities;
  }
};

context.subscriptions.push(registerIndexer(entityExtractor));

QueryProviderAdapter - Custom Search Types

Register custom query providers for specialized searches like graph traversal, path finding, or semantic similarity queries.

Location: src/adapters/queryProviderAdapter.ts

QueryProvider Interface

interface QueryProvider {
  /** Query type identifier */
  queryType: string;

  /** Human-readable name */
  name: string;

  /** Description */
  description?: string;

  /** Capabilities (pagination, filtering, ranking) */
  capabilities?: QueryCapabilities;

  /** Validate parameters (return true or error message) */
  validate?(params: QueryParams): true | string;

  /** Execute the query */
  execute(params: QueryParams, db: unknown): Promise<QueryResponse>;
}

interface QueryResponse {
  results: QueryResult[];
  totalCount?: number;
  executionTimeMs?: number;
  metadata?: Record<string, unknown>;
}

Built-in Query Providers

Query TypeDescriptionParameters
graph-pathFind shortest path between filesfrom, to, maxHops
related-filesFind files related by links and tagsfilePath, limit

Example: Custom Similarity Query

import { registerQueryProvider, QueryProvider } from './adapters/queryProviderAdapter';

const similarityProvider: QueryProvider = {
  queryType: 'semantic-similarity',
  name: 'Semantic Similarity Search',
  description: 'Find semantically similar files using embeddings',

  validate(params) {
    if (!params.filePath) return 'Missing filePath';
    return true;
  },

  async execute(params, db) {
    const filePath = params.filePath as string;
    const limit = (params.limit as number) || 10;

    // Query embedding vectors and compute similarity
    // ...

    return {
      results: similarFiles.map(f => ({
        type: 'file',
        id: f.path,
        label: f.name,
        score: f.similarity
      }))
    };
  }
};

context.subscriptions.push(registerQueryProvider(similarityProvider));

GraphDataProviderAdapter - Graph Enrichment

Add custom data to link graph nodes and edges, or inject additional edges beyond the standard file links (e.g., semantic relationships).

Location: src/adapters/graphDataAdapter.ts

GraphDataProvider Interface

interface GraphDataProvider {
  id: string;
  name: string;
  description?: string;
  priority?: number;

  /** Enrich node with custom properties */
  enrichNode?(node: GraphNode, context: GraphDataContext): Promise<NodeEnrichment | undefined>;

  /** Enrich edge with custom properties */
  enrichEdge?(edge: GraphEdge, context: GraphDataContext): Promise<EdgeEnrichment | undefined>;

  /** Add custom edges (e.g., semantic relationships) */
  getCustomEdges?(filePath: string, context: GraphDataContext): Promise<CustomEdge[]>;

  /** Filter out nodes */
  filterNode?(node: GraphNode, context: GraphDataContext): boolean;

  /** Filter out edges */
  filterEdge?(edge: GraphEdge, context: GraphDataContext): boolean;
}

interface NodeEnrichment {
  color?: string;
  size?: number;
  shape?: string;
  label?: string;
  tooltip?: string;
  group?: string;
  importance?: number;
  properties?: Record<string, unknown>;
}

Built-in Providers

Provider IDDescription
link-count-importanceSizes nodes based on link count
recency-coloringColors nodes by modification recency

Example: Cluster Provider

import { registerGraphDataProvider, GraphDataProvider } from './adapters/graphDataAdapter';

const clusterProvider: GraphDataProvider = {
  id: 'topic-clusters',
  name: 'Topic Clustering',
  description: 'Group files by topic similarity',

  async enrichNode(node, context) {
    // Compute cluster membership
    const cluster = await getFileCluster(node.id, context.db);

    return {
      group: cluster.name,
      color: cluster.color,
      properties: { clusterId: cluster.id }
    };
  },

  async getCustomEdges(filePath, context) {
    // Add edges to semantically similar files
    const similar = await getSimilarFiles(filePath, context.db);

    return similar.map(s => ({
      from: filePath,
      to: s.path,
      type: 'semantic-similarity',
      label: 'Similar',
      style: 'dashed',
      color: '#999'
    }));
  }
};

context.subscriptions.push(registerGraphDataProvider(clusterProvider));

Potential Future Extension Points

These patterns could be extended for additional plugin types:

Speed Commands

Custom single-key commands at headline beginnings.

interface SpeedCommand {
  key: string;
  description: string;
  execute(editor: TextEditor, heading: HeadingNode): Promise<void>;
}

Capture Templates

Custom org-capture templates.

interface CaptureTemplate {
  key: string;
  description: string;
  template: string;
  target: CaptureTarget;
}

Entity Definitions

Custom special character entities (like org-entities).

interface OrgEntity {
  name: string;
  latex: string;
  html: string;
  unicode: string;
}

Document Linters

Custom lint rules for org documents.

interface LintRule {
  id: string;
  severity: 'error' | 'warning' | 'info';
  check(document: OrgDocument): Diagnostic[];
}

VS Code Extension API

External VS Code extensions can contribute to scimax via the extension API.

Accessing the API

// In your extension's activate() function
export async function activate(context: vscode.ExtensionContext) {
  const scimaxExt = vscode.extensions.getExtension('scimax.scimax-vscode');

  if (!scimaxExt) {
    console.log('Scimax extension not installed');
    return;
  }

  // Ensure scimax is activated
  const scimaxApi = scimaxExt.isActive
    ? scimaxExt.exports
    : await scimaxExt.activate();

  // Now use the API...
}

Available API Methods

registerBabelExecutor(executor)

Register a custom language executor for source blocks.

const disposable = scimaxApi.registerBabelExecutor({
  languages: ['ruby', 'rb'],
  async execute(code, context) {
    return { success: true, stdout: 'result', stderr: '', executionTime: 100 };
  },
  async isAvailable() {
    return true;
  }
});

context.subscriptions.push(disposable);

createSimpleExecutor(options)

Helper to create a command-based executor.

const rubyExec = scimaxApi.createSimpleExecutor({
  languages: ['ruby'],
  command: 'ruby',
  extension: '.rb',
});

context.subscriptions.push(scimaxApi.registerBabelExecutor(rubyExec));

registerLinkType(handler)

Register a custom link type handler.

const disposable = scimaxApi.registerLinkType({
  type: 'jira',
  description: 'Jira issue links',
  resolve: (path) => ({
    displayText: path,
    url: `https://jira.example.com/browse/${path}`,
    tooltip: `Open Jira issue ${path}`
  }),
  export: (path, desc, backend) => {
    const url = `https://jira.example.com/browse/${path}`;
    const text = desc || path;
    if (backend === 'html') return `<a href="${url}">${text}</a>`;
    if (backend === 'latex') return `\\href{${url}}{${text}}`;
    return text;
  }
});

context.subscriptions.push(disposable);

registerLinkFollowHandler(handler)

Register a VS Code action for when a link type is clicked.

const disposable = scimaxApi.registerLinkFollowHandler({
  type: 'jira',
  async follow(path, context) {
    await vscode.env.openExternal(
      vscode.Uri.parse(`https://jira.example.com/browse/${path}`)
    );
  }
});

context.subscriptions.push(disposable);

registerBlockExport(handler)

Register a custom export handler for special blocks.

const disposable = scimaxApi.registerBlockExport({
  blockType: 'callout',
  export: (content, backend, ctx) => {
    if (backend === 'html') return `<div class="callout">${content}</div>`;
    if (backend === 'latex') return `\\begin{tcolorbox}${content}\\end{tcolorbox}`;
    return content;
  },
});

context.subscriptions.push(disposable);

registerBlockHighlight(config)

Register visual highlighting for custom block types in the editor.

const disposable = scimaxApi.registerBlockHighlight({
  blockType: 'callout',
  backgroundColor: 'rgba(100, 149, 237, 0.1)',
  borderColor: 'rgba(100, 149, 237, 0.5)',
  headerColor: 'cornflowerblue',
});

context.subscriptions.push(disposable);

registerExportHook(hook)

Register a hook to customize the export process.

const disposable = scimaxApi.registerExportHook({
  id: 'my-custom-hook',
  // Modify options before export
  preExport: (ctx) => ({ ...ctx.options, toc: true }),
  // Transform output after export
  postExport: (output, ctx) => {
    if (ctx.backend === 'html') {
      return `<!-- Custom Header -->\n${output}`;
    }
    return undefined;
  },
  // Filter specific elements
  elementFilter: (rendered, ctx) => {
    if (ctx.element.type === 'paragraph') {
      return `<div class="para">${rendered}</div>`;
    }
    return undefined;
  },
});

context.subscriptions.push(disposable);

createWrapperHook(id, options)

Helper to create a simple wrapper hook.

const hook = scimaxApi.createWrapperHook('my-wrapper', {
  backend: 'html',           // Optional: filter by backend
  before: '<!-- Start -->',
  after: '<!-- End -->',
  priority: 10,              // Optional: higher runs first
});

context.subscriptions.push(scimaxApi.registerExportHook(hook));

createElementReplacerHook(id, options)

Helper to create a hook that modifies specific element types.

const hook = scimaxApi.createElementReplacerHook('para-wrapper', {
  elementType: ['paragraph', 'headline'],  // Can be string or array
  backend: 'html',                         // Optional: filter by backend
  replace: (rendered, element) => {
    return `<div class="custom">${rendered}</div>`;
  },
});

context.subscriptions.push(scimaxApi.registerExportHook(hook));

Direct Registry Access

For advanced use cases, you can access the registries directly:

// Access all registries
const { registries } = scimaxApi;

// Check registered block types
registries.blockExport.getBlockTypes();  // ['warning', 'note', ...]

// Check if a block has a custom handler
registries.blockExport.hasHandler('callout');  // true/false

// Get highlight config for a block type
registries.blockHighlight.getConfig('warning');

// Get all export hooks
registries.exportHook.getHooks();  // Sorted by priority

// Check if a hook is registered
registries.exportHook.hasHook('my-hook');  // true/false

Utility Functions

// Check if a language is supported
scimaxApi.isLanguageSupported('python');  // true

// Get all registered languages
scimaxApi.getRegisteredLanguages();  // ['python', 'py', 'bash', ...]

Core Library Extraction

The src/parser/ directory is designed to be extractable as a standalone npm library (@scimax/org-core) with no VS Code dependencies.

What's in Core

  • AST types (orgElementTypes.ts)

  • Parser (orgParser.ts, orgExportParser.ts, orgParserUnified.ts)

  • Export backends (orgExport*.ts)

  • Link resolution (orgLinkTypes.ts - resolve/export only)

  • Entities (orgEntities.ts)

  • Babel execution (orgBabel.ts)

What's in VS Code Layer

  • Providers (src/org/*.ts)

  • Link follow actions (src/adapters/linkFollowAdapter.ts)

  • Commands and UI

  • Database and indexing