How it works

gramdown does one job: turn the .docx file Grammarly hands you when you export a document into Markdown. This page covers what that file looks like, how the conversion works, and where it has limits.

What a Grammarly export looks like

A Grammarly export is a Word document, but not a tidy one. It is consistent in a few specific, awkward ways, and gramdown is built around exactly these:

  • Code is body text in a monospace font. Grammarly sets code in Courier New (the font it uses for anything it treats as code) but leaves it as an ordinary paragraph. There is no “Code” style, no language, no syntax colours: nothing marks it as code except the font.
  • Code is indented with non-breaking spaces. The invisible U+00A0 character, not ordinary spaces or a tab. Paste that text elsewhere and the indentation collapses.
  • Every link is underlined. Word underlines hyperlinks automatically, whether or not anyone intended it.

A converter that takes this document at face value produces Markdown that needs a lot of hand-cleaning. gramdown assumes it is always looking at a Grammarly export and reads those quirks as the structure they stand in for.

The four stages

gramdown runs the file through four stages:

.docx bytes

   │  preprocess    expose the document's structure
   │  mammoth       .docx  →  HTML
   │  turndown      HTML  →  Markdown
   │  postprocess   tidy up the Markdown

Markdown string

mammoth and turndown are libraries: one turns a .docx into simple HTML, the other turns HTML into GitHub-Flavored Markdown. preprocess and postprocess are gramdown’s own work on either side of them.

Preprocess

mammoth (the next stage) deliberately ignores direct formatting like fonts and colours and only acts on named styles. So gramdown rewrites the document first: it unzips the .docx, edits word/document.xml, and rezips it. Every all-monospace paragraph is tagged with a “Source Code” paragraph style, and monospace stretches inside normal prose get a character style. Consecutive code paragraphs, blank lines included, are merged into a single block. After this step, the code Grammarly left as plain Courier New paragraphs is something the next stage can recognise.

mammoth

mammoth converts the rewritten .docx into simple, semantic HTML. Because it works from styles rather than formatting, the styles injected in the previous step come out as <pre> and <code> elements, and Word’s automatic link underline mostly falls away on its own. An underline that was deliberate is kept, through mammoth’s style map.

turndown

turndown converts that HTML into Markdown, with the turndown-plugin-gfm add-on for tables and the rest of GitHub-Flavored Markdown. It is configured for # headings, fenced ``` code blocks, - bullets, _ and ** for emphasis, inline links, and no line wrapping (one line per paragraph). Underline, subscript and superscript are kept as inline HTML tags, since Markdown has no syntax for them. The non-breaking-space indentation inside code is swapped back to real spaces here, so the code is safe to copy.

Postprocess

A final pass over the Markdown: runs of blank lines collapse to one, trailing whitespace is trimmed, and the file ends with exactly one newline. The insides of code blocks are left exactly as they are: their blank lines and spacing are part of the code.

Why not pandoc?

pandoc is the usual answer to “convert a document to Markdown”. It handles dozens of formats and it is very good at it. But it is a general-purpose tool, so it reads a Grammarly export literally: monospace paragraphs stay paragraphs, the non-breaking-space indentation comes through unchanged, every link keeps its underline. The result compiles but reads badly, and cleaning up the code blocks by hand is most of the work.

gramdown only has to handle one input, so it can do the opposite: treat those quirks as signal. It is also plain JavaScript with no system dependencies, so there is nothing to install alongside it. pandoc is a separate binary.

Known limitations

  • Code fences have no language. Grammarly records none, so there is nothing to recover. --guess-lang on the command line, or the guessLanguage option in the API, makes a heuristic guess that always needs a human pass. Without it, fences are left bare.
  • Underline, subscript and superscript come through as inline HTML, not Markdown. There is no GitHub-Flavored Markdown equivalent to convert them to.