Skip to content

Releases: Aeven-AI/HyperMarkdown

v0.4.4

Choose a tag to compare

@olibaron olibaron released this 11 Sep 09:35

Fixed

A finished block settles to a plain render once, after the fade it started has run. A streamed block wore the animated shape for its whole life; the closed render then replaced it with markup that carried no animation at all. The words had finished fading and did not move, but the swap itself was visible everywhere else — the code gutter was rebuilt from scratch with every line number restarting its fade, and a table's last row jumped from partial opacity to full in a single frame.

Each block kind now books one settled pass, a second after it closes, which is when its fade has finished and the swap is therefore invisible:

  • MarkdownCode schedules the pass instead of swapping children inside render(), and a flag — not the children — decides whether it has been applied, so a settle that produced nothing to adopt cannot re-arm it; reset() cancels one still booked, and a block that was never streaming books none at all;
  • line numbers are handed the code cache's tally in both phases, since the settled tree holds the same lines, and remember the last tally: a caller that withdraws it can no longer collapse the gutter to the empty-state entry and remount every line;
  • tables and markup take the same pass from the renderer, rebuilt from a cache of its own so a later block cannot race it;
  • a finished block carries no data-animate-word, data-animate-key or data-animate-marker. Its frame is byte-identical to the same document streamed with animation: false, and blocks already settled are never re-passed when new ones stream.

Toolbar tooltip CSS is no longer bundled. tippy.js became an optional peer in 0.4.2 so that a consumer which never renders a toolbar would not pay for it, but the stylesheet still inlined tippy's base CSS: every consumer shipped it, and the build in turn needed the package installed, which nothing did — npm run build could not pass from a clean npm ci.

The main stylesheet now carries only the toolbar skin and imports nothing from node_modules. A host that renders toolbars imports tippy's own sheet beside the one it already imports for KaTeX:

import "@aeven-ai/hypermarkdown/styles.css";
import "tippy.js/dist/tippy.css";

The built dist/hypermarkdown.css drops by the 1.4 kB tippy sheet, from 46 478 to 45 064 bytes, and the build assertion still guards the skin. tippy.js stays a peer and a devDependency, exactly as KaTeX does: tsc resolves its types and the tooltip loader test needs the runtime.

The word and marker fades also drop will-change: opacity, which promoted a compositor layer per word for no measured gain.

Compatibility

No API change. A host that renders block toolbars needs one extra stylesheet import — tippy.js/dist/tippy.css — and without it the toolbars still work, with the tips simply losing their positioning and sizing. Coverage stays at 100%. Patch release.

v0.4.3

Choose a tag to compare

@olibaron olibaron released this 10 Sep 19:05

Fixed

Block toolbars no longer cost work on every scroll frame. Every code, diagram, and table toolbar registered its own capturing window scroll listener and requested an animation frame per scroll event. Each frame measured its block with getBoundingClientRect and called classList.add or classList.remove — for every block on the page, on screen or not, whether or not the state changed. DOMTokenList add and remove rewrite the class attribute even when the token is already in the requested state, so each block produced a layout read and a mutation record per frame. A host scrolling a 64k px transcript measured ~600 mutation records and ~2,000 rect reads per second from toolbars alone; afterwards the same scroll measured 9 mutation records per second.

watchStickyHeader replaces the per-block listeners:

  • one shared IntersectionObserver tracks which blocks are on screen; an off-screen block cannot span the toolbar band, so it is cleared and never measured;
  • one capturing scroll/resize listener is attached only while some block is on screen;
  • each pass reads every on-screen block before writing any class, so a frame lays out once;
  • the scroll class is toggled only when the stuck state flips.

Without IntersectionObserver, every block counts as on screen (the previous behaviour, without the redundant writes).

Compatibility

No API change. The toolbar's scroll class keeps its meaning and thresholds; onViewportScroll stays exported. Patch release.

v0.4.2

Choose a tag to compare

@olibaron olibaron released this 08 Sep 11:10

Changed

tippy.js is optional, and loads on the first tooltip. It was a hard dependency imported at module load, so every consumer paid for it in install size and in the bundle — including the many that never render a toolbar. It is now an optional peer dependency, imported dynamically the first time a tooltip mounts. Install it and tooltips are unchanged; leave it out and nothing breaks.

Every control stays named without it. With tippy absent the imperative show/hide handle is inert and the trigger keeps the native title it already carried, so toolbar buttons remain labelled and reachable — a plainer tooltip, not a missing one.

The loader survives both module shapes. tippy.js@6 ships main and module with no exports map, so the callable export sits at a different depth depending on the resolver: a bundler reaching for ESM finds it at default, Node taking the CommonJS build finds it at default.default. The loader descends until something is callable instead of assuming a depth. The import is attempted once per module; a failed load leaves the retry open for a later tooltip, and one that resolves without a callable is not retried.

