English | 简体中文
Offline Word (.docx) → Markdown desktop converter that gets merged cells and multi-page tables right.
“Markdown 快转” in the app UI.
Almost every docx → Markdown tool falls apart on the tables found in real business documents:
- Merged cells: headers merged horizontally or vertically get split up or dropped, so columns shift;
- Multi-page tables: a table split by a page break in Word comes out as two broken tables;
- Line breaks, empty cells and nested paragraphs inside cells: the whole table usually collapses into a single line.
The conversion engine post-processes these cases specifically. Vertically merged cells are padded with placeholders across the rows they span, so every row has the same number of columns and nothing shifts. Horizontally merged cells collapse into a single cell of text. Table fragments separated by a page break are stitched back into one table, with the duplicated header/separator rows at the break removed so only one proper header row remains. The resulting Markdown tables can be pasted straight into a docs site or knowledge base. Documents are converted locally; only images are uploaded, and only to object storage you configure.
- Batch conversion: drag and drop or click + to add multiple
.docxfiles; conversions run concurrently with live progress. - Lossless tables: merged cells, multi-page tables and line breaks inside cells are all restored correctly.
- Image hosting: images in the document are uploaded automatically to your own Alibaba Cloud OSS bucket (via STS temporary credentials, no long-lived keys stored on disk) and appear in the Markdown as accessible links. OSS currently has to be configured in Settings before converting.
- Preview / source / save as: results render as a preview as soon as conversion finishes, with a toggle to view the Markdown source. Both the source document and the result can be saved anywhere.
- History: conversion records are stored locally and listed on launch, with fuzzy search, deletion, retention period and storage location settings.
- Dark / light / follow system: switchable from Settings.
- Local conversion: documents never pass through any third-party service. No network requests are made apart from uploading images to your OSS bucket.
Download the package for your platform from Releases.
- macOS: unzip and drag
doc_shift_app.appinto Applications. The app is not notarized by Apple, so the first launch is blocked: on macOS 14 and earlier, right-click and choose "Open"; on macOS 15 and later, double-click once, then go to System Settings → Privacy & Security and click "Open Anyway" at the bottom. Alternatively runxattr -cr /Applications/doc_shift_app.appand double-click as usual. - Linux: unzip and run
bundle/doc_shift_app. - Windows: unzip and run
doc_shift_app.exe. The binary is not code-signed, so SmartScreen shows "Windows protected your PC"; click "More info → Run anyway".
- Flutter 3.47 (stable)
- Python ≥ 3.12
- On Linux desktop, the GTK development libraries required by Flutter
git clone <this repo> && cd doc_shift
# Python conversion engine: venv or conda, pick one
cd engine
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" # venv
# or, inside an activated conda environment:
pip install -e ".[dev]" && export DOCMD_PYTHON="$(which python)"
# Flutter app
cd ../app && flutter pub getThe app and the packaging scripts look for a Python with docmd installed in this order:
DOCMD_PYTHON → engine/.venv → python3 on PATH. With conda, the easiest option is to put
DOCMD_PYTHON in your shell config.
cd app
flutter run -d macos # or linux / windowsIn development the app launches the conversion engine as a subprocess using the Python above, so engine changes do not require repackaging.
# Flutter: widget tests plus integration tests that spawn the real engine subprocess
cd app && flutter analyze && flutter test
# Python: conversion engine and sidecar unit tests
cd engine && pytest # .venv/bin/pytest when using venvThe engine is bundled with PyInstaller (onedir) into a standalone directory shipped alongside the app, so end users do not need Python.
scripts/build_macos_release.sh # run on macOS, produces the .app and a distributable zip
scripts/build_linux_release.sh # run on Linux, produces build/linux/x64/release/bundle/
.\scripts\build_windows_release.ps1 # run in Windows PowerShell, produces the Release directory and a zipAll three scripts build the engine first, then the Flutter app, and finally verify that the engine is
actually inside the output. Windows requires the Visual Studio 2022 "Desktop development with C++"
workload (the official requirement for Flutter Windows desktop builds). If PowerShell refuses to run the
script, run Set-ExecutionPolicy -Scope Process Bypass first. The Windows packaging flow has not yet been
verified on real hardware; feedback is welcome.
macOS notes:
- Ad-hoc signing is used, so no Apple developer account is needed. To use your own certificate, copy
app/macos/Runner/Configs/Signing.local.xcconfig.exampletoSigning.local.xcconfigin the same directory and fill in the certificate name (that file is git-ignored). - The engine directory lives under
.app/Contents/Resources/rather thanContents/MacOS/, because codesign requires every file in the latter to be signable as code, and the PyInstaller output contains data files. - OSS credentials are not stored in the system keychain (ad-hoc signing would trigger a keychain authorization prompt on every build). Instead they are encrypted with AES-256-GCM and stored in the app data directory with file permissions 0600. The key is derived from a hardware identifier of the machine plus a random salt, so the ciphertext cannot be decrypted on another machine; the app will ask you to enter the credentials again.
The logo source is app/assets/logo/logo.svg. After changing it, run scripts/generate_icons.sh
(requires rsvg-convert and Pillow) to regenerate the platform icons and the in-app logo.
The version number is maintained only in the version field of app/pubspec.yaml. To release, bump it,
commit, then create and push a tag with the same name:
git tag v1.0.1 && git push origin v1.0.1GitHub Actions (.github/workflows/release.yml) packages the app on Linux, macOS and Windows in parallel
and attaches the artifacts to the Release of the same name, with auto-generated release notes. The
pipeline fails if the tag does not match the pubspec version. Triggering the workflow manually only builds
without publishing, which is useful for verification.
┌──────────────────────────┐ HTTP / WebSocket (127.0.0.1) ┌──────────────────────────────┐
│ Flutter desktop (app/) │ ───────────────────────────────▶ │ Python engine (engine/) │
│ UI · queue · history · │ ◀─────────────────────────────── │ docx→md · table post- │
│ settings │ progress / results │ processing · OSS upload │
└──────────────────────────┘ └──────────────────────────────┘
On startup the app spawns the engine subprocess. The engine listens on a random local port and hands the port plus a one-time token back through the first line of stdout; every subsequent request carries the token, so other processes on the same machine cannot reach it. Each file maps to one conversion job, and progress is pushed over WebSocket.
| Layer | Technology |
|---|---|
| Desktop app | Flutter + Riverpod, drift (SQLite) for history |
| Conversion engine | Python: mammoth + BeautifulSoup + markdownify + custom table post-processing |
| IPC | FastAPI + uvicorn |
| Image upload | Alibaba Cloud STS + alibabacloud_oss_v2 |
| Packaging | PyInstaller + Flutter desktop |
Issues and PRs are welcome. Before submitting, make sure flutter analyze, flutter test and pytest
pass, and that Dart code is formatted with dart format. For changes touching macOS packaging or signing,
please state which macOS version you verified on. If you have a docx that converts poorly, attaching it to
an Issue (with sensitive content removed) is the most valuable contribution you can make.
MIT © 2026 zed.Zhao
