Skip to content

Markdown cannot be detected: reconsider content-only detection, or take the file name as a hint #760

Description

@andiwand

FileType::markdown is the only document format that detection can never reach.
file_type_table.cpp declares detect_by_content = false for it, and it is one
of exactly two rows that do (the other being excel_binary_workbook). So a .md
comes back as text_file, and list_file_types never offers markdown at all —
the caller has to already know, and pass FileType::markdown.

That is the right call for content sniffing: CommonMark has no signature, and
every plain text file is valid markdown. Sniffing it would mean every .txt
becoming a markdown document.

But it leaves a real gap, because the file name is already at the API boundary
and thrown away
:

DecodedFile::DecodedFile(const std::string &path, const Logger &logger)
    : DecodedFile(File::from_disk(path), logger) {}

File::from_disk(path) keeps the bytes and drops the name, and neither
magic.cpp nor open_strategy.cpp mentions an extension anywhere. Meanwhile
odr::file_type_by_file_extension exists in the public API and is used for
everything except deciding what a file is.

Why it is worth reconsidering now

Markdown is not the only format this shape applies to, it is just the first where
it bites. Anything text-based and signature-less has the same problem, and the
text branch of open_strategy already guesses among siblings by trial decode:

auto text = std::make_shared<text::TextFile>(file);
result.push_back(FileType::text_file);
try { result.push_back(csv::CsvFile(text).file_type()); } catch (...) {}
try { result.push_back(json::JsonFile(text).file_type()); } catch (...) {}

A .md file that happens to parse as CSV is offered as CSV today, while markdown
— the thing the name says it is — is not offered at all.

Options

1. Leave it, document it

Hosts that know the name pass FileType::markdown. Costs nothing; keeps the
asymmetry that a .md opened by path renders as preformatted text while the same
bytes opened with the type render as prose.

2. Take the name as a hint

Carry the file name (or just an extension) into detection and let it rank the
candidates content detection produced, rather than override them. Something like
a field on DecodePreference, or a File::from_disk that remembers its name.

  • A .md lists [text_file, markdown] instead of [text_file], with markdown
    first, so a caller taking the first entry gets prose.
  • Nothing is claimed on the name alone: a .md holding a zip is still a zip.
  • The precedent is already there — file_type_priority in DecodePreference
    exists to reorder exactly this list.
  • The open question is where the name lives. File is bytes today, and
    from_memory has no name, so the hint has to be optional everywhere.

3. Allow name-only detection for signature-less types

Let detect_by_content = false types be produced when the extension says so and
nothing else claimed the bytes
. Narrower than (2) and needs no ranking, but it
makes the extension authoritative for one class of types, which is a different
policy from the rest of the table.

4. Sniff markdown after all

Score for markdown constructs — headings, fenced code, link syntax, list markers
— and offer it below text_file past a threshold. No API change, works for
from_memory, but it is a heuristic on a format defined so that plain prose is
valid, so the threshold would be arbitrary and every .txt a coin flip.

Recommendation

(2), with (1) as the fallback if the name plumbing is not worth it. It is the
option that matches how DecodePreference already works, keeps content the only
thing that can claim bytes, and generalises to the next signature-less format
instead of special-casing markdown.

Worth settling before markdown detection is depended on anywhere, since (2) and
(3) differ in what list_file_types returns and that is observable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions