diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@
 
 ## [Unreleased]
 
+## [0.4.0.1] - 2026-11-09
+
+### Added
+
+- Added a Debian package for Linux x86_64 to the release workflow. The package installs the executable, the README, the changelog, the license and a man page, and depends on `libc6 (>= 2.35)` and `libgmp10`.
+
+### Fixed
+
+- Preserved the heading hierarchy when a page has more than three distinct Zim levels. The Markdown cap moved from `###` to `######`, so level four and level five headings no longer collapse into level three.
+
+## [0.4.0.0] - 2026-11-09
+
+### Added
+
+- Added `--formatting html|bold|italic|strike` for inline Zim formatting that Markdown cannot express. `html` is the default and keeps `<u>`, `<sub>` and `<sup>`; `bold`, `italic` and `strike` collapse underline, subscript and superscript into the chosen emphasis. Anchors stay HTML in every mode, and `markdown-to-zim` rejects the option.
+
+### Changed
+
+- Documented `--formatting` in the CLI, conversion, domain and architecture guides, and added a notebook walkthrough to the CLI guide.
+- Corrected the release guide to build with `-fdevelopment`, sign tags through `tag.gpgSign`, and keep the Hackage token out of the environment.
+
 ## [0.3.1.0] - 2026-10-29
 
 ### Added
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -20,8 +20,9 @@
 - Converts a single page to standard output or to a file.
 - Converts a whole notebook recursively, reconstructing its folder structure.
 - Accepts the notebook directory or its `notebook.zim` configuration file as input.
-- Strips the leading Zim metadata header and normalizes heading levels to `#`, `##`, and `###`.
+- Strips the leading Zim metadata header and normalizes heading levels into consecutive Markdown levels.
 - Copies or links external images with `--images copy|symlink|relative`, so images and attachments keep working.
+- Collapses inline formatting without a Markdown counterpart into bold, italic or strikethrough with `--formatting html|bold|italic|strike`.
 - Resolves internal Zim links across the notebook.
 - Converts Markdown back to Zim, including notebook structure.
 - Supports headings, emphasis, lists, task lists, links, images, anchors, tables, blockquotes, horizontal rules, and code blocks.
@@ -55,15 +56,23 @@
 
 ## Prebuilt binaries
 
