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
9 changes: 7 additions & 2 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
runs-on: ubuntu-22.04
# Skip on the weekly schedule — that's for the full-benchmark job only
if: github.event_name != 'schedule'
permissions:
pull-requests: write

steps:
- name: Checkout
Expand Down Expand Up @@ -112,10 +114,13 @@ jobs:
- name: Post PR comment
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
BENCH_EXIT_CODE: ${{ steps.bench.outputs.exit_code }}
BENCH_SUMMARY: ${{ steps.bench.outputs.summary }}
with:
script: |
const exitCode = '${{ steps.bench.outputs.exit_code }}';
const summary = `${{ steps.bench.outputs.summary }}`;
const exitCode = process.env.BENCH_EXIT_CODE || '0';
const summary = process.env.BENCH_SUMMARY || '';
const passed = exitCode === '0';
const icon = passed ? '✅' : '❌';
const headline = passed
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/golden.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
golden:
name: Golden Tests
runs-on: ubuntu-22.04 # pinned — do NOT change to ubuntu-latest
permissions:
pull-requests: write

steps:
- name: Checkout code
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,24 @@ jobs:
needs.path-filter.outputs.changed_markdown == 'true' ||
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_markdown
run: flutter test --no-pub
run: |
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi

- name: Test hyper_render_highlight
if: >-
needs.path-filter.outputs.changed_highlight == 'true' ||
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_highlight
run: flutter test --no-pub
run: |
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi

- name: Test hyper_render_clipboard
if: >-
needs.path-filter.outputs.changed_clipboard == 'true' ||
needs.path-filter.outputs.changed_core == 'true'
working-directory: packages/hyper_render_clipboard
run: flutter test --no-pub
run: |
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi

- name: Upload test results
if: failure()
Expand Down
10 changes: 10 additions & 0 deletions .pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ scripts/
# Internal publish helpers
pubspec.yaml.backup
pubspec_publish_ready.yaml
pubspec_dev.yaml

# Flutter .metadata files (auto-generated, not useful to consumers)
.metadata

# Internal archive / historical comparison docs
archive/

# Coverage artifacts
coverage/

# IDE files
*.iml
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## [1.2.2] - 2026-04-02

### 🐛 Bug Fixes

