hyper_render 1.3.2
hyper_render: ^1.3.2 copied to clipboard
Render HTML/Markdown/Delta at 60 FPS. The only Flutter renderer with CSS float layout, crash-free text selection, and CJK Ruby typography. Drop-in flutter_html alternative.
Changelog #
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.ellipsisVisibleLengthtracks how many leading characters survive each truncation pass;getSelectedTextclamps the visible range against it and skips fully-suppressed fragments. State is reset at the top of every_performLineLayoutso a wider re-layout un-hides previously truncated text.- Selection-drag hit-test made lenient —
_lineIndexAtaccepts aclampOutOfBoundsflag (truefor drag,falsefor tap). When a selection handle drags past the first/last line by a pixel, the index now snaps to the nearest line instead of returning-1and freezing. - Dead-code removal —
_characterToFragment/_fragmentRangesfields inRenderHyperBoxwere populated each layout but never read; deleted along with theirclear()and populate loops. - Table cell block-content fallback — when
cellContentBuilderisnulland a cell contains<div>/<p>children,_buildCellContentnow renders the inline run plus each block child via a defaultColumn/Textfallback instead of dropping the content. (Previously onlyHyperRenderWidgetcallers were safe.) - Table grid total-cell cap — added
_kMaxTotalCells = 100 000. A pathological<table>whoserowCount × columnCountexceeds the cap now renders a visible "Table too large to render" placeholder instead of allocating an 8 MBnullgrid on the UI thread. HyperAnimatedWidgetcontroller lifecycle hardened — switched fromSingleTickerProviderStateMixintoTickerProviderStateMixin(the previous mixin asserted on the secondcreateTicker()whendidUpdateWidgetrecreated the controller after a prop change). Start delay now uses a retainedTimerthat is cancelled ondidUpdateWidget/dispose, eliminating duplicateforward()calls in fast-rebuild scenarios (live editor typing).
Performance
HtmlAdapter.extractCssregex fast-path — for inputs ≥ 32 KB or with no<styletag at all (the common Markdown/Delta case),extractCssnow skips the full html5lib parse on the UI thread and uses a focused regex. Saves 50–300 ms on a 200 KB document on a mid-range Android.
Cross-package Polish
MarkdownContentParserrenamed toDefaultMarkdownParser— aligns withDefaultHtmlParser/DefaultCssParser. The old name remains as a@Deprecatedtypedef so existing callers compile; new code should use the new name.hyper_render_devtoolsnow has tests +dev_dependenciesblock —UdtSerializerround-trip + truncation cap +register()idempotency. Previously the package shipped zero tests.hyper_render_mathpubspec description normalised — replaced the YAML folded-scalar (>) form with a plain string for consistency with the other six packages.pubspec_publish_ready.yamlandscripts/prepare_publish.shversion sync — both now pin^1.3.2, eliminating the previous 1.3.1/1.3.2 mismatch that would have faileddart pub publish --dry-run.
Tests
71 new tests added across 11 files covering every fix above: URL safety scheme blocklist (core), HTML adapter URL gate, CSS parser edge cases, markdown GFM (tables/task-lists/autolinks/code-fence/heading), highlight edge cases (malformed source, 5 KB load, every theme), clipboard filename sanitisation, UDT serializer shape + truncation, animation controller race / dispose, table cell fallback + total-cell cap, extractCss perf, ellipsis copy + selection clamp regressions. Full suite: 1764 passing, 0 failing.
1.3.1 - 2026-05-14 #
⚠️ Migration from 1.3.0 #
hyper_render_clipboard and hyper_render_math are no longer transitive dependencies of hyper_render. If you use either, add them explicitly:
dependencies:
hyper_render: ^1.3.1
hyper_render_clipboard: ^1.3.1 # only if you use SuperClipboardHandler
hyper_render_math: ^1.3.1 # only if you use MathNodePlugin / LatexNodePlugin
✨ New CSS Properties #
list-style-type: All 11 marker types —disc,circle,square,decimal,decimal-leading-zero,lower-alpha,upper-alpha,lower-latin,upper-latin,lower-roman,upper-roman,nonelist-style-position:inside/outsidelist-styleshorthand: parses type and position in any orderbackground-repeat:repeat,repeat-x,repeat-y,no-repeat,space,roundbackground-position: keyword (center,top left, etc.) and percentage values
🚀 Performance #
- Selection rects cached:
getSelectionRects()now called once per drag event (was 3×) — stored in_selectionRectsfield, eliminating redundant layout walks during selection drag - Auto-scroll proportional speed:
_autoScrollIfNearEdgescales 0–20 px/frame based on finger proximity to edge (was fixed 15 px/frame) HyperTeardropHandlePainterdeduplicated: renamed, made public, and exported fromhyper_render_core; duplicate implementation in the virtualized overlay removed
🐛 Bug Fixes #
- Edge-to-edge images:
width: 100%images now truly fill their container — no internal margin offset
🏗️ Build Fixes #
- Decoupled native dependencies:
hyper_render_clipboardandhyper_render_mathremoved from roothyper_renderdefault dependencies — eliminates thecompileSdk = 34Gradle requirement for basic usage - Removed outdated
compileSdkworkaround from example app's Android Gradle config
1.3.0 - 2026-05-03 #
✨ New Features #
- New Plugin Package:
hyper_render_math(packages/hyper_render_math): Added first-party support for mathematical formulas via LaTeX/MathML. It uses a customHyperNodePluginto render math content using theflutter_math_forkpackage. This milestone release consolidates all recent architectural improvements and bug fixes into a stable minor version.
🚀 Performance & Stability #
- Test Coverage Optimization: Increased global test coverage to >85% with new comprehensive suites for parsers, adapters, and selection logic.
- Golden Test Alignment: Updated golden tests for consistent multi-platform rendering validation.
- Improved Widget Test Robustness: Updated
find.byType(HyperRenderWidget)assertions to handle multiple instances in the tree caused by virtualization and float nesting. Paint()memory optimization: Replaced inlinePaint()allocations in hot paint paths with reusable fields, reducing GC pressure during smooth scrolling.- Incremental layout hash collision fix: Improved the fingerprinting of document sections to prevent cache collisions on duplicate content.
🐛 Bug Fixes #
- Markdown CRLF normalisation: Content is now normalised to LF before splitting, fixing stray carriage-return characters in code blocks on Windows.
- Virtualized Heading Protection: Added guards to prevent virtualized sections from orphaning headings (Heading Widow/Orphan protection).
- Config & Scheme Propagation: Fixed issues where
useMicrotaskParsingandallowedCustomSchemeswere dropped during CSS-driven config rebuilds. - Float Layout precision: Explicit CSS
widthandheightare now strictly respected for non-image float elements. - Selection logic refinement: Fixed edge cases for text selection across off-screen chunks in virtualized lists.
- Android & iOS Build compatibility: Modernized Gradle configuration and iOS project settings for better ARM64 simulator and modern SDK support.
- SVG Sanitization: Added an atomic SVG sanitization path to preserve structural elements while stripping dangerous attributes.
- Plugin Propagation: Ensured
pluginRegistryis correctly passed to nested renderers inside floated containers.
1.2.2 - 2026-04-02 #
🐛 Bug Fixes #
- Android build failure with modern compileSdk (
example/android/build.gradle.kts):irondash_engine_context 0.5.5was compiled against android-31 but its transitiveandroidx.fragment:1.7.1dependency hasminCompileSdk=34, causing AGP 8'scheckAarMetadatato block the build. Added asubprojects { afterEvaluate { compileSdk = 35 } }override in the example's root Gradle file. README now documents the same one-line workaround for app-level projects. (#5) - SVG invisible with
sanitize: true(html_sanitizer.dart):<svg>was not indefaultAllowedTagsso the sanitizer unwrapped it, destroying the SVG structure. Added an atomic SVG sanitization path that strips<script>and dangerous attributes while preserving all structural SVG elements (path,circle,g,use, etc.). selectabletoggle ignored after build (hyper_viewer.dart): Togglingselectablefromfalse→truenever createdVirtualizedSelectionController, andtrue→falsenever disposed it. Fixed indidUpdateWidget.- Deep-link tap silently blocked (
hyper_viewer.dart):_safeOnLinkTaponly checkedwidget.allowedCustomSchemesbut ignoredrenderConfig.extraLinkSchemes, causing deep-links registered viaHyperRenderConfigto be silently dropped. Both sources are now consulted. - CSS change didn't invalidate section cache (
hyper_viewer.dart):_hashSectionhashes only text content, so acustomCsschange that alters layout/appearance would incorrectly reuse cached sections._sectionHashesis now reset whenevercustomCsschanges indidUpdateWidget. - Markdown/Delta virtualized/paged mode rendered as single section (
hyper_viewer.dart): The sync fallback path wrapped the entire parsed document as one section, defeating virtualization. Added_splitIntoSections()to chunk Markdown/Delta documents at block boundaries, matching the HTML isolate path. renderConfigchange only partially detected (hyper_viewer.dart):didUpdateWidgetcompared onlyvirtualizationChunkSizeinstead of the fullHyperRenderConfig. Now uses full value equality (available since theoperator==fix) so any config change triggers a re-parse.- CSS float class names not detected (
html_adapter.dart):_containsFloatChildmissed Bootstrap/Tailwind float class names (float-left,pull-right,alignleft, etc.), causing premature section splits after float-containing blocks. Common class patterns are now detected heuristically.
1.2.1 - 2026-03-31 #
🏗️ Maintenance #
- Pub.dev compliance: Fixed internal dependency constraints to use version ranges instead of path dependencies in the published package.
- Virtualized screenshot description: Refined screenshot metadata in
pubspec.yamlfor better display on pub.dev. - Metadata cleanup: Removed stale comments and aligned topics for better discovery.
1.2.0 - 2026-03-30 #
✨ New Features #
-
Multi-tier Plugin API (
hyper_render_core): Third-party packages can now render arbitrary HTML tags as custom Flutter widgets viaHyperNodePlugin/HyperPluginRegistry.- Block tier (
isInline == false, default): widget takes full available width with CSS margins. - Inline tier (
isInline == true): widget flows inside text lines; intrinsic size measured inperformLayoutviagetMaxIntrinsicWidth / getMinIntrinsicHeight. - Register at startup:
HyperPluginRegistry()..register(MyPlugin())and pass toHyperViewer(pluginRegistry: ...).
- Block tier (
-
Dirty-flag incremental layout (
hyper_viewer.dart): Only re-layout sections whose content changed. EachDocumentNodechunk is fingerprinted withObject.hashAllover childtextContent; unchanged sections are reused on the next parse, andValueKey(hash)onRepaintBoundarylets Flutter skip re-layout and repaint entirely. ~90 0x0p+0yout rebuild reduction for live-updating feeds. -
Paged mode (
HyperRenderMode.paged):PageView.builder-based rendering, one document chunk per page. Suitable for e-book / epub / reader UIs.- Supply a
HyperPageControllerfor programmatic navigation (animateToPage,nextPage,previousPage,jumpToPage) andValueNotifier<int> currentPagefor reactive page indicators.
- Supply a
♿ Accessibility (WCAG 2.1 AA) #
- Image alt-text semantic nodes (
render_hyper_box_accessibility.dart):<img alt="…">elements now produce a discreteSemanticsNodeat the image's layout rect. Screen-reader users can navigate to images element-by-element (WCAG 1.1.1 Non-text Content). Previously alt text only appeared in the flat document-level label. aria-labelon links honored (render_hyper_box_accessibility.dart): If an<a>element carries anaria-labelattribute, that value is used as the link's semantic label instead of its text content (WCAG 4.1.2 Name, Role, Value).
🏗️ Refactor — Dead-code elimination #
- Removed 31 duplicate files from root
lib/src/that were identical or outdated copies of the canonical implementations inpackages/hyper_render_core. Rootlib/src/now contains only the 17 files that are genuinely unique to the root package (parsers, sanitizer,HyperViewer, virtualized selection,capture_extension). LazyImageQueuesingleton deduplication:lib/src/core/lazy_image_queue.dartwas a separate implementation that created a secondLazyImageQueue.instance— meaningLazyImageQueue.instance.cancel()called from outsideHyperViewerhit a different singleton than the oneHyperViewerused internally. Root now re-exportsLazyImageQueuedirectly fromhyper_render_core(single shared instance).- Added missing v1.2.0 symbols to root re-export:
HyperRenderConfig,LazyImageQueue,HyperNodePlugin,HyperPluginRegistry,HyperPluginBuildContext,LoadingSkeleton,HyperErrorWidget,FloatCarryoverare now all accessible frompackage:hyper_render. - Consolidated double export: The redundant second
export 'package:hyper_render_core' show HyperRenderConfig'line was folded into the main re-export block.
🐛 Bug Fixes #
-
Copy action produced empty clipboard (
virtualized_selection_overlay.dart,hyper_selection_overlay.dart): TheListener.onPointerDowncallback cleared the active selection before the Copy button'sonPressedcould fire, soClipboard.setDatareceived an empty string. Fixed by guardingclearSelection()behind a_showMenu/_showContextMenucheck (matching the pattern already used in the non-virtualized overlay). -
Context menu outside hit-testable bounds (
hyper_selection_overlay.dart,virtualized_selection_overlay.dart): When a selection was near the top of the widget the computedtopfor thePositionedmenu went negative.Stack(clipBehavior: Clip.none)allows visual overflow but Flutter hit-testing is still bounded by the parent — the Copy button was unreachable. Fixed by clamping the top offset:.clamp(0.0, double.infinity). -
Scroll vs. text-selection conflict (
render_hyper_box.dart):handleEvent(PointerMoveEvent)bypassed the gesture arena and fired on every pointer move, creating accidental selections during scrolling. Removed raw-event selection tracking and moved selection initiation to aLongPressGestureRecognizerat the widget layer — this correctly competes with the parent scroll view'sVerticalDragGestureRecognizer, so a quick swipe scrolls while a 500 ms hold begins a text selection (matching iOS/Android native behaviour). -
Virtualized copy menu never appeared (
virtualized_selection_overlay.dart): Per-chunkRenderHyperBox._selectionwas set by the old pointer-event tracking, butVirtualizedSelectionController(cross-chunk selection) was never populated, sohasSelectionremainedfalseand the menu was never shown. Fixed by routing the long-press start throughVirtualizedSelectionController.startSelection(). -
Selection Escape key fix (
hyper_selection_overlay.dart):Escapekey failed to clear selection because the internalFocusNodewasn't reliably focused after selection was established. Fixed by calling_focusNode.requestFocus()insidestartSelectionAt. -
constlint fix (hyper_render_widget.dart):HyperPluginBuildContextinstantiation changed toconstto silenceprefer_const_constructors.
1.1.4 - 2026-03-28 #
🐛 Bug Fixes #
-
display:nonenot respected in renderer (render_hyper_box_layout.dart): Added early-return guard in_tokenizeNode— elements withdisplay:noneno longer produce any layout fragments and are correctly hidden. Previously, elements styled withdisplay:none(e.g. Wikipedia[edit]section links) were still rendered. -
<hr>rendered as line break (html_adapter.dart):<hr>now correctly returns a styledBlockNodewith a top border (borderColor: #CCCCCC, borderWidth: 1px), matching browser behavior. Previously it was incorrectly treated identically to<br>. -
Whitespace-only space nodes dropped between inline elements (
html_adapter.dart): Text nodes consisting only of horizontal spaces (e.g." "between<b>text</b> <i>more</i>) were being silently dropped by.trim().isEmpty, causing missing word-separating spaces. Fixed to only drop nodes that contain newlines (structural indentation whitespace), not pure-space nodes. -
TextPaintercache hash collision (render_hyper_box.dart): The_LruCache<int, TextPainter>key was computed withObject.hash()which can collide for large documents with many distinct text styles, leading to wrong text metrics and subtle layout glitches. Replaced with a new_TextPainterKeyclass using full value equality over all 9 style fields.
1.0.0 - 2026-03-01 #
First stable release. Core features, plugin architecture, and cross-platform support are production-ready.
