Skip to content

Releases: brewkits/hyper_render

HyperRender v1.3.2

19 May 03:09

Choose a tag to compare

[1.3.2] - 2026-05-18

Bug Fixes (Critical)

  • [DEADLOCK] LazyImageQueue no longer deadlocks on a synchronously-throwing loader — if a user-supplied HyperImageLoader threw before invoking its onLoad/onError callback, _active was never decremented; after maxConcurrent such throws the queue stopped processing every subsequent image until app restart. _startLoad now wraps the loader call in try/catch and routes any synchronous exception through the same idempotent error path used by the async callback.
  • [SECURITY] Sanitizer now validates ALL URL-bearing attributes — previously only href and src were checked, leaving poster, data, cite, background, longdesc, usemap, manifest, xlink:href, formaction, action, icon, and srcset as XSS bypass vectors (e.g. <video poster="javascript:...">). Added urlBearingAttributes constant and routes every match through isSafeUrl. srcset is split into candidates and each candidate's URL is validated independently.
  • [SECURITY] isTap no longer fires when the pointer never went down inside the widgethandleEvent previously treated downPosition == null as a valid tap, so a finger swiping into the widget from outside and lifting up would trigger onLinkTap on whatever fragment was under the lift point. Now requires BOTH a recorded down position AND a movement within tapSlop.
  • [BUG-1] Images no longer permanently disappear after a Low Memory WarningclearMemoryCaches() disposed the image cache but never re-triggered _loadImages(). Visible images were stuck in the empty-placeholder state until the user scrolled the section out of view and back to force a detach+attach cycle. The cache-clear path now re-enqueues image loads via LazyImageQueue so visible images reload through the normal priority pipeline.
  • [BUG-2] _hashSection now invalidates on attribute changes — the previous fingerprint only hashed text content + child count, so changing only <img src="a.jpg"><img src="b.jpg"> (or class/id/style) produced the same hash. _mergeSections would silently reuse the stale DocumentNode, freezing dynamic UI at the first rendered version. The new recursive hash walks the subtree and includes tagName, type, text, atomic src/alt, all attributes (keys sorted), and per-depth child counts.
  • [BUG-3] Eliminated 1-frame layout flash with dangling floats_onFloatCarryover previously deferred the cross-section update via addPostFrameCallback + setState, so section N+1 always laid out once with empty initialFloats before the corrected pass. Added onRenderBoxReady callback on HyperRenderWidget and VirtualizedChunk; _HyperViewerState keeps a Map<int, RenderHyperBox> registry and pushes new floats directly onto section N+1's RenderObject during section N's layout, so the pipeline owner picks up the change in the same frame.
  • [C-1] HyperSelectionOverlay now forwards config, pluginRegistry, enableComplexFilters — plugins, custom link schemes, keyframe animations and filter settings were silently ignored in sync+selectable and paged+selectable modes. All three params are now accepted by HyperSelectionOverlay and forwarded to the inner HyperRenderWidget.
  • [C-2] Fixed GPU memory leak in image cache_imageCache was missing an onEvict callback, so ui.Image GPU textures were never disposed when entries were evicted from the LRU. Added onEvict: (ci) => ci.image?.dispose() to free GPU memory promptly on eviction.
  • [C-3] Removed dead _parseIsolate / _parseReceivePort code — these fields were declared but never assigned, making _cancelParsing() a no-op. Cleaned up unused dart:isolate import and fields; _parseId counter remains the mechanism for discarding stale parse results.
  • [C-4] TextPainter global cache now respects HyperRenderConfig.textPainterCacheSize — was hardcoded to 500 regardless of config (default 5000). Added RenderHyperBox.setGlobalTextCacheSize() static method; HyperViewer calls it in initState and didUpdateWidget.

Bug Fixes (High)

  • [H-1] HyperRenderConfig.operator== and hashCode now include useMicrotaskParsing — changing only this field no longer fails to trigger a re-parse.
  • [H-2] ComputedStyle.copyWith() now copies _explicitlySet — previously the result had an empty explicit-set, causing inheritFrom() to overwrite all copyWith'd properties with parent styles, breaking the CSS cascade.
  • [H-3] _containsFloatChild detects float:left (no space) and Bootstrap/Tailwind class namesfloat:left, float-left, float-right, float-start, float-end, pull-left, pull-right are now detected, preventing incorrect section splits in virtualized mode.
  • [H-4] isSafeUrl() blocks file:, mhtml:, and about: schemes — these can access local filesystem, trigger MHTML exploits, or enable sandbox-escape via about:blank on Android/iOS.

