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.
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-10-26.