- **Android build failure with modern compileSdk** (`example/android/build.gradle.kts`): `irondash_engine_context 0.5.5` was compiled against android-31 but its transitive `androidx.fragment:1.7.1` dependency has `minCompileSdk=34`, causing AGP 8's `checkAarMetadata` to block the build. Added a `subprojects { afterEvaluate { compileSdk = 35 } }` override in the example's root Gradle file. README now documents the same one-line workaround for app-level projects. ([#5](https://github.com/brewkits/hyper_render/issues/5))
- **SVG invisible with `sanitize: true`** (`html_sanitizer.dart`): `<svg>` was not in `defaultAllowedTags` so 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.).
- **`selectable` toggle ignored after build** (`hyper_viewer.dart`): Toggling `selectable` from `false` → `true` never created `VirtualizedSelectionController`, and `true` → `false` never disposed it. Fixed in `didUpdateWidget`.
- **Deep-link tap silently blocked** (`hyper_viewer.dart`): `_safeOnLinkTap` only checked `widget.allowedCustomSchemes` but ignored `renderConfig.extraLinkSchemes`, causing deep-links registered via `HyperRenderConfig` to be silently dropped. Both sources are now consulted.
- **CSS change didn't invalidate section cache** (`hyper_viewer.dart`): `_hashSection` hashes only text content, so a `customCss` change that alters layout/appearance would incorrectly reuse cached sections. `_sectionHashes` is now reset whenever `customCss` changes in `didUpdateWidget`.
- **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.
- **`renderConfig` change only partially detected** (`hyper_viewer.dart`): `didUpdateWidget` compared only `virtualizationChunkSize` instead of the full `HyperRenderConfig`. Now uses full value equality (available since the `operator==` fix) so any config change triggers a re-parse.
- **CSS float class names not detected** (`html_adapter.dart`): `_containsFloatChild` missed 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
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

```yaml
dependencies:
hyper_render: ^1.2.1
hyper_render: ^1.2.2
```

```dart
Expand All @@ -53,6 +53,21 @@ 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).

---

## 🏗️ Why Switch? The Architecture Argument
Expand Down
13 changes: 13 additions & 0 deletions example/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ subprojects {
project.evaluationDependsOn(":app")
}

// Workaround: irondash_engine_context 0.5.5 was compiled against android-31 but
// transitively requires androidx.fragment:1.7.1 which has minCompileSdk=34.
// AGP 8 checkAarMetadata blocks the build. Override compileSdk for all library
// subprojects so the check passes.
// Tracked: https://github.com/brewkits/hyper_render/issues/5
subprojects {
afterEvaluate {
extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
compileSdk = 35
}
}
}

tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
8 changes: 2 additions & 6 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ class DemoHomePage extends StatelessWidget {
subtitle:
'Full e-book solution with paged mode, themes, and library management',
color: Colors.deepPurple,
onTap: () => Navigator.push(
context, MaterialPageRoute(builder: (_) => const LibraryScreen())),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const LibraryScreen())),
),
// ── Layout ────────────────────────────────────────────────────────
_buildSectionHeader(context, 'Layout'),
Expand Down Expand Up @@ -3110,7 +3110,6 @@ class VideoDemo extends StatelessWidget {
</video>
''',
onLinkTap: (url) async {

final uri = Uri.tryParse(url);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.platformDefault);
Expand All @@ -3133,7 +3132,6 @@ class VideoDemo extends StatelessWidget {
</video>
''',
onLinkTap: (url) async {

final uri = Uri.tryParse(url);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.platformDefault);
Expand Down Expand Up @@ -3175,7 +3173,6 @@ class VideoDemo extends StatelessWidget {
</div>
''',
onLinkTap: (url) async {

final uri = Uri.tryParse(url);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.platformDefault);
Expand Down Expand Up @@ -3218,7 +3215,6 @@ class VideoDemo extends StatelessWidget {
''',
selectable: true,
onLinkTap: (url) async {

final uri = Uri.tryParse(url);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.platformDefault);
Expand Down
25 changes: 21 additions & 4 deletions example/lib/reader_app/book_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ class Book {
final String content;
final BookType type;
final String? description;
// New persistent fields
int lastPage;
bool isBookmarked;

// Persistent fields - marked as final for immutability where possible
final int lastPage;
final bool isBookmarked;

Book({
required this.id,
Expand All @@ -24,6 +24,23 @@ class Book {
this.lastPage = 0,
this.isBookmarked = false,
});

Book copyWith({
int? lastPage,
bool? isBookmarked,
}) {
return Book(
id: id,
title: title,
author: author,
coverUrl: coverUrl,
content: content,
type: type,
description: description,
lastPage: lastPage ?? this.lastPage,
isBookmarked: isBookmarked ?? this.isBookmarked,
);
}
}

class MockLibrary {
Expand Down
55 changes: 35 additions & 20 deletions example/lib/reader_app/library_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class _LibraryScreenState extends State<LibraryScreen> {

List<Book> get _filteredBooks {
return MockLibrary.books.where((book) {
final matchesSearch = book.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
book.author.toLowerCase().contains(_searchQuery.toLowerCase());
final matchesSearch =
book.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
book.author.toLowerCase().contains(_searchQuery.toLowerCase());
final matchesBookmark = !_showOnlyBookmarked || book.isBookmarked;
return matchesSearch && matchesBookmark;
}).toList();
Expand All @@ -31,11 +32,15 @@ class _LibraryScreenState extends State<LibraryScreen> {
body: CustomScrollView(
slivers: [
SliverAppBar.large(
title: const Text('My Library', style: TextStyle(fontWeight: FontWeight.bold)),
title: const Text('My Library',
style: TextStyle(fontWeight: FontWeight.bold)),
actions: [
IconButton(
icon: Icon(_showOnlyBookmarked ? Icons.bookmark : Icons.bookmark_border),
onPressed: () => setState(() => _showOnlyBookmarked = !_showOnlyBookmarked),
icon: Icon(_showOnlyBookmarked
? Icons.bookmark
: Icons.bookmark_border),
onPressed: () =>
setState(() => _showOnlyBookmarked = !_showOnlyBookmarked),
),
],
),
Expand All @@ -58,13 +63,16 @@ class _LibraryScreenState extends State<LibraryScreen> {
),
),
),

if (recentBooks.isNotEmpty && _searchQuery.isEmpty && !_showOnlyBookmarked) ...[

if (recentBooks.isNotEmpty &&
_searchQuery.isEmpty &&
!_showOnlyBookmarked) ...[
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Text('Continue Reading',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
child: Text('Continue Reading',
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
),
SliverToBoxAdapter(
Expand All @@ -86,8 +94,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Text('All Books',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
child: Text('All Books',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
),

Expand All @@ -99,7 +107,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
children: [
Icon(Icons.library_books, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('No books found', style: TextStyle(color: Colors.grey)),
Text('No books found',
style: TextStyle(color: Colors.grey)),
],
),
),
Expand Down Expand Up @@ -140,27 +149,32 @@ class _LibraryScreenState extends State<LibraryScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10),
BoxShadow(
color: Colors.black.withValues(alpha: 0.05), blurRadius: 10),
],
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(book.coverUrl, width: 80, height: 120, fit: BoxFit.cover),
child: Image.network(book.coverUrl,
width: 80, height: 120, fit: BoxFit.cover),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(book.title,
style: const TextStyle(fontWeight: FontWeight.bold),
maxLines: 2, overflow: TextOverflow.ellipsis),
Text(book.author, style: const TextStyle(fontSize: 12, color: Colors.grey)),
Text(book.title,
style: const TextStyle(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis),
Text(book.author,
style: const TextStyle(fontSize: 12, color: Colors.grey)),
const Spacer(),
const Text('Last read: 2 hours ago', style: TextStyle(fontSize: 10, color: Colors.grey)),
const Text('Last read: 2 hours ago',
style: TextStyle(fontSize: 10, color: Colors.grey)),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(2),
Expand Down Expand Up @@ -206,7 +220,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
),
if (book.isBookmarked)
const Positioned(
top: 8, right: 8,
top: 8,
right: 8,
child: Icon(Icons.bookmark, color: Colors.amber, size: 28),
),
],
Expand Down
Loading
Loading