Skip to content

Commit 5fa1dfe

Browse files
authored
Merge pull request #25 from RZEROSTERN/feat/phase-4-quality-and-safety
Phase 4 completed: Code quality and safety
2 parents 0f5fb4f + f26bbc2 commit 5fa1dfe

15 files changed

Lines changed: 1157 additions & 32 deletions

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## [0.4.0] - 2026-03-25 — Phase 4: Quality & Safety
2+
3+
### Null-safety hardening
4+
* `JsonDocumentSource.parse()` now wraps `jsonDecode` in try/catch — invalid JSON returns an empty `BlockDocument` instead of throwing.
5+
* `HeaderMapper``level` field uses `is int` guard instead of a bare `as int?` cast.
6+
* `ImageMapper``file` object and all three `bool` fields (`withBorder`, `stretched`, `withBackground`) use safe type checks.
7+
* `AttachesMapper``file` object uses `is Map` guard; `size` handles both `int` and `num` (double) via switch expression.
8+
* `EmbedMapper``width` and `height` handle both `int` and `num` via switch expression.
9+
* `LinkToolMapper``image` sub-map uses `is Map` guard.
10+
* `TableMapper``withHeadings` uses `is bool` guard.
11+
* `ChecklistMapper``checked` uses `is bool` guard.
12+
13+
### Unknown block type logging
14+
* `JsonDocumentSource` emits a `dart:developer` `log` entry (`name: 'EditorJSFlutter'`) when an unknown block type is silently dropped during parsing.
15+
16+
### Tests (91 total)
17+
* **`test/unit/html_sanitizer_test.dart`** (9 tests) — script tags, iframes, event handlers, edge cases.
18+
* **`test/unit/mappers_test.dart`** (46 tests) — all 14 mappers; happy path, missing fields, wrong-type fields.
19+
* **`test/unit/editor_controller_test.dart`** (17 tests) — all mutations, full undo/redo state machine.
20+
* **`test/unit/json_document_source_test.dart`** — parse, unknown-type drop, invalid JSON, round-trip.
21+
* **`test/widget/renderers_test.dart`** (9 tests) — header, delimiter, code, warning, checklist, embed, attaches.
22+
23+
---
24+
125
## [0.3.0] - 2026-03-25 — Phase 3: Editor features
226

327
### Undo / redo

lib/src/data/datasources/json_document_source.dart

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'dart:developer' as dev;
23

34
import '../../domain/entities/block_document.dart';
45
import '../../domain/entities/block_entity.dart';
@@ -13,15 +14,41 @@ class JsonDocumentSource implements DocumentRepository {
1314

1415
@override
1516
BlockDocument parse(String jsonString) {
16-
final Map<String, dynamic> root = jsonDecode(jsonString) as Map<String, dynamic>;
17-
final rawBlocks = (root['blocks'] as List<dynamic>?) ?? [];
17+
late final Map<String, dynamic> root;
18+
try {
19+
root = jsonDecode(jsonString) as Map<String, dynamic>;
20+
} on FormatException catch (_) {
21+
return BlockDocument(
22+
time: DateTime.now().millisecondsSinceEpoch,
23+
version: '',
24+
blocks: const [],
25+
);
26+
} on TypeError catch (_) {
27+
return BlockDocument(
28+
time: DateTime.now().millisecondsSinceEpoch,
29+
version: '',
30+
blocks: const [],
31+
);
32+
}
33+
34+
final rawBlocks =
35+
root['blocks'] is List ? root['blocks'] as List<dynamic> : <dynamic>[];
1836

1937
final blocks = rawBlocks
2038
.whereType<Map<String, dynamic>>()
2139
.map((b) {
22-
final type = (b['type'] as String?) ?? '';
23-
final data = (b['data'] as Map<String, dynamic>?) ?? {};
24-
return registry.parse(type, data);
40+
final type = b['type'] is String ? b['type'] as String : '';
41+
final data = b['data'] is Map<String, dynamic>
42+
? b['data'] as Map<String, dynamic>
43+
: <String, dynamic>{};
44+
final entity = registry.parse(type, data);
45+
if (entity == null && type.isNotEmpty) {
46+
dev.log(
47+
'Unknown block type ignored: "$type"',
48+
name: 'EditorJSFlutter',
49+
);
50+
}
51+
return entity;
2552
})
2653
.whereType<BlockEntity>()
2754
.toList();

lib/src/data/mappers/attaches_mapper.dart

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ class AttachesMapper implements BlockMapper<AttachesBlock> {
99

1010
@override
1111
AttachesBlock fromJson(Map<String, dynamic> data) {
12-
final file = data['file'] as Map<String, dynamic>?;
12+
final file = data['file'] is Map ? data['file'] as Map<String, dynamic> : null;
1313
return AttachesBlock(
14-
url: (file?['url'] as String?) ?? '',
15-
name: file?['name'] as String?,
16-
extension: file?['extension'] as String?,
17-
size: file?['size'] as int?,
18-
title: data['title'] as String?,
14+
url: file != null && file['url'] is String ? file['url'] as String : '',
15+
name: file != null && file['name'] is String ? file['name'] as String : null,
16+
extension: file != null && file['extension'] is String
17+
? file['extension'] as String
18+
: null,
19+
size: switch (file?['size']) {
20+
int v => v,
21+
num v => v.toInt(),
22+
_ => null,
23+
},
24+
title: data['title'] is String ? data['title'] as String : null,
1925
);
2026
}
2127
}

lib/src/data/mappers/checklist_mapper.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ class ChecklistMapper implements BlockMapper<ChecklistBlock> {
99

1010
@override
1111
ChecklistBlock fromJson(Map<String, dynamic> data) {
12-
final rawItems = (data['items'] as List<dynamic>?) ?? [];
12+
final rawItems =
13+
data['items'] is List ? data['items'] as List<dynamic> : <dynamic>[];
1314
return ChecklistBlock(
1415
items: rawItems.whereType<Map<String, dynamic>>().map((i) {
1516
return ChecklistItem(
16-
text: (i['text'] as String?) ?? '',
17-
checked: (i['checked'] as bool?) ?? false,
17+
text: i['text'] is String ? i['text'] as String : '',
18+
checked: i['checked'] is bool ? i['checked'] as bool : false,
1819
);
1920
}).toList(),
2021
);

lib/src/data/mappers/embed_mapper.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,20 @@ class EmbedMapper implements BlockMapper<EmbedBlock> {
99

1010
@override
1111
EmbedBlock fromJson(Map<String, dynamic> data) => EmbedBlock(
12-
service: (data['service'] as String?) ?? '',
13-
source: (data['source'] as String?) ?? '',
14-
embed: (data['embed'] as String?) ?? '',
15-
width: data['width'] as int?,
16-
height: data['height'] as int?,
17-
caption: data['caption'] as String?,
12+
service: data['service'] is String ? data['service'] as String : '',
13+
source: data['source'] is String ? data['source'] as String : '',
14+
embed: data['embed'] is String ? data['embed'] as String : '',
15+
width: switch (data['width']) {
16+
int v => v,
17+
num v => v.toInt(),
18+
_ => null,
19+
},
20+
height: switch (data['height']) {
21+
int v => v,
22+
num v => v.toInt(),
23+
_ => null,
24+
},
25+
caption:
26+
data['caption'] is String ? data['caption'] as String : null,
1827
);
1928
}

lib/src/data/mappers/header_mapper.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ class HeaderMapper implements BlockMapper<HeaderBlock> {
99

1010
@override
1111
HeaderBlock fromJson(Map<String, dynamic> data) => HeaderBlock(
12-
text: (data['text'] as String?) ?? '',
13-
level: (data['level'] as int?) ?? 1,
12+
text: data['text'] is String
13+
? data['text'] as String
14+
: (data['text']?.toString() ?? ''),
15+
level: data['level'] is int ? data['level'] as int : 1,
1416
);
1517
}

lib/src/data/mappers/image_mapper.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ class ImageMapper implements BlockMapper<ImageBlock> {
99

1010
@override
1111
ImageBlock fromJson(Map<String, dynamic> data) {
12-
final file = data['file'] as Map<String, dynamic>?;
12+
final file = data['file'] is Map ? data['file'] as Map<String, dynamic> : null;
1313
return ImageBlock(
14-
url: (file?['url'] as String?) ?? '',
15-
caption: data['caption'] as String?,
16-
withBorder: (data['withBorder'] as bool?) ?? false,
17-
stretched: (data['stretched'] as bool?) ?? false,
18-
withBackground: (data['withBackground'] as bool?) ?? false,
14+
url: file != null && file['url'] is String ? file['url'] as String : '',
15+
caption: data['caption'] is String ? data['caption'] as String : null,
16+
withBorder: data['withBorder'] is bool ? data['withBorder'] as bool : false,
17+
stretched: data['stretched'] is bool ? data['stretched'] as bool : false,
18+
withBackground: data['withBackground'] is bool ? data['withBackground'] as bool : false,
1919
);
2020
}
2121
}

lib/src/data/mappers/link_tool_mapper.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class LinkToolMapper implements BlockMapper<LinkToolBlock> {
1010
@override
1111
LinkToolBlock fromJson(Map<String, dynamic> data) {
1212
final rawMeta = data['meta'] as Map<String, dynamic>?;
13-
final rawImage = rawMeta?['image'] as Map<String, dynamic>?;
13+
final rawImage = rawMeta?['image'] is Map ? rawMeta!['image'] as Map<String, dynamic> : null;
1414

1515
final meta = rawMeta == null
1616
? null

lib/src/data/mappers/table_mapper.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ class TableMapper implements BlockMapper<TableBlock> {
99

1010
@override
1111
TableBlock fromJson(Map<String, dynamic> data) {
12-
final rawContent = (data['content'] as List<dynamic>?) ?? [];
12+
final rawContent =
13+
data['content'] is List ? data['content'] as List<dynamic> : <dynamic>[];
1314
return TableBlock(
14-
withHeadings: (data['withHeadings'] as bool?) ?? false,
15+
withHeadings: data['withHeadings'] is bool ? data['withHeadings'] as bool : false,
1516
content: rawContent.map((row) {
1617
final List<dynamic> rowList = row is List ? row : <dynamic>[];
1718
return rowList.map((cell) => cell.toString()).toList();

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: editorjs_flutter
22
description: A new Flutter package project.
3-
version: 0.3.0
3+
version: 0.4.0
44
homepage: https://github.com/RZEROSTERN/editorjs-flutter
55

66
environment:

0 commit comments

Comments
 (0)