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()) {