hyper_render 1.0.0
hyper_render: ^1.0.0 copied to clipboard
Flutter rendering engine for HTML, Markdown, and Quill Delta. Single RenderObject — no widget tree. Supports CSS Float, Flexbox, Grid, Ruby/Furigana, and text selection.
Changelog #
All notable changes to HyperRender are documented in this file.
Format follows Keep a Changelog. Versioning follows Semantic Versioning.
1.0.0 - 2026-03-01 #
First stable release. Core features, plugin architecture, and cross-platform support are production-ready.
Pre-publish fixes (2026-03-01) #
docs/→doc/— renamed documentation directory to follow pub.dev layout conventionHyperViewer— removedimplementation_importslint violations; addedonErrorcallback; parsing is now async viaFuture.microtask(non-blocking UI);HyperRenderMode.virtualizednow renders each top-level block as an independentHyperRenderWidgetinsideListView.builder;enableZoomnow correctly wraps content inInteractiveViewer;_isComplexHtmlcached instead of recomputed on everybuild()HyperRenderWidget— removed null-guard aroundchildWidget; removed unused importDefaultCssParser— fixed redundant?.null-check on non-nullableString.trim()- Sub-packages — SDK constraint
>=3.5.0 <4.0.0;vector_math: ^2.2.0;share_plus: ^10.0.0;topicsadded to all sub-package pubspecs
Bug Fixes #
-
CssRuleIndexancestor-class misindex (CRITICAL) —_analyzeSelector()previously checked for.and#before checking for combinator characters, so a rule likenav.main-nav lior.sidebar .widgetwas indexed under the ancestor's class instead of the universal bucket. Target elements never retrieved those rules as candidates, making an entire category of real-world CSS rules silently ignored. Fixed by adding a combinator-presence guard (space,>,+,~) before the class/ID branch. -
Image layout division-by-zero (MAJOR) — When a
ui.Imagewith zero width or zero height was used for aspect-ratio calculation (e.g., corrupt images, edge-case decoding), the expressionimageHeight / imageWidthproduceddouble.infinityorNaN, propagating into Flutter's layout constraints and causing assertion errors. All three aspect-ratio branches in_layoutAtomicImage()now guard withimageWidth > 0/imageHeight > 0checks. -
Ruby
<rt>text null dereference (MINOR) —HtmlAdapter._buildRubyNode()accessedchild.textwithout?? ''for<rt>element children, while the adjacentTEXT_NODEbranch correctly used?? ''. Fixed by adding?? ''. -
HtmlSanitizerstrips ARIA/accessibility attributes (MEDIUM) —aria-label,aria-hidden,aria-expanded,role, and allaria-*attributes were silently removed by the default sanitizer allowlist, making the documented ARIA/accessibility features non-functional whensanitize: true(the default). Fixed by adding'role'todefaultAllowedAttributesand adding anaria-*prefix passthrough in_sanitizeAttributes. -
@importblocked in CSS —@importdirectives are now stripped from<style>tag CSS (viaHtmlAdapter.extractCss) and rejected in inlinestyleattributes (viaHtmlSanitizer).HtmlSanitizer.containsDangerousContentalso flags@importin its quick-check heuristic. -
!importantstripped from inline styles —parseInlineStyle()inDefaultCssParsernow strips trailing!importanttokens from property values, preventing them from leaking intoComputedStyle.
Security #
customCsssecurity note — dartdoc forHyperViewer.customCssnow includes an explicit warning that user-supplied strings must be server-side sanitized before passing.
New Features #
onImageTapcallback —HyperViewerandHyperRenderWidgetnow acceptonImageTap: (url) {}to handle image tap events.allowedAttributesparameter —HyperViewernow exposesallowedAttributes(all 3 constructors) to override the sanitizer's default attribute allowlist.HyperRenderConfig— new static-field class for configuring global rendering defaults before app startup:HyperRenderConfig.imageCacheMaxMb(default: 50 MB)HyperRenderConfig.textPainterCacheMaxEntries(default: 5000)HyperRenderConfig.configure({imageCacheMaxMb, textPainterCacheMaxEntries})
hyper_render_clipboardre-enabled — the clipboard sub-package is now included inpubspec.yamland exported fromlib/hyper_render.dart.
Rendering Engine #
- Custom
RenderObject-based layout engine (RenderHyperBox) — no widget tree overhead per text node - Unified Document Tree (UDT) model:
BlockNode,InlineNode,AtomicNode,TextNode,RubyNode,TableNode - Full CSS cascade: UA defaults → author
<style>rules → inlinestyle=""→ inheritance - CSS specificity calculated correctly (ID > class > element); stable sort by source index for equal specificity
CssRuleIndex: O(1) HashMap-backed CSS rule lookup — 3–16µs median regardless of stylesheet size (measured at 100–5,000 rules)
Layout #
- Block and inline layout with correct margin collapsing
- Flexbox (
display: flex) with direction, wrap, align-items, justify-content, gap - CSS Grid (
display: grid) with template columns/rows and span support - Float layout (
float: left/right) with full text reflow — wrapping text around floated images works as expected - Table layout with content-based column widths, colspan/rowspan, nested tables
display: nonefully respected
CSS Properties #
- Typography:
font-size(px, em, rem),font-weight,font-style,font-family,line-height,letter-spacing,word-spacing,text-align,text-decoration,text-transform,white-space,vertical-align - Box model:
width/height,min-/max-variants,margin,padding,border,border-radius - Color: named colors (full CSS4 palette of 147 names),
#RGB,#RRGGBB,#RGBA,#RRGGBBAA,rgb(),rgba(),hsl(),hsla() - Backgrounds:
background-color,background-image(basic) - Layout:
display,float,clear,overflow,position: static/relative,opacity - Flexbox:
flex-direction,flex-wrap,justify-content,align-items,align-content,align-self,flex-grow,flex-shrink,flex-basis,order,gap - Grid:
grid-template-columns,grid-template-rows,grid-auto-flow,grid-column,grid-row - CSS custom properties (
--var-name) andvar()resolution with nested references calc()expressions with px/em/rem arithmetic, including negative numberstransitionandanimationparsed intoComputedStyle(visual playback viaHyperAnimatedWidget)transformviaMatrix4(translation, rotation, scale)- Inline style tokenizer is bracket-aware —
calc(50% - 8px)insidestyle=""parses correctly
HTML Tags #
- Text:
<p>,<div>,<span>,<br>,<hr> - Headings:
<h1>–<h6> - Formatting:
<strong>/<b>,<em>/<i>,<u>,<s>/<del>,<mark>,<code>,<pre>,<sub>,<sup>,<abbr>,<cite>,<q>,<kbd>,<samp> - Lists:
<ul>,<ol>,<li>with 9 list-style-type variants (disc, circle, square, decimal, lower/upper-roman, lower/upper-alpha, none) - Tables:
<table>,<thead>,<tbody>,<tfoot>,<tr>,<th>,<td>with full colspan/rowspan - Links:
<a>withhref,target,aria-label - Media:
<img>(network + asset),<video>/<audio>(styled placeholder viaDefaultMediaWidget) - Semantic:
<article>,<section>,<aside>,<nav>,<main>,<header>,<footer>,<blockquote>,<figure>,<figcaption> - Interactive:
<details>/<summary>(tap to expand/collapse) - CJK:
<ruby>/<rt>(furigana rendering above base text), kinsoku shori line-breaking rules - Metadata:
<style>(full CSS injection),<link>(external stylesheet — href captured, not fetched) - SVG:
<svg>inline (placeholder; full renderer in progress) - Unsupported:
<canvas>,<form>,<input>,<select>,<iframe>— usewidgetBuilderto inject native Flutter widgets for these
Content Formats #
- HTML5 (default)
- CommonMark Markdown with GFM extensions (tables, strikethrough, task lists) via
HyperViewer.markdown() - Quill Delta JSON via
HyperViewer.delta()
Text Selection #
- Cross-fragment text selection rendered as a single continuous highlight
- Selection drag handles on mobile
- Context menu (Copy, Select All) — customisable via
selectionMenuActionsBuilder getSelectedText(),selectAll(),clearSelection()onRenderHyperBoxonSelectionChangedcallback for external state updates
Security #
HtmlSanitizerwith allowlist-based tag and attribute filtering — enabled by default for HTML contentjavascript:,vbscript:,data:text,data:applicationURL schemes blocked inhref/srcattributes- CSS
expression()stripped from inline styles - Null-byte injection in tag names blocked (strips
\x00before parsing) allowedTagsparameter to extend the default allowlistsanitize: falseopt-out for trusted, backend-controlled HTML
Accessibility #
Semanticswrapper onHyperViewerwith configurablesemanticLabel(default:'Article content')excludeSemantics: truefor decorative content- Per-element semantic nodes:
isHeaderfor<h1>–<h6>,isLinkfor<a>,isImagefor<img>,isButtonfor<button> - List ordinal hints: "Item 2 of 5" for
<li>within<ul>/<ol> - ARIA attribute support:
aria-labeloverrides semantic label on any element;rolemaps to Flutter semantic flags (button, heading, region) - Landmark elements (
<nav>,<main>,<header>,<footer>) emit labeledSemanticsNodeentries <pre>announced as "Code block: [content]" for screen readers- Text direction:
dir="rtl"/dir="ltr"per-element, inherited
Performance #
- Image LRU cache:
LinkedHashMap-backed with 50 MB byte-budget; oldest loadedui.Imageobjects are evicted and disposed when limit is exceeded - Viewport culling: off-screen fragments skipped during paint (disabled in test/golden environment where no clip layer is present)
- Selection highlight uses
computeLineMetrics()ascent+descent for tight glyph bounds — avoids CSS line-height inflating the highlight rect HyperRenderMode.auto: sync rendering below ~30 KB; switches to virtualizedListViewfor larger documents
Measured on macOS (Darwin 25.2.0, Flutter Desktop, release mode):
| Document size | Median parse time |
|---|---|
| 1 KB | 27 ms |
| 10 KB | 69 ms |
| 50 KB | 276 ms |
| 100 KB | 575 ms |
CSS lookup: 3–16 µs median (100–5,000 rules). Source: benchmark/RESULTS.md.
Plugin Interfaces #
ContentParser— custom content format (implementparseWithOptions)CodeHighlighter— syntax highlighting (returnInlineSpantree)CssParserInterface— custom CSS parsingImageClipboardHandler— copy/save image data from<img>nodesHyperWidgetBuilder = Widget? Function(UDTNode node)— replace any atomic node with a native Flutter widget; returnnullto use default rendering
Developer Tools #
captureKey: GlobalKeyonHyperViewer+HyperCaptureExtensionto export rendered content as PNGdebugShowHyperRenderBounds: truedraws debug outlines around each render boxpackages/hyper_render_devtools/— Flutter DevTools extension for inspecting the UDT and fragment layoutHtmlHeuristics.isComplex()— detects documents withposition:fixed,<canvas>,z-index, etc.; use withfallbackBuilderto show a WebView instead
Packages #
hyper_render— top-level package, re-exports everything; depends onhyper_render_core,hyper_render_html,hyper_render_markdownhyper_render_core— rendering engine with no external dependencies (except Flutter SDK)hyper_render_html— HTML parser, CSS parser, HTML sanitizerhyper_render_markdown— CommonMark + GFM parser (wrapsmarkdownpackage)hyper_render_clipboard— image copy/save viasuper_clipboard
Tests #
164 unit and integration tests passing (style, layout, accessibility, security, performance, widget capture).