Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e557111
feat(streaming): implement AI/LLM real-time streaming engine v1.8.0 w…
vietnguyentuan2019 Aug 22, 2026
ecf1c16
feat(streaming): add tokensPerSecond TPS telemetry, bindCustomStream …
vietnguyentuan2019 Aug 22, 2026
c980b01
feat(demo): refine responsive controls toolbar and autoStart support …
vietnguyentuan2019 Aug 22, 2026
cc0dcee
test(streaming): add comprehensive burst throttling, syntax repair, a…
vietnguyentuan2019 Aug 22, 2026
1b4097e
fix(core): resolve audit issues across auto-scroll, package versions,…
vietnguyentuan2019 Aug 22, 2026
99712a7
chore(release): enhance publish dry-run script with unreleased upstre…
vietnguyentuan2019 Aug 22, 2026
3aab42e
fix(streaming): repair HyperViewer streaming-mode reparse, fade, scro…
vietnguyentuan2019 Sep 5, 2026
be6259c
fix(streaming): guard HyperStreamingController against post-terminal …
vietnguyentuan2019 Sep 5, 2026
fe97306
fix(streaming): scope StreamSyntaxNormalizer scans past closed code f…
vietnguyentuan2019 Sep 5, 2026
2a274d7
docs(streaming): document the 10 fixes in CHANGELOG and correct over-…
vietnguyentuan2019 Sep 5, 2026
3f33d0b
demo(streaming): remove fabricated benchmark numbers, add virtualized…
vietnguyentuan2019 Sep 5, 2026
faedaab
test(streaming): lock in that code-highlighting and KaTeX never crash…
vietnguyentuan2019 Sep 5, 2026
86fc22f
docs: drop unverified '60 FPS' / '5x faster' claims from package desc…
vietnguyentuan2019 Sep 5, 2026
627c0a5
chore: strip internal ticket-ID prefixes from code comments
vietnguyentuan2019 Sep 5, 2026
45c7f99
docs: refresh CSS_PROPERTIES_MATRIX.md and LIMITATIONS.md for v1.8.0
vietnguyentuan2019 Sep 5, 2026
bba584c
fix(publish): make prepare_publish.sh account for hyper_render_epub
vietnguyentuan2019 Sep 5, 2026
ed23bb7
test(security): add real fuzz suite and ReDoS/timing regression tests
vietnguyentuan2019 Sep 5, 2026
1dd404b
test(example): add AiStreamingDemo to the all-demos smoke test
vietnguyentuan2019 Sep 5, 2026
cb94c38
docs: refresh test count and hyper_render_epub version in README
vietnguyentuan2019 Sep 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 1.8.0

- **AI & LLM Real-Time Token Streaming Engine**:
- `HyperStreamingController`: High-performance streaming controller for batching rapid SSE / WebSocket token bursts with 16ms frame-aligned throttling by default, backing off adaptively (up to `maxThrottleDuration`, default 200ms) once the accumulated buffer passes 10,000 / 50,000 characters — bounding total reparse cost over a long stream's lifetime.
- `StreamSyntaxNormalizer`: Transient auto-repair utility for incomplete in-flight tokens (auto-closes unclosed code block fences, inline code, bold/italic asterisks, incomplete table rows, incomplete links, and truncated HTML tags).
- `HyperTypingCaret`: Animated typing caret widget with customizable blinking styles (`bar`, `block`, `underscore`, `dot`, `custom`).
- `HyperViewer.streaming(...)`: Dedicated streaming constructor with stick-to-bottom auto-scroller, syntax auto-repair, and typing caret integration.
- Interactive AI streaming demo added to showcase app (`example/lib/ai_streaming_demo.dart`), including a virtualized-mode toggle and an error-simulation control.

### 🐛 Fixes