Bug Fixes (Medium)

  • [M-1] _effectiveConfig is now cached — was allocating a new HyperRenderConfig on every build() call (every scroll frame). Cache is invalidated when renderConfig, allowedCustomSchemes, or document keyframes change.
  • [M-2] HyperViewer.fromNode now accepts pluginRegistry and onError — previously hardcoded to null, making plugins and error handling unavailable for pre-parsed AST consumers.
  • [M-3] _buildPagedContent no longer allocates a discarded HyperRenderWidget — restructured to if/else so only one widget is built per page in selectable mode.
  • [M-4] _TextPainterKey now includes wordSpacing — two fragments with identical text but different word-spacing no longer share the same TextPainter, preventing incorrect layout widths.

Performance (Low)

  • [L-1] LazyImageQueue._findQueued is now O(1) — added _urlToQueued secondary index; previously O(N) causing O(N²) batch behavior with many simultaneous image loads.
  • [L-2] _hasDetailFragments flag replaces O(N) scanperformLayout no longer scans all fragments to check for <details> elements; flag is set during tokenization.

Fixes (Low)

  • [L-3] _splitIntoSections no longer overwrites existing node parents — changed child.parent = current to if (child.parent == null) child.parent = current to avoid corrupting ancestor-chain traversal on reused section nodes.
  • [L-4] Removed dead _draggingHandle field from HyperSelectionOverlayState.

Correctness & Robustness

  • Hash collision resilience on Web_accumulateHashParts now also mixes in text.length for every TextNode, significantly reducing the chance that two long-but-distinct strings hash to the same slot on the JS target (where Object.hashAll has weaker dispersion than the Dart VM).
  • computeMinIntrinsicWidth handles icon fonts, emoji, and dingbats — the previous "longest-by-char-count word" heuristic miscalculated when a single PUA glyph (Material Icons, Font Awesome) or emoji renders far wider than a Latin letter. When the fragment contains any code point in U+E000–U+F8FF, U+2600–U+27BF, or U+1F000+, the entire fragment is measured instead of just the longest word.
  • RenderHyperBox.detach() now cancels shimmer state — a ListView item that detached mid-shimmer (scrolled out of cache) and later re-attached kept a stale _shimmerEpoch, producing a 1-frame phase jump on re-mount. The frame callback is now cancelled and _shimmerEpoch reset.

New

  • HyperRenderConfig.useRepaintBoundary (default true) — opt out of the outer-section RepaintBoundary wrapper. RenderHyperBox is already an internal repaint boundary, so this is mostly an escape hatch for very low-RAM Android devices (≤ 1.5 GB) rendering image-heavy long documents with a custom small virtualizationChunkSize, where many concurrent GPU layers could exhaust VRAM before the texture cache evicts.

Second-Pass Senior Review (2026-05-18 → 2026-05-19)

A second multi-disciplinary review (PM/BA/SA/principal mobile) surfaced a further batch of issues addressed in this same release. Highlights:

Security

  • UrlSafety consolidated in hyper_render_core/util/url_safety.dart — root HtmlSanitizer.isSafeUrl and the hyper_render_markdown sub-package's URL gate previously had independent copies that drifted: the sub-package missed file:/mhtml:/about:. Both now delegate to the shared helper; no future drift is possible.
  • HtmlAdapter defence-in-depth URL gate<img src> and <a href> are now routed through UrlSafety.isSafe even when the upstream HtmlSanitizer is bypassed (callers that invoke HtmlAdapter().parse() directly or render with sanitize: false). Blocked href collapses to #; blocked src collapses to ''.
  • hyper_render_clipboard filename hardening (path traversal)_getFilenameFromUrl already stripped path separators from URL-decoded filenames, but saveImageBytes(filename:) and shareImageBytes(filename:) concatenated caller-supplied strings raw. Every save/share path now runs through a single _sanitiseFilename helper.
  • Markdown inline HTML pre-sanitised — when HyperViewer.markdown(sanitize: true) (default) is used with enableInlineHtml: true (default), raw <script>/<style>/<iframe> blocks are now stripped via HtmlSanitizer before reaching the markdown parser, so they can no longer flash as visible text or become a self-rendering plugin's XSS surface.

