| // Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:convert'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'package:html/dom.dart' show Element; | |
| import 'package:html/parser.dart' show parseFragment; | |
| import 'package:markdown/markdown.dart' | |
| show | |
| AutolinkExtensionSyntax, | |
| BlockSyntax, | |
| InlineSyntax, | |
| StrikethroughSyntax, | |
| TableSyntax, | |
| markdownToHtml; | |
| import 'package:path/path.dart' as p; | |
| import '../test/util.dart'; | |
| // Locate the "tool" directory. Use `Isolate.resolvePackageUriSync` so that this | |
| // works with the test package, which loads this suite into an isolate. | |
| final String toolDir = p.fromUri( | |
| Isolate.resolvePackageUriSync( | |
| Uri.parse('package:markdown/'), | |
| )!.resolve('../tool'), | |
| ); | |
| File getStatsFile(String prefix) => | |
| File(p.join(toolDir, '${prefix}_stats.json')); | |
| Map<String, List<CommonMarkTestCase>> loadCommonMarkSections( | |
| String testPrefix, | |
| ) { | |
| final testFile = File(p.join(toolDir, '${testPrefix}_tests.json')); | |
| final testsJson = testFile.readAsStringSync(); | |
| final testArray = jsonDecode(testsJson) as List; | |
| final sections = <String, List<CommonMarkTestCase>>{}; | |
| for (final exampleMap in testArray) { | |
| final exampleTest = CommonMarkTestCase.fromJson( | |
| exampleMap as Map<String, dynamic>, | |
| ); | |
| final sectionList = sections.putIfAbsent( | |
| exampleTest.section, | |
| () => <CommonMarkTestCase>[], | |
| ); | |
| sectionList.add(exampleTest); | |
| } | |
| return sections; | |
| } | |
| class Config { | |
| static final Config commonMarkConfig = Config._( | |
| 'common_mark', | |
| 'https://spec.commonmark.org/0.30/', | |
| ); | |
| static final Config gfmConfig = Config._( | |
| 'gfm', | |
| 'https://github.github.com/gfm/', | |
| ); | |
| final String prefix; | |
| final String baseUrl; | |
| Config._(this.prefix, this.baseUrl); | |
| } | |
| class CommonMarkTestCase { | |
| final String markdown; | |
| final String section; | |
| final int example; | |
| final String html; | |
| final int startLine; | |
| final int endLine; | |
| final Set<String> extensions; | |
| CommonMarkTestCase( | |
| this.example, | |
| this.section, | |
| this.startLine, | |
| this.endLine, | |
| this.markdown, | |
| this.html, | |
| this.extensions, | |
| ); | |
| factory CommonMarkTestCase.fromJson(Map<String, dynamic> json) { | |
| return CommonMarkTestCase( | |
| json['example'] as int, | |
| json['section'] as String /*!*/, | |
| json['start_line'] as int, | |
| json['end_line'] as int, | |
| json['markdown'] as String /*!*/, | |
| json['html'] as String, | |
| json['extensions'] == null | |
| ? const {} | |
| : Set.from(json['extensions'] as List), | |
| ); | |
| } | |
| @override | |
| String toString() => '$section - $example'; | |
| } | |
| enum CompareLevel { strict, loose, fail, error } | |
| class CompareResult { | |
| final CompareLevel compareLevel; | |
| final CommonMarkTestCase testCase; | |
| final String? result; | |
| CompareResult(this.testCase, this.result, this.compareLevel); | |
| } | |
| CompareResult compareResult( | |
| Config config, | |
| CommonMarkTestCase testCase, { | |
| bool throwOnError = false, | |
| bool verboseFail = false, | |
| bool verboseLooseMatch = false, | |
| Set<String> extensions = const {}, | |
| }) { | |
| var enabletagfilter = false; | |
| String output; | |
| final inlineSyntaxes = <InlineSyntax>[]; | |
| final blockSyntaxes = <BlockSyntax>[]; | |
| for (final extension in extensions) { | |
| switch (extension) { | |
| case 'autolink': | |
| inlineSyntaxes.add(AutolinkExtensionSyntax()); | |
| break; | |
| case 'strikethrough': | |
| inlineSyntaxes.add(StrikethroughSyntax()); | |
| break; | |
| case 'table': | |
| blockSyntaxes.add(const TableSyntax()); | |
| break; | |
| case 'tagfilter': | |
| enabletagfilter = true; | |
| break; | |
| default: | |
| throw UnimplementedError('Unimplemented extension "$extension"'); | |
| } | |
| } | |
| try { | |
| output = markdownToHtml( | |
| testCase.markdown, | |
| inlineSyntaxes: inlineSyntaxes, | |
| blockSyntaxes: blockSyntaxes, | |
| enableTagfilter: enabletagfilter, | |
| ); | |
| } catch (err, stackTrace) { | |
| if (throwOnError) { | |
| rethrow; | |
| } | |
| if (verboseFail) { | |
| _printVerboseFailure( | |
| config.baseUrl, | |
| 'ERROR', | |
| testCase, | |
| 'Thrown: $err\n$stackTrace', | |
| ); | |
| } | |
| return CompareResult(testCase, null, CompareLevel.error); | |
| } | |
| if (testCase.html == output) { | |
| return CompareResult(testCase, output, CompareLevel.strict); | |
| } | |
| final expectedParsed = parseFragment(testCase.html); | |
| final actual = parseFragment(output); | |
| final looseMatch = _compareHtml(expectedParsed.children, actual.children); | |
| if (!looseMatch && verboseFail) { | |
| _printVerboseFailure(config.baseUrl, 'FAIL', testCase, output); | |
| } | |
| if (looseMatch && verboseLooseMatch) { | |
| _printVerboseFailure(config.baseUrl, 'LOOSE', testCase, output); | |
| } | |
| return CompareResult( | |
| testCase, | |
| output, | |
| looseMatch ? CompareLevel.loose : CompareLevel.fail, | |
| ); | |
| } | |
| String _indent(String s) => | |
| s.splitMapJoin('\n', onNonMatch: (n) => ' ${whitespaceColor(n)}'); | |
| void _printVerboseFailure( | |
| String baseUrl, | |
| String message, | |
| CommonMarkTestCase testCase, | |
| String actual, | |
| ) { | |
| print( | |
| '$message: $baseUrl#example-${testCase.example} ' | |
| '@ ${testCase.section}', | |
| ); | |
| print('input:'); | |
| print(_indent(testCase.markdown)); | |
| print('expected:'); | |
| print(_indent(testCase.html)); | |
| print('actual:'); | |
| print(_indent(actual)); | |
| print('-----------------------'); | |
| } | |
| /// Compare two DOM trees for equality. | |
| bool _compareHtml( | |
| List<Element> expectedElements, | |
| List<Element> actualElements, | |
| ) { | |
| if (expectedElements.length != actualElements.length) { | |
| return false; | |
| } | |
| for (var childNum = 0; childNum < expectedElements.length; childNum++) { | |
| final expected = expectedElements[childNum]; | |
| final actual = actualElements[childNum]; | |
| if (expected.runtimeType != actual.runtimeType) { | |
| return false; | |
| } | |
| if (expected.localName != actual.localName) { | |
| return false; | |
| } | |
| if (expected.attributes.length != actual.attributes.length) { | |
| return false; | |
| } | |
| final expectedAttrKeys = expected.attributes.keys.toList(); | |
| expectedAttrKeys.sort(); | |
| final actualAttrKeys = actual.attributes.keys.toList(); | |
| actualAttrKeys.sort(); | |
| for (var attrNum = 0; attrNum < actualAttrKeys.length; attrNum++) { | |
| final expectedAttrKey = expectedAttrKeys[attrNum]; | |
| final actualAttrKey = actualAttrKeys[attrNum]; | |
| if (expectedAttrKey != actualAttrKey) { | |
| return false; | |
| } | |
| if (expected.attributes[expectedAttrKey] != | |
| actual.attributes[actualAttrKey]) { | |
| return false; | |
| } | |
| } | |
| final childrenEqual = _compareHtml(expected.children, actual.children); | |
| if (!childrenEqual) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } |