Migrating to 3.0
3.0 changes no conversion output. Not one byte, in either surviving mode
(Balanced and Minimal). That is asserted, not hoped: a test converts a corpus
of 58 documents in both modes and compares every byte with what the published
2.9.0 produced, from goldens generated by running 2.9.0
(mode_identity.rs).
So there are no fixtures to re-verify and no output to diff. Everything below
is compile-and-rename: names that went away, and one change to what the file
functions return.
If you only call the string functions — html_to_markdown,
html_to_markdown_with, html_to_markdown_many and their per-language forms —
with Balanced, Minimal or no mode, 3.0 needs nothing from you. See
What did not change.
This is the first breaking release since 2.0.0, and the first one mdka has
staged deliberately: 2.0.0 was a rewrite whose changes were reconstructed after
the fact, with no runway and no guide. It is the price of removing
things that did nothing and of giving the file functions a result shape that says
what it means; the release before it (2.9.0) changed nothing and warned about the
last removal, and 3.0 followed it immediately, so you may not have seen a
single warning — this page is written for that reader.
At a glance
| You use | What breaks |
|---|---|
Strict, Semantic or Preserve (any binding, any string form) | The name is gone. Use Balanced: the output is identical |
preserve_classes, preserve_data_attrs, preserve_aria_attrs (and, Rust only, preserve_unknown_attrs, drop_presentation_attrs) | Gone. Delete them: they never changed the output |
unwrap_unknown_wrappers | Gone. Delete it: it could not change the output (why) |
| Rust: the file functions | ConvertResult is gone; single returns PathBuf, bulk returns Vec<FileOutcome> |
Node.js: anything that converts a file, or ends in With | Almost everyone. ConvertResult is gone and four functions are gone (longest section) |
| Python: the file functions | Single returns str; BulkConvertResult is now FileOutcome |
CLI: --mode strict|semantic|preserve, --preserve-classes, --preserve-data, --preserve-aria, --unwrap-wrappers | Each fails with a message that says what to do |
The shape behind the file-function changes
One distinction explains every change to what the file functions return: can it fail, not one versus many.
- A string conversion cannot fail, so it returns a string, or a list of them. No result type. Unchanged.
- One file can fail, and you asked about exactly one thing, so failure fails
the call, through each language’s own channel — a Rust
Err, a Python raise, a Node rejection. It returns the destination path and nothing else; the input path you passed in is not repeated back. - Many files must not let one bad file hide the rest, so each file gets its own
outcome, and the outcome says which file it belongs to:
FileOutcome.
Removed modes and options (all bindings)
Modes: Strict, Semantic, Preserve → Balanced
They were aliases of Balanced: byte-for-byte the same output, and they could not
differ. Replace the name; nothing else changes.
A mode read from a string — a config file, an environment variable, --mode —
cannot be found by the compiler, so change those strings now. What each surface
does with a removed name:
| Surface | Result |
|---|---|
Rust "strict".parse::<ConversionMode>() | Err("conversion mode 'strict' was removed in 3.0; it was an alias of 'balanced'. Use 'balanced'.") |
Node.js { mode: 'strict' } | throws (htmlToMarkdown, htmlToMarkdownMany) or rejects (htmlToMarkdownAsync, the file functions) with the same message |
| Python | ConversionMode.Strict is an AttributeError. A mode is never a string in Python |
CLI --mode strict | exit status 1, the same message on stderr |
That message is deliberately different from unknown conversion mode: bogus: a
removed name is an obsolete configuration, not a wrong one.
Options that never did anything
| Removed | Existed in |
|---|---|
preserve_classes, preserve_data_attrs, preserve_aria_attrs | Rust, Node.js (preserveClasses, preserveDataAttrs, preserveAriaAttrs), Python, CLI (--preserve-classes, --preserve-data, --preserve-aria) |
preserve_unknown_attrs, drop_presentation_attrs | Rust only — there is nothing to remove in Node.js, Python or the CLI |
the builder ConversionOptions::preserve_aria_attrs | Rust |
Markdown has no syntax for HTML attributes, so “preserve” or “drop” an attribute was
never expressible in the output. They were deprecated in 2.2.0. Delete them; the
output is unchanged. preserve_ids and drop_interactive_shell are the two
options that act on the output, and they stay.
unwrap_unknown_wrappers
This one you may not have been warned about, so here is all of it. It was an option
that controlled whether a wrapper element — <div>, <section>, <article>,
<main> — had its tag removed. It could not change the output: unwrapping a
wrapper removes the tag but keeps the paragraph break it stood for, and Markdown has
no wrapper element to show the difference. It was deprecated in 2.9.0 and removed
in 3.0, which followed immediately, so almost no one ran a version that warned.
Removing the call changes nothing. Delete unwrap_unknown_wrappers (Rust,
Python), unwrapUnknownWrappers (Node.js) or --unwrap-wrappers (CLI). Minimal
still unwraps wrappers, and Balanced still renders them; that is a property of the
mode, and the two write the same bytes.
If wrapper handling ever becomes expressible in Markdown, it returns as a new option, designed for what it can then do. This one does not come back.
Rust
html_file_to_markdown and html_file_to_markdown_with return the destination
path, not a ConvertResult:
// before (2.9.0)
let result = mdka::html_file_to_markdown("page.html", None::<&str>)?; // ConvertResult
println!("{} -> {}", result.src.display(), result.dest.display());
// after (3.0)
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dest = mdka::html_file_to_markdown("page.html", None::<&str>)?; // PathBuf
println!("page.html -> {}", dest.display()); // you already had the input path
Ok(())
}
html_files_to_markdown and html_files_to_markdown_with return
Vec<FileOutcome>, one per input in input order, and lose their 'a lifetime
parameter: the outcome owns its src.
// before (2.9.0): Vec<(&P, Result<PathBuf, MdkaError>)>
for (src, result) in mdka::html_files_to_markdown(&files, out_dir) {
match result {
Ok(dest) => println!("{} -> {}", src, dest.display()),
Err(e) => eprintln!("{src}: {e}"),
}
}
// after (3.0): Vec<FileOutcome>
use std::path::Path;
fn main() {
let files = vec!["a.html", "b.html"];
for outcome in mdka::html_files_to_markdown(&files, Path::new("out/")) {
let src = outcome.src.display();
match outcome.result {
Ok(dest) => println!("{src} -> {}", dest.display()),
Err(e) => eprintln!("{src}: {e}"),
}
}
}
FileOutcome is pub struct FileOutcome { pub src: PathBuf, pub result: Result<PathBuf, MdkaError> }.
FileOutcome is not Clone, and this is the one break that is not a rename.
MdkaError wraps std::io::Error, which cannot be cloned, so neither can the
outcome. If you cloned parts of the old tuple, restructure: move the outcome apart
(let FileOutcome { src, result } = outcome;), or clone only the half that can be
cloned (outcome.src.clone(), outcome.result.as_ref().ok().cloned()).
ConversionMode::parse_mode is deprecated, not removed. It discards the reason a
name was rejected, so "strict" and "bogus" both give None. Use str::parse,
which reports why:
use mdka::options::ConversionMode;
let mode: ConversionMode = "minimal".parse().expect("a valid mode");
assert_eq!(mode, ConversionMode::Minimal);
assert!("strict".parse::<ConversionMode>().unwrap_err().contains("was removed in 3.0"));
Also gone: the variants ConversionMode::Strict, Semantic and Preserve; the
fields preserve_classes, preserve_data_attrs, preserve_aria_attrs,
preserve_unknown_attrs, drop_presentation_attrs and unwrap_unknown_wrappers
of ConversionOptions; and its builder preserve_aria_attrs.
Node.js
This is the longest section, and that is the point: Node.js is the binding that
3.0 touches most. Its file functions returned an object that served both single
and bulk conversion, so every Node.js caller that converts a file is affected;
and it is the only binding that had two conventions at once — htmlToMarkdownWith
beside htmlToMarkdownMany(htmls, options?) — so four function names go.
Functions: the …With names are gone
JavaScript has optional arguments, so the options are now an optional last argument
on the base name. Rust and Python keep their _with pairs; that is their idiom.
| Removed | Use |
|---|---|
htmlToMarkdownWith(html, options) | htmlToMarkdown(html, options?) |
htmlToMarkdownWithAsync(html, options) | htmlToMarkdownAsync(html, options?) |
htmlFileToMarkdownWith(path, outDir, options) | htmlFileToMarkdown(path, outDir?, options?) |
htmlFilesToMarkdownWith(paths, outDir, options) | htmlFilesToMarkdown(paths, outDir, options?) |
htmlToMarkdownMany(htmls, options?) and version() are unchanged. To find your
uses: grep -rn "With(\|WithAsync(" --include='*.js' --include='*.ts' . and check
each hit is one of the four above.
// after (3.0)
const { htmlToMarkdown, htmlToMarkdownAsync } = require('mdka')
const html = '<nav>menu</nav><h1>Title</h1><p>Body</p>'
// was htmlToMarkdownWith(html, {...})
const md = htmlToMarkdown(html, { mode: 'minimal', dropInteractiveShell: true })
// was htmlToMarkdownWithAsync(html, {...})
async function main() {
const mdAsync = await htmlToMarkdownAsync(html, { mode: 'minimal' })
console.log(md, mdAsync)
}
main()
Single file: a string, and failure rejects
htmlFileToMarkdown used to resolve to a ConvertResult whose dest? and error?
were declared optional even though a successful single-file call never left dest
unset and never set error. It now resolves to the destination path, a
string, and a failure rejects the promise with an Error whose message
starts IO error: :
// after (3.0)
const { htmlFileToMarkdown } = require('mdka')
async function main() {
const dest = await htmlFileToMarkdown('page.html', 'out/') // was: r.dest
console.log(`wrote ${dest}`)
try {
await htmlFileToMarkdown('/no/such/file.html')
} catch (e) {
console.error(e.message) // "IO error: …"
}
}
main()
Code that read r.src should use the path it passed in; code that checked r.error
should use try/catch (or .catch) instead.
Many files: FileOutcome, with ok
htmlFilesToMarkdown resolves to Array<FileOutcome>, one per input in input order:
{ src, dest?, error?, ok }. ok is new — Node.js did not have it. Exactly one
of dest and error is set, and ok says which (TypeScript cannot express “exactly
one of”, which is why ok exists):
// after (3.0)
const { htmlFilesToMarkdown } = require('mdka')
async function main() {
for (const r of await htmlFilesToMarkdown(['a.html', 'b.html'], 'out/')) {
if (r.ok) console.log(`${r.src} -> ${r.dest}`)
else console.error(`${r.src}: ${r.error}`) // was: if (r.error) …
}
}
main()
A failing file does not reject the promise and does not stop the others. The
promise rejects only when the call as a whole cannot proceed — the output directory
cannot be created (cannot create out_dir: …) or the options are wrong. That is the
difference by arity, on purpose: with one file, failing the call is the answer; with
many, one bad file must not hide the rest.
TypeScript
ConvertResult is no longer exported; import FileOutcome. At 3.0, JsConversionOptions
had three fields, mode, preserveIds and dropInteractiveShell; later releases have added
more — see Options for the current set:
import { htmlFilesToMarkdown, type FileOutcome, type JsConversionOptions } from 'mdka'
const opts: JsConversionOptions = { mode: 'minimal' }
async function main() {
const outcomes: FileOutcome[] = await htmlFilesToMarkdown(['a.html'], 'out/', opts)
for (const o of outcomes) console.log(o.ok, o.src)
}
main()
An unrecognised option key is now an error
This is a behaviour change for code that was already wrong. Until 3.0, napi
silently dropped any option key it did not know, so { preserveClasses: true }, or
a typo like { mdoe: 'minimal' }, did nothing and told you nothing. It is now
rejected, naming the key, and a key removed in 3.0 says so:
option 'preserveClasses' was removed in 3.0; Markdown has no attribute syntax, so it never changed the output. Remove it: the output is the same without it.
option 'unwrapUnknownWrappers' was removed in 3.0; it could not change the output. Remove it: the output is the same without it.
unknown option 'mdoe'. Valid options: mode, preserveIds, dropInteractiveShell
The synchronous functions (htmlToMarkdown, htmlToMarkdownMany) throw it; the
Promise-returning ones (htmlToMarkdownAsync, htmlFileToMarkdown,
htmlFilesToMarkdown) reject with it. So a call that passed a removed option and
worked before will now fail: search your code for preserveClasses,
preserveDataAttrs, preserveAriaAttrs and unwrapUnknownWrappers and delete
them. (The TypeScript compiler already flagged them.) null and undefined are
still accepted as “no options”.
Python
html_file_to_markdown and html_file_to_markdown_with return the destination
path as a str, and still raise MdkaError on failure. They used to return a
ConvertResult, which is gone:
# after (3.0)
import mdka
dest = mdka.html_file_to_markdown("page.html", "out/") # was: result.dest
print(f"wrote {dest}")
try:
mdka.html_file_to_markdown("/no/such/file.html")
except mdka.MdkaError as e:
print(e) # IO error: …
BulkConvertResult is renamed FileOutcome, with the same four fields (src,
dest, error, ok). Exactly one of dest and error is set and ok says which.
It is frozen: you cannot assign to its attributes. Its repr is now Python’s own
with all four fields — it used to print error=Some("…"), a Rust Option, and omit
dest and ok:
# after (3.0)
import mdka
for r in mdka.html_files_to_markdown(["a.html", "b.html"], "out/"):
if r.ok:
print(r.src, "->", r.dest)
else:
print("skipped", r.src, r.error)
# repr(r) == "FileOutcome(src='a.html', dest='out/a.md', error=None, ok=True)"
A failing file does not raise and does not stop the others. MdkaError is raised only
if the output directory cannot be created.
Also gone: ConversionMode.Strict, Semantic and Preserve (AttributeError), and
the keyword arguments preserve_classes, preserve_data_attrs, preserve_aria_attrs
and unwrap_unknown_wrappers (TypeError: … got an unexpected keyword argument).
Rename ConvertResult or BulkConvertResult in your type annotations to str and
FileOutcome. mdka.version() is unchanged.
CLI
Five things were removed. Each now exits with status 1 and one line on stderr that says what to do, so a script that still passes one fails loudly instead of looking as though it worked:
$ mdka --mode strict page.html
error: conversion mode 'strict' was removed in 3.0; it was an alias of 'balanced'. Use 'balanced'.
$ mdka --preserve-classes page.html
error: `--preserve-classes` was removed in 3.0; Markdown has no attribute syntax, so it never changed the output. Remove it from the command line: the output is the same without it.
$ mdka --unwrap-wrappers page.html
error: `--unwrap-wrappers` was removed in 3.0; it could not change the output. Remove it from the command line: the output is the same without it.
$ mdka --mode bogus page.html
error: unknown conversion mode: bogus. Valid: balanced|minimal
--mode semantic, --mode preserve, --preserve-data and --preserve-aria read like
the first two. Remove the flag, or change the mode to balanced; the output is the
same. --preserve-ids, --no-preserve-ids and --drop-shell are unchanged. The
in.html -> in.md progress lines are unchanged.
What did not change
A migration guide that only lists losses reads like a bigger break than it is. Unchanged
in 3.0:
- The string conversions, in every language:
html_to_markdown,html_to_markdown_with,html_to_markdown_many,html_to_markdown_many_with(Rust, Python),htmlToMarkdown,htmlToMarkdownAsync,htmlToMarkdownMany(Node.js — which gained an optionaloptionsargument, an additive change). BalancedandMinimal, unrenamed and byte-identical.preserve_idsanddrop_interactive_shell, the two options that act on output.version(), in all three.- Every element and escaping rule. No output moved: see the opening.
MdkaError, and Python’sConversionMode,MdkaErrorand_withfunction pairs.
Finding what you need to change
| Binding | Search for |
|---|---|
| Rust | ConvertResult, html_file_to_markdown, html_files_to_markdown, parse_mode, Strict, Semantic, Preserve, preserve_classes, preserve_data_attrs, preserve_aria_attrs, preserve_unknown_attrs, drop_presentation_attrs, unwrap_unknown_wrappers — the compiler will find most of them |
| Node.js | htmlToMarkdownWith, WithAsync, htmlFileToMarkdown, htmlFilesToMarkdown, ConvertResult, .dest, .error, preserveClasses, preserveDataAttrs, preserveAriaAttrs, unwrapUnknownWrappers, and any mode: string |
| Python | ConvertResult, BulkConvertResult, html_file_to_markdown, Strict, Semantic, Preserve, preserve_classes, preserve_data_attrs, preserve_aria_attrs, unwrap_unknown_wrappers |
| CLI and scripts | --mode, --preserve-classes, --preserve-data, --preserve-aria, --unwrap-wrappers; and any config file that sets a mode |