Layout & Selection

  • Unbounded-width crash fixedRenderHyperBox.performLayout and _computeHeightForWidth clamp _maxWidth to a finite fallback when the constraint is double.infinity (Row without Expanded, horizontal SingleChildScrollView). Before this, _FlexFragment.layout propagated infinity into a BoxConstraints(minWidth: ∞) and tripped Flutter's minWidth < double.infinity assertion.
  • text-overflow: ellipsis no longer leaks hidden text via copyFragment.ellipsisVisibleLength track...
Read more

v1.3.1 — Decouple plugins, CSS list-style & background, selection performance

14 May 12:30
fcd3584

Choose a tag to compare

⚠️ Migration from 1.3.0

hyper_render_clipboard and hyper_render_math are now opt-in — no longer bundled in the root hyper_render package. Add them explicitly if needed:

dependencies:
  hyper_render: ^1.3.1
  hyper_render_clipboard: ^1.3.1   # image copy/save/share
  hyper_render_math: ^1.3.1        # LaTeX/MathML

If you don't use either feature, just bump the version — no other changes required.


What's New

New CSS Properties

  • list-style-type: disc, circle, square, decimal, decimal-leading-zero, lower-alpha, upper-alpha, lower-latin, upper-latin, lower-roman, upper-roman, none
  • list-style-position: inside / outside
  • list-style shorthand
  • background-repeat: repeat, repeat-x, repeat-y, no-repeat, space, round
  • background-position: keyword and percentage values

Performance

  • Selection drag: getSelectionRects() cached (1× per event, was 3×)
  • Auto-scroll speed now proportional to finger distance from edge
  • HyperTeardropHandlePainter deduplicated — exported from hyper_render_core

Bug Fixes

  • Edge-to-edge images: width: 100% now truly fills container (no internal margin)
  • Android build: no longer requires compileSdk = 35 workaround for default usage

Packages & Docs

  • All 7 sub-package CHANGELOGs updated with [1.3.1] entries
  • All READMEs: ecosystem cross-link table, corrected version references, fixed plugin API docs
  • Migration guide updated for 1.3.0 → 1.3.1

Full changelog: https://github.com/brewkits/hyper_render/blob/main/CHANGELOG.md

v1.3.0 — Math plugin, CSS cascade fix, customCss for Markdown/Delta

03 May 23:51

Choose a tag to compare

What's New in v1.3.0

This is a significant release that ships a new first-party math rendering package, fixes two CSS correctness bugs reported by users, completes the hyper_render_math package with full documentation and examples, and consolidates all improvements from the v1.2.x patch cycle.


🆕 New Package: hyper_render_math

A first-party plugin package for rendering LaTeX / MathML formulas inside HyperRender documents.

  • MathNodePlugin handles both <math> and <latex> tags via the HyperNodePlugin API
  • Renders using flutter_math_fork — KaTeX-quality output
  • Supports display mode (block, centered) and inline mode (flows inside text lines) via isInline
  • Formula source passed through src attribute or tag text content
  • Ships with LICENSE, CHANGELOG, example app, and pub.dev topics
final registry = HyperPluginRegistry()..register(const MathNodePlugin());

HyperViewer(
  html: r'<math src="x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}" />',
  pluginRegistry: registry,
)

🐛 Bug Fixes

Issue #9 — CSS width/height overridden by HTML attributes (#9)

Root cause: render_hyper_box_layout.dart used node.intrinsicWidth ?? node.style.width, so HTML presentation attributes (width="345") always shadowed CSS rules (img { width: 600px }). This violated the CSS cascade spec where author stylesheets take priority over element attributes.

Fix: Swapped priority to node.style.width ?? node.intrinsicWidth at all 4 layout paths in render_hyper_box_layout.dart — normal layout, secondary layout pass, float layout (image loaded), and float layout (image loading placeholder).

