Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8b83460
feat: add standalone HTML support
github-actions[bot] Aug 28, 2026
b20d2d9
fix: address HTML charset review feedback
marcellmanfrin Aug 28, 2026
8dfa216
fix: harden HTML content detection
marcellmanfrin Aug 28, 2026
341be1b
test: cover HTML detection review regressions
marcellmanfrin Aug 28, 2026
50b9629
fix: address HTML review regressions
marcellmanfrin Aug 28, 2026
9920d30
fix: address second HTML review regressions
marcellmanfrin Aug 28, 2026
df61b31
fix: match HTML5 preflight depth semantics
marcellmanfrin Aug 28, 2026
076e77d
fix: address final HTML review findings
marcellmanfrin Aug 28, 2026
fbe5339
test: consolidate HTML review regressions
marcellmanfrin Aug 29, 2026
c21198b
test: add HTML corpus integration regressions
marcellmanfrin Aug 29, 2026
0c4c81c
test: add controlled HTML corpus fixtures
marcellmanfrin Aug 29, 2026
adaf984
test: add LibreOffice HTML corpus regressions
marcellmanfrin Aug 29, 2026
529aaa2
test: add LibreOffice HTML corpus fixtures
marcellmanfrin Aug 29, 2026
2bca8ee
test: cover structural children in nested list wrappers
marcellmanfrin Aug 29, 2026
ff5820f
fix: preserve structural children in HTML lists
marcellmanfrin Aug 29, 2026
6372cf0
style: format HTML corpus changes
marcellmanfrin Aug 29, 2026
bbff29a
test: refine LibreOffice numbering invariants
marcellmanfrin Aug 29, 2026
fe53a0a
test: include HTML fixtures in corpus detection
marcellmanfrin Aug 29, 2026
b6c0889
test: add HTML corpus snapshots
marcellmanfrin Aug 29, 2026
5bd85ba
test: keep corpus walk helper local
marcellmanfrin Aug 29, 2026
6c78e2c
style: format corpus walk helper
marcellmanfrin Aug 29, 2026
ab653fb
test: isolate HTML corpus fixture helper
marcellmanfrin Aug 29, 2026
6c70669
test: cover non-rendering children inside lists
marcellmanfrin Aug 29, 2026
75cc2d2
fix: ignore non-rendering children when grouping lists
marcellmanfrin Aug 29, 2026
c6b7bb1
fix: preserve structural depth across anchor recovery
marcellmanfrin Aug 30, 2026
32c6474
feat: reconcile MHTML with HTML head c6b7bb18
marcellmanfrin Aug 30, 2026
ee58d6a
fix: handle framesets and relative HTML images
marcellmanfrin Aug 30, 2026
ce79482
test: update HTML image snapshots
marcellmanfrin Aug 30, 2026
ee4aefd
fix: reconcile MHTML resource hardening
marcellmanfrin Aug 30, 2026
c1c854c
Merge reconciled MHTML hardening candidate into PR 149 head
Aug 31, 2026
108180e
fix: require multipart media type for MIME preflight nesting
Aug 31, 2026
da61872
fix: preserve collected assets for frameset documents
Aug 31, 2026
bfebd99
refactor: share default link/anchor resolution across HTML contexts
Aug 31, 2026
b50bc1b
fix: close implied paragraphs and preserve bare-hash links in HTML
Sep 1, 2026
b67f51e
fix: bound quoted-printable parts early and resolve relative bases
Sep 1, 2026
274f465
fix: complete the paragraph-closing start tag set in the HTML preflight
Sep 1, 2026
de11157
test: make the relative-base regression discriminate the fix
Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
337 changes: 337 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ cfb = "0.14.0"
csv = "1.4.0"
flate2 = "1"
encoding_rs = "0.8.35"
html5ever = "0.39.0"
log = "0.4"
mail-parser = { version = "0.11.8", features = ["full_encoding"] }
pdf-inspector = "1.14.2"
quick-xml = "0.41.0"
scraper = { version = "0.27.0", default-features = false }
zip = { version = "8.6.0", default-features = false, features = ["deflate"] }

[profile.release]
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ Only documents that need OCR leave the machine, and the whole document goes, sin
| OpenDocument | `.odt`, `.ods`, `.odp` |
| Rich Text Format | `.rtf` |
| EPUB | `.epub` |
| HTML | `.html`, `.htm` |
| MHTML | `.mhtml`, `.mht` |
| CSV | `.csv` |
| PDF | `.pdf` |

Expand Down
2 changes: 1 addition & 1 deletion node/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

const { readFile, writeFile } = require('node:fs/promises')

