Skip to content

Commit ff99674

Browse files
fix: resolve 8 bugs + bump version to 1.2.2 (#6)
Fixes Android build failure (#5) and 7 additional bugs found during QA review. **Bugs fixed:** - Android build failure: irondash_engine_context compileSdk mismatch (fixes #5) - SVG invisible with sanitize:true — atomic SVG sanitization added - HyperRenderConfig identity-compare causing unnecessary re-layouts - selectable toggle not handled in didUpdateWidget - Deep-link taps silently blocked (allowedCustomSchemes vs extraLinkSchemes split) - CSS change bypassed section hash cache - Markdown/Delta entire doc rendered as single section in virtualized/paged mode - renderConfig change only partially detected in didUpdateWidget - CSS float class names (Bootstrap/Tailwind) not detected in _containsFloatChild **CI fixes:** - benchmark.yml: backtick-in-template-literal JS syntax error - benchmark.yml/golden.yml: missing pull-requests:write permission - test.yml: skip packages with no test/ directory
1 parent f650745 commit ff99674

46 files changed

Lines changed: 916 additions & 467 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/benchmark.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ jobs:
3232
runs-on: ubuntu-22.04
3333
# Skip on the weekly schedule — that's for the full-benchmark job only
3434
if: github.event_name != 'schedule'
35+
permissions:
36+
pull-requests: write
3537

3638
steps:
3739
- name: Checkout
@@ -112,10 +114,13 @@ jobs:
112114
- name: Post PR comment
113115
if: always() && github.event_name == 'pull_request'
114116
uses: actions/github-script@v7
117+
env:
118+
BENCH_EXIT_CODE: ${{ steps.bench.outputs.exit_code }}
119+
BENCH_SUMMARY: ${{ steps.bench.outputs.summary }}
115120
with:
116121
script: |
117-
const exitCode = '${{ steps.bench.outputs.exit_code }}';
118-
const summary = `${{ steps.bench.outputs.summary }}`;
122+
const exitCode = process.env.BENCH_EXIT_CODE || '0';
123+
const summary = process.env.BENCH_SUMMARY || '';
119124
const passed = exitCode === '0';
120125
const icon = passed ? '✅' : '❌';
121126
const headline = passed

.github/workflows/golden.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ jobs:
3838
golden:
3939
name: Golden Tests
4040
runs-on: ubuntu-22.04 # pinned — do NOT change to ubuntu-latest
41+
permissions:
42+
pull-requests: write
4143

4244
steps:
4345
- name: Checkout code

.github/workflows/test.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,24 @@ jobs:
127127
needs.path-filter.outputs.changed_markdown == 'true' ||
128128
needs.path-filter.outputs.changed_core == 'true'
129129
working-directory: packages/hyper_render_markdown
130-
run: flutter test --no-pub
130+
run: |
131+
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
131132
132133
- name: Test hyper_render_highlight
133134
if: >-
134135
needs.path-filter.outputs.changed_highlight == 'true' ||
135136
needs.path-filter.outputs.changed_core == 'true'
136137
working-directory: packages/hyper_render_highlight
137-
run: flutter test --no-pub
138+
run: |
139+
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
138140
139141
- name: Test hyper_render_clipboard
140142
if: >-
141143
needs.path-filter.outputs.changed_clipboard == 'true' ||
142144
needs.path-filter.outputs.changed_core == 'true'
143145
working-directory: packages/hyper_render_clipboard
144-
run: flutter test --no-pub
146+
run: |
147+
if [ -d test ]; then flutter test --no-pub; else echo "No tests in this package — skipping."; fi
145148
146149
- name: Upload test results
147150
if: failure()

.pubignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ scripts/
4343
# Internal publish helpers
4444
pubspec.yaml.backup
4545
pubspec_publish_ready.yaml
46+
pubspec_dev.yaml
47+
48+
# Flutter .metadata files (auto-generated, not useful to consumers)
49+
.metadata
50+
51+
# Internal archive / historical comparison docs
52+
archive/
53+
54+
# Coverage artifacts
55+
coverage/
4656

4757
# IDE files
4858
*.iml

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [1.2.2] - 2026-04-02
4+
5+
### 🐛 Bug Fixes
6+
7+
- **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))
8+
- **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.).
9+
- **`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`.
10+
- **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.
11+
- **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`.
12+
- **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.
13+
- **`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.
14+
- **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.
15+
316
## [1.2.1] - 2026-03-31
417

