hyper_render_core 1.6.0
hyper_render_core: ^1.6.0 copied to clipboard
Core engine for HyperRender. Universal Document Tree, single-RenderObject layout with CSS float, Flexbox, Grid, CJK typography, and crash-free text selection.
Changelog — hyper_render_core #
1.6.0 #
⚠️ Behavior Change — text scaling (WCAG 1.4.4) #
RenderHyperBoxandRenderRubyTextnow accept aTextScalerand apply it to everyTextPainter(measurement + paint), so rendered text honours the device's accessibility text-scaling setting. Previously all text was measured atTextScaler.noScaling.HyperRenderWidgetgained an optionaltextScalerparam (null →MediaQuery.textScalerOf(context));_TextPainterKeynow includes the scaler so the process-global painter cache doesn't collide across scales. TheRenderHyperBox.textScalersetter routes through_invalidateLayout()(not a baremarkNeedsLayout()) so a scaler-only change actually re-measures rather than being skipped by the fragment-version fast-path. Existing content re-renders larger when the user's system font size is increased — passTextScaler.noScalingto opt out.
✨ New CSS Features #
text-alignexecutes on the canvas path:_positionFragmentsnow shifts each line by the free space according to the block's inheritedtext-align(LTR center/right). Because selection/hit-testing readfragment.offsetdirectly, they stay correct with no extra work. Previously text-align only reached widget-tierTextwidgets. RTL unchanged (right-packed).- RTL
text-alignoverride: a box-level RTL tree (textDirection: rtl) now honours an EXPLICITtext-align(left/center) instead of always right-packing; an unset RTL paragraph still right-packs (start). The explicit flag is read from the block ancestor via_lineTextAlignIsExplicit(the line's text fragment only inherited the value, so its own flag is false). text-align: justify: distributes free space across a line's internal word gaps via a per-fragmentFragment.justifyWordSpacing(folded into the effective style by_effectiveFragmentStyle, used by paint, selection and the position pass so glyphs, hit boxes and offsets agree). Skipped on the last line of a block and any line ending in<br>(CSS). The trailing wrap-space is excluded from the gap count and its width added back, so the final glyph reaches the edge exactly. Recomputed from scratch each layout pass (reset to 0) so it never compounds.text-indent: newComputedStyle.textIndent(+ resolver case + inheritance in_applyInheritance), applied to the first line of each block in_positionFragmentsvia a nearest-block-ancestor lookup.- Block width constraints —
width,max-width,min-width, incl.%: all resolved at block-start in_performLineLayoutvia the per-block padding stack (a single unified block that picks a target content width then inflates the right inset).min-widthwins overmax-widthper CSS. Percentages resolve against the containing block's content width (_maxWidth − parentLeftInset − parentRightInset), so nested%nests correctly. NewComputedStyle.widthPercent/maxWidthPercent/textIndentPercentfields (threaded through constructor/copyWith/inheritance;textIndentPercentinherits, width ones don't). The block-start-fragment emission guard now fires for ANY width constraint, not just absolutemax-width.%height remains unsupported. animation-play-state:running/pausedparsed (longhand +animationshorthand) and executed. NewHyperAnimationPlayStateenum,ComputedStyle.animationPlayStatefield, andHyperAnimatedWidget.pausedflag. Paused animations hold their current frame and resume from it; pausing before the initial delay restarts the delay countdown on resume.- Canvas-tier block animation:
RenderHyperBoxnow executesanimation-namefor content it paints directly on its ownCanvas(plain block-level paragraphs/divs), not just widget-tier content. Newrender_hyper_box_animation.dart:_BlockAnimationStatetracks per-node elapsed time (accumulated+epoch, frozen/resumed across pause instead of mirroringAnimationController), driven by aSchedulerBinding.scheduleFrameCallbackloop (same pattern as the existing image-loading shimmer — noTickerProvider) that self-terminates once no block is bothrunningand unfinished.paint()gained a_paintAnimatedBlockspass that composites each animated block's own decoration + owned text/ruby fragments inside a singlesaveLayerbefore the normal decoration/text passes (which skip anything already painted this way) — a single alpha layer avoids double-blending a block's background through its own text when animatingopacity. The layer'ssaveLayerbounds arenull(current clip), not the block's static rect — passing the pre-transform rect as bounds clipped a translated/scaled block to its original position wheneveropacityandtransformcombined (caught by a new golden test before release, not shipped broken). Rects/fragment groupings are precomputed once perperformLayout, not per paint frame.
🐛 Bug Fixes #
- Non-finite CSS values crashed layout:
_parseLength/_parseLengthWithContext/_parseFontSizeinresolver.dartused baredouble.tryParse, which returnsInfinityfor1e999andNaNfornan. These reached box constraints and threwdebugAssertDoesMeetConstraints. New_tryParseFinite/_scaleFinitehelpers (single choke point) reject non-finite values — including overflow after unit multiplication — so the declaration is ignored. display: nonenot executed on the canvas path:_tokenizeNodenow returns early forDisplayType.none(covers block/inline/text/atomic uniformly and stops recursion), and_TableGrid.fromTableNodefiltersdisplay:nonerows and cells from the grid. Previously hidden content was measured, painted and selectable.- First no-margin block lost its padding:
_handleBlockNode's block-start-fragment emission guard now also fires on non-zero padding, not just margin or a width constraint. Previously a first<div style="padding:…">(no default margin) had its padding dropped. Surfaced by the newtest/style/css_execution_guard_test.dart. <a>withouthrefstyled as a link: the UA stylesheet inresolver.dartapplied the anchor colour/underline to every<a>. It now skips anchors with no (or empty)href, matching browsers'a[href]targeting.- Animation iteration counter leaked across rebuilds:
_HyperAnimatedWidgetStatenow resets its iteration counter whenever the controller is rebuilt, so a swapped-in animation plays its fullanimation-iteration-countinstead of inheriting the previous animation's progress. line-heightin absolute units used the wrong reference font-size:resolver.dart'scase 'line-height'divided a px/em length byparentFontSizeunconditionally; when the same element also declared its ownfont-size, that's the wrong reference per CSS (should be the element's own resolved font-size). Now usesstyle.fontSizewhenfont-sizewas already processed earlier in the same declaration block, falling back toparentFontSizeonly when this element doesn't override font-size at all.<p> </p>collapsed to zero height:Fragment.isWhitespaceand the layout tokenizer's whitespace-collapsing regex both usedString.trim()/\s+, which — unlike CSS — also match U+00A0 ( ). A text run consisting only of a non-breaking space was misclassified as droppable/collapsible source whitespace. Newsrc/util/html_whitespace.dart(isCssWhitespaceOnly,cssWhitespaceRun) defines the CSS-precise whitespace set (space/tab/LF/CR/FF only) and is now used byFragment.isWhitespace,RenderHyperBox's_kWhitespaceSplitter, and bothHtmlAdapterimplementations (hyper_render_htmland the root package's).<ol start="N">was ignored:render_hyper_box_layout.dart's list-marker numbering always seeded the first<li>at 1. It now readsparentBlock.attributes['start']for the first item of each<ol>(defaulting to 1 on absent/malformed values), so ordered lists can start at an arbitrary ordinal. Sibling lists keep independent counters via the existing_listItemIndicesmap.
♻️ Internal #
curveFromHyperTiming,resolveHyperKeyframes,matrix4FromHyperKeyframe(animation_controller.dart) are now shared top-level helpers used by both the widget-tier and canvas-tier animation code, replacing three near-duplicate private implementations.- New
src/util/html_whitespace.dart, exported from the package barrel — same pattern as the existingUrlSafetyshared helper (single source of truth for a rule every adapter must apply identically, instead of each adapter/layout stage rolling its own whitespace check and risking drift).
1.5.0 - 2026-07-05 #
✨ New CSS Features #
cubic-bezier()/steps()timing functions: newHyperTimingFunction.cubicBezier/stepsenum values plusHyperTimingParams(HyperCubicBezierParams,HyperStepsParams) carried onHyperTransitionandComputedStyle.animationTimingParams.steps()/step-start/step-endrender through the newHyperStepsCurve;cubic-bezier()maps to Flutter'sCubic. Shorthand parsing is paren-aware so inner commas are preserved;xcontrol points are clamped to[0, 1].- Animatable
color/background-color:HyperKeyframegainedcolor/backgroundColor(interpolated viaColor.lerp).HyperAnimatedWidgetapplies them withDefaultTextStyle.merge+ColoredBox;HyperTransitionWidgetanimates them withAnimatedDefaultTextStyle+AnimatedContainer.StyleResolver.parseCssColoris now a public static so adapters reuse the same color grammar.
🩺 Diagnostics #
HyperMemoryMetrics/HyperMemoryDebug: debug-only snapshot of what each memory-pressure cycle released.LazyImageQueue.pendingCountadded.
🐛 Bug Fixes #
- Zero-width
BorderSide+border-radiusassertion (issue #12): newcssBorderFromStylehelper maps0pxsides toBorderSide.nonein bothhyper_render_widget.dartandflex_container_widget.dart; the flex-container path also now honoursborder-style: none. StyleResolver.parseCssColorhardened: returnsnull(noFormatException) on malformed hex or out-of-int64rgb()/rgba()values — required now that color parsing runs on arbitrary@keyframesvalues.
1.4.0 - 2026-06-24 #
✨ New CSS Features #
aspect-ratio:W/Hand bare-number syntax parsed; applied to<img>/<video>sizing across all width-only/height-only/neither-specified layout branches.transitionexecution: newHyperTransitionWidgetanimatesopacity/transformacross style changes using the declared duration and timing function; wired intoHyperRenderWidget._maybeAnimate.animation-iteration-count: infinite: now loops viaAnimationController.repeat(); addedalternateflag soanimation-direction: alternate/alternate-reverseis handled distinctly fromreverse.- Float Carryover paint completion:
imagePixelOffsetis now consumed in_paintFloatImages— a tall floated image overhanging a virtualized section boundary continues painting from the correct offset in the next chunk. - ~25 new resolver cases for previously-silent properties:
white-space,word-spacing,text-transform,text-decoration-color,min/max-width/height,overflow*,border-top/right/bottom/color/width,animationshorthand + sub-properties,transition,aspect-ratio.
🐛 Bug Fixes #
remunits now parsed correctly in_parseLength(previously misparsed via theembranch).text-decorationno longer incorrectly inherited (not inheritable per CSS spec);text-transforminheritance added instead.- Linear-gradient diagonal corner directions (
to top right, etc.) now set bothbeginandendcorrectly. filternow composes all entries in a chain, not just the first two.border: nonenow zeroes width instead of leaving the 1px default.- Division-by-zero guard added to unitless
line-heightresolution. - Float layout no longer allocates a spread list (
[...left, ...right]) per line. HyperTextSelectionnow implementsoperator==, eliminating redundant repaints on unchanged selections.setGlobalTextCacheSizenow disposes the previous cache instead of leakingTextPainters.
1.3.4 - 2026-06-04 #
🔧 Fixes #
- Static Analysis Compliance: Suppressed deprecated
SizeTransition.axisAlignmentlints with// ignore: deprecated_member_useto maintain backwards compatibility with older Flutter SDKs (>=3.10) while securing 160/160 points on pub.dev.
1.3.3 - 2026-06-04 #
✨ New CSS & Layout #
object-fitsupport: Addedobject-fitproperty parsing (cover,contain,fill,none,scale-down) to control image resizing within its block container.- Float carryover
imagePixelOffset: EnhancedFloatCarryoverto carryimagePixelOffsetacross sections to allow precise partial painting of tall floated images.
✨ New APIs & Configs #
onMemoryPressurecallback: AddedonMemoryPressureparameter to widgets to allow host applications to coordinate resource disposal with HyperRender's cache invalidation.imageConcurrency: ConfiguredimageConcurrencysetting inHyperRenderConfigand wired it intoLazyImageQueue.
🐛 Bug Fixes & Refinement #
- TextPainter Cache: Replaced the global
TextPaintercache with a reference-counted multi-viewer safe cache. - Color Parsing: Fixed rgb/rgba parsing bugs by properly mapping
csslibfunction parameters.
1.3.2 - 2026-05-19 #
🔒 Security #
UrlSafety.isSafeadded (lib/src/util/url_safety.dart) — canonical scheme blocklist (javascript:,vbscript:,data:image/svg, non-imagedata:,file:,mhtml:,about:) with control-character smuggling defence. The rootHtmlSanitizer.isSafeUrland the markdown sub-package's URL gate now both delegate here so no scheme can drift between adapters.HyperViewer.markdown(sanitize:true)pre-sanitises markdown content viaHtmlSanitizerso raw<script>/<style>/<iframe>blocks can no longer surviveenableInlineHtml.
🐛 Critical Layout Fix #
- Unbounded-width crash eliminated —
RenderHyperBox.performLayoutand_computeHeightForWidthnow clamp_maxWidthto_kUnboundedWidthFallback = 800.0when constraints aredouble.infinity(Row without Expanded, horizontalSingleChildScrollView, intrinsic queries from unbounded parents). Previously_FlexFragment.layoutpropagated infinity intoBoxConstraints(minWidth: ∞)and tripped Flutter'sminWidth < double.infinityassertion.
🐛 Selection & Ellipsis #
text-overflow: ellipsisno longer leaks hidden text via copy —Fragment.ellipsisVisibleLengthrecords 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 text that was previously truncated.- Selection drag is now lenient on edge overshoot —
_lineIndexAt(dy, clampOutOfBounds: true)is used during handle drag, so a finger that drifts past the first/last line by a pixel snaps to the nearest line instead of freezing. Tap hit-testing (_findFragmentAtPosition) keeps the strict semantics. - Dead
_characterToFragment/_fragmentRangesfields removed — they were populated in_buildCharacterMappingeach layout but never read. Layout micro-saving and one less GC pressure point.
🐛 Table #
- Cell BlockNode content no longer disappears — when
cellContentBuilderisnulland a cell contains<div>/<p>children,_buildCellContentnow renders the inline run plus each block child via a defaultColumn/Textfallback. Previously only callers that went throughHyperRenderWidget(which auto-supplies a builder) were safe. - Total-cell cap
_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.
🐛 Animations #
HyperAnimatedWidgetcontroller lifecycle hardened — switched fromSingleTickerProviderStateMixintoTickerProviderStateMixin; the previous mixin asserted on the secondcreateTicker()whendidUpdateWidgetrecreated the controller. The start delay now uses a retainedTimerthat is cancelled ondidUpdateWidget/dispose, eliminating duplicateforward()calls in fast-rebuild scenarios.
🧪 Tests #
- +27 tests added across
url_safety_test,animation_controller_race_test,table_review_fixes_test. Full sub-package suite green.
1.3.1 - 2026-05-14 #
✨ New CSS Properties #
list-style-type: All 11 values —disc,circle,square,decimal,decimal-leading-zero,lower-alpha,upper-alpha,lower-latin,upper-latin,lower-roman,upper-roman,nonelist-style-position:inside/outside(default)list-styleshorthand: parses<type> <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()called once per drag event (was 3×); stored in_selectionRectsfield — eliminates redundant layout walks during selection drag - Auto-scroll proportional speed:
_autoScrollIfNearEdgenow scales 0–20 px/frame based on finger distance from edge (was fixed 15 px/frame) HyperTeardropHandlePainterdeduplicated: renamed toHyperTeardropHandlePainter, made public, and exported from core; duplicate in the virtualized overlay deleted
🐛 Bug Fixes #
- Edge-to-edge images:
_kImageMarginset to0.0—width: 100%images now truly fill their container with no internal margin offset
1.3.0 - 2026-05-03 #
✨ New Features #
HyperNodePlugin/HyperPluginRegistry(src/interfaces/node_plugin.dart): Plugin API for custom widget rendering of arbitrary HTML tag names. Block tier (full-width, CSS margins) and inline tier (flows with text, intrinsic-measured) supported.- Plugin layout wiring (
render_hyper_box.dart,render_hyper_box_layout.dart):blockPluginTags/inlinePluginTagssets added toRenderHyperBoxwith layout-invalidating setters._tokenizeNodeintercepts plugin tags; Step 1.7_measureInlinePluginFragments()queries child intrinsic dimensions before line layout runs. - Plugin widget wiring (
hyper_render_widget.dart):pluginRegistryfield added;_collectAtomicChildrenchecks plugin registry first;createRenderObject/updateRenderObjectsync tag sets to the render object. - CSS: Box shadow, linear-gradient, advanced border styles (dashed/dotted)
- CSS: Full Flexbox support (direction, wrap, gap, align-self, grow/shrink/basis)
- CSS: CSS Variables
var(),transition,animation-*parsing - CSS:
computed_styleexpanded with 120+ additional properties - CSS Grid:
display: gridwithgrid-template-columns,span,gap - Style:
resolver.dartexpanded — specificity engine, cascade improvements - Widgets:
HyperRenderWidget— adaptive selection colors, theme-aware; newenableComplexFiltersflag to gatesaveLayercalls for backdrop-filter/filter effects - Widgets:
HyperSelectionOverlay— improved handle rendering with tight bounding boxes - Rendering:
render_hyper_box_layout.dart— float algorithm improvements; O(1)_fragmentChildMapchild lookup; O(1)_nodeRectCacheaccessibility rect lookup - Rendering:
render_hyper_box_paint.dart— retina-ready images, anti-aliasing - Performance:
_buildNodeRectCache()builds O(1) accessibility rects during layout (Step 8), depth-capped at 32 levels
♿ Accessibility (WCAG 2.1 AA) #
<img alt>→ discreteSemanticsNode: Images with non-emptyalttext now generate an individualSemanticsNodeat the image's layout rect — VoiceOver/TalkBack users can navigate to images element-by-element (WCAG 1.1.1)aria-labelhonored on<a>elements: Anchor elements witharia-labelnow use that attribute as the link's accessible label instead of accumulated text content (WCAG 4.1.2)
🐛 Bug Fixes #
HyperRenderWidgetcompilation error: Resolved a signature mismatch in recursive widget construction wherecodeHighlighterwas passed outside ofconfigandpluginRegistrywas missing- Float layout: Explicit CSS
widthandheightproperties are now correctly respected for non-image float elements - Plugin propagation:
pluginRegistryis correctly passed to nested renderers, allowing custom tags to work inside floated containers - Scroll vs. text-selection conflict: Removed
PointerMoveEventselection tracking fromhandleEvent— selection now initiated viaLongPressGestureRecognizerat the widget layer - Context menu outside hit-testable bounds:
Positioned(top: menuY - 56)clamped to0.0— Copy button is always reachable near the top of the widget display:nonenot respected: Guard in_tokenizeNode— elements withdisplay:noneproduce no layout fragments_TextPainterKeyhash collision: ReplacedObject.hash()int key with full value-equality struct — eliminates subtle layout glitches on large documents- Inline images not loaded after async parse:
documentsetter now calls_loadImages()when the render box is attached - Image loading spinner invisible:
frameBuilderno longer wraps theloadingBuilderplaceholder inAnimatedOpacity(opacity:0)—TweenAnimationBuilderfade-in applied on first decoded frame instead - Ruby selection — 5 bugs fixed:
FragmentType.rubywas silently skipped in every selection pipeline step, causing character offset desynchronisation for all content after a ruby fragment LineInfo.characterCount: now counts ruby base-text characters (was 0 for ruby fragments)details_widget.dart: Fixed undefinedDetailsNodeclass — field type changed toUDTNodewithattributes.containsKey('open')for HTML-spec-compliant initial state- Selection:
getSelectedText()now inserts\nat block element boundaries so copied text respects paragraph/list structure - Layout Bug 1:
characterOffsetno longer addstrimmedLeadingto second fragment — selection mapping was off by the number of trimmed leading spaces - Layout Bug 2:
_sameLinkContext()guard prevents merging text nodes from different<a>ancestors — fixes incorrect link tap targets - Layout Bug 3:
_layoutFloat()early-returns when_maxWidth.isInfinite— prevents crash in unconstrained layouts; usesgetMaxIntrinsicWidth/Heightinstead ofchild.layout()to eliminate double-layout - Layout Bug 4: Null/empty guard in
_measureFragmentsforfragment.text— no longer crashes on atomic/ruby fragments - Memory:
_disposeLinkRecognizers()called indocumentsetter — fixes recognizer leak when document is replaced - Nested decorations:
nodeToDecoratedchanged fromMap<UDTNode, UDTNode>toMap<UDTNode, List<UDTNode>>— inner spans no longer overwrite outer spans prefer_const_constructors:HyperPluginBuildContextconstruction changed toconst
🔬 Tests #
- +17 tests —
ruby_layout_test.dart:LineInfo.characterCountwith ruby, selection offset accumulation - +27 tests —
ruby_layout_test.dart: RubyNode model, Fragment.ruby lifecycle, document tree traversal - +30 tests —
float_layout_test.dart: HyperFloat/HyperClear enums, node construction, LineInfo insets - +44 tests —
text_breaking_test.dart: canBreak, isWhitespace, ComputedStyle overflow, CJK/Kinsoku - +52 tests —
layout_algorithm_test.dart: characterOffset regression, rect computation, link context - +32 tests —
details_element_test.dart:<details>/<summary>model and widget open/close behavior - +53 tests —
rtl_bidi_test.dart: HyperTextDirection, hyperDirection inheritance, Arabic/Hebrew text, RTL widget integration dart fixapplied to test files: 73prefer_constissues resolved — 0 analyzer issues
1.2.0 - 2026-03-30 #
- First stable release. Core UDT model, RenderObject engine, plugin interfaces.