<!-- Before: width was 345px regardless of CSS -->
<!-- After: CSS wins, width is 600px -->
<img src="photo.jpg" width="345" height="345">
<style>img { width: 600px; height: 300px; }</style>

Issue #8customCss ignored for Markdown and Delta content (#8)

Root cause: In hyper_viewer.dart, the cssToApply variable was only populated inside the if (contentType == html) block. For Markdown and Delta content types, customCss was silently discarded — the StyleResolver received an empty string.

Fix: Initialized cssToApply = widget.customCss ?? '' before the content-type check, so all three content types (html, markdown, delta) correctly receive and apply custom CSS rules.

// Now works for Markdown and Delta too
HyperViewer(
  content: markdownContent,
  contentType: HyperContentType.markdown,
  customCss: 'a { color: #0066cc; } h1 { font-size: 28px; }',
)

Additional fixes

  • <hr> returned wrong node type: html_adapter.dart now returns a BlockNode with a solid bottom border instead of LineBreakNode
  • AtomicNode.svg() wrong constructor params: Fixed named parameter mismatch in html_adapter.dart
  • Duplicate [1.3.0] CHANGELOG headers: Consolidated across all sub-packages

🏗️ Infrastructure & Quality

  • Tests relocated to sub-packages: html_adapter_test.dart, markdown_adapter_test.dart, code_highlighter_test.dart moved into their respective package test/ directories
  • hyper_render_highlight example: Corrected class reference to DefaultCodeHighlighter
  • publish.sh / prepare_publish.sh: Updated for v1.3.0 and now include hyper_render_math in all publish/analysis steps
  • All READMEs updated to reference ^1.3.0
  • 1,637 tests passing, 0 failures (+ 5 math package tests)
  • flutter analyze — 0 issues across all packages

📦 Packages in this release

Package Version
hyper_render 1.3.0
hyper_render_core 1.3.0
hyper_render_html 1.3.0
hyper_render_markdown 1.3.0
hyper_render_highlight 1.3.0
hyper_render_clipboard 1.3.0
hyper_render_devtools 1.3.0
hyper_render_math 1.3.0 (new)

Upgrading

dependencies:
  hyper_render: ^1.3.0
  # or individual packages:
  hyper_render_core: ^1.3.0
  hyper_render_math: ^1.3.0  # new

No breaking changes. Drop-in upgrade from v1.2.x.

v1.2.2 — 8 Bug Fixes + Android Build Fix

02 Apr 05:04
ff99674

Choose a tag to compare

What's Changed

🐛 Bug Fixes

  • Android build failure (example/android/build.gradle.kts): irondash_engine_context 0.5.5 compiled against android-31 conflicts with androidx.fragment:1.7.1 (minCompileSdk=34). Added subprojects { compileSdk = 35 } override. Closes #5.
  • SVG invisible with sanitize: true (html_sanitizer.dart): Added atomic SVG sanitization path that preserves structure while stripping <script> and dangerous attributes.
  • HyperRenderConfig identity-compare (hyper_render_config.dart): Added operator== / hashCode — prevents unnecessary re-layouts on every frame when _effectiveConfig merges @keyframes into a new object.
  • selectable toggle ignored (hyper_viewer.dart): didUpdateWidget now creates/disposes VirtualizedSelectionController correctly when selectable changes.
  • Deep-link taps silently blocked (hyper_viewer.dart): _safeOnLinkTap now checks both allowedCustomSchemes AND renderConfig.extraLinkSchemes.
  • CSS change bypassed section cache (hyper_viewer.dart): _sectionHashes now reset in didUpdateWidget when customCss changes.
  • Markdown/Delta rendered as single section (hyper_viewer.dart): Added _splitIntoSections() — large Markdown/Delta docs now chunk correctly in virtualized/paged mode.
  • renderConfig change partially detected (hyper_viewer.dart): didUpdateWidget now uses full value equality instead of only comparing virtualizationChunkSize.
  • CSS float class names not detected (html_adapter.dart): _containsFloatChild now recognises Bootstrap/Tailwind patterns (float-left, pull-right, alignleft, etc.).