Tooltip creation reads current props. tippy now arrives a microtask after the effect runs, so creation reads the latest props at that moment rather than the ones captured when the effect was scheduled, and an unmount during the load disposes cleanly instead of leaving an orphaned instance.

Docs

The docs site is measured with Google Analytics, through the classic preset's own gtag plugin rather than a snippet in headTags — the site is a single-page app after first paint, so a bare snippet would count the entry page and nothing after it. Production builds only: docusaurus start injects no tag.

Compatibility

No API change: Tooltip keeps its props and its show/hide handle. The one thing to know is installation — tippy.js is no longer installed for you. A consumer that wants rich tooltips should add tippy.js@^6.3.1 alongside this package; one that does not gets native titles and a smaller bundle. Patch release.

v0.4.1

Choose a tag to compare

@olibaron olibaron released this 02 Sep 11:44

Fixed

The wheel is left to the page. Zoom took every wheel event over a diagram, so scrolling stopped the moment the cursor crossed one — and a document can be mostly diagram. Zoom now waits for Ctrl (or ), which is what the browser itself zooms on and what a trackpad pinch already sends: pinching still zooms the diagram, two fingers still scroll the page.

Edges fade instead of cutting. A zoomed diagram ended mid-stroke at the border. A 10px fade masks each edge now. It is a mask rather than a painted overlay, so it fades to transparent and sits correctly over a panel, a page, or nothing without being told the colour — and a rounded container needs no special case, its corner arc falling inside the faded band.

A held zoom button repeats. Stepping a tenth at a time meant a click per step. Holding zoom in or out now repeats after a short delay, and stops on release, on cancel, on leaving the button, and on unmount.

Notes

The fade sits on an internal .mermaid-pan-zoom-viewport element wrapping the diagram content — on the content itself the mask would scale and pan along with the diagram instead of staying pinned to the border. A host that styles diagram internals by class may want to know it is there.

The publish workflow is gone. Publishing runs locally under the account's two-factor auth, so it had stopped running on releases in 0.2.1 and had nothing left to do.

Compatibility

No API change: no new options, no signature changes. Patch release.

v0.4.0

Choose a tag to compare

@olibaron olibaron released this 02 Sep 10:14

Added

Pan and zoom a diagram in place.

A wide flowchart or a dense sequence diagram had one way to be read at size: fullscreen, which takes over the page to look at a single block. A diagram now carries its own view instead — Zoom in, Zoom out and Reset zoom and pan on the toolbar, with the wheel to zoom and dragging to pan, by mouse or by touch.

It is on by default:

<HyperMarkdown
  md={markdown}
  plugins={plugins}
  controls={{ diagram: { copy: true, fullscreen: true, panZoom: false } }}
/>

panZoom: false leaves copy and fullscreen alone; diagram: false still hides the toolbar entirely, pan and zoom with it. That switch is why controls.diagram now takes a DiagramControls rather than the BlockControls code blocks and tables share — it is the one control the others have no use for.

Three translation keys (zoomIn, zoomOut, resetView) and three icons of the same names join the overridable set.

Fixed

A list marker fades in with the words beside it. The bullet and the ordered number are drawn by the stylesheet rather than written into the document, so the word-fade pass had nothing to wrap and they arrived at full opacity next to text still fading in. They now fade on the same curve and duration.

Documentation

Translation and icon key tables now list every key, including exitFullScreen, undocumented since v0.1.9.

Compatibility

No breaking changes. Every new field is optional, and every new string and icon has a default, so existing configuration keeps working untouched.

v0.3.0

Choose a tag to compare

@olibaron olibaron released this 01 Sep 18:59

Added

preview — where a code block's HTML preview opens.

Field Type Description
url string or (id) => string The page to open. {id} is replaced with the block's id.
storageKey string or (id) => string Key the HTML is written to. Defaults to preview-{id}. Only used with url.
<HyperMarkdown md={markdown} preview={{ url: "/preview-code/{id}" }} />

Changed

With nothing configured, a preview now opens as a page in its own right.

The Preview button used to write the HTML to localStorage under preview-<id> and open /preview-code/<id>. Both were one application's conventions, baked into the library — every other consumer got a 404 tab.

The default now opens the HTML from a blob URL: a real document with a real address, which reloads, inspects and views source like any other page, and runs in its own scope rather than the host's. It asks nothing of the host — no route, no storage, no navigation. Where no window can be had at all (a blocked popup, or a host embedded without allow-popups) the block reports it through onAlert, as it already did.

Set preview.url to take the previous behaviour back; a host that already serves a preview page keeps it with one prop, and its storage key is unchanged by default.