const FORMATS = 'doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv'
const FORMATS = 'doc, docx, odt, pdf, ppt, pptx, rtf, epub, html, mhtml, xlsx, ods, odp, csv'

const HELP = `anydoc: convert documents to GitHub-Flavored Markdown

Expand Down
10 changes: 10 additions & 0 deletions node/html.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { formatFromBytes, formatFromExtension, toMarkdownBytes } from './index.js'

test('standalone HTML is exposed through the Node binding', async () => {
const input = Buffer.from('<!doctype html><h1>Hello</h1><p><b>world</b></p>')
assert.equal(formatFromExtension('html'), 'html')
assert.equal(formatFromBytes(input), 'html')
assert.equal(await toMarkdownBytes(input), '# Hello\n\n**world**\n')
})
9 changes: 6 additions & 3 deletions node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export declare const enum Format {
pptx = 'pptx',
rtf = 'rtf',
epub = 'epub',
html = 'html',
mhtml = 'mhtml',
xlsx = 'xlsx',
ods = 'ods',
odp = 'odp',
Expand All @@ -136,9 +138,10 @@ export declare const enum Format {

/**
* Detect the format from the content itself: the signature and identity each
* container specification designates (PDF header, RTF open group, OLE stream
* names, ZIP package mimetype/content types). Plain-text formats (CSV) carry
* no signature and return `null`; so does anything unrecognized.
* container specification designates (PDF header, RTF open group, MIME HTML
* aggregate, OLE stream names, ZIP package mimetype/content types).
* Plain-text formats (CSV) carry no signature and return `null`; so does
* anything unrecognized.
*/
export declare function formatFromBytes(bytes: Uint8Array): Format | null

Expand Down
18 changes: 18 additions & 0 deletions node/mhtml.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { formatFromBytes, formatFromExtension, toMarkdownBytes } from './index.js'

const input = Buffer.from(
'Snapshot-Content-Location: https://example.test/page\r\n' +
'MIME-Version: 1.0\r\n' +
'Content-Type: multipart/related; type="text/html"; boundary="b"\r\n\r\n' +
'--b\r\nContent-Type: text/html\r\n\r\n' +
'<!doctype html><h1>Hello MHTML</h1>\r\n--b--\r\n'
)

test('MHTML is exposed through the Node binding', async () => {
assert.equal(formatFromExtension('mhtml'), 'mhtml')
assert.equal(formatFromExtension('mht'), 'mhtml')
assert.equal(formatFromBytes(input), 'mhtml')
assert.equal(await toMarkdownBytes(input), '# Hello MHTML\n')
})
13 changes: 10 additions & 3 deletions node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub enum Format {
pptx,
rtf,
epub,
html,
mhtml,
xlsx,
ods,
odp,
Expand All @@ -43,6 +45,8 @@ impl From<Format> for anydoc::Format {
Format::pptx => anydoc::Format::Pptx,
Format::rtf => anydoc::Format::Rtf,
Format::epub => anydoc::Format::Epub,
Format::html => anydoc::Format::Html,
Format::mhtml => anydoc::Format::Mhtml,
Format::xlsx => anydoc::Format::Excel,
Format::ods => anydoc::Format::Ods,
Format::odp => anydoc::Format::Odp,
Expand All @@ -62,6 +66,8 @@ impl From<anydoc::Format> for Format {
anydoc::Format::Pptx => Format::pptx,
anydoc::Format::Rtf => Format::rtf,
anydoc::Format::Epub => Format::epub,
anydoc::Format::Html => Format::html,
anydoc::Format::Mhtml => Format::mhtml,
anydoc::Format::Excel => Format::xlsx,
anydoc::Format::Ods => Format::ods,
anydoc::Format::Odp => Format::odp,
Expand All @@ -71,9 +77,10 @@ impl From<anydoc::Format> for Format {
}

/// Detect the format from the content itself: the signature and identity each
/// container specification designates (PDF header, RTF open group, OLE stream
/// names, ZIP package mimetype/content types). Plain-text formats (CSV) carry
/// no signature and return `null`; so does anything unrecognized.
/// container specification designates (PDF header, RTF open group, MIME HTML
/// aggregate, OLE stream names, ZIP package mimetype/content types).
/// Plain-text formats (CSV) carry no signature and return `null`; so does
/// anything unrecognized.
#[napi]
pub fn format_from_bytes(bytes: Uint8Array) -> Option<Format> {
anydoc::Format::from_bytes(&bytes).map(Format::from)
Expand Down
10 changes: 5 additions & 5 deletions python/anydoc/_anydoc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import os
from typing import Literal, final

Format = Literal[
"doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "xlsx", "ods", "odp", "csv"
"doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "html", "mhtml", "xlsx", "ods", "odp", "csv"

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Type checkers still reject "html" and "mhtml" when callers use the public anydoc.to_markdown_bytes or anydoc.to_document APIs. Add both literals to python/anydoc/__init__.py's public Format alias as well.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/anydoc/_anydoc.pyi, line 7:

<comment>Type checkers still reject `"html"` and `"mhtml"` when callers use the public `anydoc.to_markdown_bytes` or `anydoc.to_document` APIs. Add both literals to `python/anydoc/__init__.py`'s public `Format` alias as well.</comment>

<file context>
@@ -4,7 +4,7 @@ import os
 
 Format = Literal[
-    "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "xlsx", "ods", "odp", "csv"
+    "doc", "docx", "odt", "pdf", "ppt", "pptx", "rtf", "epub", "html", "mhtml", "xlsx", "ods", "odp", "csv"
 ]
 
</file context>
Fix with cubic

]

class ConvertError(Exception):
Expand Down Expand Up @@ -50,10 +50,10 @@ class MissingPartError(ConvertError):

def format_from_bytes(data: bytes | bytearray) -> Format | None:
"""Detect the format from the content itself: the signature and identity
each container specification designates (PDF header, RTF open group, OLE
stream names, ZIP package mimetype/content types). Plain-text formats
(CSV) carry no signature and return `None`; so does anything
unrecognized."""
each container specification designates (PDF header, RTF open group, MIME
HTML aggregate, OLE stream names, ZIP package mimetype/content types).

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new detection documentation omits standalone HTML detection even though format_from_bytes now returns html for HTML markers. Add the HTML-marker rule so the public stub accurately documents the new format.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/anydoc/_anydoc.pyi, line 54:

<comment>The new detection documentation omits standalone HTML detection even though `format_from_bytes` now returns `html` for HTML markers. Add the HTML-marker rule so the public stub accurately documents the new format.</comment>

<file context>
@@ -50,10 +50,10 @@ class MissingPartError(ConvertError):
-    (CSV) carry no signature and return `None`; so does anything
-    unrecognized."""
+    each container specification designates (PDF header, RTF open group, MIME
+    HTML aggregate, OLE stream names, ZIP package mimetype/content types).
+    Plain-text formats (CSV) carry no signature and return `None`; so does
+    anything unrecognized."""
</file context>
Suggested change
HTML aggregate, OLE stream names, ZIP package mimetype/content types).
HTML aggregate, standalone HTML markers, OLE stream names, ZIP package mimetype/content types).
Fix with cubic

Plain-text formats (CSV) carry no signature and return `None`; so does
anything unrecognized."""

def format_from_extension(extension: str) -> Format | None:
"""The format an extension names, with or without a leading dot."""
Expand Down
11 changes: 7 additions & 4 deletions python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ create_exception!(
/// Format names, as the extension that identifies each format. Container
/// variants that share a parser (`.docm`, `.xlsm`, `.ppsx`, ...) map onto
/// these via `format_from_bytes` or `format_from_extension`.
const FORMATS: [(&str, anydoc::Format); 12] = [
const FORMATS: [(&str, anydoc::Format); 14] = [
("doc", anydoc::Format::Doc),
("docx", anydoc::Format::Docx),
("odt", anydoc::Format::Odt),
Expand All @@ -76,6 +76,8 @@ const FORMATS: [(&str, anydoc::Format); 12] = [
("pptx", anydoc::Format::Pptx),
("rtf", anydoc::Format::Rtf),
("epub", anydoc::Format::Epub),
("html", anydoc::Format::Html),
("mhtml", anydoc::Format::Mhtml),
("xlsx", anydoc::Format::Excel),
("ods", anydoc::Format::Ods),
("odp", anydoc::Format::Odp),
Expand Down Expand Up @@ -139,9 +141,10 @@ fn convert_error(py: Python<'_>, error: anydoc::ConvertError) -> PyErr {
}

/// Detect the format from the content itself: the signature and identity each
/// container specification designates (PDF header, RTF open group, OLE stream
/// names, ZIP package mimetype/content types). Plain-text formats (CSV) carry
/// no signature and return `None`; so does anything unrecognized.
/// container specification designates (PDF header, RTF open group, MIME HTML
/// aggregate, OLE stream names, ZIP package mimetype/content types).
/// Plain-text formats (CSV) carry no signature and return `None`; so does
/// anything unrecognized.
#[pyfunction]
fn format_from_bytes(data: Vec<u8>) -> Option<&'static str> {
anydoc::Format::from_bytes(&data).map(format_name)
Expand Down
15 changes: 15 additions & 0 deletions python/tests/test_html.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import unittest

import anydoc


class HtmlBindingTests(unittest.TestCase):
def test_standalone_html_is_exposed(self):
data = b'<!doctype html><h1>Hello</h1><p><b>world</b></p>'
self.assertEqual(anydoc.format_from_extension('html'), 'html')
self.assertEqual(anydoc.format_from_bytes(data), 'html')
self.assertEqual(anydoc.to_markdown_bytes(data), '# Hello\n\n**world**\n')


if __name__ == '__main__':
unittest.main()
22 changes: 22 additions & 0 deletions python/tests/test_mhtml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import unittest
import anydoc

INPUT = (
b'Snapshot-Content-Location: https://example.test/page\r\n'
b'MIME-Version: 1.0\r\n'
b'Content-Type: multipart/related; type="text/html"; boundary="b"\r\n\r\n'
b'--b\r\nContent-Type: text/html\r\n\r\n'
b'<!doctype html><h1>Hello MHTML</h1>\r\n--b--\r\n'
)


class MhtmlTests(unittest.TestCase):
def test_mhtml_binding(self):
self.assertEqual(anydoc.format_from_extension('mhtml'), 'mhtml')
self.assertEqual(anydoc.format_from_extension('mht'), 'mhtml')
self.assertEqual(anydoc.format_from_bytes(INPUT), 'mhtml')
self.assertEqual(anydoc.to_markdown_bytes(INPUT), '# Hello MHTML\n')

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This binding test only exercises the happy path (detection + conversion) and none of the MHTML-specific behavior this PR adds: the base64-reserve / quoted-printable bounds / nesting-depth / media-type-guard resource limits, Content-ID/Content-Location/base-href resolution, or the MHTML-over-HTML detection preference. A regression in error propagation through the binding (e.g. ResourceLimitError with its limit attribute) or in embedded-resource resolution would pass this test undetected. Add at least one case that exercises an MHTML resource limit (like python/tests/test_anydoc.py::test_conversion_errors_raise_the_subclass_that_names_the_failure) and one that resolves an embedded image via cid:/Content-Location, since these are the new, security-sensitive paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/tests/test_mhtml.py, line 18:

<comment>This binding test only exercises the happy path (detection + conversion) and none of the MHTML-specific behavior this PR adds: the base64-reserve / quoted-printable bounds / nesting-depth / media-type-guard resource limits, Content-ID/Content-Location/base-href resolution, or the MHTML-over-HTML detection preference. A regression in error propagation through the binding (e.g. `ResourceLimitError` with its `limit` attribute) or in embedded-resource resolution would pass this test undetected. Add at least one case that exercises an MHTML resource limit (like `python/tests/test_anydoc.py::test_conversion_errors_raise_the_subclass_that_names_the_failure`) and one that resolves an embedded image via `cid:`/`Content-Location`, since these are the new, security-sensitive paths.</comment>

<file context>
@@ -0,0 +1,22 @@
+        self.assertEqual(anydoc.format_from_extension('mhtml'), 'mhtml')
+        self.assertEqual(anydoc.format_from_extension('mht'), 'mhtml')
+        self.assertEqual(anydoc.format_from_bytes(INPUT), 'mhtml')
+        self.assertEqual(anydoc.to_markdown_bytes(INPUT), '# Hello MHTML\n')
+
+
</file context>
Fix with cubic



if __name__ == '__main__':
unittest.main()
113 changes: 111 additions & 2 deletions src/formats/detect.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//! Content-based format detection.
//!
//! Identifies the format from what each specification designates as the
//! container's identity, never from heuristics over document content:
//! Identifies the format primarily from what each specification designates as
//! the container's identity. Standalone HTML is the one intentional content
//! heuristic: after an optional BOM/leading whitespace, a leading HTML5
//! `<!DOCTYPE ... html>` or `<html>` marker identifies `Format::Html`.
//!
//! - PDF: the `%PDF-` header (ISO 32000; implementations accept leading
//! junk, bounded here at 1024 bytes).
Expand Down Expand Up @@ -41,12 +43,117 @@ pub(crate) fn from_bytes(bytes: &[u8]) -> Option<Format> {
if bytes.starts_with(b"PK\x03\x04") {
return detect_zip(bytes);
}
if looks_like_html(bytes) {
return Some(Format::Html);
}
if bytes[..bytes.len().min(1024)].windows(5).any(|w| w == b"%PDF-") {
return Some(Format::Pdf);
}
None
}

fn looks_like_html(bytes: &[u8]) -> bool {
if let Some(rest) = bytes.strip_prefix(&[0xFF, 0xFE]) {
return looks_like_utf16_html(rest, true);
}
if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
return looks_like_utf16_html(rest, false);
}
let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
looks_like_ascii_html(bytes)
}

fn looks_like_utf16_html(bytes: &[u8], little_endian: bool) -> bool {
let (pairs, _) = bytes.as_chunks::<2>();
let mut index = 0;
while pairs.get(index).is_some_and(|pair| {
utf16_ascii_unit(*pair, little_endian).is_some_and(|b| b.is_ascii_whitespace())
}) {
index += 1;
}

if utf16_html_prefix(pairs, index, little_endian, b"<html") {
return true;
}

const DOCTYPE: &[u8] = b"<!doctype";
if !utf16_prefix_eq_ignore_ascii_case(pairs, index, little_endian, DOCTYPE) {
return false;
}
index += DOCTYPE.len();
if !pairs.get(index).is_some_and(|pair| {
utf16_ascii_unit(*pair, little_endian).is_some_and(|b| b.is_ascii_whitespace())
}) {
return false;
}
while pairs.get(index).is_some_and(|pair| {
utf16_ascii_unit(*pair, little_endian).is_some_and(|b| b.is_ascii_whitespace())
}) {
index += 1;
}
utf16_html_prefix(pairs, index, little_endian, b"html")
}

fn utf16_ascii_unit(pair: [u8; 2], little_endian: bool) -> Option<u8> {
let unit = if little_endian { u16::from_le_bytes(pair) } else { u16::from_be_bytes(pair) };
(unit <= 0x7F).then_some(unit as u8)
}

fn utf16_prefix_eq_ignore_ascii_case(
pairs: &[[u8; 2]],
start: usize,
little_endian: bool,
prefix: &[u8],
) -> bool {
let Some(slice) = pairs.get(start..start + prefix.len()) else {
return false;
};
slice.iter().zip(prefix).all(|(pair, expected)| {
utf16_ascii_unit(*pair, little_endian)
.is_some_and(|byte| byte.eq_ignore_ascii_case(expected))
})
}

fn utf16_html_prefix(pairs: &[[u8; 2]], start: usize, little_endian: bool, prefix: &[u8]) -> bool {
utf16_prefix_eq_ignore_ascii_case(pairs, start, little_endian, prefix)
&& pairs.get(start + prefix.len()).is_none_or(|pair| {
utf16_ascii_unit(*pair, little_endian)
.is_some_and(|b| b.is_ascii_whitespace() || matches!(b, b'>' | b'/'))
})
}

fn looks_like_ascii_html(bytes: &[u8]) -> bool {
let bytes = bytes.trim_ascii_start();
html_doctype_prefix(bytes) || html_prefix(bytes, b"<html")
}

fn html_doctype_prefix(bytes: &[u8]) -> bool {
const DOCTYPE: &[u8] = b"<!doctype";
let Some(head) = bytes.get(..DOCTYPE.len()) else {
return false;
};
if !head.eq_ignore_ascii_case(DOCTYPE) {
return false;
}
let Some(rest) = bytes.get(DOCTYPE.len()..) else {
return false;
};
if !rest.first().is_some_and(u8::is_ascii_whitespace) {
return false;
}
html_prefix(rest.trim_ascii_start(), b"html")
}

fn html_prefix(bytes: &[u8], prefix: &[u8]) -> bool {
let Some(head) = bytes.get(..prefix.len()) else {
return false;
};
head.eq_ignore_ascii_case(prefix)
&& bytes
.get(prefix.len())
.is_none_or(|b| b.is_ascii_whitespace() || matches!(b, b'>' | b'/'))
}

/// Classify an OLE compound file by its mandated content stream. Encrypted
/// OOXML packages (`EncryptedPackage`) stay `None`: the inner format is
/// unknowable, and the frontend reports `Encrypted` precisely.
Expand Down Expand Up @@ -236,6 +343,8 @@ mod tests {
junk.extend_from_slice(b"%PDF-1.4");
assert_eq!(from_bytes(&junk), Some(Format::Pdf));
assert_eq!(from_bytes(b"{\\rtf1\\ansi hi}"), Some(Format::Rtf));
assert_eq!(from_bytes(b"<!DOCTYPE html><html></html>"), Some(Format::Html));
assert_eq!(from_bytes(b"\xEF\xBB\xBF <HTML><body>x</body></HTML>"), Some(Format::Html));
assert_eq!(from_bytes(b"a,b,c\n1,2,3\n"), None);
assert_eq!(from_bytes(b""), None);
}
Expand Down
Loading
Loading