518
### 🏗️ Maintenance

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
```yaml
4141
dependencies:
42-
hyper_render: ^1.2.1
42+
hyper_render: ^1.2.2
4343
```
4444
4545
```dart
@@ -53,6 +53,21 @@ HyperViewer(
5353

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

56+
> **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`:
57+
>
58+
> ```kotlin
59+
> // android/build.gradle.kts (root — not app/build.gradle.kts)
60+
> subprojects {
61+
> afterEvaluate {
62+
> extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
63+
> compileSdk = 35
64+
> }
65+
> }
66+
> }
67+
> ```
68+
>
69+
> This overrides `compileSdk` for all library sub-projects so AGP's `checkAarMetadata` passes. Tracked in [#5](https://github.com/brewkits/hyper_render/issues/5).
70+
5671
---
5772
5873
## 🏗️ Why Switch? The Architecture Argument

example/android/build.gradle.kts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ subprojects {
1919
project.evaluationDependsOn(":app")
2020
}
2121

22+
// Workaround: irondash_engine_context 0.5.5 was compiled against android-31 but
23+
// transitively requires androidx.fragment:1.7.1 which has minCompileSdk=34.
24+
// AGP 8 checkAarMetadata blocks the build. Override compileSdk for all library
25+
// subprojects so the check passes.
26+
// Tracked: https://github.com/brewkits/hyper_render/issues/5
27+
subprojects {
28+
afterEvaluate {
29+
extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)?.apply {
30+
compileSdk = 35
31+
}
32+
}
33+
}
34+
2235
tasks.register<Delete>("clean") {
2336
delete(rootProject.layout.buildDirectory)
2437
}