🔧 CI Fixes

  • benchmark.yml: Fixed JS SyntaxError from backtick-quoted fixture names inside template literal — use process.env instead of direct interpolation.
  • benchmark.yml + golden.yml: Added pull-requests: write permission for comment-posting steps.
  • test.yml: Guard sub-package test steps with [ -d test ] check — hyper_render_markdown, hyper_render_highlight, hyper_render_clipboard have no test directories.

Full Changelog: https://github.com/brewkits/hyper_render/blob/main/CHANGELOG.md

v1.2.0 — Plugin API, Paged Mode, Incremental Layout

31 Mar 08:53
349689c

Choose a tag to compare

What's new in v1.2.0

✨ New Features

Plugin API — First-class extensibility for custom HTML tags:

final registry = HyperPluginRegistry()
  ..register(MyChartPlugin())
  ..register(MyMapPlugin());

HyperViewer(html: content, pluginRegistry: registry)

Paged mode — Built-in e-book / reader UI:

final controller = HyperPageController();
HyperViewer(
  html: content,
  mode: HyperRenderMode.paged,
  pageController: controller,
)

Incremental layout — Dirty-flag fingerprinting means ~90% fewer rebuilds for live-updating feeds.

CSS @Keyframes — Full animation support with custom keyframe lookup from <style> tags.

♿ Accessibility (WCAG 2.1 AA)

  • <img alt="…"> → discrete SemanticsNode at image rect (WCAG 1.1.1)
  • aria-label on <a> elements honored (WCAG 4.1.2)

🏗️ Refactor

  • Removed 31 duplicate files from root lib/src/ — canonical source now in hyper_render_core
  • LazyImageQueue singleton consolidated (single shared instance)

🐛 Bug Fixes

  • XSS: javascript: URLs blocked in HTML, Markdown, and Delta adapters
  • display:none elements no longer produce layout fragments
  • Selection-vs-scroll conflict resolved
  • Context menu position clamped to visible bounds

📦 Infra

  • All packages: sdk >=3.5.0, csslib ^1.0.2, flutter_lints ^5.0.0
  • share_plus ^12.0.0, Android AGP 8.12.1, 16 KB page alignment
  • 36 new tests for v1.2.0 features

Migration

See MIGRATION_GUIDE for details. The public API is backwards-compatible — upgrading from v1.1.x requires no code changes.

Upgrade

dependencies:
  hyper_render: ^1.2.0

v1.1.2 — Binary Search Selection · DevTools v1.0.0 · 3-Pipeline CI

25 Mar 04:31

Choose a tag to compare

What's New

⚡ O(log N) Binary Search Text Selection

Hit-testing now uses _lineStartOffsets[] precomputed at layout time. Selection stays instant even on 1,000-line documents — no more O(N) linear scan per touch event.

🈶 Ruby Clipboard Format

Fully-selected ruby fragments are copied as base(ふりがな) (e.g. 東京(とうきょう)). Partial selections copy base text only, keeping character offsets consistent. 5 ruby selection pipeline bugs fixed that caused offset desynchronisation for all content after a ruby fragment.

🔭 hyper_render_devtools v1.0.0 — First Full Release

  • UDT Tree inspector — browse the full document model in Flutter DevTools
  • Computed Style panel — see inherited vs. declared values and specificity winners
  • Float region visualizer — highlight floated-block boundaries in layout
  • Demo mode — explore the inspector without a live app

🔁 3-Pipeline CI Architecture

Pipeline Trigger Purpose
Pre-flight (analyze.yml) Every PR/push dart format + flutter analyze --fatal-infos, skip docs-only
Core Validation (test.yml) PR: selective per-package on ubuntu-22.04 Fast gate < 5 min
Core Validation (test.yml) Push to main: 3-OS × 2-channel matrix Full platform coverage
Visual regression (golden.yml) Every PR/push Pixel-stable, pinned Noto fonts
Layout regression (benchmark.yml) Every PR/push Hard 16 ms / 60 FPS budget per fixture

🧪 Golden Tests — Float · RTL · CJK Coverage

9 new pixel-stable test cases:

  • Float layout: float_left, float_right, float_clear
  • RTL/BiDi: rtl_arabic, rtl_hebrew, rtl_mixed
  • CJK + Ruby: cjk_ruby, cjk_kinsoku, float_cjk

