Timeline DSL

unlisted

by keroway

Render .tdsl timeline DSL code blocks as SVG previews in Obsidian.

Updated 2mo agoMIT
View on GitHub

obsidian-tdsl

CI License: MIT npm: @keroway/tdsl-wasm timeline-dsl

An Obsidian plugin that renders Timeline DSL (tdsl) code blocks as interactive SVG timelines in the live preview.

日本語版: README.ja.md

Preview

Timeline DSL rendered in Obsidian (light mode)

Timeline DSL rendered in Obsidian (dark mode)

Features

  • SVG timeline previewtdsl code blocks are rendered as SVG directly in the Obsidian live preview / reading view
  • Inline syntax errors — Parse and semantic errors are shown with line/column numbers inside the note, without leaving the editor
  • Dark mode support — Automatically follows Obsidian's body.theme-dark class; colour palette adapts to Catppuccin-style dark colours
  • XSS-safe SVG insertion — SVG output is parsed via DOMParser and inserted with document.adoptNode; no innerHTML, no script execution
  • Pan/zoom & fullscreen — Wheel-zoom, drag-pan, and a Fullscreen button on every rendered timeline. Enabled by default; composes with //! scale: fit and horizontal scrolling. Toggle in the settings tab.
  • Export toolbar — Every rendered timeline gets a toolbar with Copy SVG, Copy standalone HTML, Copy PNG, and Save as file actions
  • Mobile-enabledisDesktopOnly: false; the plugin is not blocked from running on Obsidian Mobile, but has not yet been extensively verified on iOS/Android. Feedback via GitHub Issues is welcome.
  • Zero network requests — The Timeline DSL WASM renderer is bundled inline; no external calls at render time

Usage

Write a tdsl fenced code block in any note:

```tdsl
timeline "Heian Period" {
  unit year;
  range 781..1185;
}

lane "Emperor" as emperor {}

span emperor 781..806 "Kanmu" {};
span emperor 806..809 "Heizei" {};
span emperor 809..823 "Saga" {};
```

> **Syntax notes:** every property inside the `timeline { … }` block ends with `;`,
> ranges use `start..end` (not `start to end`), and each `span` / `event` /
> `event_range` statement ends with a trailing `;` after its `{ … }` block.
> `lane` and `group` declarations take **no** trailing `;`.

Supported DSL features in Obsidian

The following DSL constructs work fully inside Obsidian:

timeline block

Declares the title, time unit, display range, and colour mappings.

timeline "Chinese Dynasties" {
    title "Chinese Dynasties";
    unit year;
    range -500..2000;
    calendar proleptic_gregorian;
    color_map {
        dynasty: "#3366cc";
        war:     "#cc0000";
    }
}

unit accepts year, month, day, hour, minute, or second.

calendar is optional and accepts proleptic_gregorian (default) or julian. It affects how date literals are interpreted; omitting it is equivalent to proleptic_gregorian.

For sub-day units, write the bounds as ISO 8601 date-times. A UTC offset (or Z) goes inside the literal — there is no separate timezone property on the timeline block:

timeline "Launch day" {
    unit hour;
    range 2026-01-01T00:00:00+09:00..2026-01-02T00:00:00+09:00;
}

lane declaration

Defines a vertical category. Use as to assign an internal ID used by span / event.

lane "Han" as han { kind dynasty; order 20; }

group block

Groups multiple lanes with a shared label and visual boundary.

group "Ancient China" {
    lane "Qin" as qin { kind dynasty; order 10; }
    lane "Han" as han { kind dynasty; order 20; }
}

span / event / event_range

Three types of time elements attached to a lane:

// Duration (start..end) — note the trailing ; after the block
span han -206..220 "Han Dynasty" { tags ["dynasty"]; };

// Point event
event han -209 "Dazexiang Uprising" {};

// Range event (wars, disasters, etc.)
event_range han 184..204 "Yellow Turban Rebellion" { tags ["war"]; };
now — open-ended ranges

span and event_range accept now as the end of a range to mean "still going". It is not valid as the start, and event (a point in time) does not take it:

span main 2010..now "Ongoing project" {};
event_range main 2020..now "Still running" {};

now resolves at render time to the current year (UTC), so pair it with unit year. With unit day and date literals such as 2026-03-01..now the end still resolves to the year, which produces a "start > end" warning.

Open-ended items also get the tdsl-item-open-ended class, so a snippet in your vault's CSS can style them (a fading edge, a dashed border) — and their tooltip shows the end as "in progress" rather than a number.

