123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 |
- /*
- * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <AK/LexicalPath.h>
- #include <AK/Queue.h>
- #include <AK/URL.h>
- #include <AK/URLParser.h>
- #include <LibCore/ArgsParser.h>
- #include <LibCore/DeprecatedFile.h>
- #include <LibCore/File.h>
- #include <LibMain/Main.h>
- #include <LibXML/DOM/Document.h>
- #include <LibXML/DOM/Node.h>
- #include <LibXML/Parser/Parser.h>
- static bool g_color = false;
- static bool g_only_contents = false;
- enum class ColorRole {
- PITag,
- PITarget,
- PIData,
- AttributeName,
- Eq,
- AttributeValue,
- Tag,
- Text,
- Comment,
- Reset,
- Doctype,
- Keyword,
- };
- static void color(ColorRole role)
- {
- if (!g_color)
- return;
- switch (role) {
- case ColorRole::PITag:
- case ColorRole::Doctype:
- out("\x1b[{};{}m", 1, "38;5;223");
- break;
- case ColorRole::PITarget:
- out("\x1b[{};{}m", 1, "38;5;23");
- break;
- case ColorRole::PIData:
- out("\x1b[{};{}m", 1, "38;5;43");
- break;
- case ColorRole::AttributeName:
- out("\x1b[38;5;27m");
- break;
- case ColorRole::Eq:
- break;
- case ColorRole::AttributeValue:
- out("\x1b[38;5;46m");
- break;
- case ColorRole::Tag:
- out("\x1b[{};{}m", 1, "38;5;220");
- break;
- case ColorRole::Text:
- break;
- case ColorRole::Comment:
- out("\x1b[{};{}m", 3, "38;5;250");
- break;
- case ColorRole::Reset:
- out("\x1b[0m");
- break;
- case ColorRole::Keyword:
- out("\x1b[38;5;40m");
- break;
- }
- }
- static void dump(XML::Node const& node)
- {
- node.content.visit(
- [](XML::Node::Text const& text) {
- out("{}", text.builder.string_view());
- },
- [](XML::Node::Comment const& comment) {
- color(ColorRole::Comment);
- out("<!--{}-->", comment.text);
- color(ColorRole::Reset);
- },
- [](XML::Node::Element const& element) {
- color(ColorRole::Tag);
- out("<{}", element.name);
- color(ColorRole::Reset);
- if (!element.attributes.is_empty()) {
- for (auto& attribute : element.attributes) {
- auto quote = attribute.value.contains('"') ? '\'' : '"';
- color(ColorRole::AttributeName);
- out(" {}", attribute.key);
- color(ColorRole::Eq);
- out("=");
- color(ColorRole::AttributeValue);
- out("{}{}{}", quote, attribute.value, quote);
- color(ColorRole::Reset);
- }
- }
- if (element.children.is_empty()) {
- color(ColorRole::Tag);
- out("/>");
- color(ColorRole::Reset);
- } else {
- color(ColorRole::Tag);
- out(">");
- color(ColorRole::Reset);
- for (auto& node : element.children)
- dump(*node);
- color(ColorRole::Tag);
- out("</{}>", element.name);
- color(ColorRole::Reset);
- }
- });
- }
- static void dump(XML::Document& document)
- {
- if (!g_only_contents) {
- {
- color(ColorRole::PITag);
- out("<?");
- color(ColorRole::Reset);
- color(ColorRole::PITarget);
- out("xml");
- color(ColorRole::Reset);
- color(ColorRole::PIData);
- out(" version='{}'", document.version() == XML::Version::Version10 ? "1.0" : "1.1");
- color(ColorRole::Reset);
- color(ColorRole::PITag);
- outln("?>");
- }
- for (auto& pi : document.processing_instructions()) {
- color(ColorRole::PITag);
- out("<?");
- color(ColorRole::Reset);
- color(ColorRole::PITarget);
- out("{}", pi.key);
- color(ColorRole::Reset);
- if (!pi.value.is_empty()) {
- color(ColorRole::PIData);
- out(" {}", pi.value);
- color(ColorRole::Reset);
- }
- color(ColorRole::PITag);
- outln("?>");
- }
- if (auto maybe_doctype = document.doctype(); maybe_doctype.has_value()) {
- auto& doctype = *maybe_doctype;
- color(ColorRole::Doctype);
- out("<!DOCTYPE ");
- color(ColorRole::Tag);
- out("{}", doctype.type);
- if (!doctype.markup_declarations.is_empty()) {
- color(ColorRole::Reset);
- out(" [\n");
- for (auto& entry : doctype.markup_declarations) {
- entry.visit(
- [&](XML::ElementDeclaration const& element) {
- color(ColorRole::Doctype);
- out(" <!ELEMENT ");
- color(ColorRole::Tag);
- out("{} ", element.type);
- element.content_spec.visit(
- [&](XML::ElementDeclaration::Empty const&) {
- color(ColorRole::Keyword);
- out("EMPTY");
- },
- [&](XML::ElementDeclaration::Any const&) {
- color(ColorRole::Keyword);
- out("ANY");
- },
- [&](XML::ElementDeclaration::Mixed const&) {
- },
- [&](XML::ElementDeclaration::Children const&) {
- });
- color(ColorRole::Doctype);
- outln(">");
- },
- [&](XML::AttributeListDeclaration const& list) {
- color(ColorRole::Doctype);
- out(" <!ATTLIST ");
- color(ColorRole::Tag);
- out("{}", list.type);
- for (auto& attribute : list.attributes) {
- color(ColorRole::AttributeName);
- out(" {} ", attribute.name);
- color(ColorRole::Keyword);
- attribute.type.visit(
- [](XML::AttributeListDeclaration::StringType) {
- out("CDATA");
- },
- [](XML::AttributeListDeclaration::TokenizedType type) {
- switch (type) {
- case XML::AttributeListDeclaration::TokenizedType::ID:
- out("ID");
- break;
- case XML::AttributeListDeclaration::TokenizedType::IDRef:
- out("IDREF");
- break;
- case XML::AttributeListDeclaration::TokenizedType::IDRefs:
- out("IDREFS");
- break;
- case XML::AttributeListDeclaration::TokenizedType::Entity:
- out("ENTITY");
- break;
- case XML::AttributeListDeclaration::TokenizedType::Entities:
- out("ENTITIES");
- break;
- case XML::AttributeListDeclaration::TokenizedType::NMToken:
- out("NMTOKEN");
- break;
- case XML::AttributeListDeclaration::TokenizedType::NMTokens:
- out("NMTOKENS");
- break;
- }
- },
- [](XML::AttributeListDeclaration::NotationType const& type) {
- out("NOTATION ");
- color(ColorRole::Reset);
- out("( ");
- bool first = true;
- for (auto& name : type.names) {
- color(ColorRole::Reset);
- if (first)
- first = false;
- else
- out(" | ");
- color(ColorRole::AttributeValue);
- out("{}", name);
- }
- color(ColorRole::Reset);
- out(" )");
- },
- [](XML::AttributeListDeclaration::Enumeration const& type) {
- color(ColorRole::Reset);
- out("( ");
- bool first = true;
- for (auto& name : type.tokens) {
- color(ColorRole::Reset);
- if (first)
- first = false;
- else
- out(" | ");
- color(ColorRole::AttributeValue);
- out("{}", name);
- }
- color(ColorRole::Reset);
- out(" )");
- });
- out(" ");
- attribute.default_.visit(
- [](XML::AttributeListDeclaration::Required) {
- color(ColorRole::Keyword);
- out("#REQUIRED");
- },
- [](XML::AttributeListDeclaration::Implied) {
- color(ColorRole::Keyword);
- out("#IMPLIED");
- },
- [](XML::AttributeListDeclaration::Fixed const& fixed) {
- color(ColorRole::Keyword);
- out("#FIXED ");
- color(ColorRole::AttributeValue);
- out("\"{}\"", fixed.value);
- },
- [](XML::AttributeListDeclaration::DefaultValue const& default_) {
- color(ColorRole::AttributeValue);
- out("\"{}\"", default_.value);
- });
- }
- color(ColorRole::Doctype);
- outln(">");
- },
- [&](XML::EntityDeclaration const& entity) {
- color(ColorRole::Doctype);
- out(" <!ENTITY ");
- entity.visit(
- [](XML::GEDeclaration const& declaration) {
- color(ColorRole::Tag);
- out("{} ", declaration.name);
- declaration.definition.visit(
- [](DeprecatedString const& value) {
- color(ColorRole::AttributeValue);
- out("\"{}\"", value);
- },
- [](XML::EntityDefinition const& definition) {
- if (definition.id.public_id.has_value()) {
- color(ColorRole::Keyword);
- out("PUBLIC ");
- color(ColorRole::PITarget);
- out("\"{}\" ", definition.id.public_id->public_literal);
- } else {
- color(ColorRole::Keyword);
- out("SYSTEM ");
- }
- color(ColorRole::PITarget);
- out("\"{}\" ", definition.id.system_id.system_literal);
- if (definition.notation.has_value()) {
- color(ColorRole::Keyword);
- out(" NDATA ");
- color(ColorRole::PITarget);
- out("{}", *definition.notation);
- }
- });
- color(ColorRole::Tag);
- outln(">");
- },
- [](XML::PEDeclaration const& declaration) {
- color(ColorRole::Tag);
- out("{} ", declaration.name);
- declaration.definition.visit(
- [](DeprecatedString const& value) {
- color(ColorRole::AttributeValue);
- out("\"{}\"", value);
- },
- [](XML::ExternalID const& id) {
- if (id.public_id.has_value()) {
- color(ColorRole::Keyword);
- out("PUBLIC ");
- color(ColorRole::PITarget);
- out("\"{}\" ", id.public_id->public_literal);
- } else {
- color(ColorRole::Keyword);
- out("SYSTEM ");
- }
- color(ColorRole::PITarget);
- out("\"{}\"", id.system_id.system_literal);
- });
- color(ColorRole::Tag);
- outln(">");
- });
- },
- [&](XML::NotationDeclaration const&) {
- });
- }
- color(ColorRole::Reset);
- out("]");
- }
- color(ColorRole::Doctype);
- outln(">");
- }
- }
- dump(document.root());
- }
- static DeprecatedString s_path;
- static auto parse(StringView contents)
- {
- return XML::Parser {
- contents,
- {
- .preserve_comments = true,
- .resolve_external_resource = [&](XML::SystemID const& system_id, Optional<XML::PublicID> const&) -> ErrorOr<DeprecatedString> {
- auto base = URL::create_with_file_scheme(s_path);
- auto url = URLParser::parse(system_id.system_literal, base);
- if (!url.is_valid())
- return Error::from_string_literal("Invalid URL");
- if (url.scheme() != "file")
- return Error::from_string_literal("NYI: Nonlocal entity");
- auto file = TRY(Core::File::open(url.path(), Core::File::OpenMode::Read));
- return DeprecatedString::copy(TRY(file->read_until_eof()));
- },
- },
- };
- }
- enum class TestResult {
- Passed,
- Failed,
- RunnerFailed,
- };
- static HashMap<DeprecatedString, TestResult> s_test_results {};
- static void do_run_tests(XML::Document& document)
- {
- auto& root = document.root().content.get<XML::Node::Element>();
- VERIFY(root.name == "TESTSUITE");
- Queue<XML::Node*> suites;
- auto dump_cases = [&](auto& root) {
- for (auto& node : root.children) {
- auto element = node->content.template get_pointer<XML::Node::Element>();
- if (!element)
- continue;
- if (element->name != "TESTCASES" && element->name != "TEST")
- continue;
- suites.enqueue(node);
- }
- };
- dump_cases(root);
- auto base_path = LexicalPath::dirname(s_path);
- while (!suites.is_empty()) {
- auto& node = *suites.dequeue();
- auto& suite = node.content.get<XML::Node::Element>();
- if (suite.name == "TESTCASES") {
- dump_cases(suite);
- continue;
- }
- if (suite.name == "TEST") {
- Vector<StringView> bases;
- for (auto* parent = node.parent; parent; parent = parent->parent) {
- auto& attributes = parent->content.get<XML::Node::Element>().attributes;
- auto it = attributes.find("xml:base");
- if (it == attributes.end())
- continue;
- bases.append(it->value);
- }
- auto type = suite.attributes.find("TYPE")->value;
- StringBuilder path_builder;
- path_builder.append(base_path);
- path_builder.append('/');
- for (auto& entry : bases.in_reverse()) {
- path_builder.append(entry);
- path_builder.append('/');
- }
- auto test_base_path = path_builder.to_deprecated_string();
- path_builder.append(suite.attributes.find("URI")->value);
- auto url = URL::create_with_file_scheme(path_builder.string_view());
- if (!url.is_valid()) {
- warnln("Invalid URL {}", path_builder.string_view());
- s_test_results.set(path_builder.string_view(), TestResult::RunnerFailed);
- continue;
- }
- auto file_result = Core::File::open(url.path(), Core::File::OpenMode::Read);
- if (file_result.is_error()) {
- warnln("Read error for {}: {}", url.path(), file_result.error());
- s_test_results.set(url.path(), TestResult::RunnerFailed);
- continue;
- }
- warnln("Running test {}", url.path());
- auto contents = file_result.value()->read_until_eof();
- if (contents.is_error()) {
- warnln("Read error for {}: {}", url.path(), contents.error());
- s_test_results.set(url.path(), TestResult::RunnerFailed);
- continue;
- }
- auto parser = parse(contents.value());
- auto doc_or_error = parser.parse();
- if (doc_or_error.is_error()) {
- if (type == "invalid" || type == "error" || type == "not-wf")
- s_test_results.set(url.path(), TestResult::Passed);
- else
- s_test_results.set(url.path(), TestResult::Failed);
- continue;
- }
- auto out = suite.attributes.find("OUTPUT");
- if (out != suite.attributes.end()) {
- auto out_path = LexicalPath::join(test_base_path, out->value).string();
- auto file_result = Core::File::open(out_path, Core::File::OpenMode::Read);
- if (file_result.is_error()) {
- warnln("Read error for {}: {}", out_path, file_result.error());
- s_test_results.set(url.path(), TestResult::RunnerFailed);
- continue;
- }
- auto contents = file_result.value()->read_until_eof();
- if (contents.is_error()) {
- warnln("Read error for {}: {}", out_path, contents.error());
- s_test_results.set(url.path(), TestResult::RunnerFailed);
- continue;
- }
- auto parser = parse(contents.value());
- auto out_doc_or_error = parser.parse();
- if (out_doc_or_error.is_error()) {
- warnln("Parse error for {}: {}", out_path, out_doc_or_error.error());
- s_test_results.set(url.path(), TestResult::RunnerFailed);
- continue;
- }
- auto out_doc = out_doc_or_error.release_value();
- if (out_doc.root() != doc_or_error.value().root()) {
- s_test_results.set(url.path(), TestResult::Failed);
- continue;
- }
- }
- if (type == "invalid" || type == "error" || type == "not-wf")
- s_test_results.set(url.path(), TestResult::Failed);
- else
- s_test_results.set(url.path(), TestResult::Passed);
- }
- }
- }
- ErrorOr<int> serenity_main(Main::Arguments arguments)
- {
- StringView filename;
- bool run_tests { false };
- Core::ArgsParser parser;
- parser.set_general_help("Parse and dump XML files");
- parser.add_option(g_color, "Syntax highlight the output", "color", 'c');
- parser.add_option(g_only_contents, "Only display markup and text", "only-contents", 'o');
- parser.add_option(run_tests, "Run tests", "run-tests", 't');
- parser.add_positional_argument(filename, "File to read from", "file");
- parser.parse(arguments);
- s_path = Core::DeprecatedFile::real_path_for(filename);
- auto file = TRY(Core::File::open(s_path, Core::File::OpenMode::Read));
- auto contents = TRY(file->read_until_eof());
- auto xml_parser = parse(contents);
- auto result = xml_parser.parse();
- if (result.is_error()) {
- if (xml_parser.parse_error_causes().is_empty()) {
- warnln("{}", result.error());
- } else {
- warnln("{}; caused by:", result.error());
- for (auto const& cause : xml_parser.parse_error_causes())
- warnln(" {}", cause);
- }
- return 1;
- }
- auto doc = result.release_value();
- if (run_tests) {
- do_run_tests(doc);
- size_t passed = 0;
- size_t failed = 0;
- size_t runner_error = 0;
- size_t total = 0;
- for (auto& entry : s_test_results) {
- total++;
- switch (entry.value) {
- case TestResult::Passed:
- passed++;
- break;
- case TestResult::Failed:
- failed++;
- break;
- case TestResult::RunnerFailed:
- runner_error++;
- break;
- }
- }
- outln("{} passed, {} failed, {} runner failed of {} tests run.", passed, failed, runner_error, total);
- return 0;
- }
- dump(doc);
- if (!g_only_contents)
- outln();
- return 0;
- }
|