From 36d22222dad97efbd765e8dd465ea0d3d81bbd94 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 29 Aug 2026 19:24:40 +0200 Subject: [PATCH] feat(file): name html as a file type, without a renderer for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.html` had no `FileType`, so a well formed one decoded as xml and a host that trusted the answer showed the page's source. Both apps had to keep their own list to route around it — exactly the kind of list the format table exists to remove. The row names html, htm, xhtml, text/html and application/xhtml+xml, and declares nothing: no `open`, no `translate_html`. A passthrough renderer would be of no use to a host that has a web view already, and it would put the library in the business of rewriting a page's own links and assets. Detection is by name, as it is for markdown — html has no dependable signature. Closes #763 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UCM9ja9LbjNPPZGJZSw2xL --- CHANGELOG.md | 4 ++++ README.md | 1 + apple/include/OdrCoreObjC/ODRFile.h | 2 ++ apple/src/ODRFile.mm | 2 ++ jni/java/app/opendocument/core/FileType.java | 2 +- python/src/bind_file.cpp | 4 +++- src/odr/file.hpp | 4 ++++ src/odr/internal/file_type_table.cpp | 13 ++++++++++ test/src/odr_test.cpp | 25 +++++++++++++++++++- 9 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f733141..76061a0ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ The release run heads these entries with the version and opens a fresh - The zip backend is `miniz/3.1.1`, up from `3.0.2`. Rendered output is unchanged. +- New `FileType::hypertext_markup_language` for `html`, `htm`, `xhtml`, + `text/html` and `application/xhtml+xml`. Classification only: no `open`, no + `translate_html`, and never detected from its bytes. + ## v6.11.0 - 2026-08-29 - No view declares `` any more. A link back into what diff --git a/README.md b/README.md index 091e0b956..f58f7932a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ opening one throws: - wpd (WordPerfect) - xlsb (Excel binary workbook — an OOXML package whose workbook parts are binary rather than spreadsheetml) +- html / htm / xhtml (route to a web view; not detected from its bytes) ## Asking what is supported diff --git a/apple/include/OdrCoreObjC/ODRFile.h b/apple/include/OdrCoreObjC/ODRFile.h index ac19d087c..0895e5eba 100644 --- a/apple/include/OdrCoreObjC/ODRFile.h +++ b/apple/include/OdrCoreObjC/ODRFile.h @@ -87,6 +87,8 @@ typedef NS_ENUM(NSInteger, ODRFileType) { ODRFileTypeIworkPages, ODRFileTypeIworkNumbers, ODRFileTypeIworkKeynote, + + ODRFileTypeHypertextMarkupLanguage, } NS_SWIFT_NAME(FileType); typedef NS_ENUM(NSInteger, ODRFileCategory) { diff --git a/apple/src/ODRFile.mm b/apple/src/ODRFile.mm index d616b1ab5..af31d340a 100644 --- a/apple/src/ODRFile.mm +++ b/apple/src/ODRFile.mm @@ -95,6 +95,8 @@ ODR_SAME_ENUM(ODRFileTypeIworkPages, odr::FileType::iwork_pages); ODR_SAME_ENUM(ODRFileTypeIworkNumbers, odr::FileType::iwork_numbers); ODR_SAME_ENUM(ODRFileTypeIworkKeynote, odr::FileType::iwork_keynote); +ODR_SAME_ENUM(ODRFileTypeHypertextMarkupLanguage, + odr::FileType::hypertext_markup_language); ODR_SAME_ENUM(ODRFileCategoryUnknown, odr::FileCategory::unknown); ODR_SAME_ENUM(ODRFileCategoryText, odr::FileCategory::text); diff --git a/jni/java/app/opendocument/core/FileType.java b/jni/java/app/opendocument/core/FileType.java index 74b1f5e17..590ccedec 100644 --- a/jni/java/app/opendocument/core/FileType.java +++ b/jni/java/app/opendocument/core/FileType.java @@ -17,7 +17,7 @@ public enum FileType { QUICKTIME_VIDEO, THIRD_GENERATION_PARTNERSHIP_VIDEO, MATROSKA_VIDEO, AUDIO_VIDEO_INTERLEAVE, SCALABLE_VECTOR_GRAPHICS, WINDOWS_ICON, JPEG_XL, JPEG_2000, PHOTOSHOP_DOCUMENT, WINDOWS_METAFILE, ENHANCED_METAFILE, XML, - IWORK_PAGES, IWORK_NUMBERS, IWORK_KEYNOTE; + IWORK_PAGES, IWORK_NUMBERS, IWORK_KEYNOTE, HYPERTEXT_MARKUP_LANGUAGE; static FileType fromNative(int code) { return code < 0 ? null : values()[code]; diff --git a/python/src/bind_file.cpp b/python/src/bind_file.cpp index 0d1163d8e..360f8994d 100644 --- a/python/src/bind_file.cpp +++ b/python/src/bind_file.cpp @@ -86,7 +86,9 @@ void odr_python::bind_file(py::module_ &m) { .value("xml", odr::FileType::xml) .value("iwork_pages", odr::FileType::iwork_pages) .value("iwork_numbers", odr::FileType::iwork_numbers) - .value("iwork_keynote", odr::FileType::iwork_keynote); + .value("iwork_keynote", odr::FileType::iwork_keynote) + .value("hypertext_markup_language", + odr::FileType::hypertext_markup_language); py::enum_(m, "FileCategory") .value("unknown", odr::FileCategory::unknown) diff --git a/src/odr/file.hpp b/src/odr/file.hpp index 579773560..7b2532952 100644 --- a/src/odr/file.hpp +++ b/src/odr/file.hpp @@ -162,6 +162,10 @@ enum class FileType { // and no fixture pins those two, so nothing detects or decodes them yet. iwork_numbers, iwork_keynote, + + // Classification only - never detected from its bytes. + // https://en.wikipedia.org/wiki/HTML + hypertext_markup_language, }; /// @brief Collection of file categories. diff --git a/src/odr/internal/file_type_table.cpp b/src/odr/internal/file_type_table.cpp index 03e3e41ee..aeed25f19 100644 --- a/src/odr/internal/file_type_table.cpp +++ b/src/odr/internal/file_type_table.cpp @@ -262,6 +262,9 @@ constexpr std::array keynote_mimetypes{ "application/x-iwork-keynote-sffkey"sv, }; +constexpr std::array html_extensions{"html"sv, "htm"sv, "xhtml"sv}; +constexpr std::array html_mimetypes{"text/html"sv, "application/xhtml+xml"sv}; + // The single source of truth behind every public format lookup; `odr_test` // asserts one row per `FileType` and capabilities that match the engines. // @@ -798,6 +801,16 @@ constexpr std::array table{ .open = true, .translate_html = true, .color_scheme = true}}, + + // Named but not decoded. Not `detect_by_content` - html has no dependable + // signature, so the caller routes on the file name. + Row{FileType::hypertext_markup_language, + "html"sv, + html_extensions, + html_mimetypes, + FileCategory::text, + DocumentType::unknown, + {}}, }; /// Finds the row whose list, selected by @p list, contains @p needle. diff --git a/test/src/odr_test.cpp b/test/src/odr_test.cpp index 1f7a5fd2c..65fc256f2 100644 --- a/test/src/odr_test.cpp +++ b/test/src/odr_test.cpp @@ -27,7 +27,8 @@ namespace { std::vector every_file_type() { std::vector result; for (auto i = static_cast(FileType::unknown); - i <= static_cast(FileType::iwork_keynote); ++i) { + i <= static_cast(FileType::hypertext_markup_language); + ++i) { result.push_back(static_cast(i)); } return result; @@ -129,6 +130,28 @@ TEST(FileTypeTable, canonical_alias_is_the_first_one) { } } +/// Without the row an `.html` decodes as xml and shows its own source. +TEST(FileTypeTable, html_is_named_but_not_decoded) { + const FileType html = FileType::hypertext_markup_language; + + EXPECT_EQ(file_type_by_file_extension("html"), html); + EXPECT_EQ(file_type_by_file_extension("htm"), html); + EXPECT_EQ(file_type_by_file_extension("xhtml"), html); + EXPECT_EQ(file_type_by_mimetype("text/html"), html); + EXPECT_EQ(mimetype_by_file_type(html), "text/html"); + EXPECT_EQ(file_category_by_file_type(html), FileCategory::text); + + const FileTypeCapabilities capabilities = capabilities_by_file_type(html); + EXPECT_FALSE(capabilities.detect_by_content); + EXPECT_FALSE(capabilities.open); + EXPECT_FALSE(capabilities.translate_html); + + const std::string page = "

hi

"; + EXPECT_THROW(std::ignore = + open(File::from_memory(page), html, Logger::null()), + UnknownFileType); +} + /// `FileType::unknown` is the only type we refuse to name a MIME type for. TEST(FileTypeTable, only_unknown_has_no_mimetype) { for (const FileType type : every_file_type()) {