📊 Layout Regression Benchmark

6 HTML fixtures (simple paragraph → 100-paragraph article) with hard millisecond budgets run on every PR. Any fixture exceeding 16 ms (60 FPS) fails the build.


Upgrade

dependencies:
  hyper_render: ^1.1.2

No breaking changes. All previous HyperViewer APIs remain identical.


Full details in CHANGELOG.md.

v1.1.0

22 Mar 16:28

Choose a tag to compare

What's new

Rendering

  • Premium visual polish: precision borders, skeleton shimmer gradient, adaptive selection colors
  • Typography overhaul — font features (ligatures, proportional figures), consistent TextHeightBehavior, retina-ready FilterQuality.medium on all images
  • Anti-aliasing explicitly enabled on all paint operations

Bug fixes

  • Copy/paste across block elements (<li>, <h3>, <p>) now correctly inserts newlines
  • Fix _sameLinkContext() guard — incorrect link tap targets when fragments merge across <a> boundaries
  • Fix float crash on unconstrained parent width
  • Fix TapGestureRecognizer leak when document is replaced
  • Fix _fragmentChildMap O(N) scan → O(1) lookup in paint cycle
  • Fix _nodeRectCache O(N²) accessibility rect computation → O(N)
  • Fix display:none not respected in _tokenizeNode and _collectAtomicChildren

pub.dev

  • All sub-packages bumped to v1.1.0 (html, markdown, highlight, clipboard, devtools)
  • screenshots: field added with 6 demo GIFs visible in pub.dev gallery
  • vector_math updated to ^2.2.0
  • README rewritten with absolute image URLs (GIFs now render correctly on pub.dev)
  • Package description optimized for search discoverability

Tests

  • 678 tests passing, 0 failures
  • Added virtualized mode integration tests
  • Added real URL assertions to link tap tests
  • Updated golden images to match current rendering output

Demos

  • New CJK languages demo
  • Why HyperRender showcase with live 16-feature comparison matrix

Install

dependencies:
  hyper_render: ^1.1.0

Quick start

import 'package:hyper_render/hyper_render.dart';

HyperViewer(
  html: articleHtml,
  onLinkTap: (url) => launchUrl(Uri.parse(url)),
)

HyperRender v1.0.0

08 Mar 12:36

Choose a tag to compare

HyperRender v1.0.0 — Initial Public Release

A custom RenderObject-based engine that renders HTML, Markdown, and Quill Delta natively on Flutter Canvas — without WebViews, without widget trees.

What's included

  • CSS Float layout — text wrapping around floated images, architecturally impossible in widget-tree renderers
  • CJK typography — Ruby/Furigana with proper centering, Kinsoku line-breaking across the full line
  • Continuous text selection — select across headings, paragraphs, and table cells without widget-boundary breaks
  • CSS Variables + calc() — full custom property cascade
  • Flexbox and CSS Grid layout
  • Smart table layout — two-pass W3C column-width algorithm, colspan/rowspan, three overflow strategies
  • <details>/<summary> — collapsible sections, no JavaScript required
  • Multi-format input — HTML, Markdown, Quill Delta
  • Built-in XSS sanitizationjavascript:, vbscript:, SVG data URIs, expression() blocked by default
  • Virtualized renderingListView.builder mode for large documents
  • Screenshot exportGlobalKey.toPngBytes()
  • WebView fallback via HtmlHeuristics.isComplex()

Packages published

Package pub.dev
hyper_render https://pub.dev/packages/hyper_render
hyper_render_core https://pub.dev/packages/hyper_render_core
hyper_render_html https://pub.dev/packages/hyper_render_html
hyper_render_markdown https://pub.dev/packages/hyper_render_markdown
hyper_render_highlight https://pub.dev/packages/hyper_render_highlight
hyper_render_clipboard https://pub.dev/packages/hyper_render_clipboard

Benchmarks (macOS Desktop, Apple Silicon, Flutter release mode)

Document Parse time
1 KB 27 ms
10 KB 69 ms
50 KB 276 ms

Mobile performance has not been independently verified — run benchmarks on your own hardware.