11import 'dart:async' ;
22import 'dart:convert' ;
33import 'dart:io' ;
4+ import 'dart:math' as math;
45
56import 'package:file_picker/file_picker.dart' ;
67import 'package:flutter/foundation.dart' ;
@@ -10,6 +11,7 @@ import 'package:htmltopdfwidgets/htmltopdfwidgets.dart' as html2pdf;
1011import 'package:markdown/markdown.dart' as md;
1112import 'package:markdown_editor/device_preference_notifier.dart' ;
1213import 'package:markdown_editor/l10n/generated/app_localizations.dart' ;
14+ import 'package:markdown_editor/widgets/MarkdownBody/custom_checkbox.dart' ;
1315import 'package:markdown_editor/widgets/MarkdownBody/custom_image_config.dart' ;
1416import 'package:markdown_editor/widgets/MarkdownBody/custom_text_node.dart' ;
1517import 'package:markdown_editor/widgets/MarkdownBody/latex_node.dart' ;
@@ -33,6 +35,9 @@ class _HomeState extends State<Home> {
3335 static const _methodChannel = MethodChannel (
3436 "com.adeeteya.markdown_editor/channel" ,
3537 );
38+ static final RegExp _taskListLinePattern = RegExp (
39+ r'^(?:[-+*]|\d+[.)])\s+\[(?: |x|X)\]' ,
40+ );
3641 String _filePath = "/storage/emulated/0/Download" ;
3742 String _fileName = 'Markdown' ;
3843 bool _isPreview = false ;
@@ -263,6 +268,7 @@ class _HomeState extends State<Home> {
263268 final config = isDark
264269 ? MarkdownConfig .darkConfig
265270 : MarkdownConfig .defaultConfig;
271+ int checkboxIndex = - 1 ;
266272 return Card (
267273 child: SizedBox (
268274 height: double .infinity,
@@ -283,7 +289,22 @@ class _HomeState extends State<Home> {
283289 CustomTextNode (node.textContent, config, visitor),
284290 richTextBuilder: Text .rich,
285291 ),
286- config: config.copy (configs: [CustomImgConfig ()]),
292+ config: config.copy (
293+ configs: [
294+ CustomImgConfig (),
295+ CheckBoxConfig (
296+ builder: (checked) {
297+ checkboxIndex++ ;
298+ final currentIndex = checkboxIndex;
299+ return CustomCheckbox (
300+ key: ValueKey ('markdown-task-checkbox-$currentIndex ' ),
301+ checked: checked,
302+ onChanged: () => _toggleTaskListCheckbox (currentIndex),
303+ );
304+ },
305+ ),
306+ ],
307+ ),
287308 ),
288309 ),
289310 ),
@@ -346,6 +367,70 @@ class _HomeState extends State<Home> {
346367 );
347368 }
348369
370+ List <int > _taskListMarkerPositions (String text) {
371+ final positions = < int > [];
372+ final lines = text.split ('\n ' );
373+ var offset = 0 ;
374+
375+ for (final line in lines) {
376+ var sanitizedLine = line;
377+ if (sanitizedLine.endsWith ('\r ' )) {
378+ sanitizedLine = sanitizedLine.substring (0 , sanitizedLine.length - 1 );
379+ }
380+ sanitizedLine = sanitizedLine.trimLeft ();
381+ while (sanitizedLine.startsWith ('>' )) {
382+ sanitizedLine = sanitizedLine.substring (1 ).trimLeft ();
383+ }
384+ if (_taskListLinePattern.hasMatch (sanitizedLine)) {
385+ final bracketIndex = line.indexOf ('[' );
386+ if (bracketIndex != - 1 ) {
387+ positions.add (offset + bracketIndex);
388+ }
389+ }
390+ offset += line.length + 1 ;
391+ }
392+
393+ return positions;
394+ }
395+
396+ void _toggleTaskListCheckbox (int index) {
397+ final markerPositions = _taskListMarkerPositions (_inputText);
398+ if (index < 0 || index >= markerPositions.length) {
399+ return ;
400+ }
401+ final markerStart = markerPositions[index];
402+ if (markerStart + 2 >= _inputText.length) {
403+ return ;
404+ }
405+ final currentState = _inputText[markerStart + 1 ];
406+ final newState = (currentState == 'x' || currentState == 'X' ) ? ' ' : 'x' ;
407+ final updatedText = _inputText.replaceRange (
408+ markerStart,
409+ markerStart + 3 ,
410+ '[$newState ]' ,
411+ );
412+ final currentSelection = _textEditingController.selection;
413+ final collapsedSelection = currentSelection.isValid
414+ ? TextSelection (
415+ baseOffset: math.min (
416+ currentSelection.baseOffset,
417+ updatedText.length,
418+ ),
419+ extentOffset: math.min (
420+ currentSelection.extentOffset,
421+ updatedText.length,
422+ ),
423+ )
424+ : TextSelection .collapsed (offset: updatedText.length);
425+ setState (() {
426+ _inputText = updatedText;
427+ _textEditingController.value = TextEditingValue (
428+ text: updatedText,
429+ selection: collapsedSelection,
430+ );
431+ });
432+ }
433+
349434 @override
350435 Widget build (BuildContext context) {
351436 return GestureDetector (
0 commit comments