diff --git a/mobile-app/lib/models/learn/curriculum_model.dart b/mobile-app/lib/models/learn/curriculum_model.dart index 8f8063792..c84864a40 100644 --- a/mobile-app/lib/models/learn/curriculum_model.dart +++ b/mobile-app/lib/models/learn/curriculum_model.dart @@ -69,7 +69,12 @@ class Block { }); static bool checkIfStepBased(String superblock) { - return superblock == '2022/responsive-web-design'; + List stepBasedSuperBlocks = [ + '2022/responsive-web-design', + 'javascript-algorithms-and-data-structures-v8' + ]; + + return stepBasedSuperBlocks.contains(superblock); } factory Block.fromJson( diff --git a/mobile-app/lib/ui/views/learn/block/block_view.dart b/mobile-app/lib/ui/views/learn/block/block_view.dart index 478ce8f53..b0ee341b6 100644 --- a/mobile-app/lib/ui/views/learn/block/block_view.dart +++ b/mobile-app/lib/ui/views/learn/block/block_view.dart @@ -7,6 +7,7 @@ import 'package:freecodecamp/ui/views/learn/block/block_viewmodel.dart'; import 'package:freecodecamp/ui/views/learn/widgets/download_button_widget.dart'; import 'package:freecodecamp/ui/views/learn/widgets/open_close_icon_widget.dart'; import 'package:freecodecamp/ui/views/learn/widgets/progressbar_widget.dart'; +import 'package:freecodecamp/ui/views/news/html_handler/html_handler.dart'; import 'package:freecodecamp/ui/widgets/drawer_widget/drawer_widget_view.dart'; import 'package:stacked/stacked.dart'; @@ -46,6 +47,8 @@ class BlockView extends StatelessWidget { bool hasProgress = calculateProgress > 0; + HTMLParser parser = HTMLParser(context: context); + return Column( children: [ BlockHeader( @@ -75,15 +78,9 @@ class BlockView extends StatelessWidget { vertical: 8, horizontal: 16, ), - child: Text( - blockString, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - height: 1.2, - fontFamily: 'Lato', - color: Colors.white.withOpacity(0.87), - ), + child: Wrap( + children: parser.parse('

$blockString

', + fontColor: Colors.white), ), ), if (model.isDev && !isCertification) @@ -221,7 +218,18 @@ class BlockHeader extends StatelessWidget { model.isOpen, ); }, - minVerticalPadding: 24, + minVerticalPadding: 18, + leading: !isCertification + ? model.challengesCompleted == block.challenges.length + ? const Icon( + Icons.check_circle, + size: 20, + ) + : const Icon( + Icons.circle_outlined, + size: 20, + ) + : null, trailing: !isCertification ? OpenCloseIcon( block: block, @@ -229,25 +237,11 @@ class BlockHeader extends StatelessWidget { ) : null, title: Row( - crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (!isCertification) - Padding( - padding: const EdgeInsets.fromLTRB(0, 8, 8, 8), - child: model.challengesCompleted == block.challenges.length - ? const Icon( - Icons.check_circle, - size: 20, - ) - : const Icon( - Icons.circle_outlined, - size: 20, - ), - ), Expanded( child: Text( block.name, - maxLines: 2, + maxLines: model.isOpen ? 5 : 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.bold, diff --git a/mobile-app/lib/ui/views/learn/index.html b/mobile-app/lib/ui/views/learn/index.html new file mode 100644 index 000000000..f6731778a --- /dev/null +++ b/mobile-app/lib/ui/views/learn/index.html @@ -0,0 +1,228 @@ +[log] + + + + + + + + + + diff --git a/mobile-app/lib/ui/views/learn/test_runner.dart b/mobile-app/lib/ui/views/learn/test_runner.dart index f54564445..65ec22e94 100644 --- a/mobile-app/lib/ui/views/learn/test_runner.dart +++ b/mobile-app/lib/ui/views/learn/test_runner.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:freecodecamp/app/app.locator.dart'; import 'package:freecodecamp/enums/ext_type.dart'; @@ -46,40 +47,18 @@ class TestRunner extends BaseViewModel { document.getElementsByTagName('HEAD')[0].append(node); } - String? script = await returnScript( - challenge.files[0].ext, - challenge, - testing: testing, - ); - - Document scriptToNode = parse(script); - - Node bodyNode = - scriptToNode.getElementsByTagName('BODY').first.children.isNotEmpty - ? scriptToNode.getElementsByTagName('BODY').first.children.first - : scriptToNode.getElementsByTagName('HEAD').first.children.first; - - document.body!.append(bodyNode); + String script = await returnScript( + challenge.files[0].ext, + challenge, + testing: testing, + ) ?? + ''; - // Get user's script elements. + Document parsedScript = parse(script); + Node scriptNode = parsedScript.getElementsByTagName('SCRIPT')[0]; + document.head!.append(scriptNode); - if (challenge.files[0].ext == Ext.html) { - String htmlFile = await fileService.getFirstFileFromCache( - challenge, - Ext.html, - testing: testing, - ); - - Document parseCacheDocument = parse(htmlFile); - - List scriptElements = parseCacheDocument.querySelectorAll( - 'SCRIPT', - ); - - for (int i = 0; i < scriptElements.length; i++) { - document.body!.append(scriptElements[i]); - } - } + log(document.outerHtml); if (!testing) { controller!.loadData( @@ -89,6 +68,7 @@ class TestRunner extends BaseViewModel { ); } + log(document.outerHtml); return document.outerHtml; } @@ -105,12 +85,13 @@ class TestRunner extends BaseViewModel { return parsedTest; } - // This function is used in the returnScript function to correctly parse - // HTML challenge (user code) it will firstly get the file from the cache, (it returns the first challenge file if in testing mode) - // Otherwise it will return the first instance of that challenge in the cache. Next will be adding the style tags (only if - // linked) + // This Function parses the CODE FROM THE USER into one HTML file. This function + // itself has nothing to do with parsing the test-runner document. - Future htmlFlow( + // CODE from the user is concatenated: HTML,CSS,JS and does not get executed; + // meaning it is all text; Example: https://pastebin.com/v75dQ1xa + + Future parseCodeVariable( Challenge challenge, Ext ext, { bool testing = false, @@ -125,41 +106,111 @@ class TestRunner extends BaseViewModel { firstHTMlfile, ); - String parsedWithStyleTags = await fileService.parseCssDocmentsAsStyleTags( - challenge, - firstHTMlfile, - testing: testing, - ); + // Concatenate CSS + List cssFiles = + challenge.files.where((file) => file.ext == Ext.css).toList(); - if (challenge.id != '646c48df8674cf2b91020ecc') { - firstHTMlfile = fileService.changeActiveFileLinks( - parsedWithStyleTags, + if (cssFiles.isNotEmpty) { + String file = await fileService.getExactFileFromCache( + challenge, + cssFiles[0], + testing: testing, ); + + firstHTMlfile += file; + } + + List jsFiles = + challenge.files.where((file) => file.ext == Ext.js).toList(); + + if (jsFiles.isNotEmpty) { + String file = await fileService.getExactFileFromCache( + challenge, + jsFiles[0], + testing: testing, + ); + + firstHTMlfile += file; } return firstHTMlfile; } - // This function parses the JavaScript code so that it has a head and tail (code) - // It is used in the returnScript function to correctly parse JavaScript. + // parsed frame document (also now as Iframe) this will be the document that + // is tested against. Meaning the __runtTest function is assigned to the document + // object. - Future javaScritpFlow( + Future parseFrameDocument( Challenge challenge, Ext ext, { - bool testing = false, + testing = false, }) async { - var content = testing - ? challenge.files[0].contents - : await fileService.getFirstFileFromCache(challenge, Ext.js); - - content = fileService.changeActiveFileLinks( - content, + String html = await fileService.getFirstFileFromCache( + challenge, + ext, + testing: testing, ); - return content - .replaceAll('\\', '\\\\') - .replaceAll('`', '\\`') - .replaceAll('\$', r'\$'); + Document document = parse(html); + + List cssFiles = + challenge.files.where((file) => file.ext == Ext.css).toList(); + List jsFiles = + challenge.files.where((file) => file.ext == Ext.js).toList(); + + if (cssFiles.isNotEmpty) { + String css = await fileService.getExactFileFromCache( + challenge, + cssFiles[0], + testing: testing, + ); + + List styleElements = document.getElementsByTagName('link'); + + for (Element element in styleElements) { + if (element.attributes.containsKey('href')) { + if (element.attributes['href'] == 'styles.css') { + element.attributes.removeWhere((key, value) => key == 'href'); + element.attributes.addAll({'data-href': 'styles.css'}); + } else if (element.attributes['href'] == './styles.css') { + element.attributes.removeWhere((key, value) => key == 'href'); + element.attributes.addAll({'data-href': './styles.css'}); + } + } + } + + Element style = document.createElement('style'); + style.innerHtml = css; + style.id = 'fcc-injected-styles'; + + log(style.attributes.entries.toString()); + + document.head!.append(style); + } + + if (jsFiles.isNotEmpty) { + String js = await fileService.getExactFileFromCache( + challenge, + jsFiles[0], + testing: testing, + ); + + List scripts = document.getElementsByTagName('script'); + + for (Element script in scripts) { + script.remove(); + } + // We need to create a custom named tag instead of using the default script + // tag as the Dart html parser breaks when parsing script tags that are inside + // JavaScript strig variables. E.g. for variable "doc". + Element script = document.createElement('custom-inject'); + script.innerHtml = js; + script.attributes.addAll({'data-src': './script.js'}); + + document.body!.append(script); + } + + return document.outerHtml.toString(); } // This returns the script that needs to be run in the DOM. If the test in the document fail it will @@ -171,33 +222,25 @@ class TestRunner extends BaseViewModel { Challenge challenge, { bool testing = false, }) async { - List? scriptFile = - challenge.files.where((element) => element.name == 'script').toList(); - String? code; - if (ext == Ext.html || ext == Ext.css) { - code = await htmlFlow( - challenge, - ext, - testing: testing, - ); - } else if (ext == Ext.js) { - code = await javaScritpFlow( - challenge, - ext, - testing: testing, - ); - } + // TODO: Have to update code generation to handle only JS files + // if (ext == Ext.html || ext == Ext.css) { + code = await parseCodeVariable( + challenge, + ext, + testing: testing, + ); + // } - if (ext == Ext.html || ext == Ext.css) { - String tail = challenge.files[0].tail ?? ''; + // if (ext == Ext.html || ext == Ext.css) { + String tail = challenge.files[0].tail ?? ''; - return ''''''; - } else if (ext == Ext.js) { - String? head = challenge.files[0].head ?? ''; - String? tail = (challenge.files[0].tail ?? '').replaceAll('\\', '\\\\'); - - return '''