| // 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:mirrors'; |
| |
| 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 mirrors so that this works with the test |
| // package, which loads this suite into an isolate. |
| String get toolDir { |
| final path = (reflect(loadCommonMarkSections) as ClosureMirror) |
| .function |
| .location! |
| .sourceUri |
| .path; |
| |
| return p.dirname(path); |
| } |
| |
| 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', |
| 'http://spec.commonmark.org/0.28/', |
| ); |
| 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; |
| } |