Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
# Changelog

## [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:

```yaml
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`, `none`
- **`list-style-position`**: `inside` / `outside`
- **`list-style` shorthand**: parses type and position in any order
- **`background-repeat`**: `repeat`, `repeat-x`, `repeat-y`, `no-repeat`, `space`, `round`
- **`background-position`**: keyword (`center`, `top left`, etc.) and percentage values

### 🚀 Performance

- **Selection rects cached**: `getSelectionRects()` now called once per drag event (was 3×) — stored in `_selectionRects` field, eliminating redundant layout walks during selection drag
- **Auto-scroll proportional speed**: `_autoScrollIfNearEdge` scales 0–20 px/frame based on finger proximity to edge (was fixed 15 px/frame)
- **`HyperTeardropHandlePainter` deduplicated**: renamed, made public, and exported from `hyper_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_clipboard` and `hyper_render_math` removed from root `hyper_render` default dependencies — eliminates the `compileSdk = 34` Gradle requirement for basic usage
- **Removed outdated `compileSdk` workaround** from example app's Android Gradle config


## [1.3.0] - 2026-05-03

### ✨ New Features
Expand Down
113 changes: 78 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# HyperRender

### Fast, robust HTML rendering for Flutter.
### The only Flutter HTML renderer with CSS float layout.

[![pub.dev](https://img.shields.io/pub/v/hyper_render.svg?label=pub.dev&color=0175C2)](https://pub.dev/packages/hyper_render)
[![pub points](https://img.shields.io/pub/points/hyper_render?label=pub%20points&color=00b4ab)](https://pub.dev/packages/hyper_render/score)
Expand All @@ -13,7 +13,7 @@
[![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` · 80%+ Test Coverage · XSS-safe**
**CSS float · crash-free selection · CJK/Furigana · `@keyframes` · 1 646 tests · XSS-safe · Zero Gradle config**

[**Quick Start**](#-quick-start) · [**Why Switch?**](#️-why-switch-the-architecture-argument) · [**API**](#-api-reference) · [**Packages**](#-packages)

Expand All @@ -26,7 +26,7 @@
| CSS Float Layout | Ruby / Furigana | Crash-Free Selection |
|:---:|:---:|:---:|
| ![CSS Float Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/float_demo.gif) | ![Ruby Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/ruby_demo.gif) | ![Selection Demo](https://raw.githubusercontent.com/brewkits/hyper_render/main/assets/selection_demo.gif) |
| Text wraps around floated images — no other Flutter HTML renderer does this | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables — tested to 100 000 chars |
| Text wraps around floated images — **no other Flutter HTML renderer does this** | Furigana centered above base glyphs, full Kinsoku line-breaking | Select across headings, paragraphs, tables — tested to 100 000 chars |

| Advanced Tables | Head-to-Head | Virtualized Mode |
|:---:|:---:|:---:|
Expand All @@ -39,7 +39,7 @@

```yaml
dependencies:
hyper_render: ^1.3.0
hyper_render: ^1.3.1
```

```dart
Expand All @@ -51,47 +51,37 @@ HyperViewer(
)
```

Zero configuration. XSS sanitization is **on by default**.

> **Android note:** `hyper_render` depends on `super_clipboard` which transitively pulls in `irondash_engine_context`. That library was compiled against Android SDK 31, but its `androidx.fragment:1.7.1` dependency requires `compileSdk ≥ 34`. Add this one-time workaround to your `android/build.gradle.kts`:
>
> ```kotlin
> // android/build.gradle.kts (root — not app/build.gradle.kts)
> subprojects {
> afterEvaluate {
> extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
> compileSdk = 35
> }
> }
> }
> ```
>
> This overrides `compileSdk` for all library sub-projects so AGP's `checkAarMetadata` passes. Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5).
Zero configuration. XSS sanitization is **on by default**. No Gradle setup required.

---

## 🏗️ 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:

> **CSS `float` is not possible in a widget tree.**
> Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a single `RenderObject` owns the entire layout.
> **CSS `float` is architecturally impossible in a widget tree.**
> Wrapping text around a floated image requires every fragment's coordinates before adjacent text can be composed. That geometry only exists when a single `RenderObject` owns the entire layout pass.

HyperRender renders the whole document inside **one custom `RenderObject`**. Float, crash-free selection, and sub-millisecond binary-search hit-testing all follow from that single design decision.
HyperRender renders the whole document inside **one custom `RenderObject`**. CSS float, crash-free selection, O(log N) binary-search hit-testing, and `@keyframes` animations all follow directly from that single architectural decision.

### Feature Matrix

| Feature | `flutter_html` | `flutter_widget_from_html` | **HyperRender** |
|---|:---:|:---:|:---:|
| `float: left / right` | ❌ | ❌ | ✅ |
| Text selection — large docs | ❌ Crashes | ❌ Crashes | ✅ Crash-free |
| Ruby / Furigana | ❌ Raw text | ❌ Raw text | ✅ |
| `<details>` / `<summary>` | ❌ | | ✅ Interactive |
| Ruby / Furigana + Kinsoku | ❌ Raw text | ❌ Raw text | ✅ |
| RTL / BiDi (Arabic, Hebrew) | ⚠️ | ⚠️ | ✅ |
| CSS Variables `var()` | ❌ | ❌ | ✅ |
| CSS `@keyframes` | ❌ | ❌ | ✅ |
| CSS `@keyframes` animation | ❌ | ❌ | ✅ |
| Flexbox / Grid | ⚠️ Partial | ⚠️ Partial | ✅ Full |
| Box shadow · `filter` | ❌ | ❌ | ✅ |
| SVG `<img src="*.svg">` | ⚠️ | ⚠️ | ✅ |
| `box-shadow` · `filter` | ❌ | ❌ | ✅ |
| `list-style-type` (all 11 values) | ⚠️ disc only | ⚠️ disc only | ✅ |
| `<details>` / `<summary>` | ❌ | ❌ | ✅ Interactive |
| Quill Delta input | ❌ | ❌ | ✅ |
| Markdown input | ❌ | ❌ | ✅ GFM |
| Modular packages | ❌ monolith | ❌ monolith | ✅ opt-in add-ons |
| Zero Gradle config | ✅ | ✅ | ✅ |

### Benchmarks

Expand Down Expand Up @@ -368,10 +358,11 @@ HTML / Markdown / Quill Delta
Kinsoku · O(log N) binary-search selection
```

- **Single RenderObject** — float layout and crash-free selection require one shared coordinate system
- **Single RenderObject** — float layout and crash-free selection require one shared coordinate system; a widget tree cannot provide this
- **O(1) CSS rule lookup** — rules indexed by tag / class / ID; constant time regardless of stylesheet size
- **O(log N) hit-testing** — `_lineStartOffsets[]` precomputed at layout time; each touch is a binary search
- **RepaintBoundary per chunk** — unmodified chunks are composited, not repainted
- **O(log N) hit-testing** — `_lineStartOffsets[]` precomputed at layout time; each touch is a binary search, not a linear scan
- **RepaintBoundary per chunk** — unmodified chunks are composited, not repainted; incremental layout caches unchanged sections by content hash
- **1 646 passing tests** — unit, widget, integration, fuzz (43 cases), and golden pixel tests across 3 OS platforms

---

Expand Down Expand Up @@ -403,13 +394,65 @@ HTML / Markdown / Quill Delta

| Package | pub.dev | Description |
|---------|---------|-------------|
| [`hyper_render`](https://pub.dev/packages/hyper_render) | [![pub](https://img.shields.io/pub/v/hyper_render.svg)](https://pub.dev/packages/hyper_render) | Convenience wrapper — one dependency, everything included |
| [`hyper_render_core`](https://pub.dev/packages/hyper_render_core) | [![pub](https://img.shields.io/pub/v/hyper_render_core.svg)](https://pub.dev/packages/hyper_render_core) | Core engine — UDT model, CSS resolver, RenderObject |
| [`hyper_render`](https://pub.dev/packages/hyper_render) | [![pub](https://img.shields.io/pub/v/hyper_render.svg)](https://pub.dev/packages/hyper_render) | Convenience wrapper — HTML, Markdown, Delta, syntax highlight |
| [`hyper_render_core`](https://pub.dev/packages/hyper_render_core) | [![pub](https://img.shields.io/pub/v/hyper_render_core.svg)](https://pub.dev/packages/hyper_render_core) | Core engine — UDT model, CSS resolver, RenderObject; zero native deps |
| [`hyper_render_html`](https://pub.dev/packages/hyper_render_html) | [![pub](https://img.shields.io/pub/v/hyper_render_html.svg)](https://pub.dev/packages/hyper_render_html) | HTML + CSS parser |
| [`hyper_render_markdown`](https://pub.dev/packages/hyper_render_markdown) | [![pub](https://img.shields.io/pub/v/hyper_render_markdown.svg)](https://pub.dev/packages/hyper_render_markdown) | Markdown adapter (GFM) |
| [`hyper_render_highlight`](https://pub.dev/packages/hyper_render_highlight) | [![pub](https://img.shields.io/pub/v/hyper_render_highlight.svg)](https://pub.dev/packages/hyper_render_highlight) | Syntax highlighting for `<code>` / `<pre>` blocks |
| [`hyper_render_clipboard`](https://pub.dev/packages/hyper_render_clipboard) | [![pub](https://img.shields.io/pub/v/hyper_render_clipboard.svg)](https://pub.dev/packages/hyper_render_clipboard) | Image copy / share |
| [`hyper_render_devtools`](https://pub.dev/packages/hyper_render_devtools) | [![pub](https://img.shields.io/pub/v/hyper_render_devtools.svg)](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools extension — UDT inspector, computed styles |
| [`hyper_render_devtools`](https://pub.dev/packages/hyper_render_devtools) | [![pub](https://img.shields.io/pub/v/hyper_render_devtools.svg)](https://pub.dev/packages/hyper_render_devtools) | Flutter DevTools extension — UDT inspector, computed styles, float visualizer |

### Optional add-ons

These packages bring native dependencies and are **not bundled** by default. Install only what you need.

| Package | pub.dev | Description |
|---------|---------|-------------|
| [`hyper_render_clipboard`](https://pub.dev/packages/hyper_render_clipboard) | [![pub](https://img.shields.io/pub/v/hyper_render_clipboard.svg)](https://pub.dev/packages/hyper_render_clipboard) | Native image copy / share via `super_clipboard` |
| [`hyper_render_math`](https://pub.dev/packages/hyper_render_math) | [![pub](https://img.shields.io/pub/v/hyper_render_math.svg)](https://pub.dev/packages/hyper_render_math) | LaTeX / MathML via `flutter_math_fork` |

#### `hyper_render_clipboard` — Native image copy / share

```yaml
dependencies:
hyper_render_clipboard: ^1.3.1
```

```dart
import 'package:hyper_render_clipboard/hyper_render_clipboard.dart';

HyperViewer(
html: html,
imageClipboardHandler: SuperClipboardHandler(),
)
```

> **Android setup required:** `super_clipboard` transitively pulls in `irondash_engine_context`, which requires `compileSdk ≥ 34`. Add this to `android/build.gradle.kts` (root file, not `app/`):
>
> ```kotlin
> subprojects {
> afterEvaluate {
> extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
> compileSdk = 35
> }
> }
> }
> ```
>
> Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5).

#### `hyper_render_math` — LaTeX / MathML rendering

```yaml
dependencies:
hyper_render_math: ^1.3.1
```

```dart
import 'package:hyper_render_math/hyper_render_math.dart';

final registry = HyperPluginRegistry()..register(const MathPlugin());
HyperViewer(html: html, pluginRegistry: registry)
```

---

Expand Down
58 changes: 33 additions & 25 deletions doc/MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
# Migration Guide

> **Current version: v1.2.0** — All v1.x releases are additive and backward-compatible. No breaking API changes.
> **Current version: v1.3.1**

## Current Version: 1.2.0
## Upgrading to 1.3.1

**No migration needed!** If you're starting fresh with HyperRender v1.2.0:
### ⚠️ Breaking change — clipboard and math are now opt-in

`hyper_render_clipboard` and `hyper_render_math` are no longer transitive dependencies of the root `hyper_render` package. If you use either, add them explicitly:

```yaml
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
```

If you don't use either feature, **no changes are needed** — just bump the version and your Android build will no longer require a `compileSdk = 35` workaround.

### New in 1.3.1

- `list-style-type`, `list-style-position`, `list-style` shorthand CSS support
- `background-repeat`, `background-position` CSS support
- Edge-to-edge images: `width: 100%` now truly fills the container
- Selection drag performance improved (rects cached, auto-scroll proportional)

---

## Starting fresh with 1.3.1

**No migration needed!** If you're starting fresh:

```yaml
dependencies:
hyper_render: ^1.2.0
# or use individual packages:
hyper_render_core: ^1.2.0
hyper_render_clipboard: ^1.2.0
hyper_render: ^1.3.1
# opt-in extras:
hyper_render_clipboard: ^1.3.1 # image copy/save/share
hyper_render_math: ^1.3.1 # LaTeX/MathML
```

```dart
Expand Down Expand Up @@ -156,28 +180,12 @@ These APIs are stable and will remain backward-compatible in v2.0:

## Getting Help

For the current v1.3.0 release:
For the current v1.3.1 release:
- See [README](../README.md) for usage
- Check [CHANGELOG](../CHANGELOG.md) for version history
- Review [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) for extending
- File issues at [GitHub Issues](https://github.com/brewkits/hyper_render/issues)

---

*Last Updated: April 29, 2026 for v1.3.0*
ard support
- Cross-platform (iOS, Android, Web, Desktop)

---

## Getting Help

For the current v1.2.0 release:
- See [README](../README.md) for usage
- Check [CHANGELOG](../CHANGELOG.md) for version history
- Review [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) for extending
- File issues at [GitHub Issues](https://github.com/your-repo/issues)

---

*Last Updated: March 30, 2026 for v1.2.0*
*Last Updated: May 14, 2026 for v1.3.1*
44 changes: 16 additions & 28 deletions doc/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ For detailed CSS property tracking, see [`internal/CSS_SUPPORT_ROADMAP.md`](inte

- Single `RenderObject` pipeline (Parse → Style → Layout → Paint)
- Float layout algorithm (`float: left/right`, `clear`) — unique advantage over FWFH
- Isolate-based HTML parsing (non-blocking UI thread)
- Async microtask-based HTML parsing (non-blocking UI thread; uses `Future.microtask` instead of a real isolate so `FakeAsync` works in widget tests)
- `ListView.builder` virtualization (low RAM on large documents)
- Full Flexbox support (90% coverage: direction, wrap, gap, align, grow/shrink/basis)
- CSS Variables `var()`, `transition`, `animation-*` parsing
Expand Down Expand Up @@ -79,12 +79,12 @@ jarring for large images. A workaround is to increase `chunkSize` so fewer split
occur near floats.

Scope:
- [ ] Add `FloatCarryover` data class to `render_hyper_box_types.dart`
- [ ] Add `danglingFloats` getter to `RenderHyperBox`
- [ ] Add `initialFloats` parameter to `RenderHyperBox` / `HyperRenderWidget`
- [ ] Seed initial floats in `_performLineLayout`
- [ ] Wire `FloatCarryover` callbacks through `VirtualizedChunk` → `HyperViewer`
- [ ] Add offset rendering support in `_paintFloatImages` for image floats
- [x] Add `FloatCarryover` data class to `render_hyper_box_types.dart`
- [x] Add `danglingFloats` getter to `RenderHyperBox`
- [x] Add `initialFloats` parameter to `RenderHyperBox` / `HyperRenderWidget`
- [x] Seed initial floats in `_performLineLayout`
- [x] Wire `FloatCarryover` callbacks through `VirtualizedChunk` → `HyperViewer`
- [ ] Add offset rendering support in `_paintFloatImages` for image floats (imagePixelOffset not yet read by painter)
- [ ] Integration test: tall float at chunk boundary shows no wasted space

---
Expand All @@ -98,22 +98,8 @@ Scope:
**Source**: Expert review recommendation
**Priority**: High — directly impacts stability on low-end devices (2 GB RAM)

The image cache is currently tuned manually. `WidgetsBindingObserver` should be
integrated so HyperRender automatically evicts caches when the OS signals memory pressure.

```dart
class HyperRenderController with WidgetsBindingObserver {
@override
void didHaveMemoryPressure() {
imageCache.evictAll(); // Flutter image cache
_internalSpanCache.clear(); // HyperRender internal span cache
super.didHaveMemoryPressure();
}
}
```

Scope:
- [ ] Implement `WidgetsBindingObserver` in `HyperRenderController`
- [x] Implement `WidgetsBindingObserver` in `HyperViewer` — `didHaveMemoryPressure` clears TextPainter cache, `LazyImageQueue.clearPending()`, and `PaintingBinding.imageCache.clear()`
- [ ] Expose `onMemoryPressure` callback for host-app customization
- [ ] Debug-mode metrics: eviction count, bytes freed
- [ ] Smoke test on a 2 GB RAM device
Expand All @@ -122,12 +108,14 @@ Scope:

Properties deferred from Phase 3 in [`internal/CSS_SUPPORT_ROADMAP.md`](internal/CSS_SUPPORT_ROADMAP.md):

- [ ] `text-shadow` — high visual impact, 1-day effort
- [ ] `text-overflow: ellipsis` — extremely common, 4-hour effort
- [ ] `box-shadow` — design system compatibility
- [ ] `list-style-type`, `list-style-position` — better `<ul>` / `<ol>` rendering
- [ ] `word-break`, `overflow-wrap` — CJK and long-URL handling
- [ ] `background-repeat`, `background-position`, `background-size`
- [x] `text-shadow` — parsed + applied to `TextStyle.shadows` in `ComputedStyle`
- [x] `text-overflow: ellipsis` — parsed + executed in `render_hyper_box_fragments.dart`
- [x] `box-shadow` — parsed + applied in `render_hyper_box_paint.dart`
- [x] `word-break`, `overflow-wrap` — parsed + executed in `render_hyper_box_layout.dart` (L1339–1375)
- [ ] `list-style-type`, `list-style-position` — not yet in resolver or painter
- [x] `background-repeat` — parsed + mapped to `ImageRepeat` in `paintImage()`
- [x] `background-position` — parsed + mapped to `Alignment` in `paintImage()` (keyword values: top/center/bottom/left/right and combinations)
- [x] `background-size` — parsed and applied

---

Expand Down
Loading
Loading