| // Copyright (c) project authors. All rights reserved. | |
| // Licensed under the MIT license. | |
| // See LICENSE file in the project root for details. | |
| /// Support code for the tests in this directory. | |
| library; | |
| import 'dart:collection'; | |
| import 'dart:io'; | |
| import 'dart:isolate'; | |
| import 'package:html/dom.dart'; | |
| import 'package:html/dom_parsing.dart'; | |
| import 'package:html/src/treebuilder.dart'; | |
| import 'package:path/path.dart' as p; | |
| typedef TreeBuilderFactory = TreeBuilder Function(bool namespaceHTMLElements); | |
| Map<String, TreeBuilderFactory>? _treeTypes; | |
| Map<String, TreeBuilderFactory>? get treeTypes { | |
| // TODO(jmesserly): add DOM here once it's implemented | |
| _treeTypes ??= {'simpletree': TreeBuilder.new}; | |
| return _treeTypes; | |
| } | |
| Future<String> get testDirectory async { | |
| final packageUriDir = p.dirname(p.fromUri(await Isolate.resolvePackageUri( | |
| Uri(scheme: 'package', path: 'html/html.dart')))); | |
| // Assume pub layout - root is parent directory to package URI (`lib/`). | |
| final rootPackageDir = p.dirname(packageUriDir); | |
| return p.join(rootPackageDir, 'test'); | |
| } | |
| Stream<String> dataFiles(String subdirectory) async* { | |
| final dir = Directory(p.join(await testDirectory, 'data', subdirectory)); | |
| await for (final file in dir.list()) { | |
| if (file is! File) continue; | |
| yield file.path; | |
| } | |
| } | |
| // TODO(jmesserly): make this class simpler. We could probably split on | |
| // "\n#" instead of newline and remove a lot of code. | |
| class TestData extends IterableBase<Map<String?, String>> { | |
| final String _text; | |
| final String newTestHeading; | |
| TestData(String filename, [this.newTestHeading = 'data']) | |
| // Note: can't use readAsLinesSync here because it splits on \r | |
| : _text = File(filename).readAsStringSync(); | |
| // Note: in Python this was a generator, but since we can't do that in Dart, | |
| // it's easier to convert it into an upfront computation. | |
| @override | |
| Iterator<Map<String?, String>> get iterator => _getData().iterator; | |
| List<Map<String?, String>> _getData() { | |
| var data = <String, String>{}; | |
| String? key; | |
| final List<Map<String?, String>> result = <Map<String, String>>[]; | |
| final lines = _text.split('\n'); | |
| // Remove trailing newline to match Python | |
| if (lines.last == '') { | |
| lines.removeLast(); | |
| } | |
| for (var line in lines) { | |
| final heading = sectionHeading(line); | |
| if (heading != null) { | |
| if (data.isNotEmpty && heading == newTestHeading) { | |
| // Remove trailing newline | |
| data[key!] = data[key]!.substring(0, data[key]!.length - 1); | |
| result.add(normaliseOutput(data)); | |
| data = <String, String>{}; | |
| } | |
| key = heading; | |
| data[key] = ''; | |
| } else if (key != null) { | |
| data[key] = '${data[key]}$line\n'; | |
| } | |
| } | |
| if (data.isNotEmpty) { | |
| result.add(normaliseOutput(data)); | |
| } | |
| return result; | |
| } | |
| /// If the current heading is a test section heading return the heading, | |
| /// otherwise return null. | |
| static String? sectionHeading(String line) { | |
| return line.startsWith('#') ? line.substring(1).trim() : null; | |
| } | |
| static Map<String, String> normaliseOutput(Map<String, String> data) { | |
| // Remove trailing newlines | |
| data.forEach((key, value) { | |
| if (value.endsWith('\n')) { | |
| data[key] = value.substring(0, value.length - 1); | |
| } | |
| }); | |
| return data; | |
| } | |
| } | |
| /// Serialize the [document] into the html5 test data format. | |
| String testSerializer(Node document) { | |
| return (TestSerializer()..visit(document)).toString(); | |
| } | |
| /// Serializes the DOM into test format. See [testSerializer]. | |
| class TestSerializer extends TreeVisitor { | |
| final StringBuffer _str; | |
| int _indent = 0; | |
| String _spaces = ''; | |
| TestSerializer() : _str = StringBuffer(); | |
| @override | |
| String toString() => _str.toString(); | |
| int get indent => _indent; | |
| set indent(int value) { | |
| if (_indent == value) return; | |
| _spaces = ' ' * value; | |
| _indent = value; | |
| } | |
| void _newline() { | |
| if (_str.length > 0) _str.write('\n'); | |
| _str.write('|$_spaces'); | |
| } | |
| @override | |
| void visitNodeFallback(Node node) { | |
| _newline(); | |
| _str.write(node); | |
| visitChildren(node); | |
| } | |
| @override | |
| void visitChildren(Node node) { | |
| indent += 2; | |
| for (var child in node.nodes) { | |
| visit(child); | |
| } | |
| indent -= 2; | |
| } | |
| @override | |
| void visitDocument(Document node) => _visitDocumentOrFragment(node); | |
| void _visitDocumentOrFragment(Node node) { | |
| indent += 1; | |
| for (var child in node.nodes) { | |
| visit(child); | |
| } | |
| indent -= 1; | |
| } | |
| @override | |
| void visitDocumentFragment(DocumentFragment node) => | |
| _visitDocumentOrFragment(node); | |
| @override | |
| void visitElement(Element node) { | |
| _newline(); | |
| _str.write(node); | |
| if (node.attributes.isNotEmpty) { | |
| indent += 2; | |
| final keys = node.attributes.keys.toList(); | |
| keys.sort((x, y) { | |
| if (x is String) return x.compareTo(y as String); | |
| if (x is AttributeName) return x.compareTo(y as AttributeName); | |
| throw StateError('Cannot sort'); | |
| }); | |
| for (var key in keys) { | |
| final v = node.attributes[key]; | |
| if (key is AttributeName) { | |
| final attr = key; | |
| key = '${attr.prefix} ${attr.name}'; | |
| } | |
| _newline(); | |
| _str.write('$key="$v"'); | |
| } | |
| indent -= 2; | |
| } | |
| visitChildren(node); | |
| } | |
| } |