Skip to content

Resources panel: images, SVGs, HTML tables with captions, typed numbering & inline references #49

@drnachio

Description

@drnachio

Overview

The current ResourcesPanel is a placeholder ("Coming Soon"). This issue tracks the full design and implementation of a resources system covering:

  1. An uploader / manager for bitmap images, SVGs, and HTML tables.
  2. A built-in table editor supporting postext's markdown microformats, inline math, and col-span / row-span.
  3. A per-resource caption that renders as the resource's figure/table foot in the viewer.
  4. A configurable list of resource types (figure, table, diagram, image, …) managed separately, each with its own auto-numbering format and reset scope.
  5. A markdown syntax extension so authors can reference resources inline, with numbers resolved automatically.

Automatic numbering follows order of first reference in the document, not creation order in the panel. Inserting a new reference earlier in the text renumbers the rest automatically.

1. Resource types — a separate concept

Location: new section in packages/postext-sandbox/src/sidebar/ConfigPanel.tsx, new config in packages/postext/src/types.ts

Resource types are a first-class, user-editable list. Each type defines:

interface ResourceType {
  id: string;                         // stable id, e.g. 'figure'
  name: string;                       // display name, e.g. "Figure"
  namePlural?: string;                // "Figures"
  shortLabel: string;                 // used in captions & references, e.g. "fig."

  // Numbering
  numberingTemplate: string;          // e.g. "{h1}.{n}" → "1.7", or "{n}" → "7"
  resetOn: 'never' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
  counterFormat: 'decimal' | 'roman-lower' | 'roman-upper' | 'alpha-lower' | 'alpha-upper';

  // Caption formatting
  captionPrefix: string;              // template, e.g. "{shortLabel} {number}. "  (produces "fig. 1.7. ")
}

The {h1}, {h2}, … tokens resolve to the current section number at the resource's first-reference point. The {n} token is the per-type counter.

Example: resetOn: 'h1', numberingTemplate: '{h1}.{n}' gives numbers like 1.1, 1.2, …, 2.1 — exactly the book convention the user described.

Each resource type keeps an independent counter: figures and tables don't interfere.

Built-in defaults provided out-of-the-box: figure, table. Users can add diagram, listing, equation, etc. from the settings UI.

2. Resource storage

Location: new packages/postext-sandbox/src/storage/resources.ts (IndexedDB), wired into the SandboxContext

type ResourceKind = 'bitmap' | 'svg' | 'table';

interface Resource {
  id: string;                 // slug-like, user-editable; must be unique
  typeId: string;             // references ResourceType.id
  kind: ResourceKind;
  caption?: string;           // rich text — parsed with the same inline microformats as body text
  altText?: string;           // accessibility; for bitmap/svg
  createdAt: number;
  updatedAt: number;

  // Kind-specific payload
  bitmap?: { fileId: string; format: 'png' | 'jpeg' | 'webp' | 'gif'; width: number; height: number; };
  svg?:    { fileId: string; };
  table?:  { model: TableModel; };
}

Binary blobs (bitmap / SVG source) live in IndexedDB, referenced by fileId — same pattern as custom fonts in #44. Tables are stored structurally (TableModel), not as raw HTML, to stay editable.

3. Table model & editor

Location: new packages/postext-sandbox/src/panels/resources/TableEditor/

interface TableCell {
  content: string;          // postext markdown microformat string
  colSpan?: number;         // default 1
  rowSpan?: number;         // default 1
  isHeader?: boolean;       // renders as <th>
  align?: 'left' | 'center' | 'right';
  verticalAlign?: 'top' | 'middle' | 'bottom';
}

interface TableModel {
  rows: TableCell[][];
  headerRowCount?: number;      // default 1
  // Optional style hooks to be expanded later (borders, zebra, column widths)
}

Cell content is parsed with the same inline microformat pipeline as body text:

Reuse is mandatory, not optional. The editor calls the existing parsers so tables can't drift from body text semantics.

Editor UX must support:

  • Add / remove rows and columns
  • Merge cells (col-span + row-span) and un-merge
  • Toggle header rows / header columns
  • Per-cell alignment
  • Keyboard navigation (Tab / Shift-Tab / arrow keys)
  • Copy / paste of TSV / CSV for bulk data entry
  • Undo / redo

4. Resources panel UI

Location: packages/postext-sandbox/src/sidebar/ResourcesPanel.tsx (currently a placeholder)

Replace the placeholder with a full manager:

  • List of resources grouped by type, with thumbnails for bitmaps and a glyph for SVG / tables.
  • "New" affordance with sub-options: Upload image, Upload SVG, New table.
  • Per-resource detail pane: id (editable, auto-suggested from filename/caption), type selector, caption editor (uses the same inline microformat input as body text so **bold** / $x^2$ etc. work), alt text for a11y, and kind-specific controls (replace file for bitmap/svg, open the table editor for tables).
  • Preview region showing exactly how the resource will render in the document, including the generated caption prefix (fig. 1.7. …) based on its type and a mock reference position.
  • Delete with confirmation — the confirmation must warn if the resource is referenced anywhere in the markdown, listing the reference count.

5. Inline reference syntax — markdown extension

Location: packages/postext/src/parse/inlineFormatting.ts, packages/postext/src/parse/blockParser.ts

The markdown language must be extended so authors can reference resources inline. Two syntaxes:

5a. Block embed — places the resource at this point in the flow

::resource{id="lighthouse-diagram"}

On its own line, this directive embeds the resource here (caption included). This is what establishes the first-reference position for numbering purposes. If a resource is only ever ::resource-embedded once, its number is assigned at that embed point.

5b. Inline reference — cross-reference only, does not embed

As shown in :ref{id="lighthouse-diagram"}, the light is visible at 20 miles.

Renders inline as the formatted reference text — by default the resource type's shortLabel + number (e.g. fig. 1.7), but with options:

:ref{id="lighthouse-diagram"}                    → "fig. 1.7"
:ref{id="lighthouse-diagram" style="number"}     → "1.7"
:ref{id="lighthouse-diagram" style="full"}       → "Figure 1.7"
:ref{id="lighthouse-diagram" text="see it"}      → "see it"  (link text, number hidden)

An inline :ref before any ::resource embed counts as the first-reference position for numbering — this is what makes renumbering-on-insert work the way the user described. Embeds and references share the same numbering pass.

5c. Numbering pass

Location: new packages/postext/src/pipeline/resourceNumbering.ts

A new pipeline step, running after block parsing but before rendering:

  1. Walk the parsed document in reading order.
  2. For each resource id encountered (via either syntax), record the first occurrence.
  3. For each resource type, assign counters in first-occurrence order, respecting the type's resetOn setting.
  4. Produce a ResourceNumberingMap: Record<resourceId, { number: string; heading: HeadingContext; }>.
  5. Pass the map to renderers so ::resource embeds get captions and :ref inlines get formatted numbers.

This mirrors the existing heading numbering pattern in packages/postext/src/numbering.ts — extend/parallel, don't duplicate.

6. Rendering

Location: packages/postext/src/canvas-backend/blockRender.ts, packages/postext-pdf/src/pdf-backend/index.ts

Canvas and PDF backends must gain:

  • Bitmap block rendering (from IndexedDB blob → canvas image / PDF embedded image).
  • SVG block rendering — rasterize for canvas preview, embed as vector in PDF.
  • Table block rendering — lay out cells with col-span / row-span, measure via existing text measurement utilities, apply microformats and inline math inside cells.
  • Caption rendering below the resource, prefixed per the resource type's captionPrefix template.
  • Inline :ref rendered as a styled link (clickable in HTML, annotated in PDF) pointing at the embed location.

All resources must participate in the existing measurement / placement pipeline so they paginate correctly and respect keepWithNext-style constraints (resource + caption shouldn't split).

7. Warnings

Location: packages/postext-sandbox/src/warnings/compute.ts

  • ::resource or :ref with an id that doesn't exist in the resources panel.
  • Resource defined in the panel but never referenced in the markdown (unused resource).
  • Resource id duplicated (two resources sharing an id).
  • Inline :ref pointing at a resource whose typeId was deleted from the type list.
  • Circular references (shouldn't be possible structurally but guard anyway).
  • Bitmap at rendered size exceeds a resolution threshold relative to its native size (blurriness hint).

8. Bundle integration

Location: issue #47

The .postext bundle format must carry resources/ binaries and a resources/index.json with the resource metadata + table models. Resource types belong in config.json (they're configuration).

Acceptance criteria

  • ResourceType list + editor UI in config settings
  • Resource storage in IndexedDB, binaries by fileId
  • Replace placeholder ResourcesPanel with upload / create / edit / delete UX
  • Bitmap upload (PNG/JPEG/WebP/GIF) with preview and replace
  • SVG upload with preview
  • Table editor: add/remove rows/cols, col-span / row-span, header toggles, alignment, keyboard nav
  • Table cells parsed with the same microformat pipeline as body text (bold, italic, code, inline math, inline :ref)
  • Caption field per resource, using the same rich inline editor
  • Markdown ::resource{id=...} block directive
  • Markdown :ref{id=... style=... text=...} inline directive
  • Resource numbering pipeline runs after parsing, assigns per-type counters in first-occurrence order
  • Numbering respects per-type resetOn (none / h1 / h2 / …)
  • Caption prefix and inline ref both pull from the same numbering map
  • Canvas and PDF backends render bitmaps, SVGs (vector in PDF), and tables with captions
  • Inline :ref is clickable and navigates to the embed in HTML; PDF emits a link annotation
  • Warnings for unknown ids, unused resources, duplicated ids, dangling type references
  • .postext bundle from #47 round-trips resources and types
  • Docs updated (docs/document-format-en.mdx, docs/configuration-en.mdx, and -es) with the new directives and type management

Out of scope (follow-ups)

  • Image editing (crop / resize / filters) inside the panel.
  • Importing tables from CSV files as a first-class action (paste works).
  • Video / audio resources.
  • LaTeX-style figure floats / placement algorithms — resources render at their embed location for now.
  • Footnote-style references.
  • Lists of figures / lists of tables auto-generated at the start of the document.
  • Bidirectional links (clicking the resource jumps to the first reference in text).
  • Non-SVG vector formats (EPS, PDF-as-image).

Related code

Related issues

  • #44 — custom fonts (same IndexedDB-blob pattern)
  • #46 — pagination / {h1} context for numbering templates
  • #47 — project bundle (must round-trip resources + types)
  • #48 — advanced heading designs (resource numbering shares the heading-context mechanism)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requesthelp wantedExtra attention is needed

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions