From 06ce34e95ed5be9892e0507ab428585752b07d73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:53:18 +0000 Subject: [PATCH 1/4] Initial plan From 443ac1435f7745abb1f5ae1923ab55eb939b5423 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:00:55 +0000 Subject: [PATCH 2/4] Update ChallengeType enum to match main repository Co-authored-by: huyenltnguyen <25715018+huyenltnguyen@users.noreply.github.com> --- .../lib/models/learn/challenge_model.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/models/learn/challenge_model.dart b/mobile-app/lib/models/learn/challenge_model.dart index 44ada1d9c..4d3c458aa 100644 --- a/mobile-app/lib/models/learn/challenge_model.dart +++ b/mobile-app/lib/models/learn/challenge_model.dart @@ -5,8 +5,7 @@ enum ChallengeType { html, // 0 js, // 1 backend, // 2 - zipline, // 3 - frontEndProject, // 3 + zipline, // 3 (frontEndProject is also 3 in main repo, but omitted to avoid duplicate enum values) backEndProject, // 4 jsProject, // 5 modern, // 6 @@ -20,7 +19,20 @@ enum ChallengeType { multifileCertProject, // 14 theOdinProject, // 15 colab, // 16 - exam // 17 + exam, // 17 + msTrophy, // 18 + multipleChoice, // 19 + python, // 20 + dialogue, // 21 + fillInTheBlank, // 22 + multifilePythonCertProject, // 23 + generic, // 24 + lab, // 25 + jsLab, // 26 + pyLab, // 27 + dailyChallengeJs, // 28 + dailyChallengePy, // 29 + examDownload // 30 } enum HelpCategory { From 00977e2f9be898648d11fe9c0aafe7c39f1f72f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:26:00 +0000 Subject: [PATCH 3/4] Update Challenge class to use ChallengeType enum instead of int Co-authored-by: huyenltnguyen <25715018+huyenltnguyen@users.noreply.github.com> --- .../integration_test/test_runner/curriculum_tests.dart | 4 ++-- mobile-app/lib/models/learn/challenge_model.dart | 9 ++++++--- mobile-app/lib/service/learn/learn_service.dart | 2 +- .../lib/ui/views/learn/challenge/challenge_view.dart | 6 +++--- .../ui/views/learn/challenge/challenge_viewmodel.dart | 4 ++-- .../views/learn/challenge/templates/template_view.dart | 4 ++-- mobile-app/lib/ui/views/learn/test_runner.dart | 4 ++-- mobile-app/test/unit/challenge_utils_test.dart | 2 +- mobile-app/test/unit/learn_file_controller_test.dart | 8 ++++---- 9 files changed, 23 insertions(+), 20 deletions(-) diff --git a/mobile-app/integration_test/test_runner/curriculum_tests.dart b/mobile-app/integration_test/test_runner/curriculum_tests.dart index 8bab0e6b0..28ebc0d13 100644 --- a/mobile-app/integration_test/test_runner/curriculum_tests.dart +++ b/mobile-app/integration_test/test_runner/curriculum_tests.dart @@ -213,7 +213,7 @@ void main() { Challenge challenge = Challenge.fromJson(currChallenge); print( - 'Challenge: ${challenge.id} - ${challenge.title} - ${challenge.challengeType}'); + 'Challenge: ${challenge.id} - ${challenge.title} - ${challenge.challengeTypeIndex}'); String getLines(String contents, [List? range]) { if (range == null || range.isEmpty) { @@ -254,7 +254,7 @@ void main() { babelWebView.webViewController, testing: true, ), - 'workerType': builder.getWorkerType(challenge.challengeType), + 'workerType': builder.getWorkerType(challenge.challengeTypeIndex), 'combinedCode': await builder.combinedCode(challenge), 'editableRegionContent': editableRegion, 'hooks': { diff --git a/mobile-app/lib/models/learn/challenge_model.dart b/mobile-app/lib/models/learn/challenge_model.dart index 4d3c458aa..6bbac7bb7 100644 --- a/mobile-app/lib/models/learn/challenge_model.dart +++ b/mobile-app/lib/models/learn/challenge_model.dart @@ -66,7 +66,7 @@ class Challenge { final String dashedName; final String superBlock; final String? videoId; - final int challengeType; + final ChallengeType challengeType; final HelpCategory helpCategory; final String? explanation; final String transcript; @@ -113,6 +113,9 @@ class Challenge { required this.hooks, }); + // Backward compatibility getter for challengeType as int + int get challengeTypeIndex => challengeType.index; + factory Challenge.fromJson(Map data) { return Challenge( id: data['id'], @@ -123,7 +126,7 @@ class Challenge { dashedName: data['dashedName'], superBlock: data['superBlock'], videoId: data['videoId'], - challengeType: data['challengeType'], + challengeType: ChallengeType.values[data['challengeType']], helpCategory: HelpCategory.fromValue(data['helpCategory']), explanation: data['explanation'] ?? '', transcript: data['transcript'] ?? '', @@ -169,7 +172,7 @@ class Challenge { 'dashedName': challenge.dashedName, 'superBlock': challenge.superBlock, 'videoId': challenge.videoId, - 'challengeType': challenge.challengeType, + 'challengeType': challenge.challengeType.index, 'helpCategory': challenge.helpCategory.value, 'transcript': challenge.transcript, 'tests': challenge.tests diff --git a/mobile-app/lib/service/learn/learn_service.dart b/mobile-app/lib/service/learn/learn_service.dart index a369685b8..53e94c4c2 100644 --- a/mobile-app/lib/service/learn/learn_service.dart +++ b/mobile-app/lib/service/learn/learn_service.dart @@ -51,7 +51,7 @@ class LearnService { String? solutionLink, }) async { String challengeId = challenge.id; - int challengeType = challenge.challengeType; + int challengeType = challenge.challengeTypeIndex; Response submitTypesRes = await _dio.get('$baseUrlV2/submit-types.json'); Map submitTypes = submitTypesRes.data; diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart index f47dfc79e..bfb72f209 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart @@ -282,7 +282,7 @@ class ChallengeView extends StatelessWidget { arguments: { 'userCode': '', 'workerType': - builder.getWorkerType(challenge.challengeType), + builder.getWorkerType(challenge.challengeTypeIndex), 'combinedCode': '', 'editableRegionContent': '', 'hooks': { @@ -554,7 +554,7 @@ class ChallengeView extends StatelessWidget { model.scaffoldKey.currentState?.openEndDrawer(); }, ), - if (challenge.challengeType != 1 && challenge.challengeType != 26) + if (challenge.challengeTypeIndex != 1 && challenge.challengeTypeIndex != 26) _panelIconButton( isActive: model.showPreview, icon: Icons.remove_red_eye_outlined, @@ -579,7 +579,7 @@ class ChallengeView extends StatelessWidget { } Widget testList(Challenge challenge, ChallengeViewModel model) { - log(challenge.challengeType.toString()); + log(challenge.challengeTypeIndex.toString()); return ListView.builder( shrinkWrap: true, physics: const ClampingScrollPhysics(), diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart index 324e0c2b2..de4663e0f 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart @@ -614,7 +614,7 @@ class ChallengeViewModel extends BaseViewModel { setTestConsoleMessages = ['

// running tests

']; // Get user code console messages - if (challenge!.challengeType == 1 || challenge!.challengeType == 26) { + if (challenge!.challengeTypeIndex == 1 || challenge!.challengeTypeIndex == 26) { final evalResult = await testController!.callAsyncJavaScript( functionBody: await builder.buildUserCode( challenge!, @@ -638,7 +638,7 @@ class ChallengeViewModel extends BaseViewModel { challenge!, _babelWebView.webViewController, ), - 'workerType': builder.getWorkerType(challenge!.challengeType), + 'workerType': builder.getWorkerType(challenge!.challengeTypeIndex), 'combinedCode': await builder.combinedCode(challenge!), 'editableRegionContent': editableRegionContent, 'hooks': { diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart index 212e9c902..927853530 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart @@ -34,7 +34,7 @@ class ChallengeTemplateView extends StatelessWidget { if (snapshot.hasData) { Challenge challenge = snapshot.data!; - int challengeType = challenge.challengeType; + int challengeType = challenge.challengeTypeIndex; List tiles = block.challengeTiles; int challNum = tiles.indexWhere((el) => el.id == challenge.id) + 1; @@ -88,7 +88,7 @@ class ChallengeTemplateView extends StatelessWidget { default: return Center( child: Text( - 'Unknown Challenge, info : ${challenge.challengeType}', + 'Unknown Challenge, info : ${challenge.challengeTypeIndex}', ), ); } diff --git a/mobile-app/lib/ui/views/learn/test_runner.dart b/mobile-app/lib/ui/views/learn/test_runner.dart index 711850780..20eac43ac 100644 --- a/mobile-app/lib/ui/views/learn/test_runner.dart +++ b/mobile-app/lib/ui/views/learn/test_runner.dart @@ -48,11 +48,11 @@ return testRes; }) async { String challengeFile = await fileService.getFirstFileFromCache( challenge, - getChallengeExt(challenge.challengeType), + getChallengeExt(challenge.challengeTypeIndex), testing: testing, ); - switch (challenge.challengeType) { + switch (challenge.challengeTypeIndex) { // JS-only challenges case 1: case 26: diff --git a/mobile-app/test/unit/challenge_utils_test.dart b/mobile-app/test/unit/challenge_utils_test.dart index 9983e80f9..34c2b2506 100644 --- a/mobile-app/test/unit/challenge_utils_test.dart +++ b/mobile-app/test/unit/challenge_utils_test.dart @@ -16,7 +16,7 @@ void main() { instructions: '', dashedName: 'challenge-$id', superBlock: 'superblock', - challengeType: 0, + challengeType: ChallengeType.html, tests: [], files: [], helpCategory: HelpCategory.htmlCss, diff --git a/mobile-app/test/unit/learn_file_controller_test.dart b/mobile-app/test/unit/learn_file_controller_test.dart index 6217ed878..5c1c289d4 100644 --- a/mobile-app/test/unit/learn_file_controller_test.dart +++ b/mobile-app/test/unit/learn_file_controller_test.dart @@ -18,7 +18,7 @@ void main() { transcript: 'this is the transcript', dashedName: 'hello-world', superBlock: '2022/responsive-web-design', - challengeType: 1, + challengeType: ChallengeType.js, helpCategory: HelpCategory.htmlCss, tests: [], hooks: Hooks(beforeAll: '', beforeEach: '', afterEach: ''), @@ -201,7 +201,7 @@ void main() { transcript: '', dashedName: 'css-test', superBlock: 'super block', - challengeType: 1, + challengeType: ChallengeType.js, helpCategory: HelpCategory.htmlCss, tests: [], hooks: Hooks(beforeAll: '', beforeEach: '', afterEach: ''), @@ -245,7 +245,7 @@ void main() { transcript: '', dashedName: 'css-test', superBlock: 'super block', - challengeType: 1, + challengeType: ChallengeType.js, helpCategory: HelpCategory.htmlCss, tests: [], hooks: Hooks(beforeAll: '', beforeEach: '', afterEach: ''), @@ -288,7 +288,7 @@ void main() { transcript: '', dashedName: 'js-test', superBlock: 'super block', - challengeType: 1, + challengeType: ChallengeType.js, helpCategory: HelpCategory.htmlCss, tests: [], hooks: Hooks(beforeAll: '', beforeEach: '', afterEach: ''), From 8bedc8b8d307ef121d66e5356c65f276f66a21bc Mon Sep 17 00:00:00 2001 From: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:33:45 +0700 Subject: [PATCH 4/4] refactor: use ChallengeType in place of int --- .../test_runner/curriculum_tests.dart | 4 +-- .../lib/models/learn/challenge_model.dart | 6 +--- .../learn/completed_challenge_model.dart | 6 ++-- .../lib/service/learn/learn_service.dart | 12 +++---- .../views/learn/challenge/challenge_view.dart | 7 ++-- .../learn/challenge/challenge_viewmodel.dart | 5 +-- .../challenge/templates/template_view.dart | 33 +++++++++--------- .../lib/ui/views/learn/test_runner.dart | 34 +++++++++---------- 8 files changed, 53 insertions(+), 54 deletions(-) diff --git a/mobile-app/integration_test/test_runner/curriculum_tests.dart b/mobile-app/integration_test/test_runner/curriculum_tests.dart index 28ebc0d13..2d45cec7d 100644 --- a/mobile-app/integration_test/test_runner/curriculum_tests.dart +++ b/mobile-app/integration_test/test_runner/curriculum_tests.dart @@ -213,7 +213,7 @@ void main() { Challenge challenge = Challenge.fromJson(currChallenge); print( - 'Challenge: ${challenge.id} - ${challenge.title} - ${challenge.challengeTypeIndex}'); + 'Challenge: ${challenge.id} - ${challenge.title} - ${challenge.challengeType.index}'); String getLines(String contents, [List? range]) { if (range == null || range.isEmpty) { @@ -254,7 +254,7 @@ void main() { babelWebView.webViewController, testing: true, ), - 'workerType': builder.getWorkerType(challenge.challengeTypeIndex), + 'workerType': builder.getWorkerType(challenge.challengeType), 'combinedCode': await builder.combinedCode(challenge), 'editableRegionContent': editableRegion, 'hooks': { diff --git a/mobile-app/lib/models/learn/challenge_model.dart b/mobile-app/lib/models/learn/challenge_model.dart index 6bbac7bb7..131d5c475 100644 --- a/mobile-app/lib/models/learn/challenge_model.dart +++ b/mobile-app/lib/models/learn/challenge_model.dart @@ -1,11 +1,10 @@ import 'package:freecodecamp/enums/ext_type.dart'; -// NOTE: For reference enum ChallengeType { html, // 0 js, // 1 backend, // 2 - zipline, // 3 (frontEndProject is also 3 in main repo, but omitted to avoid duplicate enum values) + frontEndProject, // 3 (zipline is also 3 in main repo, but omitted to avoid duplicate enum values) backEndProject, // 4 jsProject, // 5 modern, // 6 @@ -113,9 +112,6 @@ class Challenge { required this.hooks, }); - // Backward compatibility getter for challengeType as int - int get challengeTypeIndex => challengeType.index; - factory Challenge.fromJson(Map data) { return Challenge( id: data['id'], diff --git a/mobile-app/lib/models/learn/completed_challenge_model.dart b/mobile-app/lib/models/learn/completed_challenge_model.dart index 1c3c4458c..ad13de607 100644 --- a/mobile-app/lib/models/learn/completed_challenge_model.dart +++ b/mobile-app/lib/models/learn/completed_challenge_model.dart @@ -4,7 +4,7 @@ class CompletedChallenge { final String id; final String? solution; final String? githubLink; - final int? challengeType; + final ChallengeType? challengeType; final DateTime completedDate; final List files; @@ -22,7 +22,9 @@ class CompletedChallenge { id: data['id'], solution: data['solution'], githubLink: data['githubLink'], - challengeType: data['challengeType'], + challengeType: data['challengeType'] != null + ? ChallengeType.values[data['challengeType']] + : null, completedDate: DateTime.fromMillisecondsSinceEpoch(data['completedDate']), files: (data['files'] as List) .map((file) => ChallengeFile.fromJson(file)) diff --git a/mobile-app/lib/service/learn/learn_service.dart b/mobile-app/lib/service/learn/learn_service.dart index 53e94c4c2..4580175af 100644 --- a/mobile-app/lib/service/learn/learn_service.dart +++ b/mobile-app/lib/service/learn/learn_service.dart @@ -51,26 +51,26 @@ class LearnService { String? solutionLink, }) async { String challengeId = challenge.id; - int challengeType = challenge.challengeTypeIndex; + ChallengeType challengeType = challenge.challengeType; Response submitTypesRes = await _dio.get('$baseUrlV2/submit-types.json'); Map submitTypes = submitTypesRes.data; - switch (submitTypes[challengeType.toString()]) { + switch (submitTypes[challengeType.index.toString()]) { case 'tests': late Map payload; - if (challengeType == 14 || + if (challengeType == ChallengeType.multifileCertProject || challenge.block == 'javascript-algorithms-and-data-structures-projects') { payload = { 'id': challengeId, - 'challengeType': challengeType, + 'challengeType': challengeType.index, 'files': challengeFiles, }; } else { payload = { 'id': challengeId, - 'challengeType': challengeType, + 'challengeType': challengeType.index, }; } Response res = await _dio.post( @@ -91,7 +91,7 @@ class LearnService { case 'project.backEnd': Map payload = { 'id': challengeId, - 'challengeType': challengeType, + 'challengeType': challengeType.index, 'solution': solutionLink }; Response res = await _dio.post( diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart index bfb72f209..6d4b8d3a4 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_view.dart @@ -282,7 +282,7 @@ class ChallengeView extends StatelessWidget { arguments: { 'userCode': '', 'workerType': - builder.getWorkerType(challenge.challengeTypeIndex), + builder.getWorkerType(challenge.challengeType), 'combinedCode': '', 'editableRegionContent': '', 'hooks': { @@ -554,7 +554,8 @@ class ChallengeView extends StatelessWidget { model.scaffoldKey.currentState?.openEndDrawer(); }, ), - if (challenge.challengeTypeIndex != 1 && challenge.challengeTypeIndex != 26) + if (challenge.challengeType != ChallengeType.js && + challenge.challengeType != ChallengeType.jsLab) _panelIconButton( isActive: model.showPreview, icon: Icons.remove_red_eye_outlined, @@ -579,7 +580,7 @@ class ChallengeView extends StatelessWidget { } Widget testList(Challenge challenge, ChallengeViewModel model) { - log(challenge.challengeTypeIndex.toString()); + log(challenge.challengeType.index.toString()); return ListView.builder( shrinkWrap: true, physics: const ClampingScrollPhysics(), diff --git a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart index de4663e0f..1d60f3d4c 100644 --- a/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart +++ b/mobile-app/lib/ui/views/learn/challenge/challenge_viewmodel.dart @@ -614,7 +614,8 @@ class ChallengeViewModel extends BaseViewModel { setTestConsoleMessages = ['

// running tests

']; // Get user code console messages - if (challenge!.challengeTypeIndex == 1 || challenge!.challengeTypeIndex == 26) { + if (challenge!.challengeType == ChallengeType.js || + challenge!.challengeType == ChallengeType.jsLab) { final evalResult = await testController!.callAsyncJavaScript( functionBody: await builder.buildUserCode( challenge!, @@ -638,7 +639,7 @@ class ChallengeViewModel extends BaseViewModel { challenge!, _babelWebView.webViewController, ), - 'workerType': builder.getWorkerType(challenge!.challengeTypeIndex), + 'workerType': builder.getWorkerType(challenge!.challengeType), 'combinedCode': await builder.combinedCode(challenge!), 'editableRegionContent': editableRegionContent, 'hooks': { diff --git a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart index 927853530..6a9f1c54c 100644 --- a/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart +++ b/mobile-app/lib/ui/views/learn/challenge/templates/template_view.dart @@ -34,53 +34,52 @@ class ChallengeTemplateView extends StatelessWidget { if (snapshot.hasData) { Challenge challenge = snapshot.data!; - int challengeType = challenge.challengeTypeIndex; List tiles = block.challengeTiles; int challNum = tiles.indexWhere((el) => el.id == challenge.id) + 1; - switch (challengeType) { - case 0: - case 1: - case 14: - case 20: - case 25: - case 26: + switch (challenge.challengeType) { + case ChallengeType.html: + case ChallengeType.js: + case ChallengeType.multifileCertProject: + case ChallengeType.python: + case ChallengeType.lab: + case ChallengeType.jsLab: return ChallengeView( challenge: challenge, block: block, isProject: tiles.length > 1, ); - case 8: + case ChallengeType.quiz: return QuizView( challenge: challenge, block: block, ); - case 10: + case ChallengeType.pythonProject: return PythonProjectView( challenge: challenge, block: block, ); - case 11: + case ChallengeType.video: return PythonView( challenge: challenge, block: block, currentChallengeNum: challNum, ); - case 15: - case 19: + case ChallengeType.theOdinProject: + case ChallengeType.multipleChoice: return MultipleChoiceView( challenge: challenge, block: block, currentChallengeNum: challNum, ); - case 21: - case 22: + case ChallengeType.dialogue: + case ChallengeType.fillInTheBlank: return EnglishView( challenge: challenge, block: block, currentChallengeNum: challNum, ); - case 24: + case ChallengeType.generic: return ReviewView( challenge: challenge, block: block, @@ -88,7 +87,7 @@ class ChallengeTemplateView extends StatelessWidget { default: return Center( child: Text( - 'Unknown Challenge, info : ${challenge.challengeTypeIndex}', + 'Unknown Challenge, info : ${challenge.challengeType}', ), ); } diff --git a/mobile-app/lib/ui/views/learn/test_runner.dart b/mobile-app/lib/ui/views/learn/test_runner.dart index 20eac43ac..d94d352f0 100644 --- a/mobile-app/lib/ui/views/learn/test_runner.dart +++ b/mobile-app/lib/ui/views/learn/test_runner.dart @@ -48,14 +48,14 @@ return testRes; }) async { String challengeFile = await fileService.getFirstFileFromCache( challenge, - getChallengeExt(challenge.challengeTypeIndex), + getChallengeExt(challenge.challengeType), testing: testing, ); - switch (challenge.challengeTypeIndex) { + switch (challenge.challengeType) { // JS-only challenges - case 1: - case 26: + case ChallengeType.js: + case ChallengeType.jsLab: // TODO: Move to learn file service if (babelController == null) { throw Exception('Babel controller is required to transpile JS code.'); @@ -95,28 +95,28 @@ return testRes; } } - String getWorkerType(int challengeType) { + String getWorkerType(ChallengeType challengeType) { switch (challengeType) { - case 0: - case 14: - case 25: + case ChallengeType.html: + case ChallengeType.multifileCertProject: + case ChallengeType.lab: return 'dom'; - case 1: - case 26: + case ChallengeType.js: + case ChallengeType.jsLab: return 'javascript'; - case 20: - case 23: + case ChallengeType.python: + case ChallengeType.multifilePythonCertProject: return 'python'; + default: + return 'dom'; } - - return 'dom'; } - Ext getChallengeExt(int challengeType) { + Ext getChallengeExt(ChallengeType challengeType) { switch (challengeType) { // JS-only challenges - case 1: - case 26: + case ChallengeType.js: + case ChallengeType.jsLab: return Ext.js; default: return Ext.html;