example/lib/main.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ class DemoHomePage extends StatelessWidget {
130130
subtitle:
131131
'Full e-book solution with paged mode, themes, and library management',
132132
color: Colors.deepPurple,
133-
onTap: () => Navigator.push(
134-
context, MaterialPageRoute(builder: (_) => const LibraryScreen())),
133+
onTap: () => Navigator.push(context,
134+
MaterialPageRoute(builder: (_) => const LibraryScreen())),
135135
),
136136
// ── Layout ────────────────────────────────────────────────────────
137137
_buildSectionHeader(context, 'Layout'),
@@ -3110,7 +3110,6 @@ class VideoDemo extends StatelessWidget {
31103110
</video>
31113111
''',
31123112
onLinkTap: (url) async {
3113-
31143113
final uri = Uri.tryParse(url);
31153114
if (uri != null && await canLaunchUrl(uri)) {
31163115
await launchUrl(uri, mode: LaunchMode.platformDefault);
@@ -3133,7 +3132,6 @@ class VideoDemo extends StatelessWidget {
31333132
</video>
31343133
''',
31353134
onLinkTap: (url) async {
3136-
31373135
final uri = Uri.tryParse(url);
31383136
if (uri != null && await canLaunchUrl(uri)) {
31393137
await launchUrl(uri, mode: LaunchMode.platformDefault);
@@ -3175,7 +3173,6 @@ class VideoDemo extends StatelessWidget {
31753173
</div>
31763174
''',
31773175
onLinkTap: (url) async {
3178-
31793176
final uri = Uri.tryParse(url);
31803177
if (uri != null && await canLaunchUrl(uri)) {
31813178
await launchUrl(uri, mode: LaunchMode.platformDefault);
@@ -3218,7 +3215,6 @@ class VideoDemo extends StatelessWidget {
32183215
''',
32193216
selectable: true,
32203217
onLinkTap: (url) async {
3221-
32223218
final uri = Uri.tryParse(url);
32233219
if (uri != null && await canLaunchUrl(uri)) {
32243220
await launchUrl(uri, mode: LaunchMode.platformDefault);

example/lib/reader_app/book_model.dart

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ class Book {
88
final String content;
99
final BookType type;
1010
final String? description;
11-
12-
// New persistent fields
13-
int lastPage;
14-
bool isBookmarked;
11+
12+
// Persistent fields - marked as final for immutability where possible
13+
final int lastPage;
14+
final bool isBookmarked;
1515

1616
Book({
1717
required this.id,
@@ -24,6 +24,23 @@ class Book {
2424
this.lastPage = 0,
2525
this.isBookmarked = false,
2626
});
27+
28+
Book copyWith({
29+
int? lastPage,
30+
bool? isBookmarked,
31+
}) {
32+
return Book(
33+
id: id,
34+
title: title,
35+
author: author,
36+
coverUrl: coverUrl,
37+
content: content,
38+
type: type,
39+
description: description,
40+
lastPage: lastPage ?? this.lastPage,
41+
isBookmarked: isBookmarked ?? this.isBookmarked,
42+
);
43+
}
2744
}
2845

2946
class MockLibrary {

example/lib/reader_app/library_screen.dart

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ class _LibraryScreenState extends State<LibraryScreen> {
1515

1616
List<Book> get _filteredBooks {
1717
return MockLibrary.books.where((book) {
18-
final matchesSearch = book.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
19-
book.author.toLowerCase().contains(_searchQuery.toLowerCase());
18+
final matchesSearch =
19+
book.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
20+
book.author.toLowerCase().contains(_searchQuery.toLowerCase());
2021
final matchesBookmark = !_showOnlyBookmarked || book.isBookmarked;
2122
return matchesSearch && matchesBookmark;
2223
}).toList();
@@ -31,11 +32,15 @@ class _LibraryScreenState extends State<LibraryScreen> {
3132
body: CustomScrollView(
3233
slivers: [
3334
SliverAppBar.large(
34-
title: const Text('My Library', style: TextStyle(fontWeight: FontWeight.bold)),
35+
title: const Text('My Library',
36+
style: TextStyle(fontWeight: FontWeight.bold)),
3537
actions: [
3638
IconButton(
37-
icon: Icon(_showOnlyBookmarked ? Icons.bookmark : Icons.bookmark_border),
38-
onPressed: () => setState(() => _showOnlyBookmarked = !_showOnlyBookmarked),
39+
icon: Icon(_showOnlyBookmarked
40+
? Icons.bookmark
41+
: Icons.bookmark_border),
42+
onPressed: () =>
43+
setState(() => _showOnlyBookmarked = !_showOnlyBookmarked),
3944
),
4045
],
4146
),
@@ -58,13 +63,16 @@ class _LibraryScreenState extends State<LibraryScreen> {
5863
),
5964
),
6065
),
61-
62-
if (recentBooks.isNotEmpty && _searchQuery.isEmpty && !_showOnlyBookmarked) ...[
66+
67+
if (recentBooks.isNotEmpty &&
68+
_searchQuery.isEmpty &&
69+
!_showOnlyBookmarked) ...[
6370
const SliverToBoxAdapter(
6471
child: Padding(
6572
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 8),
66-
child: Text('Continue Reading',
67-
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
73+
child: Text('Continue Reading',
74+
style:
75+
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
6876
),
6977
),
7078
SliverToBoxAdapter(
@@ -86,8 +94,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
8694
const SliverToBoxAdapter(
8795
child: Padding(
8896
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 16),
89-
child: Text('All Books',
90-
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
97+
child: Text('All Books',
98+
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
9199
),
92100
),
93101

@@ -99,7 +107,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
99107
children: [
100108
Icon(Icons.library_books, size: 64, color: Colors.grey),
101109
SizedBox(height: 16),
102-
Text('No books found', style: TextStyle(color: Colors.grey)),
110+
Text('No books found',
111+
style: TextStyle(color: Colors.grey)),
103112
],
104113
),
105114
),
@@ -140,27 +149,32 @@ class _LibraryScreenState extends State<LibraryScreen> {
140149
color: Colors.white,
141150
borderRadius: BorderRadius.circular(16),
142151
boxShadow: [
143-
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 10),
152+
BoxShadow(
153+
color: Colors.black.withValues(alpha: 0.05), blurRadius: 10),
144154
],
145155
),
146156
child: Row(
147157
children: [
148158
ClipRRect(
149159
borderRadius: BorderRadius.circular(8),
150-
child: Image.network(book.coverUrl, width: 80, height: 120, fit: BoxFit.cover),
160+
child: Image.network(book.coverUrl,
161+
width: 80, height: 120, fit: BoxFit.cover),
151162
),
152163
const SizedBox(width: 16),
153164
Expanded(
154165
child: Column(
155166
mainAxisAlignment: MainAxisAlignment.center,
156167
crossAxisAlignment: CrossAxisAlignment.start,
157168
children: [
158-
Text(book.title,
159-
style: const TextStyle(fontWeight: FontWeight.bold),
160-
maxLines: 2, overflow: TextOverflow.ellipsis),
161-
Text(book.author, style: const TextStyle(fontSize: 12, color: Colors.grey)),
169+
Text(book.title,
170+
style: const TextStyle(fontWeight: FontWeight.bold),
171+
maxLines: 2,
172+
overflow: TextOverflow.ellipsis),
173+
Text(book.author,
174+
style: const TextStyle(fontSize: 12, color: Colors.grey)),
162175
const Spacer(),
163-
const Text('Last read: 2 hours ago', style: TextStyle(fontSize: 10, color: Colors.grey)),
176+
const Text('Last read: 2 hours ago',
177+
style: TextStyle(fontSize: 10, color: Colors.grey)),
164178
const SizedBox(height: 4),
165179
ClipRRect(
166180
borderRadius: BorderRadius.circular(2),
@@ -206,7 +220,8 @@ class _LibraryScreenState extends State<LibraryScreen> {
206220
),
207221
if (book.isBookmarked)
208222
const Positioned(
209-
top: 8, right: 8,
223+
top: 8,
224+
right: 8,
210225
child: Icon(Icons.bookmark, color: Colors.amber, size: 28),
211226
),
212227
],

0 commit comments

Comments
 (0)