Previewed HTML runs with your origin. Serving your own preview page is what lets you decide how far to trust it — rendering it inside a sandboxed iframe, for instance.

Compatibility

No breaking type or signature changes; preview is optional everywhere. The only behaviour change is what happens when it is absent, which previously navigated to a route the library assumed.

v0.2.1

Choose a tag to compare

@olibaron olibaron released this 01 Sep 17:37

Fixed

A table built while streaming now reports fullscreen to the host, and honours the host's UI config.

Tables reach the reader from two construction sites. The one in the component map passes the renderer, the emitter, the resolved UI and scrollDown. The streaming one passed only the renderer and scrollDown — and it builds the closed frame as well as the streaming ones, so a table that streamed kept the gap for good.

Two consequences:

  • Fullscreen told nobody. The toolbar toggled the block into fullscreen, but the optional call to the emitter went nowhere, so the host never received fullscreen:change. Chrome that moves aside for an expanded block — a fixed top nav, in the case this turned up in — stayed put and painted over the table. A streamed code block passes the emitter and behaves correctly, which is what made the two look inconsistent.
  • The host's UI config was ignored. Without the resolved UI, a streamed table fell back to the default translations, icons and toolbar controls instead of the ones the host configured.

Both are fixed by passing events and ui at the streaming construction site, matching the component map and the streaming code block.

Compatibility

No prop, type or export signature changes. Upgrade without edits.

v0.2.0

Choose a tag to compare

@olibaron olibaron released this 01 Sep 17:16

Fixed

Diagrams are no longer handed a source the engine cannot lay out.

A fence renders on every delta while it streams, so the engine sees the diagram grow one keystroke at a time. Nearly every diagram type copes: it draws what it has, or it throws and the render path catches that and keeps the frame it already had.

The data-driven types do neither. Given a row that is still being typed they lay out successfully against a range that is empty or backwards:

  • a gantt before its first task, or while its last task is half-written, produces NaN coordinates and negative bar widths
  • a pie before its first slice produces an infinite viewBox

There is no failure to catch, so the browser reported each one for every keystroke of the fence.

Those frames are now held back and the last good diagram stays on screen. Diagrams still build progressively — only the frames the engine cannot draw are skipped.

Verified against mermaid over every character prefix of both fixtures: no prefix producing NaN, infinite or negative geometry gets through, while 43 gantt and 17 pie intermediate frames still draw.

Compatibility

No prop, type or export signature changes. Consumers upgrade without edits.

v0.1.9 — toolbar order and fullscreen labelling

Choose a tag to compare

@olibaron olibaron released this 01 Sep 16:35

Changed

Copy and fullscreen swap places on code blocks, tables and diagrams. Fullscreen — the control that acts on the whole block rather than its contents — is now the last button in all three toolbars.

The first and last classes moved with the slot rather than the button, since they are the toolbar's end gutters (padding-left / padding-right). Tooltip placement follows position too: the outermost button anchors with top-end, the inner one centres on top.

If you select toolbar buttons by DOM order or by .first / .last, update those selectors.

Added

The fullscreen button now names the state it offers rather than the one it is in. exitFullScreen ("Exit full screen") joins the translations, and the label changes with the icon, so the tooltip no longer reads "Full screen" over a minimize icon.

translations is accepted as Partial<Translations>, so the new key does not break callers supplying their own.

Full Changelog: v0.1.8...v0.1.9

v0.1.8 — ReDoS fixes

Choose a tag to compare

@olibaron olibaron released this 01 Sep 11:00

Security release. Four Markdown scanners ran in polynomial time on inputs that are ordinary Markdown, reported by CodeQL as high-severity polynomial ReDoS (js/polynomial-redos).

Supersedes v0.1.6 and v0.1.7, which were tagged but never reached npm. Everything in them is included here.

Fixed

Scanner Before After
Table detection 64 s on 8 KB < 1 ms on 80 KB
Fenced-range collection quadratic 3.4 ms on 20k fences
Bracket-math conversion quadratic 1.8 ms on 20k lines
Reasoning-tag splitting quadratic 0.1 ms on 20k tags

Table detection was the one that mattered most in practice: its trigger is an unterminated final line, which is the normal shape of a table part-way through a stream, so a pipe-dense row could stall the renderer without any hostile input at all. If you stream tables, upgrade.

All four now share a single linear line index. splitReasoning additionally moved to indices, dropping an O(n²) from its repeated slicing.

Also includes a KaTeX handling fix for word animation and loading-indicator style adjustments.

Compatibility

No API changes. Each scanner was fuzzed against the regex it replaced over ~30k generated inputs — including repairTableSyntax and convertMath end to end — so output is byte-identical, including two quirks of the old fence pattern that are now pinned by tests (it matches neither an empty fence nor a CRLF closing line).

Full Changelog: v0.1.5...v0.1.8