- **`didUpdateWidget` missed `streamingController` swaps and `autoRepairSyntax` toggles**: replacing the streaming controller with a fresh instance (e.g. a new AI turn) or flipping `autoRepairSyntax` at runtime had no visible effect until the next stream notification happened to arrive. Both are now in the reparse-trigger list.
- **Content fade restarted on every streaming tick**: the 300ms fade-in was reset on every throttled notification (as often as every 16ms), so it perpetually restarted instead of settling — content flickered/stayed dim for the whole stream. The fade now plays once per streaming session and correctly replays after `reset()`.
- **`fallbackBuilder`'s complexity gate never fired during streaming**: it checked `widget.content`, which `.streaming()` hardcodes to `''`, so `HtmlHeuristics.isComplex(...)` was always evaluated against an empty string regardless of how complex the streamed HTML became. It now reads the live streaming buffer.
- **`autoScrollToBottom` silently did nothing in virtualized/`auto` mode** without an externally supplied `HyperViewerController`: the virtualized `ListView.builder` was bound to `widget.controller?.scrollController` instead of the existing `_effectiveScrollController` fallback, so the internally-created scroll controller was never attached to anything.
- **`append()` after `error()` silently resumed streaming**, erasing the error state without requiring `reset()` first (the guard only checked for `completed`, not `error`). It now rejects appends in both terminal states.
- **A synchronous exception from `append()` inside `bindStream`/`bindCustomStream`'s `onData` callback bypassed `onError` entirely** and surfaced as an unhandled zone exception, contradicting the documented "catches errors" behavior. Both now wrap `onData` (and, for `bindCustomStream`, the `mapper` call) and route genuine failures into `error()` — without overwriting an already-legitimate `completed`/`error` state if a late/duplicate event races it.
- **`StreamSyntaxNormalizer.normalizeMarkdown` scanned the entire accumulated buffer** for unmatched backticks/asterisks/`$`/brackets, so an odd character count *inside an already-closed, fully-rendered code fence* earlier in the document was miscounted as unclosed — corrupting unrelated trailing prose with a spurious appended character on every subsequent tick. Parity and link-repair scans are now scoped to the text after the last closed fence.
- **`StreamSyntaxNormalizer.normalizeHtml`'s truncated-tag detection could be fooled by a literal `>` inside a still-open tag's quoted attribute value** (e.g. `<div title="a>b`), leaving a genuinely truncated tag unstripped. Replaced the global `>` search with a quote-aware scan.

## 1.7.1