-Prebuilt Linux, macOS, and Windows binaries are attached to [GitHub releases](https://github.com/Tralen/zim-to-markdown/releases/latest). The release workflow builds Linux x86_64 on Ubuntu 22.04 (glibc 2.35 or newer), macOS ARM64, and Windows x86_64 archives. Build from source for other Linux distributions.
+Prebuilt Linux, macOS, and Windows binaries are attached to [GitHub releases](https://github.com/Tralen/zim-to-markdown/releases/latest). The release workflow builds Linux x86_64 on Ubuntu 22.04 (glibc 2.35 or newer), macOS ARM64, and Windows x86_64 archives, plus a Debian package for Linux x86_64. Build from source for other Linux distributions.
 
+On Debian and Ubuntu, install the `.deb` with:
+
+```sh
+sudo apt install ./zim-to-markdown_<version>_amd64.deb
+```
+
 ## Requirements
 
-Building from source or installing with `cabal install` requires:
+Building from source or installing with `cabal install` requires a Haskell toolchain. The project is tested with:
 
-- GHC 9.14.1
+- GHC 9.6.7 and 9.14.1
 - cabal-install 3.18.1.0
 
+The release binaries use GHC 9.14.1.
+
 On Debian and Ubuntu, install the `libgmp-dev` package before building, because GHC links against GMP.
 
 The prebuilt binaries attached to the GitHub releases do not need a Haskell toolchain.
@@ -71,10 +80,12 @@
 ## Build and test
 
 ```sh
-cabal build
-cabal test
+cabal build all -fdevelopment
+cabal test all -fdevelopment
 ```
 
+The `development` flag turns warnings into errors, as CI does. Without it, `cabal build` and `cabal test` still work but do not enforce the warning gate.
+
 ## Development
 
 Run the formatter and linter with:
@@ -111,6 +122,14 @@
 ```sh
 zim-to-markdown page.txt --output page.md
 ```
+
+Collapse inline formatting that Markdown cannot express into emphasis:
+
+```sh
+zim-to-markdown page.txt --formatting bold
+```
+
+Use `italic` or `strike` for the other emphasis styles, or keep the default `html` for `<u>`, `<sub>` and `<sup>`.
 
 Convert a whole Zim notebook, preserving its folders:
 
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -40,10 +40,11 @@
 
 import ZimToMarkdown.Converter (
     convertMarkdownDocument,
-    convertZimPageWithAssets,
+    convertZimPageWithAssetsAndFormatting,
  )
 import ZimToMarkdown.Domain (
     ConversionDirection (..),
+    FormattingMode (..),
     MarkdownDocument (..),
     RelativePath (..),
     ZimPage (..),
@@ -99,13 +100,17 @@
 isNotebookDefinition path = takeFileName path == "notebook.zim"
 
 runSingleFile :: ConversionRequest -> FilePath -> IO ()
-runSingleFile request inputPath =
-    case conversionImageMode request of
-        CopyImages -> runSingleFileCopy request inputPath
-        SymlinkImages ->
-            failWith "The symlink image mode requires a notebook input."
-        RelativeImages ->
-            failWith "The relative image mode requires a notebook input."
+runSingleFile request inputPath
+    | conversionDirection request == MarkdownToZim
+        && conversionFormatting request /= HtmlFormatting =
+        failWith "The --formatting mode applies only when converting Zim to Markdown."
+    | otherwise =
+        case conversionImageMode request of
+            CopyImages -> runSingleFileCopy request inputPath
+            SymlinkImages ->
+                failWith "The symlink image mode requires a notebook input."
+            RelativeImages ->
+                failWith "The relative image mode requires a notebook input."
 
 runSingleFileCopy :: ConversionRequest -> FilePath -> IO ()
 runSingleFileCopy request inputPath = do
@@ -118,6 +123,7 @@
                 rendered =
                     convertContent
                         (conversionDirection request)
+                        (conversionFormatting request)
                         context
                         Map.empty
                         content
@@ -139,6 +145,9 @@
     | conversionDirection request == MarkdownToZim
         && conversionImageMode request /= CopyImages =
         failWith "The --images mode applies only when converting Zim to Markdown."
+    | conversionDirection request == MarkdownToZim
+        && conversionFormatting request /= HtmlFormatting =
+        failWith "The --formatting mode applies only when converting Zim to Markdown."
     | otherwise =
         case conversionOutput request of
             Nothing ->
@@ -254,6 +263,7 @@
                             rendered =
                                 convertContent
                                     (conversionDirection request)
+                                    (conversionFormatting request)
                                     context
                                     imageTargets
                                     content
@@ -435,15 +445,21 @@
 
 convertContent ::
     ConversionDirection ->
+    FormattingMode ->
     LinkContext ->
     Map.Map Text Text ->
     Text ->
     Text
-convertContent direction context assets content =
+convertContent direction formatting context assets content =
     case direction of
         ZimToMarkdown ->
             unMarkdownDocument
-                (convertZimPageWithAssets context assets (ZimPage content))
+                ( convertZimPageWithAssetsAndFormatting
+                    formatting
+                    context
+                    assets
+                    (ZimPage content)
+                )
         MarkdownToZim ->
             unZimPage
                 (convertMarkdownDocument (MarkdownDocument content))
diff --git a/docs/architecture.md b/docs/architecture.md
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -6,7 +6,7 @@
 
 | Module | Responsibility |
 | --- | --- |
-| `ZimToMarkdown.Domain` | Core types: directions, documents, and relative paths. |
+| `ZimToMarkdown.Domain` | Core types: directions, formatting modes, documents, and relative paths. |
 | `ZimToMarkdown.Zim` | Pure Zim to Markdown block and inline conversion. |
 | `ZimToMarkdown.Markdown` | Pure Markdown to Zim block and inline conversion. |
 | `ZimToMarkdown.Converter` | Direction specific entry points used by the executable. |
@@ -22,7 +22,7 @@
 ## Data flow
 
 ```text
-argv -> parseOptions -> ConversionRequest
+argv -> parseOptions -> ConversionRequest (direction, image mode, formatting mode)
                               |
                               v
 discoverFiles -> classifyNotebookItems -> pages and assets
@@ -31,7 +31,7 @@
 file bytes -> decodeUtf8' -> ZimPage or MarkdownDocument
                               |
                               v
-convertZimPage or convertMarkdownDocument
+convertZimPageWithAssetsAndFormatting or convertMarkdownDocument
                               |
                               v
 encodeUtf8 -> stdout or output file
@@ -40,6 +40,10 @@
 ## Purity boundary
 
 Parsing, conversion, link resolution, path calculation, table normalization, and notebook classification are pure functions. Reading, writing, directory discovery, asset copying, and exiting are confined to `Main`, which turns failures into `Text` messages and a non-zero exit code instead of throwing exceptions.
+
+## Inline formatting
+
+The formatting mode from `ConversionRequest` reaches the pure converter. `HtmlFormatting` keeps `<u>`, `<sub>` and `<sup>`; `BoldFormatting`, `ItalicFormatting` and `StrikeFormatting` collapse underline, subscript and superscript into emphasis. Anchors are not formatting and stay HTML in every mode.
 
 ## Notebook behavior
 
diff --git a/docs/cli.md b/docs/cli.md
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -3,7 +3,7 @@
 ## Usage
 
 ```text
-zim-to-markdown [--direction DIRECTION] [--output PATH] INPUT
+zim-to-markdown [--direction DIRECTION] [--output PATH] [--images MODE] [--formatting MODE] INPUT
 zim-to-markdown --help
 zim-to-markdown --version
 ```
@@ -17,6 +17,7 @@
 | `-d DIRECTION`, `--direction DIRECTION` | `zim-to-markdown` (default) or `markdown-to-zim`. |
 | `-o PATH`, `--output PATH` | Output file for a single page; output directory for a notebook. |
 | `--images MODE` | External images: `copy` (default), `symlink` or `relative`; notebook conversion only. |
+| `--formatting MODE` | Inline formatting without a Markdown counterpart: `html` (default), `bold`, `italic` or `strike`; Zim to Markdown only. |
 | `-h`, `--help` | Show usage information. |
 | `-V`, `--version` | Show the application version. |
 
@@ -47,10 +48,33 @@
 
 `symlink` needs symbolic-link support: on Windows it requires Developer Mode or Administrator privileges, and backup or archive tools may dereference or skip the links. These modes read the paths written in the notebook, so only convert notebooks from a trusted source; the output can copy files from outside the notebook and the symlinks reveal absolute paths.
 
+`--formatting` controls Zim underline, subscript and superscript, which have no Markdown syntax. `html` keeps `<u>`, `<sub>` and `<sup>`; `bold`, `italic` and `strike` collapse them into `**`, `*` and `~~` emphasis. Anchors such as `{{id: name}}` stay HTML in every mode, and `markdown-to-zim` accepts only `html`.
+
 ```sh
 zim-to-markdown my-notebook --output my-notebook-markdown
 zim-to-markdown my-notebook/notebook.zim --output my-notebook-markdown
 zim-to-markdown --direction markdown-to-zim notes --output notes-zim
+```
+
+## Walkthrough
+
+The following example converts a notebook called `my-notebook` into Markdown under `my-notebook-markdown`.
+
+1. Run the conversion:
+
+   ```sh
+   zim-to-markdown my-notebook --output my-notebook-markdown
+   ```
+
+2. The program reads `my-notebook`, finds every page and asset, and creates `my-notebook-markdown`. A page such as `my-notebook/notes/page.txt` becomes `my-notebook-markdown/notes/page.md`; other files are copied unchanged.
+3. Internal links are rewritten to point at the converted pages, and images referenced outside the notebook are copied to `_assets/` by default.
+4. The command prints nothing on success and exits with status `0`. On failure it prints a message to standard error and exits with a non-zero status.
+5. The result is the `my-notebook-markdown` directory. Delete it to undo the conversion; the input notebook is not modified.
+
+For a single page, omit `--output` to print the Markdown to standard output:
+
+```sh
+zim-to-markdown page.txt
 ```
 
 ## Exit status
diff --git a/docs/conversion.md b/docs/conversion.md
--- a/docs/conversion.md
+++ b/docs/conversion.md
@@ -6,14 +6,14 @@
 
 | Zim | Markdown |
 | --- | --- |
-| `=` heading syntax, levels 1 to 5 | Normalized Markdown heading, from `#` to `###` |
+| `=` heading syntax, levels 1 to 5 | Consecutive Markdown heading, from `#` to `#####` |
 | `**bold**` | `**bold**` |
 | `//italic//` | `*italic*` |
-| `__underline__` | `<u>underline</u>` |
+| `__underline__` | `<u>underline</u>`, or `**`/`*`/`~~` with `--formatting` |
 | `''verbatim''` | `` `verbatim` `` |
 | `~~strike~~` | `~~strike~~` |
-| `_{subscript}` | `<sub>subscript</sub>` |
-| `^{superscript}` | `<sup>superscript</sup>` |
+| `_{subscript}` | `<sub>subscript</sub>`, or `**`/`*`/`~~` with `--formatting` |
+| `^{superscript}` | `<sup>superscript</sup>`, or `**`/`*`/`~~` with `--formatting` |
 | `<b>bold</b>` or `<strong>bold</strong>` | `**bold**` |
 | `<i>italic</i>` or `<em>italic</em>` | `*italic*` |
 | `{{id: name}}` | `<a id="name"></a>` |
@@ -35,7 +35,9 @@
 | Tab-indented block | `> blockquote` |
 | `\| cell \| cell \|` pipe table | pipe table with an alignment row |
 
-Leading Zim metadata header lines (`Content-Type`, `Wiki-Format`, `Creation-Date`, `Modification-Date`) are removed. Heading levels are normalized per page: distinct Zim levels are compressed into consecutive Markdown levels starting at `#` and capped at `###`. A pipe table without an alignment row receives a default alignment row.
+Leading Zim metadata header lines (`Content-Type`, `Wiki-Format`, `Creation-Date`, `Modification-Date`) are removed. Heading levels are normalized per page: distinct Zim levels are compressed into consecutive Markdown levels starting at `#` and capped at `######`, the deepest Markdown level. A pipe table without an alignment row receives a default alignment row.
+
+Underline, subscript and superscript have no Markdown syntax, so they become HTML by default. `--formatting bold`, `--formatting italic` and `--formatting strike` collapse them into the chosen emphasis instead, which keeps the text readable in renderers that ignore raw HTML. Anchors such as `{{id: name}}` are not formatting and stay HTML in every mode.
 
 Image targets lose Zim display options such as `?width=512`, because Markdown has no equivalent and the option is not part of the file name. When converting a notebook from Zim to Markdown, an image referenced with `~` or with an absolute path follows `--images`: `copy` (the default) copies it to the `_assets` directory of the output, `symlink` creates an absolute symbolic link in `_assets`, and `relative` points at the original file with `..` segments. Links are rewritten relative to each page, so the generated images open in Markdown editors; missing files keep the cleaned target.
 
diff --git a/docs/domain.md b/docs/domain.md
--- a/docs/domain.md
+++ b/docs/domain.md
@@ -4,6 +4,10 @@
 
 A `ConversionDirection` is either `ZimToMarkdown` or `MarkdownToZim`. The constructor determines which parser reads the input and which extension is used for output pages.
 
+## Formatting modes
+
+A `FormattingMode` decides how inline Zim formatting without a Markdown counterpart is rendered. `HtmlFormatting` keeps `<u>`, `<sub>` and `<sup>`; `BoldFormatting`, `ItalicFormatting` and `StrikeFormatting` collapse underline, subscript and superscript into the chosen emphasis. The mode only affects the Zim to Markdown direction, and anchors are never collapsed.
+
 ## Documents
 
 A `ZimPage` is the immutable text of one page read from a Zim wiki notebook. It still contains Zim markup and has not been interpreted. A `MarkdownDocument` is the immutable text produced by a successful conversion to Markdown.
@@ -24,7 +28,7 @@
 - `[[:page]]` targets the notebook root.
 - `[[+page]]` targets a child of the current page.
 
-The resolved target is rendered relative to the current page and with the output extension, so the generated Markdown links remain valid. The notebook conversion calls `convertZimPageWithAssets`, which receives the final target for each external image; the executable chooses between copying the image to `_assets`, creating an absolute symlink there, or linking to the original with `..`, and the library only substitutes the target. The label renderer passed to `renderZimLink` makes inline markup inside link labels recursive.
+The resolved target is rendered relative to the current page and with the output extension, so the generated Markdown links remain valid. The notebook conversion calls `convertZimPageWithAssetsAndFormatting`, which receives the final target for each external image and the formatting mode. The executable chooses between copying the image to `_assets`, creating an absolute symlink there, or linking to the original with `..`, and the library only substitutes the target. The label renderer passed to `renderZimLink` makes inline markup inside link labels recursive.
 
 ## Notebook items
 
@@ -32,4 +36,4 @@
 
 ## Requests and errors
 
-A `ConversionRequest` records the input path, the optional output path, and the direction. Command line failures are modeled by `OptionsError`. Input and output failures are reported by `Main` as `Text` messages because they describe effects at the boundary.
+A `ConversionRequest` records the input path, the optional output path, the direction, the image mode and the formatting mode. Command line failures are modeled by `OptionsError`. Input and output failures are reported by `Main` as `Text` messages because they describe effects at the boundary.
diff --git a/docs/releasing.md b/docs/releasing.md
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -6,7 +6,7 @@
 
 The version is defined only in the `version:` field of `zim-to-markdown.cabal`. Cabal generates `Paths_zim_to_markdown`, and `ZimToMarkdown.Version` exposes it to the executable and tests. Update `CHANGELOG.md` before releasing.
 
-The tag convention in use is an annotated tag named `v<version>`, for example `v0.3.0.1`.
+The tag convention in use is an annotated tag named `v<version>`, for example `v0.4.0.0`.
 
 ## Pre-flight checks
 
@@ -14,8 +14,8 @@
 
 ```sh
 cabal check
-cabal build all
-cabal test all
+cabal build all -fdevelopment
+cabal test all -fdevelopment
 cabal haddock --haddock-for-hackage
 cabal sdist
 fourmolu --mode check src app test
@@ -60,17 +60,17 @@
 
 ## Tag and GitHub release
 
-Create, verify, and push the signed tag, then create the GitHub release:
+Push `main`, create and verify the signed tag, push the tag, then create the GitHub release:
 
 ```sh
-git tag -s v<version> -m "zim-to-markdown <version>"
-git tag -v v<version>
 git push origin main
+git tag v<version> -m "zim-to-markdown <version>"
+git tag -v v<version>
 git push origin v<version>
 gh release create v<version> --verify-tag --generate-notes
 ```
 
-Do not rewrite or force-push an existing release tag. The existing `v0.3.0.1` tag predates the signing configuration and remains unsigned; sign the next tag.
+`tag.gpgSign` signs the tag automatically, so `git tag` does not need `-s`. Do not rewrite or force-push an existing release tag.
 
 ### Tag signing
 
@@ -80,6 +80,7 @@
 git config gpg.format ssh
 git config user.signingkey <path-to-public-key>
 git config gpg.ssh.allowedSignersFile <path-to-allowed-signers>
+git config commit.gpgSign true
 git config tag.gpgSign true
 ```
 
@@ -104,7 +105,7 @@
 gh workflow run release-binaries.yml -f release_tag=v<version> -f publish=true
 ```
 
-The workflow builds Linux x86_64, macOS ARM64, and Windows x86_64 archives, attaches them to the release, adds provenance attestations, and publishes a `SHA256SUMS` file. A run with `publish=false` only builds and tests the artifacts.
+The workflow builds Linux x86_64, macOS ARM64, and Windows x86_64 archives, plus a Debian package for Linux x86_64, attaches them to the release, adds provenance attestations, and publishes a `SHA256SUMS` file. A run with `publish=false` only builds and tests the artifacts. The Debian package is built by `packaging/build-deb.sh` and depends on `libc6 (>= 2.35)` and `libgmp10`.
 
 ## CI
 
@@ -112,7 +113,7 @@
 
 ## Environment and secrets
 
-The `release` environment exists and is used by the `Release binaries` workflow. If a registry upload is added to a workflow, store its token as an environment secret and map it to the environment variable read by the upload command. Do not use Actions variables for tokens. Hackage publication is currently performed locally with `HACKAGE_TOKEN` in the environment.
+The `release` environment exists and is used by the `Release binaries` workflow. If a registry upload is added to a workflow, store its token as an environment secret and map it to the environment variable read by the upload command. Do not use Actions variables for tokens. Authenticate the Hackage upload with a token stored outside the repository; never commit, print or pass it on the command line.
 
 ## Rules
 
diff --git a/docs/versioning.md b/docs/versioning.md
--- a/docs/versioning.md
+++ b/docs/versioning.md
@@ -1,6 +1,6 @@
 # Versioning
 
-The project follows the Package Version Policy with four components: `major.minor.patch.revision`. The current version is `0.3.1.0`.
+The project follows the Package Version Policy with four components: `major.minor.patch.revision`. The current version is `0.4.0.1`.
 
 ## Source of truth
 
diff --git a/packaging/build-deb.sh b/packaging/build-deb.sh
new file mode 100644
--- /dev/null
+++ b/packaging/build-deb.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+# Builds a Debian binary package for the zim-to-markdown executable.
+set -eu
+
+if [ "$#" -ne 3 ]; then
+    echo "usage: $0 VERSION BINARY OUTPUT" >&2
+    exit 2
+fi
+
+version="$1"
+binary="$2"
+output="$3"
+
+root="$(mktemp -d)"
+trap 'rm -rf "$root"' EXIT
+
+install -Dm755 "$binary" "$root/usr/bin/zim-to-markdown"
+install -Dm644 README.md "$root/usr/share/doc/zim-to-markdown/README.md"
+install -Dm644 CHANGELOG.md "$root/usr/share/doc/zim-to-markdown/CHANGELOG.md"
+install -Dm644 LICENSE "$root/usr/share/doc/zim-to-markdown/copyright"
+gzip -9 -n -c CHANGELOG.md > "$root/usr/share/doc/zim-to-markdown/changelog.gz"
+chmod 644 "$root/usr/share/doc/zim-to-markdown/changelog.gz"
+mkdir -p "$root/usr/share/man/man1"
+sed "s|@VERSION@|$version|" packaging/zim-to-markdown.1 > "$root/usr/share/man/man1/zim-to-markdown.1"
+chmod 644 "$root/usr/share/man/man1/zim-to-markdown.1"
+gzip -9 -n "$root/usr/share/man/man1/zim-to-markdown.1"
+
+strip --strip-unneeded "$root/usr/bin/zim-to-markdown"
+
+installed_size="$(du -sk "$root" | cut -f1)"
+
+mkdir -p "$root/DEBIAN"
+cat > "$root/DEBIAN/control" <<EOF
+Package: zim-to-markdown
+Version: $version
+Section: text
+Priority: optional
+Architecture: amd64
+Depends: libc6 (>= 2.35), libgmp10
+Installed-Size: $installed_size
+Maintainer: Tralen <dev@tralen.cc>
+Homepage: https://github.com/Tralen/zim-to-markdown
+Description: Converts Zim wiki pages to Markdown and back
+ A command line converter between Zim wiki markup and Markdown.
+ It converts single pages and whole notebooks in both directions,
+ copies notebook assets, preserves the folder structure, and resolves
+ internal links.
+ .
+ Zim metadata headers are removed and heading levels are normalized
+ for Markdown output.
+EOF
+
+chmod 755 "$root" "$root/usr" "$root/usr/bin" "$root/usr/share" \
+    "$root/usr/share/doc" "$root/usr/share/doc/zim-to-markdown" \
+    "$root/usr/share/man" "$root/usr/share/man/man1" "$root/DEBIAN"
+chmod 644 "$root/DEBIAN/control"
+
+mkdir -p "$(dirname "$output")"
+dpkg-deb --build --root-owner-group "$root" "$output"
diff --git a/packaging/zim-to-markdown.1 b/packaging/zim-to-markdown.1
new file mode 100644
--- /dev/null
+++ b/packaging/zim-to-markdown.1
@@ -0,0 +1,42 @@
+.TH ZIM-TO-MARKDOWN 1 "2026-11-09" "zim-to-markdown @VERSION@" "User Commands"
+.SH NAME
+zim-to-markdown \- convert between Zim wiki pages and Markdown
+.SH SYNOPSIS
+.B zim-to-markdown
+[\fB\-\-direction\fR \fIDIRECTION\fR]
+[\fB\-\-output\fR \fIPATH\fR]
+[\fB\-\-images\fR \fIMODE\fR]
+[\fB\-\-formatting\fR \fIMODE\fR]
+\fIINPUT\fR
+.SH DESCRIPTION
+.B zim-to-markdown
+converts a single Zim page or a whole notebook to Markdown, and converts
+Markdown back to Zim. A notebook is a directory or a path to its
+\fInotebook.zim\fR file.
+.SH OPTIONS
+.TP
+.BR \-d ", " \-\-direction " " \fIDIRECTION\fR
+\fBzim-to-markdown\fR (default) or \fBmarkdown-to-zim\fR.
+.TP
+.BR \-o ", " \-\-output " " \fIPATH\fR
+Output file for a single page; output directory for a notebook.
+.TP
+.BR \-\-images " " \fIMODE\fR
+External images: \fBcopy\fR (default), \fBsymlink\fR or \fBrelative\fR;
+notebook conversion only.
+.TP
+.BR \-\-formatting " " \fIMODE\fR
+Inline formatting without a Markdown counterpart: \fBhtml\fR (default),
+\fBbold\fR, \fBitalic\fR or \fBstrike\fR; Zim to Markdown only.
+.TP
+.BR \-h ", " \-\-help
+Show usage information.
+.TP
+.BR \-V ", " \-\-version
+Show the application version.
+.SH EXIT STATUS
+The process exits with 0 on success and with a non-zero status when the
+arguments are invalid or when an input or output operation fails.
+.SH SEE ALSO
+The project documentation at
+\fIhttps://github.com/Tralen/zim-to-markdown\fR.
diff --git a/src/ZimToMarkdown/Converter.hs b/src/ZimToMarkdown/Converter.hs
--- a/src/ZimToMarkdown/Converter.hs
+++ b/src/ZimToMarkdown/Converter.hs
@@ -3,15 +3,20 @@
     convertMarkdownDocument,
     convertZimPage,
     convertZimPageWithAssets,
+    convertZimPageWithAssetsAndFormatting,
 ) where
 
 import Data.Map.Strict qualified as Map
 import Data.Text (Text)
 
-import ZimToMarkdown.Domain (MarkdownDocument, ZimPage)
+import ZimToMarkdown.Domain (FormattingMode, MarkdownDocument, ZimPage)
 import ZimToMarkdown.Links (LinkContext)
 import ZimToMarkdown.Markdown (convertMarkdownToZim)
-import ZimToMarkdown.Zim (convertZimToMarkdown, convertZimToMarkdownWithAssets)
+import ZimToMarkdown.Zim (
+    convertZimToMarkdown,
+    convertZimToMarkdownWithAssets,
+    convertZimToMarkdownWithAssetsAndFormatting,
+ )
 
 -- | Converts a Zim page into a Markdown document.
 convertZimPage :: LinkContext -> ZimPage -> MarkdownDocument
@@ -26,6 +31,18 @@
     ZimPage ->
     MarkdownDocument
 convertZimPageWithAssets = convertZimToMarkdownWithAssets
+
+{- | Converts a Zim page into a Markdown document with prepared image targets
+and a formatting mode for inline markup that Markdown cannot express.
+-}
+convertZimPageWithAssetsAndFormatting ::
+    FormattingMode ->
+    LinkContext ->
+    Map.Map Text Text ->
+    ZimPage ->
+    MarkdownDocument
+convertZimPageWithAssetsAndFormatting =
+    convertZimToMarkdownWithAssetsAndFormatting
 
 -- | Converts a Markdown document into a Zim page.
 convertMarkdownDocument :: MarkdownDocument -> ZimPage
diff --git a/src/ZimToMarkdown/Domain.hs b/src/ZimToMarkdown/Domain.hs
--- a/src/ZimToMarkdown/Domain.hs
+++ b/src/ZimToMarkdown/Domain.hs
@@ -1,6 +1,7 @@
 -- | Core immutable types used by the converter.
 module ZimToMarkdown.Domain (
     ConversionDirection (..),
+    FormattingMode (..),
     MarkdownDocument (..),
     RelativePath (..),
     ZimPage (..),
@@ -12,6 +13,17 @@
 data ConversionDirection
     = ZimToMarkdown
     | MarkdownToZim
+    deriving (Eq, Show)
+
+{- | How inline formatting without a Markdown counterpart is rendered. Zim
+underline, subscript and superscript have no Markdown syntax, so they become
+HTML tags by default or collapse into emphasis when requested.
+-}
+data FormattingMode
+    = HtmlFormatting
+    | BoldFormatting
+    | ItalicFormatting
+    | StrikeFormatting
     deriving (Eq, Show)
 
 -- | A page read from a Zim wiki notebook, still containing Zim markup.
diff --git a/src/ZimToMarkdown/Options.hs b/src/ZimToMarkdown/Options.hs
--- a/src/ZimToMarkdown/Options.hs
+++ b/src/ZimToMarkdown/Options.hs
@@ -14,7 +14,7 @@
 import Data.Text (Text)
 import Data.Text qualified as Text
 
-import ZimToMarkdown.Domain (ConversionDirection (..))
+import ZimToMarkdown.Domain (ConversionDirection (..), FormattingMode (..))
 
 -- | How images referenced outside the notebook are handled.
 data ImageMode
@@ -41,6 +41,7 @@
     , conversionOutput :: Maybe OutputPath
     , conversionDirection :: ConversionDirection
     , conversionImageMode :: ImageMode
+    , conversionFormatting :: FormattingMode
     }
     deriving (Eq, Show)
 
@@ -59,6 +60,7 @@
     | UnexpectedArgument Text
     | InvalidDirection Text
     | InvalidImageMode Text
+    | InvalidFormattingMode Text
     deriving (Eq, Show)
 
 -- | Parses command line arguments without performing any input or output.
@@ -73,6 +75,7 @@
     , partialOutput :: Maybe FilePath
     , partialDirection :: ConversionDirection
     , partialImageMode :: ImageMode
+    , partialFormatting :: FormattingMode
     }
 
 emptyOptions :: PartialOptions
@@ -82,6 +85,7 @@
         , partialOutput = Nothing
         , partialDirection = ZimToMarkdown
         , partialImageMode = CopyImages
+        , partialFormatting = HtmlFormatting
         }
 
 parseConversion :: [String] -> Either OptionsError ConversionRequest
@@ -97,12 +101,14 @@
                         , conversionOutput = OutputPath <$> partialOutput options
                         , conversionDirection = partialDirection options
                         , conversionImageMode = partialImageMode options
+                        , conversionFormatting = partialFormatting options
                         }
     go _ ["--output"] = Left (MissingOptionValue "--output")
     go _ ["-o"] = Left (MissingOptionValue "-o")
     go _ ["--direction"] = Left (MissingOptionValue "--direction")
     go _ ["-d"] = Left (MissingOptionValue "-d")
     go _ ["--images"] = Left (MissingOptionValue "--images")
+    go _ ["--formatting"] = Left (MissingOptionValue "--formatting")
     go options ("--output" : value : rest) =
         go options{partialOutput = Just value} rest
     go options ("-o" : value : rest) =
@@ -113,6 +119,8 @@
         setDirection options value rest
     go options ("--images" : value : rest) =
         setImageMode options value rest
+    go options ("--formatting" : value : rest) =
+        setFormatting options value rest
     go options (argument : rest)
         | isOption argument = Left (UnknownOption (Text.pack argument))
         | otherwise =
@@ -133,6 +141,12 @@
             Just imageMode ->
                 go options{partialImageMode = imageMode} rest
 
+    setFormatting options value rest =
+        case parseFormatting value of
+            Nothing -> Left (InvalidFormattingMode (Text.pack value))
+            Just formatting ->
+                go options{partialFormatting = formatting} rest
+
     isOption ('-' : _) = True
     isOption _ = False
 
@@ -151,6 +165,15 @@
         "relative" -> Just RelativeImages
         _ -> Nothing
 
+parseFormatting :: String -> Maybe FormattingMode
+parseFormatting value =
+    case value of
+        "html" -> Just HtmlFormatting
+        "bold" -> Just BoldFormatting
+        "italic" -> Just ItalicFormatting
+        "strike" -> Just StrikeFormatting
+        _ -> Nothing
+
 -- | Renders an option parsing failure as a user facing message.
 renderOptionsError :: OptionsError -> Text
 renderOptionsError optionsError =
@@ -167,6 +190,10 @@
             "Invalid image mode: "
                 <> imageMode
                 <> ". Use copy, symlink or relative."
+        InvalidFormattingMode formatting ->
+            "Invalid formatting mode: "
+                <> formatting
+                <> ". Use html, bold, italic or strike."
 
 -- | Command line usage description.
 helpText :: Text
@@ -175,7 +202,7 @@
         [ "zim-to-markdown - converts between Zim wiki and Markdown."
         , ""
         , "Usage:"
-        , "  zim-to-markdown [--direction DIRECTION] [--output PATH] INPUT"
+        , "  zim-to-markdown [--direction DIRECTION] [--output PATH] [--images MODE] [--formatting MODE] INPUT"
         , "  zim-to-markdown --help"
         , "  zim-to-markdown --version"
         , ""
@@ -183,6 +210,8 @@
         , "  -d, --direction DIRECTION  zim-to-markdown (default) or markdown-to-zim."
         , "  -o, --output PATH          Output file for a page; output directory for a notebook."
         , "      --images MODE          External images: copy (default), symlink or relative."
+        , "      --formatting MODE      Inline formatting without a Markdown counterpart:"
+        , "                             html (default), bold, italic or strike."
         , "  -h, --help                 Show this help."
         , "  -V, --version              Show the application version."
         , ""
diff --git a/src/ZimToMarkdown/Zim.hs b/src/ZimToMarkdown/Zim.hs
--- a/src/ZimToMarkdown/Zim.hs
+++ b/src/ZimToMarkdown/Zim.hs
@@ -2,6 +2,8 @@
 module ZimToMarkdown.Zim (
     convertZimToMarkdown,
     convertZimToMarkdownWithAssets,
+    convertZimToMarkdownWithAssetsAndFormatting,
+    convertZimToMarkdownWithFormatting,
     zimImagePaths,
 ) where
 
@@ -12,7 +14,7 @@
 import Data.Text qualified as Text
 import System.FilePath (takeBaseName)
 
-import ZimToMarkdown.Domain (MarkdownDocument (..), ZimPage (..))
+import ZimToMarkdown.Domain (FormattingMode (..), MarkdownDocument (..), ZimPage (..))
 import ZimToMarkdown.Inline (
     breakOnFirstMarker,
     enclosed,
@@ -29,9 +31,19 @@
 
 -- | Converts a Zim page into Markdown.
 convertZimToMarkdown :: LinkContext -> ZimPage -> MarkdownDocument
-convertZimToMarkdown context =
-    convertZimToMarkdownWithAssets context Map.empty
+convertZimToMarkdown = convertZimToMarkdownWithFormatting HtmlFormatting
 
+{- | Converts a Zim page into Markdown, choosing how inline formatting without
+a Markdown counterpart is rendered.
+-}
+convertZimToMarkdownWithFormatting ::
+    FormattingMode ->
+    LinkContext ->
+    ZimPage ->
+    MarkdownDocument
+convertZimToMarkdownWithFormatting formatting context =
+    convertZimToMarkdownWithAssetsAndFormatting formatting context Map.empty
+
 {- | Converts a Zim page into Markdown, rewriting images to the targets the
 notebook conversion prepared for them.
 -}
@@ -40,7 +52,19 @@
     Map.Map Text Text ->
     ZimPage ->
     MarkdownDocument
-convertZimToMarkdownWithAssets context assets (ZimPage content) =
+convertZimToMarkdownWithAssets =
+    convertZimToMarkdownWithAssetsAndFormatting HtmlFormatting
+
+{- | Converts a Zim page into Markdown with prepared image targets and a
+formatting mode for inline markup that Markdown cannot express.
+-}
+convertZimToMarkdownWithAssetsAndFormatting ::
+    FormattingMode ->
+    LinkContext ->
+    Map.Map Text Text ->
+    ZimPage ->
+    MarkdownDocument
+convertZimToMarkdownWithAssetsAndFormatting formatting context assets (ZimPage content) =
     let pageLines =
             Text.splitOn
                 "\n"
@@ -49,7 +73,7 @@
      in MarkdownDocument
             ( Text.intercalate
                 "\n"
-                (renderBlocks context assets headingMap pageLines)
+                (renderBlocks formatting context assets headingMap pageLines)
             )
 
 stripZimHeader :: Text -> Text
@@ -104,8 +128,8 @@
     | InsideVerbatim
     deriving (Eq)
 
-renderBlocks :: LinkContext -> Map.Map Text Text -> Map.Map Int Int -> [Text] -> [Text]
-renderBlocks context assets headingMap = go OutsideVerbatim
+renderBlocks :: FormattingMode -> LinkContext -> Map.Map Text Text -> Map.Map Int Int -> [Text] -> [Text]
+renderBlocks formatting context assets headingMap = go OutsideVerbatim
   where
     go _ [] = []
     go state (line : rest)
@@ -117,7 +141,7 @@
             let (block, remaining) = takeTableBlock (line : rest)
              in normalizeTable block <> go OutsideVerbatim remaining
         | otherwise =
-            renderLine context assets headingMap line : go OutsideVerbatim rest
+            renderLine formatting context assets headingMap line : go OutsideVerbatim rest
 
 toggleState :: BlockState -> BlockState
 toggleState OutsideVerbatim = InsideVerbatim
@@ -126,22 +150,22 @@
 isVerbatimDelimiter :: Text -> Bool
 isVerbatimDelimiter line = Text.strip line == "'''"
 
-renderLine :: LinkContext -> Map.Map Text Text -> Map.Map Int Int -> Text -> Text
-renderLine context assets headingMap line
+renderLine :: FormattingMode -> LinkContext -> Map.Map Text Text -> Map.Map Int Int -> Text -> Text
+renderLine formatting context assets headingMap line
     | Text.null (Text.strip line) = line
     | Just (level, content) <- parseHeading line =
         let markdownLevel =
-                min 3 (Map.findWithDefault level level headingMap)
+                min 6 (Map.findWithDefault level level headingMap)
          in Text.replicate markdownLevel "#"
                 <> " "
-                <> convertInline context assets content
+                <> convertInline formatting context assets content
     | isHorizontalRule line = "---"
     | Just listLine <- parseListLine line =
-        renderListLine context assets listLine
+        renderListLine formatting context assets listLine
     | Just (depth, content) <- parseIndentedBlock line =
         Text.replicate depth "> "
-            <> renderLine context assets headingMap content
-    | otherwise = convertInline context assets line
+            <> renderLine formatting context assets headingMap content
+    | otherwise = convertInline formatting context assets line
 
 isHorizontalRule :: Text -> Bool
 isHorizontalRule line =
@@ -231,11 +255,11 @@
                 Just (Alphabetic, rest)
         _ -> Nothing
 
-renderListLine :: LinkContext -> Map.Map Text Text -> ListLine -> Text
-renderListLine context assets listLine =
+renderListLine :: FormattingMode -> LinkContext -> Map.Map Text Text -> ListLine -> Text
+renderListLine formatting context assets listLine =
     indentToSpaces (listIndent listLine)
         <> prefix
-        <> convertInline context assets (listContent listLine)
+        <> convertInline formatting context assets (listContent listLine)
   where
     prefix =
         case listMarker listLine of
@@ -272,8 +296,8 @@
     , "^{"
     ]
 
-convertInline :: LinkContext -> Map.Map Text Text -> Text -> Text
-convertInline context assets = go
+convertInline :: FormattingMode -> LinkContext -> Map.Map Text Text -> Text -> Text
+convertInline formatting context assets = go
   where
     go text =
         let (plain, marker) = breakOnFirstMarker inlineMarkers text
@@ -304,8 +328,7 @@
         , Just result <-
             enclosed "//" "//" (\inner -> "*" <> go inner <> "*") text =
             keep result
-        | Just result <-
-            enclosed "__" "__" (\inner -> "<u>" <> go inner <> "</u>") text =
+        | Just result <- enclosed "__" "__" (renderUnsupported "u") text =
             keep result
         | Just result <- enclosed "''" "''" renderInlineCode text =
             keep result
@@ -316,20 +339,26 @@
             enclosed
                 "[["
                 "]]"
-                (renderZimLink context (convertInline context assets))
+                (renderZimLink context (convertInline formatting context assets))
                 text =
             keep result
         | Just result <- enclosed "{{" "}}" (renderImage assets) text =
             keep result
-        | Just result <-
-            enclosed "_{" "}" (\inner -> "<sub>" <> go inner <> "</sub>") text =
+        | Just result <- enclosed "_{" "}" (renderUnsupported "sub") text =
             keep result
-        | Just result <-
-            enclosed "^{" "}" (\inner -> "<sup>" <> go inner <> "</sup>") text =
+        | Just result <- enclosed "^{" "}" (renderUnsupported "sup") text =
             keep result
         | otherwise = Text.take 1 text <> go (Text.drop 1 text)
 
     keep (rendered, after) = rendered <> go after
+
+    renderUnsupported :: Text -> Text -> Text
+    renderUnsupported tag inner =
+        case formatting of
+            HtmlFormatting -> "<" <> tag <> ">" <> go inner <> "</" <> tag <> ">"
+            BoldFormatting -> "**" <> go inner <> "**"
+            ItalicFormatting -> "*" <> go inner <> "*"
+            StrikeFormatting -> "~~" <> go inner <> "~~"
 
 renderAnchor :: Text -> Text
 renderAnchor inner =
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -18,9 +18,11 @@
     convertMarkdownDocument,
     convertZimPage,
     convertZimPageWithAssets,
+    convertZimPageWithAssetsAndFormatting,
  )
 import ZimToMarkdown.Domain (
     ConversionDirection (..),
+    FormattingMode (..),
     MarkdownDocument (..),
     RelativePath (..),
     ZimPage (..),
@@ -139,9 +141,9 @@
         (zimToMarkdown (Text.unlines ["=== Section ===", "", "== Subsection =="]))
         (Text.unlines ["# Section", "", "## Subsection"])
     , TestCase
-        "zim headings are capped at three levels"
+        "zim headings preserve the hierarchy"
         (zimToMarkdown (Text.unlines ["====== Title ======", "===== Two =====", "==== Three ====", "=== Four ===", "== Five =="]))
-        (Text.unlines ["# Title", "## Two", "### Three", "### Four", "### Five"])
+        (Text.unlines ["# Title", "## Two", "### Three", "#### Four", "##### Five"])
     , TestCase
         "zim metadata header removed"
         ( zimToMarkdown
@@ -184,6 +186,34 @@
         (zimToMarkdown "x^{2}")
         "x<sup>2</sup>"
     , TestCase
+        "zim underline collapses to bold"
+        (zimToMarkdownWithFormatting BoldFormatting "__under__")
+        "**under**"
+    , TestCase
+        "zim underline collapses to italic"
+        (zimToMarkdownWithFormatting ItalicFormatting "__under__")
+        "*under*"
+    , TestCase
+        "zim subscript collapses to bold"
+        (zimToMarkdownWithFormatting BoldFormatting "H_{2}O")
+        "H**2**O"
+    , TestCase
+        "zim superscript collapses to italic"
+        (zimToMarkdownWithFormatting ItalicFormatting "x^{2}")
+        "x*2*"
+    , TestCase
+        "zim underline collapses to strike"
+        (zimToMarkdownWithFormatting StrikeFormatting "__under__")
+        "~~under~~"
+    , TestCase
+        "zim subscript collapses to strike"
+        (zimToMarkdownWithFormatting StrikeFormatting "H_{2}O")
+        "H~~2~~O"
+    , TestCase
+        "zim anchor stays html when formatting collapses"
+        (zimToMarkdownWithFormatting BoldFormatting "{{id: intro}}")
+        "<a id=\"intro\"></a>"
+    , TestCase
         "zim bullet list"
         (zimToMarkdown "* item")
         "* item"
@@ -492,6 +522,32 @@
         "missing image mode value is rejected"
         (optionErrorText (parseOptions ["page.txt", "--images"]))
         "Missing value for option: --images"
+    , TestCase
+        "formatting defaults to html"
+        (formattingResult ["page.txt"])
+        "HtmlFormatting"
+    , TestCase
+        "formatting bold parses"
+        (formattingResult ["--formatting", "bold", "page.txt"])
+        "BoldFormatting"
+    , TestCase
+        "formatting italic parses"
+        (formattingResult ["--formatting", "italic", "page.txt"])
+        "ItalicFormatting"
+    , TestCase
+        "formatting strike parses"
+        (formattingResult ["--formatting", "strike", "page.txt"])
+        "StrikeFormatting"
+    , TestCase
+        "invalid formatting is rejected"
+        ( optionErrorText
+            (parseOptions ["--formatting", "bogus", "page.txt"])
+        )
+        "Invalid formatting mode: bogus. Use html, bold, italic or strike."
+    , TestCase
+        "missing formatting value is rejected"
+        (optionErrorText (parseOptions ["page.txt", "--formatting"]))
+        "Missing value for option: --formatting"
     ]
 
 zimToMarkdown :: Text -> Text
@@ -502,12 +558,31 @@
     unMarkdownDocument
         (convertZimPageWithAssets linkContext assets (ZimPage input))
 
+zimToMarkdownWithFormatting :: FormattingMode -> Text -> Text
+zimToMarkdownWithFormatting formatting input =
+    unMarkdownDocument
+        ( convertZimPageWithAssetsAndFormatting
+            formatting
+            linkContext
+            Map.empty
+            (ZimPage input)
+        )
+
 imageModeResult :: [String] -> Text
 imageModeResult arguments =
     case parseOptions arguments of
         Left optionsError -> renderOptionsError optionsError
         Right (Convert request) ->
             Text.pack (show (conversionImageMode request))
+        Right ShowHelp -> "help"
+        Right ShowVersion -> "version"
+
+formattingResult :: [String] -> Text
+formattingResult arguments =
+    case parseOptions arguments of
+        Left optionsError -> renderOptionsError optionsError
+        Right (Convert request) ->
+            Text.pack (show (conversionFormatting request))
         Right ShowHelp -> "help"
         Right ShowVersion -> "version"
 
diff --git a/zim-to-markdown.cabal b/zim-to-markdown.cabal
--- a/zim-to-markdown.cabal
+++ b/zim-to-markdown.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               zim-to-markdown
-version:            0.3.1.0
+version:            0.4.0.1
 synopsis:           Converts Zim wiki pages to Markdown and back.
 description:
     A command line converter between Zim wiki markup and Markdown.
@@ -30,6 +30,8 @@
                     docs/releasing.md
                     docs/roadmap.md
                     docs/versioning.md
+extra-source-files: packaging/build-deb.sh
+                    packaging/zim-to-markdown.1
 
 source-repository head
     type:     git