note / link / color block options

span, event, and event_range blocks accept three extra options (they are not available on lane / group, which only take kind and order):

span main 1603..1868 "Edo period" {
    note "Sakoku era";
    link "https://en.wikipedia.org/wiki/Edo_period";
    color "#8b5cf6";
};
OptionValueEffect
ididentifier stringstable identifier for the item. The Fix lint issues in current tdsl block command adds missing ids automatically
notestringextra line in the item's tooltip / accessible label
linkhttp:// or https:// URLshown as a line in the tooltip — not clickable; the renderer emits no <a> element. Any other scheme is a compile error
color#RGB / #RGBA / #RRGGBB / #RRGGBBAA, or a plain CSS colour keywordoverrides the item's fill, taking precedence over the lane colour and color_map. Other values (e.g. rgb(), url()) are rejected
sourcestringsource or provenance note. In the SVG renderer this appears as text in the tooltip / accessible label; it does not become a clickable link
originstringdescribes where the item came from (e.g. manual vs import)

note and link are surfaced through the item's <title>, aria-label and data-tdsl-tooltip attributes, so they show up as a native browser tooltip on hover and are announced by screen readers.

Rendering options (//! directives)

Because Obsidian only passes the code-block body to the renderer, per-diagram options are written as //! comment lines (ordinary DSL comments the compiler ignores). Place them anywhere in the block:

//! scale: 3
//! grid: decade
//! orientation: vertical
//! layout_style: gantt
//! events: on
timeline "Demo" { unit year; range 0..100; }
lane "Main" as main {}
span main 10..50 "An era" {};
DirectiveValuesEffect
scalepositive number, or fitpixels per year. Higher = wider / more readable. fit shrinks the timeline to the note width (no horizontal scroll). Omit for auto.
gridnone, decade, year, monthgridline density
themedefault, dark, print, pastelbuilt-in colour theme
orientationhorizontal, verticallayout direction
layout_styletimeline, gantt, group-bands, zigzagrenderer layout style. Unknown values are ignored. Omit for the renderer default.
eventson / offshow labels next to event / event_range items
tableon / offrender the accompanying item-listing table (drawn natively in the SVG)
legendon / offrender a static legend panel showing lane and tag colors
lane_heightpositive integervertical pixels per lane. Omit (or 0) for the renderer default (60 px).

The timeline renders at its natural size; if it is wider than the note column it scrolls horizontally rather than shrinking (which would make labels unreadable). Use a numeric scale to make a sparse timeline span a wider area, or //! scale: fit to shrink it to the note width for an at-a-glance view (labels scale down with the graphic).

Default options (settings tab)

Settings → Community plugins → Timeline DSL lets you set vault-wide defaults so you don't repeat the same directive in every block:

SettingValuesDefault
Default themeauto, default, dark, print, pastelauto (follow Obsidian light/dark via plugin CSS)
Default gridnone, decade, year, monthnone
Default scaleauto, fit, or a positive numberauto
Show event labels by defaulton / offoff
Default orientationhorizontal, verticalhorizontal
Default layout styleauto, timeline, gantt, group-bands, zigzagauto (renderer default)
Show table by defaulton / offoff
Show legend by defaulton / offoff
Default lane heightempty / 0, or a positive integer (px)0 (renderer default, 60 px)
Enable pan/zoomon / offon

Resolution order is block //! directive > settings default > built-in. Changes apply immediately to every open note — no need to reopen anything.

Commands

In addition to the Markdown code-block preview, the plugin adds three commands to the Obsidian command palette:

CommandWhere it worksWhat it does
Format current tdsl blockCursor inside a ```tdsl blockRe-formats the current block's DSL body using the WASM formatter. Requires the cursor to be inside the block.
Fix lint issues in current tdsl blockCursor inside a ```tdsl blockApplies auto-fixable lint rules (such as missing_id) to the current block. Does nothing if there are no fixable issues. Requires the cursor to be inside the block.
Insert timeline templateAnywhere in the noteOpens a picker that inserts a starter timeline (Historical eras, Project plan, Life of a person, or Reading log) at the cursor position. No existing block is needed.

Timeline toolbar

Every rendered timeline shows a toolbar (role="toolbar") with these actions:

ButtonWhat it does
FullscreenOpens the timeline in a fullscreen modal, giving pan/zoom more room to work in. Hidden when pan/zoom is disabled.
Copy SVGCopies the rendered SVG markup to the clipboard
Copy standalone HTMLCopies a self-contained HTML document embedding the timeline
Copy PNGCopies a rasterized PNG of the timeline to the clipboard
Save as fileSaves the rendered SVG as a file in the vault

Pan/zoom (wheel-zoom, drag-pan) is enabled by default and can be turned off in the settings tab; it composes with //! scale: fit and horizontal scrolling.

Full example

timeline "Japanese History" {
    title "Nara to Edo";
    unit year;
    range 710..1868;
    color_map {
        dynasty: "#8b5cf6";
        war:     "#ef4444";
    }
}

group "Imperial Court" {
    lane "Emperor" as emperor { kind dynasty; order 1; }
}

group "Military Government" {
    lane "Kamakura Shogunate" as kamakura { kind dynasty; order 2; }
    lane "Muromachi Shogunate" as muromachi { kind dynasty; order 3; }
    lane "Edo Shogunate"       as edo      { kind dynasty; order 4; }
}

span emperor 710..794 "Nara Period" { id "nara"; tags ["dynasty"]; };
span emperor 794..1185 "Heian Period" { id "heian"; tags ["dynasty"]; };

span kamakura  1185..1336 "Kamakura Shogunate" { id "kamakura-shogunate"; tags ["war"]; };
span muromachi 1336..1573 "Muromachi Shogunate" { id "muromachi-shogunate"; tags ["war"]; };
span edo       1603..1868 "Edo Shogunate" { id "edo-shogunate"; tags ["war"]; };

event kamakura 1185 "Minamoto no Yoritomo appointed Shogun" { id "yoritomo-appointed"; };
event edo      1868 "Meiji Restoration" { id "meiji-restoration"; };

Limitations

The following Timeline DSL features require network access or server-side processing and are not supported inside Obsidian:

FeatureReason
import wikidataWikidata HTTP requests cannot be made from the browser renderer
map blocksDepend on resolved import wikidata data
template / apply syntaxDepend on resolved import wikidata data

If a tdsl block contains import wikidata, the plugin displays a notice and renders only the static items (span / event / event_range) defined in the source.

For full Wikidata integration, use the tdsl CLI or the WebUI to pre-render to SVG/HTML.

Installation

Community Plugin (coming soon)

The plugin is not yet listed in the Obsidian Community Plugin directory. Once published, it will be installable from Settings → Community plugins → Browse by searching for Timeline DSL.

Manual installation (GitHub Release)

  1. Go to the Releases page and download the latest release assets:

    • main.js
    • manifest.json
    • styles.css
  2. Create the plugin directory in your vault (if it does not exist):

    mkdir -p <vault>/.obsidian/plugins/timeline-dsl/
    
  3. Copy the three downloaded files into that directory.

  4. In Obsidian: Settings → Community plugins → Installed plugins → enable Timeline DSL

Requires Obsidian ≥ 1.4.0.

Manual installation (development build)

  1. Clone this repository

  2. Install dependencies and build:

    pnpm install
    pnpm run build
    
  3. Copy the three output files into your vault:

    # replace <vault> with your actual vault path
    cp main.js manifest.json styles.css <vault>/.obsidian/plugins/timeline-dsl/
    
  4. In Obsidian: Settings → Community plugins → Installed plugins → enable Timeline DSL

Requires Obsidian ≥ 1.4.0.

Development

pnpm install          # install dependencies
pnpm run dev          # watch mode (rebuilds on save)
pnpm run build        # production build → main.js
pnpm run lint         # Biome lint
pnpm run typecheck    # tsc --noEmit

CI classifies the changed files and runs only the affected checks among test (pnpm run test:coverage, enforcing coverage thresholds), lint (pnpm run format:check and pnpm run lint), typecheck, build (also verifies that main.js is produced), and audit (pnpm audit --audit-level high). See CONTRIBUTING.md for a full guide.

Related projects

ProjectDescription
keroway/timeline-dslThe Timeline DSL compiler (Rust + WASM). CLI, WebUI, GitHub Actions integration
WebUIReal-time browser editor — full Wikidata support
Landing pageOverview and feature tour
VS Code extensionSyntax highlighting for .tdsl files
@keroway/tdsl-wasmWASM package used by this plugin

License

MIT © keroway

Third-party licenses for bundled dependencies are listed in THIRD-PARTY-NOTICES.md.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.