- **Pub.dev Dependency Constraint Lower Bounds**: Tightened `hyper_render_core` dependency constraint to `^1.7.0` (from `>=1.6.0 <2.0.0`) to ensure lower bound compatibility during `flutter pub downgrade` analysis, securing a perfect 160/160 score on pub.dev.
Expand Down
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Flutter](https://img.shields.io/badge/Flutter-3.10+-54C5F8.svg?logo=flutter)](https://flutter.dev)

**CSS float · crash-free selection · CJK/Furigana · `@keyframes` · 1 200+ tests · XSS-safe · Zero Gradle config**
**CSS float · crash-free selection · AI/LLM streaming · CJK/Furigana · `@keyframes` · 2 460+ tests · XSS-safe · Zero Gradle config**

<br/>

[![Try Live Web Demo](https://img.shields.io/badge/🌐_Live_Demo-Try_in_Browser-blueviolet?style=for-the-badge)](https://brewkits.github.io/hyper_render/)

<br/>

[**Live Web Playground**](https://brewkits.github.io/hyper_render/) · [**Quick Start**](#-quick-start) · [**30s Migration**](#-30-second-drop-in-migration-from-flutter_html) · [**Why Switch?**](#️-why-switch-the-architecture-argument) · [**API**](#-api-reference) · [**Packages**](#-packages)
[**Live Web Playground**](https://brewkits.github.io/hyper_render/) · [**Quick Start**](#-quick-start) · [**AI Streaming**](#-aillm-real-time-streaming) · [**30s Migration**](#-30-second-drop-in-migration-from-flutter_html) · [**Why Switch?**](#️-why-switch-the-architecture-argument) · [**API**](#-api-reference) · [**Packages**](#-packages)

</div>

Expand All @@ -34,13 +34,14 @@ Already using `flutter_html`? You don't need to rewrite your widget tree or lear
```dart
// 1. In your pubspec.yaml:
// dependencies:
// hyper_render: ^1.7.0
// hyper_render: ^1.8.0

// 2. In your Dart file — replace this single line:
// ❌ import 'package:flutter_html/flutter_html.dart';
import 'package:hyper_render/compat/flutter_html.dart';

// 3. Your existing code works out of the box with 5x faster RenderObject speed!
// 3. Your existing code works out of the box — rendered by one RenderObject
// instead of hundreds of nested widgets (see "Why Switch?" below).
Html(
data: '<h1>Hello</h1><p>Text wraps seamlessly around floats!</p>',
onLinkTap: (url, attributes, element) => launchUrl(Uri.parse(url!)),
Expand All @@ -67,7 +68,7 @@ Html(

```yaml
dependencies:
hyper_render: ^1.7.0
hyper_render: ^1.8.0
```

```dart
Expand All @@ -83,6 +84,29 @@ Zero configuration. XSS sanitization is **on by default**. No Gradle setup requi

---

## 🤖 AI / LLM Real-Time Streaming

Render live streaming token feeds from **Google Gemini, OpenAI ChatGPT, Anthropic Claude**, or WebSocket backends with frame-aligned, adaptively-throttled updates and automatic transient syntax repair.

```dart
final controller = HyperStreamingController();

// Bind directly to any Dart Stream (e.g. OpenAI / Gemini SDK):
controller.bindStream(aiTokenStream);

// Render with automatic stick-to-bottom auto-scroller and pulsing caret:
HyperViewer.streaming(
streamingController: controller,
contentType: HyperContentType.markdown,
showTypingCaret: true,
caretStyle: HyperTypingCaretStyle.bar,
autoRepairSyntax: true, // Auto-closes incomplete ```, **, $$, | on the fly
autoScrollToBottom: true, // Smoothly tracks stream tail
)
```

---

## 🏗️ Why Switch? The Architecture Argument

Most Flutter HTML libraries map each HTML tag to a Flutter widget. A 3 000-word article becomes **500+ nested widgets** — and some layout primitives simply cannot be expressed that way:
Expand All @@ -97,6 +121,7 @@ HyperRender renders the whole document inside **one custom `RenderObject`**. CSS
| Feature | `flutter_html` | `flutter_widget_from_html` | **HyperRender** |
|---|:---:|:---:|:---:|
| `float: left / right` | ❌ | ❌ | ✅ |
| AI / LLM Streaming | ❌ | ❌ | ✅ Frame-aligned, adaptive throttle |
| Text selection — large docs | ❌ Crashes | ❌ Crashes | ✅ Crash-free |
| Ruby / Furigana + Kinsoku | ❌ Raw text | ❌ Raw text | ✅ |
| RTL / BiDi (Arabic, Hebrew) | ⚠️ | ⚠️ | ✅ |
Expand Down Expand Up @@ -450,7 +475,7 @@ These packages bring specialized dependencies and are **not bundled** by default

```yaml
dependencies:
hyper_render_epub: ^0.1.0
hyper_render_epub: ^0.1.2
```

```dart
Expand Down
35 changes: 13 additions & 22 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,26 @@ This document outlines the architectural roadmap for **HyperRender** to become t
| Version | Target Date | Strategic Focus | Key Differentiators |
| :--- | :--- | :--- | :--- |
| **v1.7.0** | Current | **Production Hardening & Drop-in Migration** | Single RenderObject, 100% WASM support, 160/160 pub score, 30s `flutter_html` drop-in layer. |
| **v1.8.0** | Q3 2026 | **AI & LLM Token-Streaming Engine** | Incremental Delta-Streaming, Zero-Jank token updates, auto-scroll locking, tail-only layout invalidation. |
| **v1.8.0** | Q3 2026 | **AI & LLM Token-Streaming Engine** | Frame-throttled token updates with adaptive backoff, transient syntax auto-repair, auto-scroll locking. Tail-only layout invalidation remains a separate, unscheduled epic — see below. |
| **v1.9.0** | Q4 2026 | **Native Vector Diagramming & Headless Export** | Pure Canvas/Vector Mermaid.js & GraphViz (Zero-WebView), Headless Image & PDF byte stream generator. |
| **v2.0.0** | Q1 2027 | **Interactive Editorial & Magazine Typography** | Medium-style Text Annotation/Highlighting layer, Multi-column layout (`column-count`), Z-Index Stacking Context, Vertical Text (`writing-mode: vertical-rl`). |

---

## 🚀 v1.8.0: AI / LLM Streaming Engine (Token-by-Token Zero-Jank)
## 🚀 v1.8.0: AI / LLM Streaming Engine

### 1. Incremental Delta-Append Engine
- **Problem**: Modern LLM chat apps (ChatGPT, Claude, Notion AI) stream Markdown/HTML token-by-token. Re-parsing the full document string every 50ms causes 100% CPU spikes, severe frame drops (jank), memory thrashing, and scroll jump.
- **Solution**:
- Token-level append directly to the active UDT leaf node.
- Partial layout invalidation: only measure and lay out the trailing line fragment (`tailLineLayout`), preserving 100% of cached layout geometry for preceding paragraphs, tables, and code blocks.
- Smooth Auto-Scroll Anchor: lock viewport to stream tail without jittering scroll physics.
- **API Surface**:
```dart
final streamController = HyperDocumentStreamController();

HyperViewer.stream(
controller: streamController,
mode: HyperRenderMode.sync,
);

// As chunks arrive from LLM:
streamController.appendToken(" **instant** rendering");
```
**Shipped** (`HyperStreamingController`, `HyperViewer.streaming(...)`, `StreamSyntaxNormalizer`, `HyperTypingCaret` — see CHANGELOG 1.8.0): frame-throttled token append with **adaptive backoff** (the notification interval widens as the accumulated buffer grows past 10,000 / 50,000 chars, up to `maxThrottleDuration`), transient syntax auto-repair for Markdown/HTML, stick-to-bottom auto-scroll, typing caret. This bounds the total cost of re-parsing over the life of a long stream and is what "Zero-Jank" in this doc's earlier drafts actually refers to.

**Not shipped — the paragraph below was aspirational and did not match what got built; corrected after a production-readiness review found the mismatch:**

### 1. Incremental Delta-Append Engine (tail-only layout) — still unimplemented, own epic
- **Problem**: re-parsing and re-laying-out the *entire* accumulated document on every streaming tick, rather than only the appended tail, means total work over a stream's lifetime scales with the square of its final length. The adaptive-backoff mitigation above bounds *how often* this happens as the buffer grows, but each tick still does a full document reparse + full `RenderHyperBox` layout pass — it does not make any single tick cheaper.
- **Why it's not a small patch**: a feasibility review of `packages/hyper_render_core/lib/src/core/render_hyper_box*.dart` found this needs four largely independent subsystems, most of them outside the renderer: (a) a parser able to resume from a character offset instead of re-tokenizing from scratch, (b) a UDT model change — `TextNode.text` is currently immutable and nodes have no identity that survives across two parses, so there is no way to "find and extend the last text node" today, (c) a fragment list that supports appending instead of the current full-rebuild-every-layout design, (d) a persisted line-layout checkpoint (cursor position, in-progress float lists) that `_performLineLayout` can resume from instead of always resetting to empty. `RenderHyperBox`'s 7-file `part` architecture (shared private state across files, no interface boundary — see the Architecture section above) makes this riskier than in a normally-composed class, since nothing stops a part file from silently assuming layout is always complete and freshly computed. Some CSS behavior (`text-align: justify`, float carryover, `text-overflow: ellipsis`) is also not strictly tail-local, so "only touch the appended tail" needs a correctness argument per feature, not just an engine change.
- **Status**: deliberately deferred as a separate, scoped effort (own design + plan, own risk review) rather than folded into a bug-fix/hardening pass on a renderer every consumer of this library depends on — not just streaming users.

### 2. Live KaTeX & Syntax Highlighting in Streaming Mode
- Incremental tokenizer state tracking: maintain code-fence (` ``` `) and math-delimiter (`$$`) states across partial chunks to prevent flashing unstyled syntax during streaming.
### 2. Live KaTeX & Syntax Highlighting in Streaming Mode — verified non-issue, not scheduled
- **Original concern**: incomplete code fences / math delimiters mid-stream could flash unstyled or broken content, or crash the highlighter/KaTeX renderer.
- **Investigated and closed**: `HyperViewer.streaming()` already exposes both `codeHighlighter` and `pluginRegistry`, so both are reachable during live streaming. `flutter_highlight`'s lexer is best-effort (not a strict parser) and doesn't throw on incomplete/malformed code — covered by `code_highlighter_edge_cases_test.dart`. `flutter_math_fork`'s `Math.tex(..., onErrorFallback: ...)` wraps both its parse and build stages in a catch-all, so a delimiter-balanced-but-internally-malformed LaTeX fragment (which `StreamSyntaxNormalizer` intentionally does not try to brace-balance) safely falls through to the red-text fallback instead of crashing. No incremental tokenizer state is needed; regression tests were added to lock this behavior in (see CHANGELOG).

---

Expand Down
6 changes: 3 additions & 3 deletions doc/CSS_PROPERTIES_MATRIX.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# CSS Properties Support Matrix

Last Updated: June 24, 2026
Version: 1.4.0
Last Updated: September 6, 2026
Version: 1.8.0

This document lists CSS property support in HyperRender.

Expand Down Expand Up @@ -250,4 +250,4 @@ This document lists CSS property support in HyperRender.

---

*Last updated: July 5, 2026 — HyperRender v1.5.0*
*Last updated: September 6, 2026 — HyperRender v1.8.0*
23 changes: 22 additions & 1 deletion doc/LIMITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,27 @@ HyperViewer(html: html, mode: HyperRenderMode.sync)
Prefer explicit `pump(duration)` calls over `pumpAndSettle()` for any test that
exercises virtualised or paged mode.

### AI/LLM streaming (`HyperViewer.streaming`) is full-reparse, not tail-only

`HyperStreamingController`/`HyperViewer.streaming(...)` (v1.8.0) re-runs syntax
normalization, sanitization, parsing, and layout over the **entire**
accumulated buffer on every throttled notification — it does not append to an
existing document tree or only re-lay-out the trailing line. This is
mitigated, not eliminated, by adaptive throttle backoff: the notification
interval automatically widens once the buffer passes 10,000 / 50,000
characters (up to `maxThrottleDuration`, default 200ms), bounding total work
over a long stream's lifetime, but each individual tick's cost still scales
with the full document length, not just the newly-appended text.

A genuine tail-only incremental engine (append to the last text node in
place, resume line-layout from the last committed line) was evaluated and
deliberately deferred — it requires a parser that can resume from a character
offset, a UDT model change (`TextNode.text` is currently immutable and nodes
have no identity across two parses), and a persisted line-layout checkpoint
inside `RenderHyperBox`'s `part`-file architecture, which has no
encapsulation boundary between layout/paint/selection/accessibility. See
`ROADMAP.md`'s v1.8.0 section for the full feasibility writeup.

---

## Interactive Elements
Expand Down Expand Up @@ -217,4 +238,4 @@ the full suite plus goldens as the safety net.

---

*Last updated: July 22, 2026 — HyperRender v1.5.x (unreleased branch)*
*Last updated: September 6, 2026 — HyperRender v1.8.0*
Loading
Loading