015 - Converter registry
Purpose: Promote the implicit extension-dispatch logic in mdd convert into an explicit, importable registry under src/mdd/converters/.
Status: Implemented (2026-05-08)
Introduction
Section titled “Introduction”This spec refactors the existing internal surface — no new user-facing command. The registry unblocks attachment conversion in Confluence sync (S16), SVG rasterization (S24), and SharePoint sync (S18) — all of which need to call the same “convert this file to its markdown form” logic from outside the convert command.
Originates from research note R02 (attachments).
Requirements
Section titled “Requirements”- Define a
Converterprotocol withextensions: tuple[str, ...],output_suffix: str, andconvert(src, *, dest=None) -> ConvertResult. - Define
ConvertResultcarryingoutput_path,attachments_dir,metadata(frontmatter dict), andwarnings. - Maintain a module-level dict
CONVERTERS: dict[str, Converter]keyed by lowercased extension (including leading dot). - Public helper
converter_for(path: Path) -> Converter | Nonereturns the converter for the path’s extension, orNone. mdd convert(S03) is refactored to callconverter_for(path).convert(...)instead of dispatching on extension internally.- Behaviour and output of
mdd convertis unchanged after the refactor — same files in, same files out, same exit codes.
Reverse converters (markdown → office) are also part of the registry shape, but populated only when their target specs land:
ReverseConverterprotocol withtarget_extension: strandrender(md_path, *, dest) -> RenderResult.REVERSE_CONVERTERS: dict[str, ReverseConverter]keyed by target extension.
Implementations land in S17
(Quarto-based .md → .docx/.pptx); none in this spec.
Design Approach
Section titled “Design Approach”extensionsis a tuple of.lowercaseextensions including the leading dot. Multiple are allowed (e.g. an HTML converter could claim(".html", ".htm")).output_suffixis the suffix appended to the full source name (e.g.".md"soFoo.docx → Foo.docx.md). For converters that produce a different terminal extension (e.g..svg → .svg.png), the suffix encodes the full doubled form.convert(src)writes its output and returns metadata; it does not return content. Callers useConvertResult.output_pathto find the result.attachments_diron the result points at any directory the converter populated with embedded media (orNone).metadatais the dict that becomes YAML frontmatter on the output; the converter writes it itself, but returns it for callers who want to merge / inspect.warningsare human-readable strings; no severity levels.CONVERTERSis a plain dict, populated by each converter module registering itself.converter_for(path)doesCONVERTERS.get(path.suffix.lower()). ReturnsNonefor unknown types so callers can decide how to handle them (skip, copy through, error). Lookup is case-insensitive on the file’s extension.- Existing
.docxconversion logic moves out ofsrc/mdd/commands/convert.pyintosrc/mdd/converters/docx.py(Docling + python-docx). The command becomes a thin walker that callsconverter_for(path).convert(...)and manages incremental skip /--force/--dry-run/ progress output. - The CLI surface of
mdd convertis unchanged. Imports of internal symbols frommdd.commands.convertmay break for out-of-tree callers; none are known.
Related upstream specs
Section titled “Related upstream specs”- 003-convert-command — the command being refactored
- 016-confluence-attachment-conversion — consumer of the registry
- 017-confluence-office-publishing — populates reverse converters
- 018-sharepoint-sync — consumer of the registry
- 024-svg-rasterization — consumer of the registry
Out of scope
Section titled “Out of scope”- Adding new converters (each gets its own spec).
- A general-purpose plugin protocol for third-party converters.
Internal registry only — adding
.xlsxis editing one file insrc/mdd/converters/. - Async or streaming converter contracts. All converters are synchronous and operate on filesystem paths.
Site built 2026-07-30.