Releases: brewkits/hyper_render
HyperRender v1.3.2
[1.3.2] - 2026-05-18
Bug Fixes (Critical)
- [DEADLOCK] LazyImageQueue no longer deadlocks on a synchronously-throwing loader — if a user-supplied
HyperImageLoaderthrew before invoking its onLoad/onError callback,_activewas never decremented; aftermaxConcurrentsuch throws the queue stopped processing every subsequent image until app restart._startLoadnow 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
hrefandsrcwere checked, leavingposter,data,cite,background,longdesc,usemap,manifest,xlink:href,formaction,action,icon, andsrcsetas XSS bypass vectors (e.g.<video poster="javascript:...">). AddedurlBearingAttributesconstant and routes every match throughisSafeUrl.srcsetis split into candidates and each candidate's URL is validated independently. - [SECURITY]
isTapno longer fires when the pointer never went down inside the widget —handleEventpreviously treateddownPosition == nullas a valid tap, so a finger swiping into the widget from outside and lifting up would triggeronLinkTapon whatever fragment was under the lift point. Now requires BOTH a recorded down position AND a movement withintapSlop. - [BUG-1] Images no longer permanently disappear after a Low Memory Warning —
clearMemoryCaches()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 viaLazyImageQueueso visible images reload through the normal priority pipeline. - [BUG-2]
_hashSectionnow 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._mergeSectionswould silently reuse the staleDocumentNode, 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 —
_onFloatCarryoverpreviously deferred the cross-section update viaaddPostFrameCallback + setState, so section N+1 always laid out once with empty initialFloats before the corrected pass. AddedonRenderBoxReadycallback onHyperRenderWidgetandVirtualizedChunk;_HyperViewerStatekeeps aMap<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 byHyperSelectionOverlayand forwarded to the innerHyperRenderWidget. - [C-2] Fixed GPU memory leak in image cache —
_imageCachewas missing anonEvictcallback, soui.ImageGPU textures were never disposed when entries were evicted from the LRU. AddedonEvict: (ci) => ci.image?.dispose()to free GPU memory promptly on eviction. - [C-3] Removed dead
_parseIsolate/_parseReceivePortcode — these fields were declared but never assigned, making_cancelParsing()a no-op. Cleaned up unuseddart:isolateimport and fields;_parseIdcounter 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). AddedRenderHyperBox.setGlobalTextCacheSize()static method;HyperViewercalls it ininitStateanddidUpdateWidget.
Bug Fixes (High)
- [H-1]
HyperRenderConfig.operator==andhashCodenow includeuseMicrotaskParsing— 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, causinginheritFrom()to overwrite all copyWith'd properties with parent styles, breaking the CSS cascade. - [H-3]
_containsFloatChilddetectsfloat:left(no space) and Bootstrap/Tailwind class names —float:left,float-left,float-right,float-start,float-end,pull-left,pull-rightare now detected, preventing incorrect section splits in virtualized mode. - [H-4]
isSafeUrl()blocksfile:,mhtml:, andabout:schemes — these can access local filesystem, trigger MHTML exploits, or enable sandbox-escape viaabout:blankon Android/iOS.
Bug Fixes (Medium)
- [M-1]
_effectiveConfigis now cached — was allocating a newHyperRenderConfigon everybuild()call (every scroll frame). Cache is invalidated whenrenderConfig,allowedCustomSchemes, or document keyframes change. - [M-2]
HyperViewer.fromNodenow acceptspluginRegistryandonError— previously hardcoded tonull, making plugins and error handling unavailable for pre-parsed AST consumers. - [M-3]
_buildPagedContentno longer allocates a discardedHyperRenderWidget— restructured to if/else so only one widget is built per page in selectable mode. - [M-4]
_TextPainterKeynow includeswordSpacing— two fragments with identical text but differentword-spacingno longer share the sameTextPainter, preventing incorrect layout widths.
Performance (Low)
- [L-1]
LazyImageQueue._findQueuedis now O(1) — added_urlToQueuedsecondary index; previously O(N) causing O(N²) batch behavior with many simultaneous image loads. - [L-2]
_hasDetailFragmentsflag replaces O(N) scan —performLayoutno longer scans all fragments to check for<details>elements; flag is set during tokenization.
Fixes (Low)
- [L-3]
_splitIntoSectionsno longer overwrites existing node parents — changedchild.parent = currenttoif (child.parent == null) child.parent = currentto avoid corrupting ancestor-chain traversal on reused section nodes. - [L-4] Removed dead
_draggingHandlefield fromHyperSelectionOverlayState.
Correctness & Robustness
- Hash collision resilience on Web —
_accumulateHashPartsnow also mixes intext.lengthfor everyTextNode, significantly reducing the chance that two long-but-distinct strings hash to the same slot on the JS target (whereObject.hashAllhas weaker dispersion than the Dart VM). computeMinIntrinsicWidthhandles 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 — aListViewitem 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_shimmerEpochreset.
New
HyperRenderConfig.useRepaintBoundary(defaulttrue) — opt out of the outer-sectionRepaintBoundarywrapper.RenderHyperBoxis 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 smallvirtualizationChunkSize, 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
UrlSafetyconsolidated inhyper_render_core/util/url_safety.dart— rootHtmlSanitizer.isSafeUrland thehyper_render_markdownsub-package's URL gate previously had independent copies that drifted: the sub-package missedfile:/mhtml:/about:. Both now delegate to the shared helper; no future drift is possible.HtmlAdapterdefence-in-depth URL gate —<img src>and<a href>are now routed throughUrlSafety.isSafeeven when the upstreamHtmlSanitizeris bypassed (callers that invokeHtmlAdapter().parse()directly or render withsanitize: false). Blockedhrefcollapses to#; blockedsrccollapses to''.hyper_render_clipboardfilename hardening (path traversal) —_getFilenameFromUrlalready stripped path separators from URL-decoded filenames, butsaveImageBytes(filename:)andshareImageBytes(filename:)concatenated caller-supplied strings raw. Every save/share path now runs through a single_sanitiseFilenamehelper.- Markdown inline HTML pre-sanitised — when
HyperViewer.markdown(sanitize: true)(default) is used withenableInlineHtml: true(default), raw<script>/<style>/<iframe>blocks are now stripped viaHtmlSanitizerbefore 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 fixed —
RenderHyperBox.performLayoutand_computeHeightForWidthclamp_maxWidthto a finite fallback when the constraint isdouble.infinity(Row without Expanded, horizontalSingleChildScrollView). Before this,_FlexFragment.layoutpropagated infinity into aBoxConstraints(minWidth: ∞)and tripped Flutter'sminWidth < double.infinityassertion. text-overflow: ellipsisno longer leaks hidden text via copy —Fragment.ellipsisVisibleLengthtrack...
v1.3.1 — Decouple plugins, CSS list-style & background, selection performance
⚠️ 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/MathMLIf 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,nonelist-style-position:inside/outsidelist-styleshorthandbackground-repeat:repeat,repeat-x,repeat-y,no-repeat,space,roundbackground-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
HyperTeardropHandlePainterdeduplicated — exported fromhyper_render_core
Bug Fixes
- Edge-to-edge images:
width: 100%now truly fills container (no internal margin) - Android build: no longer requires
compileSdk = 35workaround 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
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.
MathNodePluginhandles both<math>and<latex>tags via theHyperNodePluginAPI- 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
srcattribute 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 #8 — customCss 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.dartnow returns aBlockNodewith a solid bottom border instead ofLineBreakNodeAtomicNode.svg()wrong constructor params: Fixed named parameter mismatch inhtml_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.dartmoved into their respective packagetest/directories hyper_render_highlightexample: Corrected class reference toDefaultCodeHighlighterpublish.sh/prepare_publish.sh: Updated for v1.3.0 and now includehyper_render_mathin 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 # newNo breaking changes. Drop-in upgrade from v1.2.x.
v1.2.2 — 8 Bug Fixes + Android Build Fix
What's Changed
🐛 Bug Fixes
- Android build failure (
example/android/build.gradle.kts):irondash_engine_context 0.5.5compiled against android-31 conflicts withandroidx.fragment:1.7.1(minCompileSdk=34). Addedsubprojects { 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. HyperRenderConfigidentity-compare (hyper_render_config.dart): Addedoperator==/hashCode— prevents unnecessary re-layouts on every frame when_effectiveConfigmerges@keyframesinto a new object.selectabletoggle ignored (hyper_viewer.dart):didUpdateWidgetnow creates/disposesVirtualizedSelectionControllercorrectly whenselectablechanges.- Deep-link taps silently blocked (
hyper_viewer.dart):_safeOnLinkTapnow checks bothallowedCustomSchemesANDrenderConfig.extraLinkSchemes. - CSS change bypassed section cache (
hyper_viewer.dart):_sectionHashesnow reset indidUpdateWidgetwhencustomCsschanges. - Markdown/Delta rendered as single section (
hyper_viewer.dart): Added_splitIntoSections()— large Markdown/Delta docs now chunk correctly in virtualized/paged mode. renderConfigchange partially detected (hyper_viewer.dart):didUpdateWidgetnow uses full value equality instead of only comparingvirtualizationChunkSize.- CSS float class names not detected (
html_adapter.dart):_containsFloatChildnow recognises Bootstrap/Tailwind patterns (float-left,pull-right,alignleft, etc.).
🔧 CI Fixes
benchmark.yml: Fixed JSSyntaxErrorfrom backtick-quoted fixture names inside template literal — useprocess.envinstead of direct interpolation.benchmark.yml+golden.yml: Addedpull-requests: writepermission for comment-posting steps.test.yml: Guard sub-package test steps with[ -d test ]check —hyper_render_markdown,hyper_render_highlight,hyper_render_clipboardhave no test directories.
Full Changelog: https://github.com/brewkits/hyper_render/blob/main/CHANGELOG.md
v1.2.0 — Plugin API, Paged Mode, Incremental Layout
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="…">→ discreteSemanticsNodeat image rect (WCAG 1.1.1)aria-labelon<a>elements honored (WCAG 4.1.2)
🏗️ Refactor
- Removed 31 duplicate files from root
lib/src/— canonical source now inhyper_render_core LazyImageQueuesingleton consolidated (single shared instance)
🐛 Bug Fixes
- XSS:
javascript:URLs blocked in HTML, Markdown, and Delta adapters display:noneelements 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.0v1.1.2 — Binary Search Selection · DevTools v1.0.0 · 3-Pipeline CI
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.2No breaking changes. All previous HyperViewer APIs remain identical.
Full details in CHANGELOG.md.
v1.1.0
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-readyFilterQuality.mediumon 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
TapGestureRecognizerleak when document is replaced - Fix
_fragmentChildMapO(N) scan → O(1) lookup in paint cycle - Fix
_nodeRectCacheO(N²) accessibility rect computation → O(N) - Fix
display:nonenot respected in_tokenizeNodeand_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 galleryvector_mathupdated 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.0Quick start
import 'package:hyper_render/hyper_render.dart';
HyperViewer(
html: articleHtml,
onLinkTap: (url) => launchUrl(Uri.parse(url)),
)HyperRender v1.0.0
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 sanitization —
javascript:,vbscript:, SVG data URIs,expression()blocked by default - Virtualized rendering —
ListView.buildermode for large documents - Screenshot export —
GlobalKey.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.