<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: frank</title>
    <description>The latest articles on DEV Community by frank (@mdfold).</description>
    <link>https://dev.to/mdfold</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4051706%2Ff7721660-f842-4411-ae4e-5036d351321f.jpg</url>
      <title>DEV Community: frank</title>
      <link>https://dev.to/mdfold</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mdfold"/>
    <language>en</language>
    <item>
      <title>Invisible Unicode Characters Can Break Markdown Without Looking Different</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 22 Aug 2026 22:44:45 +0000</pubDate>
      <link>https://dev.to/mdfold/invisible-unicode-characters-can-break-markdown-without-looking-different-2e3d</link>
      <guid>https://dev.to/mdfold/invisible-unicode-characters-can-break-markdown-without-looking-different-2e3d</guid>
      <description>&lt;p&gt;Two Markdown lines can look identical and still produce different HTML. Before blaming the parser, inspect the code points.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# heading
#​heading
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second line contains U+200B ZERO WIDTH SPACE after &lt;code&gt;#&lt;/code&gt;. CommonMark requires the ATX marker sequence to be followed by a space, tab, or line ending, so this is a paragraph rather than a heading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the invisible input observable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codePoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`U+&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;codePointAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;codePoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;u200Bheading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;// U+0023 U+200B U+0068 ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the source came from an unknown editor, first verify that it is actually a text file and inspect its encoding. This &lt;a href="https://mdfold.com/guides/how-to-open-a-markdown-file" rel="noopener noreferrer"&gt;guide to opening Markdown files&lt;/a&gt; is a useful checklist; the important step is inspection, not conversion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What three fixed parser versions produced
&lt;/h2&gt;

&lt;p&gt;I tested Node.js 25.3.0 with Marked 18.0.7, markdown-it 14.1.0, and commonmark.js 0.31.2. These are observed outputs under default options, not claims about every Markdown platform.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Marked&lt;/th&gt;
&lt;th&gt;markdown-it&lt;/th&gt;
&lt;th&gt;commonmark.js&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;# heading&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;leading U+FEFF before &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;paragraph, FEFF retained&lt;/td&gt;
&lt;td&gt;paragraph, FEFF omitted in output&lt;/td&gt;
&lt;td&gt;paragraph, FEFF omitted in output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+200B after &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;U+00A0 after &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;heading&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;td&gt;paragraph&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fullwidth &lt;code&gt;＃&lt;/code&gt; or &lt;code&gt;＊&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;td&gt;plain text&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The NBSP result is the useful warning: a character that looks like a space creates a real cross-parser difference. “Whitespace” is not one universal Markdown category.&lt;/p&gt;

&lt;h2&gt;
  
  
  A BOM is positional
&lt;/h2&gt;

&lt;p&gt;UTF-8 uses &lt;code&gt;EF BB BF&lt;/code&gt; as its optional signature. After decoding, that is U+FEFF. Unicode says a recognized initial BOM should be removed before text processing. The same code point in the middle of text is not automatically a BOM and must not be globally deleted.&lt;/p&gt;

&lt;p&gt;In the experiment, a leading U+FEFF prevented &lt;code&gt;#&lt;/code&gt; from being the first character seen by the Markdown grammar. An internal &lt;code&gt;alpha\uFEFFbeta&lt;/code&gt; remained present in all three outputs. Fix BOM handling at the decoding boundary, not with &lt;code&gt;replaceAll('\uFEFF', '')&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero width does not mean zero semantics
&lt;/h2&gt;

&lt;p&gt;U+200B is a format control used to indicate a word or line-break opportunity. Its effect depends on position:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;after &lt;code&gt;#&lt;/code&gt;, it prevents ATX-heading recognition;&lt;/li&gt;
&lt;li&gt;inside &lt;code&gt;**bo\u200Bld**&lt;/code&gt;, all three parsers still create &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;, while preserving U+200B in the text node.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a blanket “remove invisible characters” rule is both too broad and too weak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Look-alike punctuation is different punctuation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;＃&lt;/code&gt; is U+FF03, not U+0023. &lt;code&gt;＊&lt;/code&gt; is U+FF0A, not U+002A. All three parsers treated the fullwidth examples as ordinary text.&lt;/p&gt;

&lt;p&gt;NFKC normalization can map some compatibility characters to ASCII, but silently normalizing user source changes data. A safer editor warns, previews the proposed change, and lets the author approve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bidirectional controls require a different review model
&lt;/h2&gt;

&lt;p&gt;UAX #9 distinguishes logical order from display order. Directional controls can change what reviewers see while parsers continue reading logical code-point order.&lt;/p&gt;

&lt;p&gt;With U+202E inside a Markdown URL, none of the three parsers simply discarded it. Marked and commonmark.js percent-encoded it; markdown-it serialized the host differently through IDNA processing. The safe review target is therefore not just the visible source. Inspect the code points, parsed &lt;code&gt;href&lt;/code&gt;, and final resolved URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical ingestion policy
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Handle an initial BOM during byte decoding.&lt;/li&gt;
&lt;li&gt;Preserve raw bytes in regression fixtures.&lt;/li&gt;
&lt;li&gt;Flag &lt;code&gt;Cf&lt;/code&gt; characters and non-ASCII look-alike punctuation near Markdown delimiters.&lt;/li&gt;
&lt;li&gt;Show code point, line, column, and Unicode name in diagnostics.&lt;/li&gt;
&lt;li&gt;Preview repairs instead of silently rewriting source.&lt;/li&gt;
&lt;li&gt;Validate links after parsing and URL resolution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Invisible-character bugs are boundary bugs: the decoder, editor, Markdown grammar, HTML serializer, and browser may each make a different decision. Good tooling makes those decisions observable.&lt;/p&gt;

&lt;p&gt;Would you prefer an editor that automatically cleans invisible characters, or one that always preserves the source and only warns?&lt;/p&gt;

</description>
      <category>security</category>
      <category>markdown</category>
      <category>webdev</category>
      <category>unicode</category>
    </item>
    <item>
      <title>Markdown Images Are Network Requests, Not Embedded Assets</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Thu, 20 Aug 2026 06:08:34 +0000</pubDate>
      <link>https://dev.to/mdfold/markdown-images-are-network-requests-not-embedded-assets-5580</link>
      <guid>https://dev.to/mdfold/markdown-images-are-network-requests-not-embedded-assets-5580</guid>
      <description>&lt;p&gt;The line &lt;code&gt;![diagram](../assets/flow.png)&lt;/code&gt; does not embed an image. It creates an image node whose destination still has to be resolved, fetched, allowed, and rendered by another system.&lt;/p&gt;

&lt;p&gt;That distinction explains why a document can parse perfectly and still show broken images—or make network requests its author did not account for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing and URL resolution are separate stages
&lt;/h2&gt;

&lt;p&gt;I fixed the experiment to Node 25.3.0 and Marked 18.0.7 with GFM enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;![diagram](../assets/flow.png "Flow")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// &amp;lt;p&amp;gt;&amp;lt;img src="../assets/flow.png" alt="diagram" title="Flow"&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marked preserves the destination. A browser or publisher later resolves it against a base URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../assets/flow.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.example.test/guides/setup/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
&lt;span class="c1"&gt;// https://docs.example.test/guides/assets/flow.png&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../assets/flow.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://docs.example.test/guides/setup.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;
&lt;span class="c1"&gt;// https://docs.example.test/assets/flow.png&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same Markdown, different request. The parser did its job in both cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six cases that need different policies
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Parser output&lt;/th&gt;
&lt;th&gt;What the host must decide&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;../assets/flow.png&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;relative &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;base URL and asset copy rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/assets/logo.svg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;root-relative &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;which origin owns the root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://.../pixel.png?doc=42&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absolute &lt;code&gt;src&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;remote trust and request privacy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data:image/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;data URL&lt;/td&gt;
&lt;td&gt;sanitizer and CSP policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty alt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alt=""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decorative or accessibility defect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty destination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;src=""&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reject, rewrite, or host behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CommonMark defines image syntax and maps the description to alt text. It does not upload files or promise that a URL exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote images are third-party dependencies
&lt;/h2&gt;

&lt;p&gt;When HTML contains a remote &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;, the browser requests that resource. The remote server observes the request; the page's Referrer Policy controls how much referring-page information is sent. Query parameters can also encode a document or campaign identifier.&lt;/p&gt;

&lt;p&gt;Treat remote images like any other external dependency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allow only reviewed schemes and hosts;&lt;/li&gt;
&lt;li&gt;proxy or self-host assets when appropriate;&lt;/li&gt;
&lt;li&gt;set a deliberate Referrer Policy;&lt;/li&gt;
&lt;li&gt;restrict image sources with CSP &lt;code&gt;img-src&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;sanitize final HTML after AST transformations;&lt;/li&gt;
&lt;li&gt;never put sensitive identifiers in image URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parser configuration cannot replace those controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative paths need a publishing contract
&lt;/h2&gt;

&lt;p&gt;A stable workflow defines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;where the Markdown file lives;&lt;/li&gt;
&lt;li&gt;what base URL the final page uses;&lt;/li&gt;
&lt;li&gt;how referenced assets are copied;&lt;/li&gt;
&lt;li&gt;whether root-relative paths are allowed;&lt;/li&gt;
&lt;li&gt;what happens when an image is missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are debugging a real document, this &lt;a href="https://mdfold.com/guides/add-images-in-markdown" rel="noopener noreferrer"&gt;Markdown image path guide&lt;/a&gt; provides a concrete path checklist. The important step is to test the published URL, not just the editor preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data URLs trade path stability for other costs
&lt;/h2&gt;

&lt;p&gt;Data URLs avoid a separate file lookup, but they make source files larger and harder to review, prevent independent caching, and may be blocked by a sanitizer or CSP. They are reasonable for small controlled assets, not a universal fix for image portability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the final request graph
&lt;/h2&gt;

&lt;p&gt;My recommended pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;parse Markdown to an AST;&lt;/li&gt;
&lt;li&gt;inspect image destinations;&lt;/li&gt;
&lt;li&gt;resolve them against the declared publication base;&lt;/li&gt;
&lt;li&gt;validate scheme, host, path, and query;&lt;/li&gt;
&lt;li&gt;copy or rewrite managed assets;&lt;/li&gt;
&lt;li&gt;sanitize rendered HTML and apply CSP/referrer policy;&lt;/li&gt;
&lt;li&gt;open the public page and verify actual responses and layout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AST tells you what the author referenced. URL resolution tells you where it points. A browser test tells you what really happened.&lt;/p&gt;

&lt;p&gt;Should Markdown hosts block cross-origin images by default, or preserve compatibility and require each application to opt into a stricter policy?&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/#images" rel="noopener noreferrer"&gt;CommonMark 0.31.2: Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://url.spec.whatwg.org/" rel="noopener noreferrer"&gt;WHATWG URL Standard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img" rel="noopener noreferrer"&gt;MDN: img element&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy" rel="noopener noreferrer"&gt;MDN: Referrer Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP" rel="noopener noreferrer"&gt;MDN: Content Security Policy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tooling</category>
      <category>markdown</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Your Markdown Parser Is Not Your XSS Boundary</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:36:27 +0000</pubDate>
      <link>https://dev.to/mdfold/your-markdown-parser-is-not-your-xss-boundary-2gpk</link>
      <guid>https://dev.to/mdfold/your-markdown-parser-is-not-your-xss-boundary-2gpk</guid>
      <description>&lt;p&gt;A Markdown parser can produce exactly the right HTML and still leave your application exposed to XSS. Parsing answers what the input means. Sanitization decides which parts of that meaning are allowed to reach an HTML sink.&lt;/p&gt;

&lt;p&gt;I tested that boundary with Node.js 25.3.0, Marked 18.0.7, DOMPurify 3.4.12, and jsdom 30.0.1. The important comparison is not a screenshot. It is the HTML before and after sanitization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest useful pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rendered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;USE_PROFILES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;SANITIZE_NAMED_PROPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deliberately keeps two responsibilities separate. Marked parses Markdown. DOMPurify applies an allow-list to the HTML structure that will approach the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five cases that expose the boundary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Normal content survives
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Hello&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Safe&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The heading and HTTPS link survive both stages. A sanitizer should preserve allowed document structure, not flatten every document to text.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Raw HTML is valid Markdown, not necessarily safe HTML
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marked 18.0.7 returns the element and its event attribute unchanged. DOMPurify returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser did not fail. CommonMark supports raw HTML. The unsafe step would be treating syntactic validity as authorization to insert every attribute.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. URL schemes need their own policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;javascript:alert(1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;click&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanitized HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&amp;gt;&lt;/span&gt;click&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An element allow-list alone is insufficient. URL-bearing attributes need scheme validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Code examples must not be cleaned as attacks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;html
&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt; &lt;span class="na"&gt;onerror=&lt;/span&gt;&lt;span class="s"&gt;"alert(1)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser escapes the payload inside &lt;code&gt;pre &amp;gt; code&lt;/code&gt;. A regex that removes attack-looking source before parsing would damage legitimate security documentation. Context has to be established first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. XSS defenses extend beyond &lt;code&gt;script&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"attributes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;SANITIZE_NAMED_PROPS&lt;/code&gt;, the result becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-content-attributes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"user-content-action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This addresses DOM clobbering: attacker-controlled names can interfere with properties that application code expects to resolve normally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put sanitization after the last unsafe transform
&lt;/h2&gt;

&lt;p&gt;A practical pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;untrusted Markdown
  -&amp;gt; parser
  -&amp;gt; Markdown/HTML AST transforms
  -&amp;gt; sanitizer
  -&amp;gt; serializer
  -&amp;gt; matching HTML sink
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;rehype-sanitize&lt;/code&gt; documentation makes the ordering rule explicit: sanitize after the last unsafe operation, because a later plugin can reintroduce unsafe properties. DOMPurify's current threat model adds another constraint: do not sanitize and then freely post-process the result. The policy and the sink must stay aligned.&lt;/p&gt;

&lt;p&gt;Turning off raw HTML is a useful reduction in attack surface, but it is not a universal sanitizer. Plugins, link protocols, generated IDs, and later transforms still deserve explicit policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I verify in a conversion workflow
&lt;/h2&gt;

&lt;p&gt;When checking &lt;a href="https://mdfold.com/markdown-to-html" rel="noopener noreferrer"&gt;Markdown to HTML&lt;/a&gt;, I separate three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Did normal Markdown preserve the intended structure?&lt;/li&gt;
&lt;li&gt;Did fenced examples remain inert code?&lt;/li&gt;
&lt;li&gt;Is untrusted output safe for &lt;em&gt;this application's&lt;/em&gt; sink and policy?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are conversion checks. The third belongs to the embedding application. A converter producing structurally correct HTML does not automatically promise that arbitrary input is safe to inject into another site's DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat external Markdown as untrusted by default.&lt;/li&gt;
&lt;li&gt;Disable raw HTML where the product does not need it.&lt;/li&gt;
&lt;li&gt;Sanitize the final HTML tree with a maintained allow-list sanitizer.&lt;/li&gt;
&lt;li&gt;Model URL schemes, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and styling capabilities explicitly.&lt;/li&gt;
&lt;li&gt;Do not run arbitrary HTML-mutating plugins after sanitization.&lt;/li&gt;
&lt;li&gt;Test AST shape, rendered HTML, and sanitized HTML separately.&lt;/li&gt;
&lt;li&gt;Pin and update sanitizer versions; security fixes are part of the boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design question I keep coming back to is this: should Markdown renderers disable raw HTML by default, or should they expose it only behind an explicit host-supplied security policy?&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark 0.31.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/markedjs/marked/blob/master/docs/USING_ADVANCED.md" rel="noopener noreferrer"&gt;Marked advanced usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rehypejs/rehype-sanitize" rel="noopener noreferrer"&gt;rehype-sanitize&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/cure53/DOMPurify/wiki/Security-Goals-%26-Threat-Model" rel="noopener noreferrer"&gt;DOMPurify security goals and threat model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Markdown Heading Linter on the AST, Not With Regex</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 15 Aug 2026 22:13:26 +0000</pubDate>
      <link>https://dev.to/mdfold/build-a-markdown-heading-linter-on-the-ast-not-with-regex-2o8f</link>
      <guid>https://dev.to/mdfold/build-a-markdown-heading-linter-on-the-ast-not-with-regex-2o8f</guid>
      <description>&lt;p&gt;A heading linter sounds like a one-line regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;/^&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;gm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works until a documentation page contains a fenced example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Real heading&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;md
&lt;/span&gt;&lt;span class="gu"&gt;## This is code, not navigation&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regex finds two headings. A Markdown parser finds one heading and one code block.&lt;/p&gt;

&lt;p&gt;That difference is why I prefer to put document automation behind an AST boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source -&amp;gt; parser -&amp;gt; mdast -&amp;gt; transformer -&amp;gt; hast -&amp;gt; HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A small, testable plugin
&lt;/h2&gt;

&lt;p&gt;For this experiment I pinned &lt;code&gt;unified@11.0.5&lt;/code&gt;, &lt;code&gt;remark-parse@11.0.0&lt;/code&gt;, &lt;code&gt;remark-rehype@11.1.2&lt;/code&gt;, &lt;code&gt;rehype-stringify@10.0.1&lt;/code&gt;, and &lt;code&gt;unist-util-visit@5.1.0&lt;/code&gt; on Node 25.3.0.&lt;/p&gt;

&lt;p&gt;The plugin has two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;collect headings for a table of contents;&lt;/li&gt;
&lt;li&gt;report a diagnostic when heading depth jumps by more than one level.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;headingAudit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;textOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`Heading jumps from h&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;previousDepth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to h&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="nx"&gt;previousDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;headings&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parser decides what a heading is. The plugin only evaluates heading nodes. This keeps syntax decisions separate from project policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four cases worth keeping in CI
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Normal structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;## Install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: two heading nodes, no diagnostic, and two TOC entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inline markup and duplicate labels
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;## *API* Reference&lt;/span&gt;

&lt;span class="gu"&gt;## API Reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first heading does not contain one flat text node. Its children include an &lt;code&gt;emphasis&lt;/code&gt; node, so a robust text extractor must walk descendants. Both visible labels normalize to the same slug; the second needs a stable suffix such as &lt;code&gt;api-reference-1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Valid Markdown that violates a project rule
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Guide&lt;/span&gt;

&lt;span class="gu"&gt;### Internals&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parsing and HTML rendering succeed, but the plugin reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heading jumps from h1 to h3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a useful distinction. A parser error means the syntax cannot be interpreted under the configured grammar. A linter diagnostic means the syntax is valid but conflicts with a documentation policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A heading marker inside code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Real heading&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;md
&lt;/span&gt;&lt;span class="gu"&gt;## Not a heading&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top-level mdast nodes are &lt;code&gt;heading&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. The TOC contains only &lt;code&gt;Real heading&lt;/code&gt;. No special-case regex is needed because the parser has already resolved the context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep plugins on the right side of the bridge
&lt;/h2&gt;

&lt;p&gt;Here is the complete processing order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;unified&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remarkParse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headingAudit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remarkRehype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;addHeadingIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rehypeStringify&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;headingAudit&lt;/code&gt; expects mdast and therefore runs before &lt;code&gt;remarkRehype&lt;/code&gt;. &lt;code&gt;addHeadingIds&lt;/code&gt; writes HTML properties and therefore runs on hast after the bridge. Plugin order is part of the contract, not cosmetic configuration.&lt;/p&gt;

&lt;p&gt;When checking the same samples with a &lt;a href="https://mdfold.com/markdown-to-html" rel="noopener noreferrer"&gt;Markdown-to-HTML converter&lt;/a&gt;, I compare structural output—headings, code blocks, links—not only whether the preview looks plausible. Two pages can look similar while exposing different trees to a TOC generator, sanitizer, or editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an AST does not solve
&lt;/h2&gt;

&lt;p&gt;An AST removes a lot of context guessing, but it does not make a pipeline automatically safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two plugins can mutate the same nodes;&lt;/li&gt;
&lt;li&gt;generating a TOC before another plugin rewrites headings creates stale data;&lt;/li&gt;
&lt;li&gt;slug behavior is not defined by CommonMark and differs across platforms;&lt;/li&gt;
&lt;li&gt;raw HTML still needs an explicit trust and sanitization policy;&lt;/li&gt;
&lt;li&gt;syntax-extension plugins can change what inputs the parser accepts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rule I use is to document every plugin's input tree, output tree, mutations, diagnostics, and ordering constraints. Then test both the tree shape and the serialized output.&lt;/p&gt;

&lt;p&gt;CommonMark 0.31.2 supports the underlying separation: block structure is resolved before inline structure. Unified and remark make that structure available as mdast and provide a plugin pipeline around it. The experiment above only claims the pinned versions and inputs listed here; it is not proof that every Markdown dialect shares the same tree or slug rules.&lt;/p&gt;

&lt;p&gt;Where would you draw the boundary between plugin freedom and syntax stability in a long-lived documentation pipeline?&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Markdown Dialects Need a Capability Matrix, Not Just a Name</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:41:50 +0000</pubDate>
      <link>https://dev.to/mdfold/markdown-dialects-need-a-capability-matrix-not-just-a-name-18lg</link>
      <guid>https://dev.to/mdfold/markdown-dialects-need-a-capability-matrix-not-just-a-name-18lg</guid>
      <description>&lt;p&gt;Two tools can both claim to support “Markdown” and still disagree about tables, task lists, bare URLs, and even the HTML element used for strikethrough.&lt;/p&gt;

&lt;p&gt;That is usually not a parser bug. It is a contract problem.&lt;/p&gt;

&lt;p&gt;“Markdown support” often collapses three separate layers into one label:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a base syntax such as CommonMark;&lt;/li&gt;
&lt;li&gt;extensions such as GitHub Flavored Markdown;&lt;/li&gt;
&lt;li&gt;platform-specific configuration, sanitization, and post-processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I ran a small differential test to make those boundaries concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed environment
&lt;/h2&gt;

&lt;p&gt;Tested on August 15, 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;commonmark&lt;/code&gt; 0.31.2&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;markdown-it&lt;/code&gt; 14.3.0, default preset, no third-party plugins&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;marked&lt;/code&gt; 18.0.7, default options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comparison records generated HTML, not screenshots. That distinction matters: two results can look similar while producing different DOM structures for accessibility, export, or later transforms.&lt;/p&gt;

&lt;p&gt;Minimal setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commonmark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;markdownit&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;markdown-it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;before ~~deleted~~ after&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;commonmark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HtmlRenderer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;markdownit&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are versioned defaults, not permanent labels for the libraries. Options and plugins can change the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A pipe table is not base CommonMark
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| 1 | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observed structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commonmark 0.31.2&lt;/td&gt;
&lt;td&gt;One paragraph; pipes remain text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown-it 14.3.0&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;table&lt;/code&gt; with &lt;code&gt;thead&lt;/code&gt; and &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;marked 18.0.7&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;table&lt;/code&gt; with &lt;code&gt;thead&lt;/code&gt; and &lt;code&gt;tbody&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CommonMark 0.31.2 does not define pipe tables. GFM adds them as an extension.&lt;/p&gt;

&lt;p&gt;When I need a controlled table input for a compatibility test, I use a &lt;a href="https://mdfold.com/markdown-table-generator" rel="noopener noreferrer"&gt;Markdown table generator&lt;/a&gt; to avoid accidental delimiter mistakes, then verify the output with the actual target renderer. Generating valid source is not the same as proving platform compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A pipe inside a code span is a hard case
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| &lt;span class="sb"&gt;`x|y`&lt;/span&gt; | a&lt;span class="se"&gt;\|&lt;/span&gt;b |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the tested markdown-it and Marked defaults, the unescaped pipe inside the code span still splits the row. One cell receives the opening backtick plus &lt;code&gt;x&lt;/code&gt;; the next receives &lt;code&gt;y&lt;/code&gt; plus the closing backtick, instead of one code span.&lt;/p&gt;

&lt;p&gt;GFM’s table examples require escaping that pipe even inside a code span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
|---|---|
| &lt;span class="sb"&gt;`x\|y`&lt;/span&gt; | a&lt;span class="se"&gt;\|&lt;/span&gt;b |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why splitting a table row with &lt;code&gt;source.split('|')&lt;/code&gt; is not a parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A table-like shape can fail correctly
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| A | B |
| 1 | 2 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three parsers emitted a paragraph. Without the delimiter row, none guessed that the author intended a table.&lt;/p&gt;

&lt;p&gt;For editor diagnostics, checking for a real table node is safer than checking whether the source contains pipes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Strikethrough has two kinds of differences
&lt;/h2&gt;

&lt;p&gt;For:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;before ~~deleted~~ after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;commonmark keeps the tildes as text;&lt;/li&gt;
&lt;li&gt;markdown-it emits &lt;code&gt;&amp;lt;s&amp;gt;&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;Marked emits &lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the boundary case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;before ~not deleted~ after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only Marked recognized strikethrough in this test.&lt;/p&gt;

&lt;p&gt;There are two contracts here: whether the syntax is recognized, and which HTML structure represents it. Screenshot tests can miss the second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Task lists may be plain text or form controls
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; [x] shipped
&lt;span class="p"&gt;-&lt;/span&gt; [ ] pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;commonmark and the markdown-it default produced normal list items containing &lt;code&gt;[x]&lt;/code&gt; and &lt;code&gt;[ ]&lt;/code&gt;. Marked produced disabled checkbox inputs and marked the first one as checked.&lt;/p&gt;

&lt;p&gt;Whether a host then makes those boxes interactive is a UI and security decision, not something the Markdown syntax alone can decide.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Angle-bracket autolinks and bare URLs are different features
&lt;/h2&gt;

&lt;p&gt;All three parsers linked this CommonMark form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nv"&gt;&amp;lt;https://example.com/a_(b)&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Visit https://example.com/a_(b).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;only Marked linked the bare URL under the tested defaults. It also excluded the trailing period from the destination.&lt;/p&gt;

&lt;p&gt;markdown-it can add this behavior with &lt;code&gt;linkify&lt;/code&gt;; that option was deliberately left off so the matrix describes the default preset.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Raw HTML policy is not HTML safety
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xmp&amp;gt;&lt;/span&gt;&lt;span class="gs"&gt;**not emphasis**&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/xmp&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;commonmark and Marked preserved the &lt;code&gt;xmp&lt;/code&gt; element. markdown-it escaped the tags because raw HTML is disabled by default. All three still parsed the emphasis inside.&lt;/p&gt;

&lt;p&gt;GFM has a tag-filter extension for names such as &lt;code&gt;script&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, &lt;code&gt;iframe&lt;/code&gt;, and &lt;code&gt;xmp&lt;/code&gt;. That filter is not a complete HTML sanitizer. Marked’s own documentation also warns that its output is not sanitized.&lt;/p&gt;

&lt;p&gt;A production pipeline therefore needs two explicit answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What HTML can the parser generate?&lt;/li&gt;
&lt;li&gt;What HTML does the sanitizer allow?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The resulting capability matrix
&lt;/h2&gt;

&lt;p&gt;For these exact versions and defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;commonmark&lt;/th&gt;
&lt;th&gt;markdown-it&lt;/th&gt;
&lt;th&gt;Marked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Angle-bracket autolink&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe tables&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~~x~~&lt;/code&gt; strikethrough&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~x~&lt;/code&gt; strikethrough&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task-list checkboxes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare-URL autolinking&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw HTML enabled by default&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complete HTML sanitization&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“No” is not automatically a defect. Disabling raw HTML can be a deliberate safety boundary. markdown-it can gain more syntax through plugins. The important point is that version and configuration belong in the contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  A more useful declaration
&lt;/h2&gt;

&lt;p&gt;Instead of only storing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"flavor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gfm"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;an application could expose something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commonmark-0.31.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strikethrough"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single-or-double-tilde"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"taskList"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extendedAutolink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tagfilter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rawHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parse-then-sanitize"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That object is testable, versionable, and much harder to misunderstand than a dialect name.&lt;/p&gt;

&lt;p&gt;The open question is whether Markdown ecosystems should standardize a machine-readable capability manifest—or whether every platform will continue documenting its dialect through examples and surprises.&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Why One Newline Renders Differently Across Markdown Tools</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Fri, 14 Aug 2026 03:37:51 +0000</pubDate>
      <link>https://dev.to/mdfold/why-one-newline-renders-differently-across-markdown-tools-4lb3</link>
      <guid>https://dev.to/mdfold/why-one-newline-renders-differently-across-markdown-tools-4lb3</guid>
      <description>&lt;p&gt;Press Enter once in a Markdown editor and the preview may keep the text in one paragraph. Move the same file to a chat app and every source line may become a visible line break.&lt;/p&gt;

&lt;p&gt;That is usually not a parser bug. Three separate things are being confused:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the file's line ending (&lt;code&gt;LF&lt;/code&gt;, &lt;code&gt;CRLF&lt;/code&gt;, or &lt;code&gt;CR&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;the Markdown node (&lt;code&gt;softbreak&lt;/code&gt; or hard line break), and&lt;/li&gt;
&lt;li&gt;the browser's final layout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I tested the cases below on August 14, 2026 with fixed versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commonmark.js 0.31.2&lt;/li&gt;
&lt;li&gt;markdown-it 15.0.0&lt;/li&gt;
&lt;li&gt;Marked 18.0.9&lt;/li&gt;
&lt;li&gt;CommonMark specification 0.31.2&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Enter is a soft break by default
&lt;/h2&gt;

&lt;p&gt;Input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;alpha
beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three parsers produced one paragraph in their default configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;alpha
beta&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CommonMark calls the line ending a soft break. A renderer may emit it as a newline character or a space. Under the browser's usual &lt;code&gt;white-space: normal&lt;/code&gt;, that whitespace is collapsed, so the two words normally appear on one visual line unless the container wraps them.&lt;/p&gt;

&lt;p&gt;This distinction matters: a newline in generated HTML source is not necessarily a visible line break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two spaces and a backslash create hard breaks
&lt;/h2&gt;

&lt;p&gt;CommonMark supports two forms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;alpha  
beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;alpha&lt;span class="err"&gt;\&lt;/span&gt;
beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both produced a hard-break node and a &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; element in all three parsers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;alpha&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;
beta&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer the backslash form when a break is semantically important. It is visible during review, while trailing spaces are easy for an editor or formatter to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  LF, CRLF, and CR normalized to the same result
&lt;/h2&gt;

&lt;p&gt;I rendered these inputs separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alpha\nbeta
alpha\r\nbeta
alpha\rbeta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three parsers treated them as the same paragraph with a soft break by default. Converting a repository from CRLF to LF should therefore not change this Markdown meaning by itself.&lt;/p&gt;

&lt;p&gt;Trailing-space cleanup is a different story. Removing the second space from a hard break changes the document structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;breaks&lt;/code&gt; option changes platform behavior
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;breaks: true&lt;/code&gt;, markdown-it and Marked converted a normal paragraph newline to &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Result for &lt;code&gt;alpha\nbeta&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commonmark.js default&lt;/td&gt;
&lt;td&gt;soft break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown-it default&lt;/td&gt;
&lt;td&gt;soft break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown-it &lt;code&gt;breaks: true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marked default&lt;/td&gt;
&lt;td&gt;soft break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marked &lt;code&gt;breaks: true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That option explains many "works here, breaks there" reports. A chat renderer may intentionally preserve author-entered lines, while a documentation system follows CommonMark's paragraph behavior.&lt;/p&gt;

&lt;p&gt;Treat the option as part of the platform's Markdown dialect. Recording only "we use Markdown" is not enough for a reproducible publishing pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases expose the real boundary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One trailing space is still soft
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;alpha 
beta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not create a hard break. The parsers differed slightly in the exact HTML whitespace they retained, but not in the resulting Markdown structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two spaces at the end of a block do not create &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;alpha  

next
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CommonMark says a hard break separates inline content inside a block. It cannot occur at the end of the paragraph. commonmark.js and markdown-it removed the trailing spaces. Marked 18.0.9 retained them in the HTML string, but none of the three emitted &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The page looks the same in a browser, yet byte-for-byte HTML snapshots differ. Decide whether your tests protect semantic structure or exact serialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code spans use different whitespace rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="sb"&gt;`alpha
beta`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;&lt;/span&gt;alpha beta&lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The newline is normalized inside the code span. A backslash or two trailing spaces there do not become a hard break.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical testing strategy
&lt;/h2&gt;

&lt;p&gt;For a multi-output documentation pipeline, test three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;preserve the exact fixture, including visible line-ending and trailing-space notation;&lt;/li&gt;
&lt;li&gt;compare the parsed structure, especially soft-break and hard-break nodes;&lt;/li&gt;
&lt;li&gt;compare the target output and inspect it under the real CSS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the final target is fixed-layout output, I also check a representative file against the &lt;a href="https://mdfold.com/guides/markdown-to-pdf-keep-formatting" rel="noopener noreferrer"&gt;Markdown-to-PDF formatting workflow&lt;/a&gt;. That does not replace parser tests; it catches the separate class of layout differences introduced after parsing.&lt;/p&gt;

&lt;p&gt;My working rule is simple: use blank lines for paragraphs, a visible backslash for intentional inline breaks, and automatic wrapping for prose. Then lock parser versions and options wherever the same source must render consistently.&lt;/p&gt;

&lt;p&gt;Who should own line-break semantics in a shared document system: the author in the source, or the rendering environment for each destination?&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>markdown</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Markdown vs PDF: Stop Choosing One File for Every Stage</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Thu, 13 Aug 2026 06:24:26 +0000</pubDate>
      <link>https://dev.to/mdfold/markdown-vs-pdf-stop-choosing-one-file-for-every-stage-4nk2</link>
      <guid>https://dev.to/mdfold/markdown-vs-pdf-stop-choosing-one-file-for-every-stage-4nk2</guid>
      <description>&lt;p&gt;Markdown vs PDF: use one for the source and one for delivery&lt;/p&gt;

&lt;p&gt;The practical answer to &lt;strong&gt;Markdown vs PDF&lt;/strong&gt; is not that one format wins. Use Markdown while content is being written, reviewed, translated, or reused. Use PDF when the page arrangement has been checked and the recipient needs a stable delivery copy for reading, printing, signing, or archiving.&lt;/p&gt;

&lt;p&gt;For an important document, keep both: the &lt;code&gt;.md&lt;/code&gt; file is the editable source; the &lt;code&gt;.pdf&lt;/code&gt; file is a dated or versioned output. After changing the source, export and inspect a new PDF. Do not edit the two files independently and assume they still contain the same revision.&lt;/p&gt;

&lt;p&gt;This guide is for writers, developers, researchers, and small teams deciding which file to keep, share, or convert. It owns the format decision and source-to-output workflow. The separate &lt;a href="https://mdfold.com/guides/markdown-to-pdf-keep-formatting" rel="noopener noreferrer"&gt;format-preservation guide&lt;/a&gt; covers detailed fixes for headings, links, code, and tables after you have already chosen PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core difference is structured text versus page-oriented output
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark defines Markdown&lt;/a&gt; as a plain-text format for writing structured documents. The source uses visible markers—such as &lt;code&gt;#&lt;/code&gt; for a heading or &lt;code&gt;-&lt;/code&gt; for a list—and a renderer decides how that structure looks. A different theme can change fonts, spacing, colors, and line wrapping without changing the source words.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.loc.gov/preservation/digital/formats/fdd/fdd000030.shtml" rel="noopener noreferrer"&gt;Library of Congress PDF format description&lt;/a&gt; describes PDF as representing formatted, page-oriented documents. A PDF can contain text, images, graphics, and structure, but its defining job is to carry a composed page rather than the author's lightweight source notation.&lt;/p&gt;

&lt;p&gt;That difference leads to a useful decision table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Prefer Markdown&lt;/th&gt;
&lt;th&gt;Prefer PDF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Revise sentences and headings repeatedly&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review exact pages and line breaks&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reuse the same content on a website or in another build&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send a print-ready handout&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect small text changes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Possible, but less direct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preserve a final visual snapshot&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guarantee accessibility without checking&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row matters. Plain text is not automatically an accessible published experience, and fixed pages are not automatically well tagged. Format choice is only the start of accessibility work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our same-document test shows what each format preserves
&lt;/h2&gt;

&lt;p&gt;We tested the current &lt;a href="https://mdfold.com/markdown-to-pdf" rel="noopener noreferrer"&gt;MDFold Markdown-to-PDF converter&lt;/a&gt; with a controlled 635-byte source. It contained one heading, a three-row decision table, a link, a checklist, a block quotation, and the final marker &lt;code&gt;FORMAT-END-008&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First, we changed &lt;code&gt;SOURCE-REV-A&lt;/code&gt; to &lt;code&gt;SOURCE-REV-B&lt;/code&gt; and expanded one checklist item. The source editor and live preview both showed the new revision; the old revision disappeared from the preview. This is the Markdown advantage in concrete form: the author can change the underlying structured text directly and review the rendered result before producing a delivery file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmdfold.com%2Fimages%2Fguides%2Fmarkdown-vs-pdf-editable-source.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmdfold.com%2Fimages%2Fguides%2Fmarkdown-vs-pdf-editable-source.webp" title="The source revision changed from A to B, and the live preview immediately showed the new wording and checklist item. Captured August 7, 2026." alt="MDFold editor and preview showing revision B of the controlled Markdown document" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we used the real PDF action and inspected the exact downloaded file. It was a valid one-page PDF of 23,268 bytes. Text extraction found &lt;code&gt;SOURCE-REV-B&lt;/code&gt;, the revised checklist wording, and &lt;code&gt;FORMAT-END-008&lt;/code&gt;. These checkpoints show that the tested output came from the edited source and reached the end of the document. The output also included an MDFold-branded converter section after the fixture. That added section is visible in the screenshot and means the current export is not a source-only handoff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmdfold.com%2Fimages%2Fguides%2Fmarkdown-vs-pdf-fixed-output.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmdfold.com%2Fimages%2Fguides%2Fmarkdown-vs-pdf-fixed-output.webp" title="The exported one-page PDF retained revision B, the edited checklist wording, and the final marker in selectable text. Captured August 7, 2026." alt="One-page MDFold PDF containing revision B and the final verification marker" width="800" height="1131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This test does not prove that every Markdown feature or every PDF viewer behaves identically. It proves a narrower, useful boundary: for this fixture and current converter, editing happened in the source, while the generated PDF captured that accepted revision as a one-page output with extractable text and an added branded section. Inspect whether that extra material is acceptable before using the file as a formal deliverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose Markdown while meaning and structure can still change
&lt;/h2&gt;

&lt;p&gt;Markdown is the stronger working format when people need to change the document rather than merely comment on its appearance.&lt;/p&gt;

&lt;p&gt;Use it for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Drafting and restructuring.&lt;/strong&gt; Headings, lists, quotations, code, and links remain visible in the source instead of being placed on fixed pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewing text changes.&lt;/strong&gt; Line-based tools can compare two text revisions. The official &lt;a href="https://git-scm.com/docs/git-diff" rel="noopener noreferrer"&gt;&lt;code&gt;git diff&lt;/code&gt; documentation&lt;/a&gt; describes the mechanisms Git provides for showing changes between content states. You do not need Git to edit Markdown, but plain text makes this kind of review practical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusing content.&lt;/strong&gt; The same structured source can feed a web page, documentation site, or PDF workflow. Each renderer still needs its own acceptance check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automating repeatable output.&lt;/strong&gt; A team can keep the source, rendering rules, and assets together, then rebuild the delivery format after an approved change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working with content-focused tools.&lt;/strong&gt; Search, replacement, translation, and code review operate on the actual words and markers rather than reconstructed page objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Markdown is not a finished visual specification. A &lt;code&gt;.md&lt;/code&gt; file does not carry a universal font, paper size, margin system, page count, or print result. CommonMark also does not define every extension that popular editors offer. Tables, task lists, equations, diagrams, and raw HTML can vary between renderers. Name the intended renderer when those features matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose PDF after the pages are ready to review and share
&lt;/h2&gt;

&lt;p&gt;PDF is the stronger delivery format when the recipient cares about the composed pages more than the source notation.&lt;/p&gt;

&lt;p&gt;Use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a report whose page breaks, margins, and typography have been approved;&lt;/li&gt;
&lt;li&gt;a printable handout, invoice, form, or signed record;&lt;/li&gt;
&lt;li&gt;a review copy where everyone should refer to the same page arrangement;&lt;/li&gt;
&lt;li&gt;a final snapshot that should not silently change when a website theme changes;&lt;/li&gt;
&lt;li&gt;a recipient who expects a standard document viewer rather than a Markdown renderer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“Fixed output” does not mean “impossible to edit.” PDF software can add comments, fill fields, redact content, or modify page objects. The important distinction is that PDF is not the cleanest source for revising the original logical document. If you revise only the PDF and leave the Markdown untouched, the two versions diverge.&lt;/p&gt;

&lt;p&gt;PDF also needs final-file checks. A generated document can have clipped tables, awkward page breaks, substituted fonts, dead links, or missing structure even when the preview looked acceptable. Open the downloaded file independently and inspect the pages you will actually send.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reliable workflow keeps one source of truth
&lt;/h2&gt;

&lt;p&gt;Use this five-step handoff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write and revise in Markdown.&lt;/strong&gt; Keep linked assets in a stable location and use a revision label for consequential documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview with the intended renderer.&lt;/strong&gt; Check headings, lists, tables, links, code, images, and any renderer-specific extensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export a new PDF.&lt;/strong&gt; Do not overwrite a previously approved copy until the new output passes review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect the saved PDF.&lt;/strong&gt; Confirm page count, final marker, links, text selection, table width, image presence, page breaks, and print size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label the relationship.&lt;/strong&gt; Record which source revision produced the PDF. Store the Markdown as the master and the PDF as an output unless policy requires a different recordkeeping model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simple naming pattern prevents ambiguity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release-notes.md                 editable source
release-notes-r3.pdf             reviewed output from revision 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a recipient edits the PDF, treat those edits as feedback to reconcile into the Markdown source. Then export revision 4. This avoids two competing masters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile viewing exposes the fixed-page tradeoff
&lt;/h2&gt;

&lt;p&gt;We repeated the product check at 390 × 844 pixels. The MDFold PDF preview stayed within the browser viewport without document-level horizontal overflow and showed the same one-page result.&lt;/p&gt;

&lt;p&gt;the PDF preview remained contained without document-level horizontal overflow; detailed reading still benefits from zoom. Captured August 7, 2026.")&lt;/p&gt;

&lt;p&gt;Containment is not the same as comfortable reading. A fixed page scaled down to a phone can make table cells and body text small. Markdown rendered as a responsive web page can reflow more naturally, but the result depends on the renderer and its styles. If phone reading is the primary use, publish a responsive version and offer PDF as a secondary download. If printing or page references are primary, retain the PDF and test zoom and navigation on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility and privacy require separate checks
&lt;/h2&gt;

&lt;p&gt;Neither file extension is an accessibility certificate. Markdown can express headings, links, lists, image descriptions, and table structure, but the published renderer must carry that meaning into an accessible experience. PDF can include document structure, yet the final file still needs correct reading order, meaningful links, alternative text, language, contrast, and keyboard behavior. Adobe's &lt;a href="https://helpx.adobe.com/acrobat/using/touch-reading-order-tool-pdfs.html" rel="noopener noreferrer"&gt;Reading Order tool guidance&lt;/a&gt; explains how page reading order, headings, form labels, alternative text, and table tagging may need to be checked and repaired; a &lt;code&gt;.pdf&lt;/code&gt; extension alone does not establish those qualities.&lt;/p&gt;

&lt;p&gt;For privacy, inspect the complete workflow rather than inferring safety from a format. In this MDFold test, we observed no request to an application upload or conversion endpoint while editing and generating the controlled document. The page still loaded ordinary application assets. This observation covers the tested route and fixture on August 7, 2026; it is not a promise about unrelated links, remote images, analytics, extensions, or other tools.&lt;/p&gt;

&lt;p&gt;If the Markdown contains remote images, opening a preview may contact those hosts. If the PDF contains sensitive material, its portability makes access control and secure delivery important. Use non-sensitive test content until the workflow is approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know what this comparison cannot guarantee
&lt;/h2&gt;

&lt;p&gt;The controlled test used installed Google Chrome, the current MDFold converter, one 635-byte English fixture, one template, one desktop viewport, and one phone viewport. It covered a heading, table, link, checklist, quotation, and plain text. The current output added an MDFold-branded converter section after the fixture, so it is not suitable when a source-only or unbranded PDF is required. The test did not cover authenticated assets, complex images, equations, diagrams, embedded fonts, forms, signatures, encryption, archival profiles, screen-reader testing, every Markdown dialect, or every PDF viewer.&lt;/p&gt;

&lt;p&gt;Do not generalize the one-page result into universal layout fidelity. A longer or more complex document can expose different page breaks and conversion limits. Do not infer that extractable text guarantees correct semantic tagging or reading order. And do not discard the Markdown merely because the PDF passed: the source is what makes the next controlled revision straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final decision checklist
&lt;/h2&gt;

&lt;p&gt;Choose &lt;strong&gt;Markdown now&lt;/strong&gt; if the document still needs substantive edits, structured reuse, translation, line-by-line review, or output in more than one format.&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;PDF now&lt;/strong&gt; if the content has been accepted and recipients need reviewed pages for printing, formal delivery, signatures, or stable page references.&lt;/p&gt;

&lt;p&gt;Keep &lt;strong&gt;both&lt;/strong&gt; when the document has a lifecycle: Markdown as the maintained source, PDF as a versioned delivery artifact. Before sharing, verify that the PDF was generated from the intended source revision and inspect the actual saved file.&lt;/p&gt;

&lt;p&gt;When PDF is the right next step, &lt;a href="https://mdfold.com/markdown-to-pdf" rel="noopener noreferrer"&gt;open the Markdown-to-PDF converter&lt;/a&gt;, render the source, download the result, and complete the final-file checks above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and testing basis
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://spec.commonmark.org/0.31.2/" rel="noopener noreferrer"&gt;CommonMark 0.31.2 specification&lt;/a&gt; — Markdown's plain-text structured-document model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.loc.gov/preservation/digital/formats/fdd/fdd000030.shtml" rel="noopener noreferrer"&gt;Library of Congress: PDF format family&lt;/a&gt; — PDF's formatted, page-oriented representation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://helpx.adobe.com/acrobat/using/touch-reading-order-tool-pdfs.html" rel="noopener noreferrer"&gt;Adobe Acrobat: Reading Order tool for PDFs&lt;/a&gt; — checking and repairing reading order, tags, headings, form labels, images, and tables.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/docs/git-diff" rel="noopener noreferrer"&gt;Git documentation: git diff&lt;/a&gt; — text-change comparison mechanisms.&lt;/li&gt;
&lt;li&gt;MDFold product test, August 7, 2026 — controlled source edit, live preview, exact one-page PDF, extracted-text checkpoints, request observation, and 390-pixel phone review.&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Tested PDF-to-Markdown on Searchable, Scanned, and Table-Heavy PDFs</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sun, 09 Aug 2026 10:36:56 +0000</pubDate>
      <link>https://dev.to/mdfold/tested-pdf-to-markdown-on-searchable-scanned-and-table-heavy-pdfs-2597</link>
      <guid>https://dev.to/mdfold/tested-pdf-to-markdown-on-searchable-scanned-and-table-heavy-pdfs-2597</guid>
      <description>&lt;p&gt;PDF-to-Markdown conversion is often described as if it were a file-format rewrite:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read the PDF, emit Markdown, done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That model breaks as soon as the PDF contains anything more complicated than a single column of selectable text.&lt;/p&gt;

&lt;p&gt;I tested four controlled cases in a browser-based converter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;a two-page searchable PDF with headings, prose, and a table;&lt;/li&gt;
&lt;li&gt;a searchable PDF containing a grouped header, a blank cell, and a multiline row label;&lt;/li&gt;
&lt;li&gt;an image-only scanned PDF;&lt;/li&gt;
&lt;li&gt;the same kind of scan after OCR added a searchable text layer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The results were consistent, but not simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;searchable text was extracted;&lt;/li&gt;
&lt;li&gt;page boundaries remained identifiable;&lt;/li&gt;
&lt;li&gt;table values survived, but the table grid did not;&lt;/li&gt;
&lt;li&gt;the image-only scan returned no text;&lt;/li&gt;
&lt;li&gt;OCR made the scan extractable, but silently changed identifiers;&lt;/li&gt;
&lt;li&gt;none of the tested outputs became trustworthy Markdown without review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important lesson is not that PDF-to-Markdown “works” or “fails.” It is that &lt;strong&gt;text recovery, reading order, visual structure, and character accuracy are separate problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test setup
&lt;/h2&gt;

&lt;p&gt;I ran the current &lt;a href="https://mdfold.com/pdf-to-markdown" rel="noopener noreferrer"&gt;MDFold PDF-to-Markdown tool&lt;/a&gt; in installed Google Chrome on August 9, 2026.&lt;/p&gt;

&lt;p&gt;The controlled fixtures contained no personal or customer data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fixture&lt;/th&gt;
&lt;th&gt;What it tested&lt;/th&gt;
&lt;th&gt;Verification method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Two-page incident memo&lt;/td&gt;
&lt;td&gt;Searchable prose, page order, a small table, exact values&lt;/td&gt;
&lt;td&gt;Four unique checkpoints and two page markers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex regional table&lt;/td&gt;
&lt;td&gt;Grouped heading, multiline label, deliberate blank cell&lt;/td&gt;
&lt;td&gt;Nineteen labels and values plus visual comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image-only field note&lt;/td&gt;
&lt;td&gt;Behavior when no text layer exists&lt;/td&gt;
&lt;td&gt;Expected zero extracted words&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCR-processed scans&lt;/td&gt;
&lt;td&gt;Recognition errors in clean and difficult pages&lt;/td&gt;
&lt;td&gt;Exact source-to-output token comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I repeated the complex table and difficult OCR tests at desktop size and at a 390 × 844 phone viewport. The extracted text matched between the two viewports, and the page itself did not overflow horizontally.&lt;/p&gt;

&lt;p&gt;This is a controlled product test, not a universal benchmark. Different PDF generators, fonts, tags, languages, rotations, and OCR engines can produce different results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: searchable text survived, but the table did not
&lt;/h2&gt;

&lt;p&gt;The first fixture was a two-page incident-review memo. It contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a title and section headings;&lt;/li&gt;
&lt;li&gt;case ID &lt;code&gt;IR-317&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a three-column table;&lt;/li&gt;
&lt;li&gt;the values &lt;code&gt;Table overflow&lt;/code&gt;, &lt;code&gt;12 pages&lt;/code&gt;, and &lt;code&gt;2026-08-28&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a second page with scope and ownership notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four checkpoints appeared in the extracted output. Two page markers also appeared in the correct order.&lt;/p&gt;

&lt;p&gt;But the table did not become a Markdown pipe table. Its words were present as a flat sequence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kxxdhlvqop0x2ql1hmd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8kxxdhlvqop0x2ql1hmd.webp" alt="MDFold showing the raw extracted text from a searchable two-page PDF" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The controlled values and page order survived, but the source table was not reconstructed as Markdown table syntax. Captured August 7 and reproduced August 9, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the first distinction a converter UI can hide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Complete text is not the same thing as complete structure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A quick scan of the output might suggest success because every important word is visible. A program expecting &lt;code&gt;| Check | Result | Evidence |&lt;/code&gt;, however, would find no table at all.&lt;/p&gt;

&lt;p&gt;The correct next step was manual reconstruction based on the original PDF, not guessing columns from spaces in the extracted string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a searchable PDF still has no Markdown structure
&lt;/h2&gt;

&lt;p&gt;A PDF page describes positioned content. Markdown describes document structure.&lt;/p&gt;

&lt;p&gt;Those are different models.&lt;/p&gt;

&lt;p&gt;Mozilla’s PDF.js API exposes page text through &lt;code&gt;getTextContent()&lt;/code&gt;. It exposes a tagged structure tree separately through &lt;code&gt;getStructTree()&lt;/code&gt;, and that structure tree may be &lt;code&gt;null&lt;/code&gt;. Even when text extraction succeeds, the extractor may not receive a reliable semantic instruction saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This line is an H2.
These nine text fragments form a 3 × 3 table.
This empty region is an intentionally blank cell.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, the page may contain text fragments positioned at coordinates. The converter has to infer reading order and relationships from those fragments, unless the PDF contains usable tags or another structural layer.&lt;/p&gt;

&lt;p&gt;That inference is where apparently successful conversions become dangerous. The text may be present while its relationships are wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: every table value survived, but the blank cell became unknowable
&lt;/h2&gt;

&lt;p&gt;The complex table fixture contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a grouped &lt;code&gt;Quarter totals&lt;/code&gt; heading;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Q1&lt;/code&gt; and &lt;code&gt;Q2&lt;/code&gt; subcolumns;&lt;/li&gt;
&lt;li&gt;a multiline &lt;code&gt;North America&lt;/code&gt; row label;&lt;/li&gt;
&lt;li&gt;a deliberately blank Q2 cell for Europe;&lt;/li&gt;
&lt;li&gt;a note explaining that blank means “not reported,” not zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source looked like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsirc14lt5f35wli121ut.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsirc14lt5f35wli121ut.webp" alt="Controlled PDF table with grouped headers, a multiline label, and an intentional blank" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The visual grid carries meaning that is not contained in the individual words. Project-created fixture captured August 5, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The converter retained all 19 controlled text checkpoints, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;North America
1,240
1,318
Reviewed
Europe
980
Missing Q2
Asia Pacific
1,105
1,207
Provisional
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output still was not a table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmehgcce29o1vfga788a0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmehgcce29o1vfga788a0.webp" alt="Flat PDF extraction containing all controlled table values but no Markdown grid" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All labels and values survived, but grouped headers, cell boundaries, and the location of the blank cell did not. Captured August 5 and reproduced August 9, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From the flat text alone, a cleanup script cannot safely answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether &lt;code&gt;Quarter totals&lt;/code&gt; spans two columns;&lt;/li&gt;
&lt;li&gt;whether &lt;code&gt;North America&lt;/code&gt; is one cell or two;&lt;/li&gt;
&lt;li&gt;whether Europe’s missing number belongs to Q1 or Q2;&lt;/li&gt;
&lt;li&gt;whether the blank represents zero, missing data, or a layout gap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The footnote helps a human recover the intent, but it does not restore the missing cell coordinate.&lt;/p&gt;

&lt;p&gt;This is why table verification must be cell-oriented. Counting extracted words or checking that every number appears is insufficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: the image-only scan produced zero text
&lt;/h2&gt;

&lt;p&gt;The third fixture looked like a normal one-page field note in a PDF viewer. It had headings, numbered steps, identifiers, and measurements.&lt;/p&gt;

&lt;p&gt;But the PDF contained only page pixels. There was no searchable text layer.&lt;/p&gt;

&lt;p&gt;The result in the converter was an empty output: zero words.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0ef2f99dz0smxuhkwdd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj0ef2f99dz0smxuhkwdd.webp" alt="An image-only scanned PDF selected in MDFold with an empty Markdown result" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The file opened successfully, but there was no text layer to extract. Captured August 5 and reproduced August 9, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is not evidence that the document contains no writing. It means the extraction method found no machine-readable text.&lt;/p&gt;

&lt;p&gt;OCR must happen before text-based Markdown conversion. OCRmyPDF describes this as adding an OCR text layer to a scanned PDF while retaining the original page image. Once that layer exists, a text extractor has something to read.&lt;/p&gt;

&lt;p&gt;The boundary matters: the tested MDFold workflow extracts an existing text layer. It does not perform OCR on the image-only PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 4: OCR recovered editable text and introduced silent errors
&lt;/h2&gt;

&lt;p&gt;I processed controlled scans with local OCR, created searchable PDFs from the recognized text, and ran those PDFs through the same converter.&lt;/p&gt;

&lt;p&gt;The clear fixture produced 63 editable words. The difficult fixture produced 64.&lt;/p&gt;

&lt;p&gt;That sounds successful until the identifiers are compared exactly.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;OCR output&lt;/th&gt;
&lt;th&gt;Error type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ZX-41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;X-41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Character omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;B0O-518&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;B00-518&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Letter O changed to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;III.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;I.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Roman-numeral characters omitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OI-1058&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0I-1058&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Letter O changed to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The OCR also correctly retained &lt;code&gt;Café&lt;/code&gt;, &lt;code&gt;$1,084.70&lt;/code&gt;, &lt;code&gt;2026-08-05&lt;/code&gt;, and a faint &lt;code&gt;REVIEWED 08/05&lt;/code&gt; stamp.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6b8kgkqhydohj5hl0r7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff6b8kgkqhydohj5hl0r7.webp" alt="Editable OCR text containing correct prose and silent identifier changes" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OCR made the scan searchable, but several high-risk tokens changed without an error message. Captured August 5 and reproduced August 9, 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is a more difficult failure than empty output.&lt;/p&gt;

&lt;p&gt;Empty output is visible. A plausible but incorrect asset tag can flow into a repository, search index, or LLM prompt without being noticed.&lt;/p&gt;

&lt;p&gt;The safe question is not “Did OCR produce text?” It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which characters would cause harm if one symbol changed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For many documents, that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoice totals;&lt;/li&gt;
&lt;li&gt;dates and times;&lt;/li&gt;
&lt;li&gt;serial numbers;&lt;/li&gt;
&lt;li&gt;case IDs;&lt;/li&gt;
&lt;li&gt;chemical units;&lt;/li&gt;
&lt;li&gt;version strings;&lt;/li&gt;
&lt;li&gt;negative signs and decimal separators;&lt;/li&gt;
&lt;li&gt;names containing accents;&lt;/li&gt;
&lt;li&gt;checkboxes, list numbers, and status labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A six-step verification workflow
&lt;/h2&gt;

&lt;p&gt;The following process worked better than treating conversion as one action.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Classify the source before converting
&lt;/h3&gt;

&lt;p&gt;Try selecting text in the PDF viewer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If normal text can be selected, begin with text extraction.&lt;/li&gt;
&lt;li&gt;If only a rectangular image region can be selected, plan an OCR step.&lt;/li&gt;
&lt;li&gt;If the file mixes searchable and scanned pages, test both page types.&lt;/li&gt;
&lt;li&gt;If tables, columns, figures, or equations carry meaning, plan visual comparison even when the text is searchable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Add checkpoints to controlled tests
&lt;/h3&gt;

&lt;p&gt;For a production pipeline, create a representative fixture with unique values at the beginning, middle, and end.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IR-317"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Table overflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12 pages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-28"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expectedTables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking only the opening paragraph will not catch a missing final page or an interrupted reading order.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Separate text assertions from structure assertions
&lt;/h3&gt;

&lt;p&gt;These are different tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;requiredValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;| Check | Result | Evidence |&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my searchable fixture, the first assertion passed and the second failed. Reporting a single “conversion passed” result would have hidden the table failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Rebuild only structure you can prove
&lt;/h3&gt;

&lt;p&gt;Do not infer a table from word order alone when a blank cell or merged header exists.&lt;/p&gt;

&lt;p&gt;Open the original PDF beside the Markdown and reconstruct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;heading levels;&lt;/li&gt;
&lt;li&gt;list boundaries;&lt;/li&gt;
&lt;li&gt;table rows and columns;&lt;/li&gt;
&lt;li&gt;figure captions;&lt;/li&gt;
&lt;li&gt;page-sensitive notes;&lt;/li&gt;
&lt;li&gt;code indentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the structure cannot be proven, keep the content as clearly labelled text or choose a layout-aware extraction tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Verify exact high-risk tokens
&lt;/h3&gt;

&lt;p&gt;Create a small change ledger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| Source | Extracted | Decision |
| :--- | :--- | :--- |
| B0O-518 | B00-518 | Correct from source image |
| OI-1058 | 0I-1058 | Correct from source image |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this before polishing headings. Semantic cleanup is wasted if the underlying identifiers are wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Retain provenance
&lt;/h3&gt;

&lt;p&gt;Keep the PDF until review is complete. Page markers in Markdown can help a reviewer return to the source page.&lt;/p&gt;

&lt;p&gt;For high-risk workflows, store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the source filename or document ID;&lt;/li&gt;
&lt;li&gt;conversion date and tool version;&lt;/li&gt;
&lt;li&gt;page boundaries;&lt;/li&gt;
&lt;li&gt;OCR engine and language when applicable;&lt;/li&gt;
&lt;li&gt;manual corrections;&lt;/li&gt;
&lt;li&gt;unresolved uncertainties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markdown is easier to edit, which is useful. It also makes undocumented corrections easier to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the method by failure mode
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source condition&lt;/th&gt;
&lt;th&gt;Reasonable first method&lt;/th&gt;
&lt;th&gt;Do not assume&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple searchable prose&lt;/td&gt;
&lt;td&gt;Local text extraction&lt;/td&gt;
&lt;td&gt;Heading levels are correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Searchable PDF with columns&lt;/td&gt;
&lt;td&gt;Layout-aware extraction and reading-order review&lt;/td&gt;
&lt;td&gt;Text sequence matches visual order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple table&lt;/td&gt;
&lt;td&gt;Text extraction plus manual cell verification&lt;/td&gt;
&lt;td&gt;Spaces reconstruct a grid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merged or nested table&lt;/td&gt;
&lt;td&gt;Table-specific extraction or manual reconstruction&lt;/td&gt;
&lt;td&gt;Blank cells and spans survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image-only scan&lt;/td&gt;
&lt;td&gt;OCR first, then text extraction&lt;/td&gt;
&lt;td&gt;Opening the PDF means text exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed searchable and scanned pages&lt;/td&gt;
&lt;td&gt;Per-page detection with OCR fallback&lt;/td&gt;
&lt;td&gt;One method handled every page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equations or diagrams&lt;/td&gt;
&lt;td&gt;Visual or specialist extraction&lt;/td&gt;
&lt;td&gt;Extracted characters preserve meaning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The best converter is not always the one that produces the prettiest Markdown. It is the one whose failure modes you can detect and whose output you can verify for the document in front of you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy is a property of the whole pipeline
&lt;/h2&gt;

&lt;p&gt;During the current MDFold tests, I observed no request carrying the PDF to an application conversion endpoint. The extraction happened in the browser.&lt;/p&gt;

&lt;p&gt;That is a narrow observation, not a claim that the page is offline. Ordinary analytics requests were still present, and remote page assets can still require a network connection.&lt;/p&gt;

&lt;p&gt;The OCR step was separate. I used a local operating-system OCR engine for the controlled scans. A hosted OCR service would have a different data path, retention policy, and risk profile.&lt;/p&gt;

&lt;p&gt;Before processing sensitive material, inspect every stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF storage
  → OCR engine, if needed
  → text extractor
  → cleanup editor
  → repository, search index, or AI system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local converter cannot make a later cloud OCR or AI upload local.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this test does not prove
&lt;/h2&gt;

&lt;p&gt;The fixtures were small, English-language documents created for testing. They covered two pages, a grouped table, an intentional blank cell, clean and difficult scans, and a phone-sized browser view.&lt;/p&gt;

&lt;p&gt;They did not cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;encrypted PDFs;&lt;/li&gt;
&lt;li&gt;handwritten pages;&lt;/li&gt;
&lt;li&gt;right-to-left scripts;&lt;/li&gt;
&lt;li&gt;vertical writing;&lt;/li&gt;
&lt;li&gt;mathematical notation;&lt;/li&gt;
&lt;li&gt;multi-page tables with repeated headers;&lt;/li&gt;
&lt;li&gt;tagged-PDF accessibility quality;&lt;/li&gt;
&lt;li&gt;embedded files or interactive forms;&lt;/li&gt;
&lt;li&gt;every OCR engine or PDF producer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test also does not prove that browser-local extraction is more accurate than a hosted or AI-based service. It proves the observed boundary of one current workflow.&lt;/p&gt;

&lt;p&gt;If you want the deeper OCR cleanup procedure, I documented the exact-token method in a separate &lt;a href="https://mdfold.com/guides/scanned-pdf-to-markdown" rel="noopener noreferrer"&gt;scanned-PDF-to-Markdown guide&lt;/a&gt;. For table-specific reconstruction, the &lt;a href="https://mdfold.com/guides/pdf-tables-to-markdown" rel="noopener noreferrer"&gt;PDF table recovery guide&lt;/a&gt; includes the controlled blank-cell example.&lt;/p&gt;

&lt;h2&gt;
  
  
  The acceptance test matters more than the conversion button
&lt;/h2&gt;

&lt;p&gt;After these tests, I no longer describe PDF-to-Markdown as a single conversion.&lt;/p&gt;

&lt;p&gt;It is a pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;classify the PDF
  → recover text
  → verify reading order
  → reconstruct proven structure
  → compare exact values
  → retain provenance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the searchable fixtures, text recovery succeeded while table recovery failed. For the image-only scan, text recovery could not begin until OCR created a layer. For the OCR fixtures, extraction succeeded while character verification still found silent errors.&lt;/p&gt;

&lt;p&gt;That leads to a more useful definition of success:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Markdown is acceptable only when every important fact and relationship can be traced back to the source PDF.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you have built a PDF ingestion or RAG pipeline, which failure has caused more damage in practice: missing text, wrong reading order, flattened tables, or plausible OCR errors?&lt;/p&gt;

&lt;h2&gt;
  
  
  Focused FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a searchable PDF still produce bad Markdown?
&lt;/h3&gt;

&lt;p&gt;Yes. Searchable text proves that characters can be extracted; it does not prove correct heading levels, reading order, table cells, columns, or figure relationships. Compare the Markdown with the visible PDF structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does a scanned PDF return an empty result?
&lt;/h3&gt;

&lt;p&gt;An image-only scan has page pixels but no machine-readable text layer. Run OCR first, then verify the recognized text before rebuilding Markdown structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does OCR make the converted Markdown accurate?
&lt;/h3&gt;

&lt;p&gt;Not automatically. In this controlled test, OCR made the scans editable but silently changed four identifiers or list tokens. Exact values still need source comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  How should I test a PDF-to-Markdown pipeline?
&lt;/h3&gt;

&lt;p&gt;Use representative fixtures with unique checkpoints, assert text and structure separately, include one image-only and one table-heavy case, compare high-risk tokens exactly, and retain page-level provenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib-PDFPageProxy.html" rel="noopener noreferrer"&gt;PDF.js &lt;code&gt;PDFPageProxy&lt;/code&gt; API&lt;/a&gt; — separate text-content and structure-tree access.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://ocrmypdf.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;OCRmyPDF documentation&lt;/a&gt; — adding searchable OCR text layers to scanned PDFs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tesseract-ocr.github.io/tessdoc/ImproveQuality.html" rel="noopener noreferrer"&gt;Tesseract: Improving OCR output quality&lt;/a&gt; — scan quality, segmentation, rotation, and recognition factors.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/WCAG21/Techniques/pdf/PDF6" rel="noopener noreferrer"&gt;W3C PDF6&lt;/a&gt; — table structure in tagged PDF documents.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.github.com/gfm/#tables-extension-" rel="noopener noreferrer"&gt;GitHub Flavored Markdown table extension&lt;/a&gt; — the target pipe-table structure used in the asse&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>I Tested What a Browser-Based DOCX-to-Markdown Converter Actually Preserves</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Sun, 02 Aug 2026 05:59:41 +0000</pubDate>
      <link>https://dev.to/mdfold/i-tested-what-a-browser-based-docx-to-markdown-converter-actually-preserves-199n</link>
      <guid>https://dev.to/mdfold/i-tested-what-a-browser-based-docx-to-markdown-converter-actually-preserves-199n</guid>
      <description>&lt;p&gt;“Preserves formatting” is one of the least useful promises a document converter can make.&lt;/p&gt;

&lt;p&gt;Which formatting?&lt;/p&gt;

&lt;p&gt;A Word document can contain semantic heading styles, font sizes, colors, numbered lists, floating objects, page headers, merged table cells, tracked changes, comments, and embedded images. Markdown has no direct representation for many of those things.&lt;/p&gt;

&lt;p&gt;So I stopped asking whether DOCX-to-Markdown conversion “works” and tested a narrower question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which document structures survive, which become plain text, and which require a deliberate cleanup decision?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I created two controlled &lt;code&gt;.docx&lt;/code&gt; files and converted them with &lt;a href="https://mdfold.com/word-to-markdown" rel="noopener noreferrer"&gt;MDFold’s browser-based Word to Markdown tool&lt;/a&gt;. The first represented a well-structured technical document. The second deliberately used features that are easy to misinterpret or awkward to express in Markdown.&lt;/p&gt;

&lt;p&gt;The result was not “everything preserved” or “formatting lost.” It was much more useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;real Word heading styles became Markdown headings;&lt;/li&gt;
&lt;li&gt;bold, italic, links, bullets, and numbered lists survived;&lt;/li&gt;
&lt;li&gt;a large bold paragraph stayed bold instead of becoming a heading;&lt;/li&gt;
&lt;li&gt;table text survived, but table structure did not;&lt;/li&gt;
&lt;li&gt;headers and footers were omitted;&lt;/li&gt;
&lt;li&gt;an embedded raster image became a self-contained data URI;&lt;/li&gt;
&lt;li&gt;page layout, fonts, and visual positioning were not reproduced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the behavior this article explains and turns into a repeatable workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOCX and Markdown describe different things
&lt;/h2&gt;

&lt;p&gt;DOCX can describe both document meaning and page presentation. Markdown mostly describes lightweight document structure.&lt;/p&gt;

&lt;p&gt;Microsoft’s explanation of WordprocessingML shows that a Word document is composed of structured elements such as paragraphs and runs, with styles and properties attached to them. A paragraph can be a real &lt;code&gt;Heading 1&lt;/code&gt;, or it can be an ordinary paragraph that merely looks like a title because someone made it 22-point bold text.&lt;/p&gt;

&lt;p&gt;Those two paragraphs may look almost identical in Word. They are not equivalent to a converter.&lt;/p&gt;

&lt;p&gt;The conversion pipeline I tested uses &lt;a href="https://github.com/mwilliamson/mammoth.js/" rel="noopener noreferrer"&gt;Mammoth&lt;/a&gt; to interpret DOCX structure as HTML, then converts that HTML into Markdown. Mammoth’s own documentation is unusually honest about the boundary: DOCX and HTML have a large structural mismatch, and conversion works best when the Word document uses styles for their intended meaning.&lt;/p&gt;

&lt;p&gt;That gives us the first rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prepare Word for structural conversion, not visual imitation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your real goal is to reproduce page margins, floating text boxes, fonts, headers, or exact pagination, Markdown is the wrong destination. Use PDF for a fixed result or keep the document in DOCX for collaborative editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controlled test documents
&lt;/h2&gt;

&lt;p&gt;I used generated files rather than a convenient hand-picked customer document. This made every expected result explicit and kept personal information out of the screenshots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test A: semantic structure
&lt;/h3&gt;

&lt;p&gt;The representative file contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a real Word &lt;code&gt;Heading 1&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a real &lt;code&gt;Heading 2&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;one sentence containing bold and italic runs;&lt;/li&gt;
&lt;li&gt;an external hyperlink with descriptive text;&lt;/li&gt;
&lt;li&gt;two bullet items;&lt;/li&gt;
&lt;li&gt;two numbered steps;&lt;/li&gt;
&lt;li&gt;a distinctive final checkpoint sentence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The expected Markdown was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# DOCX Conversion Field Test&lt;/span&gt;

&lt;span class="gu"&gt;## Release checklist&lt;/span&gt;

The &lt;span class="gs"&gt;**owner**&lt;/span&gt; reviews the _generated Markdown_ before publishing.

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Open the release runbook&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/runbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Verify the title
&lt;span class="p"&gt;-&lt;/span&gt; Verify the final paragraph
&lt;span class="p"&gt;
1.&lt;/span&gt; Convert the DOCX
&lt;span class="p"&gt;2.&lt;/span&gt; Review the Markdown

FINAL CHECKPOINT: names, numbers, and links still match.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test B: features that need judgment
&lt;/h3&gt;

&lt;p&gt;The difficult file contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a visually large, bold title that was still a normal Word paragraph;&lt;/li&gt;
&lt;li&gt;underlined text;&lt;/li&gt;
&lt;li&gt;a two-column table;&lt;/li&gt;
&lt;li&gt;an embedded PNG with alternative text;&lt;/li&gt;
&lt;li&gt;a page header and footer;&lt;/li&gt;
&lt;li&gt;a distinctive final checkpoint sentence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This second file was designed to prevent a false sense of success. A converter can return non-empty Markdown while still losing the distinction that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The well-structured document converted cleanly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f2imdooi5r22ocyxnk3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f2imdooi5r22ocyxnk3.png" alt="MDFold Word to Markdown output preserving two heading levels, emphasis, a link, bullet items, numbered steps, and a final checkpoint" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The representative file preserved every checkpoint we asserted: heading levels, emphasis, link destination, both list types, and the final sentence.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The generated Markdown was not character-for-character identical to my hand-written expectation. The converter used extra spaces after list markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt;   Verify the title
&lt;span class="p"&gt;-&lt;/span&gt;   Verify the final paragraph
&lt;span class="p"&gt;
1.&lt;/span&gt;  Convert the DOCX
&lt;span class="p"&gt;2.&lt;/span&gt;  Review the Markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference is harmless in common Markdown renderers. It is also a useful reminder not to confuse stylistic normalization with information loss.&lt;/p&gt;

&lt;p&gt;The important checks were structural:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Word input&lt;/th&gt;
&lt;th&gt;Observed Markdown&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heading 1 style&lt;/td&gt;
&lt;td&gt;&lt;code&gt;# DOCX Conversion Field Test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heading 2 style&lt;/td&gt;
&lt;td&gt;&lt;code&gt;## Release checklist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bold run&lt;/td&gt;
&lt;td&gt;&lt;code&gt;**owner**&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Italic run&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_generated Markdown_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External hyperlink&lt;/td&gt;
&lt;td&gt;Markdown link with the same destination&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bullet list&lt;/td&gt;
&lt;td&gt;Hyphen list&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbered list&lt;/td&gt;
&lt;td&gt;Ordered Markdown list&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final checkpoint&lt;/td&gt;
&lt;td&gt;Same sentence in the result&lt;/td&gt;
&lt;td&gt;Preserved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the kind of Word document that converts well: the author used real structure and kept the layout linear.&lt;/p&gt;

&lt;h2&gt;
  
  
  A title that only looks like a title stays ordinary text
&lt;/h2&gt;

&lt;p&gt;The difficult document began with &lt;code&gt;VISUAL-ONLY TITLE&lt;/code&gt; in a large, bold font. It looked like a heading in Word, but it was not assigned a heading style.&lt;/p&gt;

&lt;p&gt;The observed Markdown was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gs"&gt;**VISUAL-ONLY TITLE**&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct behavior.&lt;/p&gt;

&lt;p&gt;Automatically turning every short bold paragraph into a heading would create false structure in labels, warnings, captions, and emphasized sentences. A converter should prefer the document’s semantic style over a guess based on appearance.&lt;/p&gt;

&lt;p&gt;Before conversion, open Word’s Navigation pane. If an intended section title does not appear in the document outline, apply the correct Heading style. Do not merely increase the font size.&lt;/p&gt;

&lt;p&gt;Microsoft’s &lt;a href="https://learn.microsoft.com/en-us/office/open-xml/word/how-to-apply-a-style-to-a-paragraph-in-a-word-processing-document" rel="noopener noreferrer"&gt;Open XML paragraph-style documentation&lt;/a&gt; supports this distinction: paragraph style is an explicit document property, separate from the runs that carry text and visual formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table text survived, but the table did not
&lt;/h2&gt;

&lt;p&gt;This was the most important limitation in the test.&lt;/p&gt;

&lt;p&gt;The DOCX table contained:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;Ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The observed output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner

Status

Platform

Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The words survived. The row-and-column relationship did not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xg54wckzmj0ksxddtnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xg54wckzmj0ksxddtnq.png" alt="MDFold output showing a visual-only title as bold text, flattened table cells, a data-URI image, and the final checkpoint" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The difficult file produced valid Markdown text, but valid is not the same as structurally complete. The table must be rebuilt and checked against the source.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;This matters because a flattened table can silently change meaning. Four values in the correct order may still be unusable when the reader cannot tell which header belongs to which value.&lt;/p&gt;

&lt;p&gt;For a small table, rebuild it manually and compare every cell with Word. For a large table, use a converter that explicitly supports GitHub Flavored Markdown tables, export the data separately as CSV, or use &lt;a href="https://mdfold.com/markdown-table-generator" rel="noopener noreferrer"&gt;MDFold’s Markdown table generator&lt;/a&gt; after verifying the source values.&lt;/p&gt;

&lt;p&gt;Do not trust the presence of all words as proof that the table survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embedded images can survive in an inconvenient form
&lt;/h2&gt;

&lt;p&gt;The test PNG became Markdown with a &lt;code&gt;data:&lt;/code&gt; URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;A generated status marker&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;data:image/png;base64,...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes the image self-contained: no separate file is required. It can also make the Markdown enormous, create unreadable diffs, and fail in publishing systems that block data URLs.&lt;/p&gt;

&lt;p&gt;For a repository or documentation site, my preferred workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;extract the image into an assets directory;&lt;/li&gt;
&lt;li&gt;give it a stable, descriptive filename;&lt;/li&gt;
&lt;li&gt;replace the data URI with a relative path;&lt;/li&gt;
&lt;li&gt;preserve or improve the alt text;&lt;/li&gt;
&lt;li&gt;preview the page in the actual publishing system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Green deployment status marker&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;./assets/deployment-status.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mammoth documents inline data URIs as its default image behavior. Other converters may drop images, produce placeholders, or extract separate files. “Images supported” does not tell you which of those contracts a tool provides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headers and footers were omitted—and that may be desirable
&lt;/h2&gt;

&lt;p&gt;The difficult DOCX used both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONFIDENTIAL DRAFT HEADER
PAGE FOOTER — INTERNAL REVIEW
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither appeared in the Markdown.&lt;/p&gt;

&lt;p&gt;For a web article, this is often desirable. Repeating page furniture is not part of the article’s logical reading flow. For a policy, contract, or controlled document, however, the missing classification, revision, or page note may be important.&lt;/p&gt;

&lt;p&gt;The right question is not “did the header survive?” It is “does any information in the header need a new structural home?”&lt;/p&gt;

&lt;p&gt;Move essential revision data into front matter, a document metadata block, or the opening section before conversion. Do not rely on page headers to become Markdown automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Underline is ambiguous in Markdown
&lt;/h2&gt;

&lt;p&gt;The underlined sentence returned as plain text.&lt;/p&gt;

&lt;p&gt;Markdown has no universal underline syntax, and underlining is often confused with hyperlinks on the web. Mammoth’s default mapping intentionally ignores underline rather than inventing a meaning.&lt;/p&gt;

&lt;p&gt;Decide what the underline meant in the source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;emphasis → use &lt;code&gt;*italic*&lt;/code&gt; or &lt;code&gt;**bold**&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a section label → use a real heading;&lt;/li&gt;
&lt;li&gt;an insertion → write explicit editorial text;&lt;/li&gt;
&lt;li&gt;a link → create a real hyperlink;&lt;/li&gt;
&lt;li&gt;decoration → remove it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conversion cannot make that editorial decision safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert DOCX to Markdown in a reviewable workflow
&lt;/h2&gt;

&lt;p&gt;Here is the process I would use for a real document.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepare the Word file
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Save legacy &lt;code&gt;.doc&lt;/code&gt; files as &lt;code&gt;.docx&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;Apply Heading styles to actual section headings.&lt;/li&gt;
&lt;li&gt;Accept or reject tracked changes.&lt;/li&gt;
&lt;li&gt;Resolve comments that should not become published content.&lt;/li&gt;
&lt;li&gt;Move essential header or footer information into the document body.&lt;/li&gt;
&lt;li&gt;Simplify merged tables where possible.&lt;/li&gt;
&lt;li&gt;Add useful alternative text to meaningful images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Convert the DOCX
&lt;/h3&gt;

&lt;p&gt;Open &lt;a href="https://mdfold.com/word-to-markdown" rel="noopener noreferrer"&gt;MDFold Word to Markdown&lt;/a&gt;, choose the &lt;code&gt;.docx&lt;/code&gt; file, and wait for editable Markdown to appear. In the version tested on August 2, 2026, the conversion ran in the browser and did not require a document upload endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check high-risk structures first
&lt;/h3&gt;

&lt;p&gt;Do not begin by proofreading ordinary paragraphs. Check the features most likely to change meaning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H1, H2, and H3 hierarchy;&lt;/li&gt;
&lt;li&gt;first and final paragraphs;&lt;/li&gt;
&lt;li&gt;ordered-list numbering;&lt;/li&gt;
&lt;li&gt;link destinations;&lt;/li&gt;
&lt;li&gt;every table;&lt;/li&gt;
&lt;li&gt;image references and alt text;&lt;/li&gt;
&lt;li&gt;footnotes, comments, headers, and footers;&lt;/li&gt;
&lt;li&gt;names, dates, percentages, and negative numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Repair the Markdown for its destination
&lt;/h3&gt;

&lt;p&gt;Different destinations support different Markdown extensions. GitHub Flavored Markdown supports pipe tables, while strict CommonMark does not define the same table extension. A data URI or HTML fragment may work in one renderer and fail in another.&lt;/p&gt;

&lt;p&gt;Preview the result where it will actually live: GitHub, a documentation generator, a CMS, an Obsidian vault, or another target system.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Download and verify the final &lt;code&gt;.md&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Open the downloaded file rather than assuming the editor state and saved artifact are identical. Search for the final checkpoint, inspect the Markdown source, and render it once in the target environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phone-sized workflow worked, but desktop is better for review
&lt;/h2&gt;

&lt;p&gt;I repeated the representative conversion at a 390 × 844 viewport. The file opened, the expected final checkpoint was present, the result remained editable, and the page had no document-level horizontal overflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgo6sc2f6xhvnvqmcafon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgo6sc2f6xhvnvqmcafon.png" alt="Phone-sized MDFold Word to Markdown result showing headings, emphasis, a link, lists, and download controls" width="390" height="844"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Phone conversion worked in the test. A larger screen is still the better place to compare tables, long links, and image data.
  &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Mobile support is useful for an urgent check. It does not remove the need for a careful desktop review when the source contains complex structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use a different conversion path
&lt;/h2&gt;

&lt;p&gt;Use this browser workflow for ordinary &lt;code&gt;.docx&lt;/code&gt; documents that are mostly headings, paragraphs, emphasis, lists, and links.&lt;/p&gt;

&lt;p&gt;Consider &lt;a href="https://pandoc.org/MANUAL.html" rel="noopener noreferrer"&gt;Pandoc&lt;/a&gt; or another document pipeline when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;batch conversion;&lt;/li&gt;
&lt;li&gt;repeatable command-line builds;&lt;/li&gt;
&lt;li&gt;separate media extraction;&lt;/li&gt;
&lt;li&gt;custom style mapping;&lt;/li&gt;
&lt;li&gt;footnotes, citations, or richer table handling;&lt;/li&gt;
&lt;li&gt;a conversion process that can be reviewed and versioned as code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep Word or export to PDF when the requirement is visual fidelity rather than portable structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy is part of the conversion choice
&lt;/h2&gt;

&lt;p&gt;For the tested MDFold workflow, the DOCX was read by browser-side code and the Markdown appeared locally in the editor. That is narrower than claiming that every website conversion is private.&lt;/p&gt;

&lt;p&gt;If you use another converter, determine whether the file is uploaded, retained, used for model processing, or logged. This matters for contracts, unpublished research, internal runbooks, customer data, and regulated material.&lt;/p&gt;

&lt;p&gt;Local conversion still depends on the device and browser you trust. Extensions, shared computers, managed devices, backups, and downloaded files remain part of the security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical definition of “preserved”
&lt;/h2&gt;

&lt;p&gt;After this test, I would not describe DOCX-to-Markdown quality with one percentage or one checkbox.&lt;/p&gt;

&lt;p&gt;I would ask five questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is every important sentence present?&lt;/li&gt;
&lt;li&gt;Is the heading hierarchy still correct?&lt;/li&gt;
&lt;li&gt;Are relationships—lists, links, tables, notes—still explicit?&lt;/li&gt;
&lt;li&gt;Are images usable in the target system?&lt;/li&gt;
&lt;li&gt;Did any page-level information need to be moved into document structure?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A converter can automate syntax. It cannot decide what a visual convention meant to the author.&lt;/p&gt;

&lt;p&gt;That is why DOCX-to-Markdown conversion should be treated as a small content migration, not a file-extension swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  DOCX-to-Markdown FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I convert an old &lt;code&gt;.doc&lt;/code&gt; file directly?
&lt;/h3&gt;

&lt;p&gt;Not with the MDFold workflow tested here. Save the legacy file as &lt;code&gt;.docx&lt;/code&gt; in Word, LibreOffice, or another compatible editor, then convert the new copy. Keep the original until you have checked the Markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did my Word heading become ordinary text?
&lt;/h3&gt;

&lt;p&gt;The paragraph probably looked like a heading without using a Word Heading style. Apply the correct semantic style and convert again; do not rely on font size or bold text alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I keep embedded images as base64 data?
&lt;/h3&gt;

&lt;p&gt;Only when the destination supports data URLs and a self-contained file matters more than readable source. For repositories and documentation sites, extracted image files with stable relative paths usually produce smaller, reviewable Markdown.&lt;/p&gt;

&lt;p&gt;If you have a difficult Word document, try the &lt;a href="https://mdfold.com/word-to-markdown" rel="noopener noreferrer"&gt;browser-based converter&lt;/a&gt; and share one reproducible feature that did not map cleanly. A merged table, floating text box, tracked-change sequence, or image-heavy page is more useful feedback than “formatting broke.”&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Convert Markdown to a Professional PDF (Without Broken Tables, Code, or Math)</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:11:00 +0000</pubDate>
      <link>https://dev.to/mdfold/how-to-convert-markdown-to-a-professional-pdf-without-broken-tables-code-or-math-4g9e</link>
      <guid>https://dev.to/mdfold/how-to-convert-markdown-to-a-professional-pdf-without-broken-tables-code-or-math-4g9e</guid>
      <description>&lt;p&gt;Converting Markdown to PDF is easy.&lt;/p&gt;

&lt;p&gt;Converting it into a PDF you can confidently send to a client, reviewer, professor, or teammate is a different task.&lt;/p&gt;

&lt;p&gt;A successful export needs more than a &lt;strong&gt;Download PDF&lt;/strong&gt; button. The document should have a clear hierarchy, readable typography, code that does not disappear off the page, tables that fit, working links, and equations that survive the transition from source text to fixed pages.&lt;/p&gt;

&lt;p&gt;This guide gives you a complete workflow using &lt;a href="https://mdfold.com/markdown-to-pdf/" rel="noopener noreferrer"&gt;MDFold’s Markdown to PDF converter&lt;/a&gt;. It also explains the failure cases that matter, including wide tables, long code lines, missing background colors, awkward page breaks, and content that should not be exported as PDF in the first place.&lt;/p&gt;

&lt;p&gt;I tested the workflow with headings, task lists, a numeric table, JavaScript, links, inline math, and a display equation on both desktop and a 390-pixel-wide phone viewport.&lt;/p&gt;

&lt;h2&gt;
  
  
  First decide whether PDF is the right output
&lt;/h2&gt;

&lt;p&gt;PDF is a good destination when the document is finished and its appearance should remain stable.&lt;/p&gt;

&lt;p&gt;Use PDF for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;project reports and technical proposals;&lt;/li&gt;
&lt;li&gt;resumes and portfolios;&lt;/li&gt;
&lt;li&gt;assignments and research notes;&lt;/li&gt;
&lt;li&gt;release plans and operational runbooks;&lt;/li&gt;
&lt;li&gt;documents that will be printed, signed, archived, or attached to email;&lt;/li&gt;
&lt;li&gt;anything where the recipient should read rather than rewrite.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not default to PDF for every document.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the recipient needs to do&lt;/th&gt;
&lt;th&gt;Better format&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read, print, archive, or approve a finished document&lt;/td&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit wording, comment, or use tracked changes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mdfold.com/markdown-to-word/" rel="noopener noreferrer"&gt;Word&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publish content on a website&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mdfold.com/markdown-to-html/" rel="noopener noreferrer"&gt;HTML&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue writing in a repository or notes app&lt;/td&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share a short visual card on social media&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mdfold.com/markdown-to-image/" rel="noopener noreferrer"&gt;PNG or JPG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This decision prevents a common mistake: spending time forcing an editable workflow into a fixed-page format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with Markdown that has real structure
&lt;/h2&gt;

&lt;p&gt;A converter can style your structure, but it cannot reliably invent structure that is missing.&lt;/p&gt;

&lt;p&gt;Before exporting, check that the source has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one H1 for the document title;&lt;/li&gt;
&lt;li&gt;H2 sections for the main ideas;&lt;/li&gt;
&lt;li&gt;H3 headings only when they belong inside an H2 section;&lt;/li&gt;
&lt;li&gt;blank lines around lists, tables, blockquotes, and code fences;&lt;/li&gt;
&lt;li&gt;language names on fenced code blocks;&lt;/li&gt;
&lt;li&gt;meaningful link text instead of repeated “click here” labels;&lt;/li&gt;
&lt;li&gt;concise table cells;&lt;/li&gt;
&lt;li&gt;image URLs that the browser can actually load.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the representative document I used for testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# API Reliability Review&lt;/span&gt;

&lt;span class="gs"&gt;**Owner:**&lt;/span&gt; Platform Team  
&lt;span class="gs"&gt;**Status:**&lt;/span&gt; Ready for review
&lt;span class="gt"&gt;
&amp;gt; Goal: reduce failed requests without hiding operational risk.&lt;/span&gt;

&lt;span class="gu"&gt;## Release checks&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; [x] Retry policy documented
&lt;span class="p"&gt;-&lt;/span&gt; [x] Error budget reviewed
&lt;span class="p"&gt;-&lt;/span&gt; [ ] Final approval

| Metric | Current | Target |
|---|---:|---:|
| Availability | 99.92% | 99.95% |
| P95 latency | 280 ms | 220 ms |

&lt;span class="gu"&gt;## Example&lt;/span&gt;

&lt;span class="p"&gt;```&lt;/span&gt;&lt;span class="nl"&gt;javascript
&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;```&lt;/span&gt;

Inline formula: $E = mc^2$

$$
S = &lt;span class="se"&gt;\s&lt;/span&gt;um_{i=1}^{n} x_i
$$

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Open the runbook&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/runbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a better test than a document containing only headings and paragraphs. It exercises the parts most likely to fail when content moves onto fixed pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert Markdown to PDF in five steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Open or paste the Markdown
&lt;/h3&gt;

&lt;p&gt;Open the &lt;a href="https://mdfold.com/markdown-to-pdf/" rel="noopener noreferrer"&gt;Markdown to PDF converter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can type directly in the editor, paste existing Markdown, or use &lt;strong&gt;Open MD&lt;/strong&gt; to select a &lt;code&gt;.md&lt;/code&gt;, &lt;code&gt;.markdown&lt;/code&gt;, or &lt;code&gt;.txt&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Do not style the document yet. First, confirm that the content itself is complete. Check the title, section order, links, table values, code, and equations.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Review the live preview before choosing a template
&lt;/h3&gt;

&lt;p&gt;The preview should show the document’s logical structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the title is visually distinct;&lt;/li&gt;
&lt;li&gt;section levels are obvious;&lt;/li&gt;
&lt;li&gt;task-list states are recognizable;&lt;/li&gt;
&lt;li&gt;numeric columns align correctly;&lt;/li&gt;
&lt;li&gt;code appears as code rather than ordinary prose;&lt;/li&gt;
&lt;li&gt;inline and display math render;&lt;/li&gt;
&lt;li&gt;the link remains identifiable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwdq3s9hd6brgqti3121.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwdq3s9hd6brgqti3121.webp" alt="MDFold editor showing Markdown source beside a styled PDF preview" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Keep the source and preview visible together while correcting structure. Styling a broken hierarchy only makes the problem more attractive.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;During my test, the preview preserved the title, metadata labels, blockquote, three task items, two-row table, JavaScript block, link, and both equation forms.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Choose a template for the reader, not for decoration
&lt;/h3&gt;

&lt;p&gt;MDFold currently provides 24 document styles grouped into business, minimal, academic, technical, and creative categories.&lt;/p&gt;

&lt;p&gt;Choose according to the job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Good starting direction&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Technical report or runbook&lt;/td&gt;
&lt;td&gt;GitHub Style, Modern, or another restrained technical style&lt;/td&gt;
&lt;td&gt;Strong code contrast and familiar hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proposal or client report&lt;/td&gt;
&lt;td&gt;Classic, Business Gray, or Corporate Blue&lt;/td&gt;
&lt;td&gt;Conservative presentation and clear sections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Essay or research note&lt;/td&gt;
&lt;td&gt;Research Journal, Academic Green, or a readable serif style&lt;/td&gt;
&lt;td&gt;Comfortable for longer reading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resume&lt;/td&gt;
&lt;td&gt;Simple, Pure Black &amp;amp; White, or Soft Ink&lt;/td&gt;
&lt;td&gt;Keeps attention on content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Announcement or short handout&lt;/td&gt;
&lt;td&gt;A restrained creative style&lt;/td&gt;
&lt;td&gt;Adds identity without overwhelming the message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule is simple: the template should make the document easier to understand. If the reader notices the theme before the content, reduce the decoration.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Adjust typography and spacing only when the preview shows a problem
&lt;/h3&gt;

&lt;p&gt;Use &lt;strong&gt;Edit template&lt;/strong&gt; to adjust colors, body and heading fonts, type size, line height, and page padding.&lt;/p&gt;

&lt;p&gt;Make one change at a time and look for a specific improvement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce the body size slightly when a dense table barely overflows;&lt;/li&gt;
&lt;li&gt;increase line height when long paragraphs feel crowded;&lt;/li&gt;
&lt;li&gt;reduce page padding when code needs a little more horizontal room;&lt;/li&gt;
&lt;li&gt;increase code contrast if punctuation is hard to distinguish;&lt;/li&gt;
&lt;li&gt;use a calmer accent color for formal documents;&lt;/li&gt;
&lt;li&gt;keep body text dark on a light background for print-heavy use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid shrinking everything to rescue one difficult element. A table that needs unreadably small body text should be simplified or split instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Download, save as PDF, and inspect the artifact
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Download PDF&lt;/strong&gt;. MDFold prepares the print-focused document and opens the browser’s print dialog. Choose &lt;strong&gt;Save as PDF&lt;/strong&gt; as the destination.&lt;/p&gt;

&lt;p&gt;The final controls depend on your browser and operating system. Browser printing can apply print-specific styles, page dimensions, orientation, and margins; this is part of the standard web printing model described by &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Printing" rel="noopener noreferrer"&gt;MDN’s printing guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before saving:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the destination is &lt;strong&gt;Save as PDF&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check the page size and orientation.&lt;/li&gt;
&lt;li&gt;Review the page count.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Background graphics&lt;/strong&gt; if template colors or code shading are missing.&lt;/li&gt;
&lt;li&gt;Inspect a page containing a table or code block.&lt;/li&gt;
&lt;li&gt;Save the PDF.&lt;/li&gt;
&lt;li&gt;Open the saved file in a PDF reader and repeat the critical checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last step matters. The saved PDF is the deliverable; the browser preview is only evidence of what you expect to receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the failures that appear in real documents
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A wide table runs beyond the page
&lt;/h3&gt;

&lt;p&gt;Markdown tables describe rows, columns, and alignment. They do not define a print layout.&lt;/p&gt;

&lt;p&gt;Fix the source in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shorten verbose column headings.&lt;/li&gt;
&lt;li&gt;Replace paragraph-length cells with short phrases.&lt;/li&gt;
&lt;li&gt;Remove columns that are not needed by this audience.&lt;/li&gt;
&lt;li&gt;Split one large table into two focused tables.&lt;/li&gt;
&lt;li&gt;Reduce page padding slightly.&lt;/li&gt;
&lt;li&gt;Use landscape orientation if the print dialog provides it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not solve every table problem by reducing the font until nobody can comfortably read it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Long code lines are clipped or create horizontal scrolling
&lt;/h3&gt;

&lt;p&gt;PDF pages have a fixed width. A long command, URL, string, or generated line cannot expand the page.&lt;/p&gt;

&lt;p&gt;Prefer readable source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;baseDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maximumDelay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseDelay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maximumDelay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For commands or output that cannot be wrapped safely, use a smaller code font, shorten the example, or move the complete version to a linked repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code backgrounds disappear in the saved PDF
&lt;/h3&gt;

&lt;p&gt;Enable &lt;strong&gt;Background graphics&lt;/strong&gt; in the browser print dialog.&lt;/p&gt;

&lt;p&gt;Browsers often treat printed output differently from screen output. The saved result can therefore omit a background that was visible in the live preview unless printing backgrounds is enabled.&lt;/p&gt;

&lt;p&gt;After changing the setting, inspect one code-heavy page before saving the complete document.&lt;/p&gt;

&lt;h3&gt;
  
  
  A heading is stranded at the bottom of a page
&lt;/h3&gt;

&lt;p&gt;Page breaks are calculated from the final type size, spacing, margins, and content.&lt;/p&gt;

&lt;p&gt;Try these fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shorten the paragraph before the heading;&lt;/li&gt;
&lt;li&gt;reduce excessive spacing;&lt;/li&gt;
&lt;li&gt;move a related sentence into the next section;&lt;/li&gt;
&lt;li&gt;split an oversized table or code block;&lt;/li&gt;
&lt;li&gt;choose a slightly more compact template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid inserting a series of blank lines. Blank lines are content, not reliable page-break instructions, and they become fragile when the document changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  An image is missing or blurry
&lt;/h3&gt;

&lt;p&gt;For a remote image, open its URL in a browser first. If the browser cannot load it, the converter cannot render it into the preview.&lt;/p&gt;

&lt;p&gt;Use an image large enough for its displayed size, but do not insert an enormous source just because the final page is small. Check that diagrams remain legible in the saved PDF at normal zoom.&lt;/p&gt;

&lt;p&gt;If the document must work offline or the image URL requires authentication, replace it with a stable accessible source before exporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  A formula does not render
&lt;/h3&gt;

&lt;p&gt;The page renders supported math notation with KaTeX. KaTeX documents its available commands in the official &lt;a href="https://katex.org/docs/supported.html" rel="noopener noreferrer"&gt;supported-functions reference&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inline math uses single dollar delimiters;&lt;/li&gt;
&lt;li&gt;display math uses double dollar delimiters;&lt;/li&gt;
&lt;li&gt;backslashes were not removed while copying;&lt;/li&gt;
&lt;li&gt;the command is supported;&lt;/li&gt;
&lt;li&gt;braces are balanced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when a formula renders correctly, inspect its width. A long display equation may need to be split across lines or rewritten before it can fit a portrait page.&lt;/p&gt;

&lt;h3&gt;
  
  
  A link looks right but opens the wrong destination
&lt;/h3&gt;

&lt;p&gt;Click important links in the preview before export, then test them again in the saved PDF.&lt;/p&gt;

&lt;p&gt;Use descriptive text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Read the deployment runbook&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/runbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is more useful than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Click here&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://example.com/runbook&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the PDF may be printed, consider whether the destination also needs to appear in a references section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use this 60-second preflight before sending the PDF
&lt;/h2&gt;

&lt;p&gt;Do not reread every sentence through the converter interface. Review the artifact systematically.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;What to inspect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First page&lt;/td&gt;
&lt;td&gt;Title, author or owner, date, and opening hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structure&lt;/td&gt;
&lt;td&gt;H2/H3 order, list indentation, blockquotes, and section flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;Table values, percentages, dates, and formulas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;Wrapping, indentation, contrast, and missing characters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Images&lt;/td&gt;
&lt;td&gt;Loading, sharpness, captions, and page fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links&lt;/td&gt;
&lt;td&gt;Important destinations in the saved PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;Stranded headings, nearly blank pages, split tables, and clipped content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final page&lt;/td&gt;
&lt;td&gt;Truncation, accidental notes, and expected closing content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For legal, financial, medical, academic, or compliance-sensitive material, compare critical quotations, numbers, formulas, and references against the source. Conversion saves formatting work; it does not replace responsibility for the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to your Markdown
&lt;/h2&gt;

&lt;p&gt;In the version tested for this article, editing, rendering, template selection, and print preparation happened in the browser. The Markdown did not need to be uploaded to a conversion server.&lt;/p&gt;

&lt;p&gt;The browser and operating system handle the final &lt;strong&gt;Save as PDF&lt;/strong&gt; step.&lt;/p&gt;

&lt;p&gt;This is useful for private drafts, source code, internal documentation, and unpublished reports, but “local” does not make the surrounding environment automatically safe. A compromised device, untrusted browser extension, publicly shared computer, or remote image URL can still create privacy risks.&lt;/p&gt;

&lt;p&gt;Use non-sensitive image sources, keep the browser updated, and follow your organization’s document-handling rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile works for editing and review, but desktop is better for final pagination
&lt;/h2&gt;

&lt;p&gt;At a 390 × 844 viewport, MDFold showed separate &lt;strong&gt;Write&lt;/strong&gt; and &lt;strong&gt;Preview&lt;/strong&gt; controls. The test document’s table and equations remained accessible without page-level horizontal overflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mjyvjk2jx375l6p0qxc.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2mjyvjk2jx375l6p0qxc.webp" alt="MDFold Markdown to PDF converter in a phone-sized layout" width="390" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The phone layout is useful for corrections and preview checks. A larger screen remains more comfortable for reviewing page breaks and wide tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use mobile when you need to correct wording, inspect a short document, or produce a quick export. Use a desktop or tablet for long reports, detailed template adjustment, and final pagination review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does MDFold create the PDF directly?
&lt;/h3&gt;

&lt;p&gt;MDFold prepares the document and opens the browser print dialog. Select &lt;strong&gt;Save as PDF&lt;/strong&gt; there. Available page sizes, orientations, margins, headers, footers, and background options depend on the browser and operating system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Markdown tables, code blocks, and math be exported?
&lt;/h3&gt;

&lt;p&gt;They appeared in the live preview during the representative test for this article. Difficult content still needs inspection: wide tables and long code can exceed the printable width, and long equations may need rewriting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my saved PDF look different from the preview?
&lt;/h3&gt;

&lt;p&gt;Check the selected printer or destination, page size, margins, scale, orientation, and &lt;strong&gt;Background graphics&lt;/strong&gt; setting. Print settings affect the final file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use custom HTML inside Markdown?
&lt;/h3&gt;

&lt;p&gt;Do not assume arbitrary HTML or browser-specific styling will survive every Markdown renderer and print path. Prefer standard Markdown for portable documents, then verify any custom HTML in both the preview and saved PDF.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Markdown to PDF private?
&lt;/h3&gt;

&lt;p&gt;The MDFold workflow tested for this article handled the Markdown and print preparation in the browser. You should still consider device security, browser extensions, remote images, and your organization’s rules for sensitive documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real goal is not conversion—it is a trustworthy artifact
&lt;/h2&gt;

&lt;p&gt;A good Markdown-to-PDF workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choose PDF only when the document is ready for fixed presentation;&lt;/li&gt;
&lt;li&gt;give the Markdown a clear structure;&lt;/li&gt;
&lt;li&gt;test difficult content, not only paragraphs;&lt;/li&gt;
&lt;li&gt;choose typography for the reader;&lt;/li&gt;
&lt;li&gt;inspect the print settings;&lt;/li&gt;
&lt;li&gt;open and verify the saved PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That process takes a little longer than clicking &lt;strong&gt;Download&lt;/strong&gt;, but it prevents the failures people actually notice after you send the file.&lt;/p&gt;

&lt;p&gt;Try the workflow in the &lt;a href="https://mdfold.com/markdown-to-pdf/" rel="noopener noreferrer"&gt;free MDFold Markdown to PDF converter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you find a table, code sample, equation, or page-break case that does not behave as expected, share the smallest Markdown example that reproduces it. A reproducible edge case is far more valuable than a vague “the formatting looks wrong.”&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Shipped a Privacy-First Markdown Converter in 3 Days. Here’s What the Launch Post Usually Leaves Out</title>
      <dc:creator>frank</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:18:11 +0000</pubDate>
      <link>https://dev.to/mdfold/i-shipped-a-privacy-first-markdown-converter-in-3-days-heres-what-the-launch-post-usually-leaves-p66</link>
      <guid>https://dev.to/mdfold/i-shipped-a-privacy-first-markdown-converter-in-3-days-heres-what-the-launch-post-usually-leaves-p66</guid>
      <description>&lt;p&gt;Most launch posts follow the same script:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I had an idea, built it quickly with AI, shipped it, and here is the feature list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the clean version.&lt;/p&gt;

&lt;p&gt;The useful version includes the wrong assumptions, the features that were deliberately cut, the conversions that looked correct until I opened the downloaded file, and the uncomfortable fact that “works on my machine” is not the same as “works for a stranger.”&lt;/p&gt;

&lt;p&gt;So this is the useful version.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://mdfold.com/" rel="noopener noreferrer"&gt;MDFold&lt;/a&gt;, a free set of browser-based tools for moving documents into and out of Markdown. The first working version took three days. It now handles eight practical workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF to Markdown&lt;/li&gt;
&lt;li&gt;Markdown to PDF&lt;/li&gt;
&lt;li&gt;Markdown to Word&lt;/li&gt;
&lt;li&gt;Word to Markdown&lt;/li&gt;
&lt;li&gt;Markdown to HTML&lt;/li&gt;
&lt;li&gt;HTML to Markdown&lt;/li&gt;
&lt;li&gt;Markdown to PNG or JPG&lt;/li&gt;
&lt;li&gt;Visual and CSV-based Markdown table generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No account is required, and the conversion work happens in the browser.&lt;/p&gt;

&lt;p&gt;But those bullet points are the least interesting part of the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The original idea was too broad
&lt;/h2&gt;

&lt;p&gt;My first mental model was simple: “convert Markdown to anything.”&lt;/p&gt;

&lt;p&gt;It sounded focused. It was not.&lt;/p&gt;

&lt;p&gt;Every output format carries a different promise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Word document should remain editable.&lt;/li&gt;
&lt;li&gt;A PDF should be visually stable and printable.&lt;/li&gt;
&lt;li&gt;HTML should be clean enough to publish or reuse.&lt;/li&gt;
&lt;li&gt;An image should be sharp at the dimensions the user selected.&lt;/li&gt;
&lt;li&gt;Markdown extracted from PDF, Word, or HTML should be readable, not merely technically valid.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A universal &lt;strong&gt;Convert&lt;/strong&gt; button would have hidden those differences. It also would have produced a product that was easy to demo and frustrating to use.&lt;/p&gt;

&lt;p&gt;The first important decision was therefore not about code. It was to give each workflow its own interface and success criteria.&lt;/p&gt;

&lt;p&gt;Markdown to PDF needs page preview and print-aware styling. Word to Markdown needs a cleanup step. A table generator needs row, column, alignment, paste, and CSV controls. Treating these as one feature would have made every workflow worse.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;scope is not the number of pages you build. Scope is the number of promises you make.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy changed the architecture, not just the copy
&lt;/h2&gt;

&lt;p&gt;“Privacy-first” is easy to place under a logo. I wanted it to describe observable behavior.&lt;/p&gt;

&lt;p&gt;In MDFold, the document content stays in the current browser during editing, preview, conversion, and export. There is no upload step for the converter to work. Saved preferences and reusable templates stay on that device as well.&lt;/p&gt;

&lt;p&gt;That choice created useful constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no upload queue.&lt;/li&gt;
&lt;li&gt;A slow connection does not become a slow conversion after the app has loaded.&lt;/li&gt;
&lt;li&gt;The server does not need to store a stranger’s report, contract, draft, or source code.&lt;/li&gt;
&lt;li&gt;Clearing browser data can remove locally saved preferences and templates.&lt;/li&gt;
&lt;li&gt;Moving to another device does not automatically bring those local settings with you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two points matter. Local storage is a privacy advantage, but it is not cloud sync. A trustworthy product should explain both sides of that trade-off.&lt;/p&gt;

&lt;p&gt;This also gave me a simple product test: if a conversion required sending the document to an application endpoint, the implementation had broken the product promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The editor became the product
&lt;/h2&gt;

&lt;p&gt;I originally thought of the editor as a place to paste input before clicking a button.&lt;/p&gt;

&lt;p&gt;That was wrong.&lt;/p&gt;

&lt;p&gt;Conversion is rarely a one-click event. People notice a heading that should be smaller, a table that is too wide, an image that needs a better URL, or a code block with a missing language label. They need a correction loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write, paste, or open the source.&lt;/li&gt;
&lt;li&gt;Inspect the result.&lt;/li&gt;
&lt;li&gt;Fix the source or presentation.&lt;/li&gt;
&lt;li&gt;Export.&lt;/li&gt;
&lt;li&gt;Open the exported file and verify it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MDFold therefore uses a live editor and preview instead of an upload form followed by a download screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwdq3s9hd6brgqti3121.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcwdq3s9hd6brgqti3121.webp" alt="MDFold Markdown editor beside a live PDF preview" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The source and output remain visible together, so formatting problems can be fixed before export.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This led to several features that did not look essential in the first sketch but became important in real use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;focus mode for long editing sessions;&lt;/li&gt;
&lt;li&gt;fullscreen mode for detailed review;&lt;/li&gt;
&lt;li&gt;quick actions for headings, code, lists, and tables;&lt;/li&gt;
&lt;li&gt;24 document themes across business, minimal, academic, technical, and creative styles;&lt;/li&gt;
&lt;li&gt;editable colors, fonts, type size, line height, and page spacing;&lt;/li&gt;
&lt;li&gt;a short first-use tour that can be dismissed and reopened;&lt;/li&gt;
&lt;li&gt;separate write and preview views on a phone-sized screen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is impressive in isolation. Together, they shorten the correction loop.&lt;/p&gt;

&lt;p&gt;That is a better product goal than maximizing the feature count.&lt;/p&gt;

&lt;h2&gt;
  
  
  “It rendered” was not a sufficient test
&lt;/h2&gt;

&lt;p&gt;The biggest trap in document tools is trusting the preview.&lt;/p&gt;

&lt;p&gt;A preview can look perfect while the downloaded file is malformed. A file can download successfully and still be unreadable by the program the user opens next.&lt;/p&gt;

&lt;p&gt;I used a representative Markdown document containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;H1, H2, and H3 headings;&lt;/li&gt;
&lt;li&gt;bold and italic text;&lt;/li&gt;
&lt;li&gt;ordered and unordered lists;&lt;/li&gt;
&lt;li&gt;links and blockquotes;&lt;/li&gt;
&lt;li&gt;a table;&lt;/li&gt;
&lt;li&gt;a fenced JavaScript block;&lt;/li&gt;
&lt;li&gt;inline and block math;&lt;/li&gt;
&lt;li&gt;an external image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I checked the output according to the format:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;What I checked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DOCX&lt;/td&gt;
&lt;td&gt;The file is a readable Word archive and the document structure is present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;Headings, rich text, links, code, and table markup are present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG&lt;/td&gt;
&lt;td&gt;The downloaded image has the requested dimensions and remains sharp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPG&lt;/td&gt;
&lt;td&gt;The file is a valid JPEG rather than renamed PNG data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF&lt;/td&gt;
&lt;td&gt;The print preview, pagination, background styling, and save control work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;td&gt;Reading order and useful structure survive the source conversion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I also tested the rendered site, not only the individual conversion functions. The current check covers the homepage, all eight tools, the tools directory, and the practical guides.&lt;/p&gt;

&lt;p&gt;This caught a class of problems that unit-level checks miss: a converter can be correct while its route fails to render, its first-use overlay blocks the download button, or its phone layout creates horizontal overflow.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;test the artifact in the program that will consume it, and test the complete user path that creates it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF to Markdown forced me to be honest
&lt;/h2&gt;

&lt;p&gt;PDF is a presentation format. Markdown represents document structure. Moving from one to the other requires inference.&lt;/p&gt;

&lt;p&gt;A digitally generated PDF with a clear reading order may convert into useful Markdown. A multi-column layout can scramble that order. A scanned PDF may contain no useful text layer at all. Complex tables, positioned captions, footnotes, and decorative layout can require cleanup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27nvc7ohyanf0xszdrv8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27nvc7ohyanf0xszdrv8.webp" alt="Extracted Markdown and preview shown after a PDF conversion" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A successful extraction still needs a human check for reading order, headings, tables, and missing content.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I could have hidden those limitations behind “AI-powered” language or claimed support for every PDF. That might improve a landing-page headline, but it would make the product less useful.&lt;/p&gt;

&lt;p&gt;MDFold instead gives the extracted Markdown back in an editor. The user can inspect the text order, correct headings, repair a table, and compare the preview before downloading.&lt;/p&gt;

&lt;p&gt;Some files need OCR. Some need manual cleanup. Some are better handled by a specialized document-reconstruction tool.&lt;/p&gt;

&lt;p&gt;Saying that clearly is part of the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI accelerated implementation, but it did not own the decisions
&lt;/h2&gt;

&lt;p&gt;I used AI-assisted development throughout the build. It was especially helpful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;generating a first implementation from a precise interface specification;&lt;/li&gt;
&lt;li&gt;applying the same behavior across several converter pages;&lt;/li&gt;
&lt;li&gt;proposing test cases;&lt;/li&gt;
&lt;li&gt;finding inconsistent labels and repeated layout problems;&lt;/li&gt;
&lt;li&gt;turning a failure into a reproducible check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was less useful when the instruction was vague.&lt;/p&gt;

&lt;p&gt;“Make the converter better” produces plausible changes.&lt;/p&gt;

&lt;p&gt;“At 390 × 844, the first-use guide must not create horizontal overflow; the editor, preview switch, and download action must remain reachable” produces work that can be judged.&lt;/p&gt;

&lt;p&gt;The speed did not come from asking an AI to build an entire product in one prompt. It came from repeatedly doing four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define one observable outcome.&lt;/li&gt;
&lt;li&gt;Implement the smallest coherent change.&lt;/li&gt;
&lt;li&gt;Run the real workflow.&lt;/li&gt;
&lt;li&gt;Keep the failure as a permanent check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI made each loop faster. It did not remove the need for the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The redesign happened after the product worked
&lt;/h2&gt;

&lt;p&gt;My first version was functional, but the interface still made people work too hard to understand it.&lt;/p&gt;

&lt;p&gt;The tools were not visible early enough. Example content looked too much like user content. Long documents needed more space. First-time users needed guidance, but a large welcome dialog would interrupt returning users.&lt;/p&gt;

&lt;p&gt;The fixes were small and specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move the actual workspace higher on the page;&lt;/li&gt;
&lt;li&gt;label bundled content as an example;&lt;/li&gt;
&lt;li&gt;add editor focus and fullscreen modes;&lt;/li&gt;
&lt;li&gt;show a brief guide only on the first converter visit;&lt;/li&gt;
&lt;li&gt;remember dismissal locally;&lt;/li&gt;
&lt;li&gt;keep a “Quick tour” control for anyone who wants it again;&lt;/li&gt;
&lt;li&gt;make the phone layout switch cleanly between writing and previewing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most valuable design work was not adding visual decoration. It was removing moments of uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did not build
&lt;/h2&gt;

&lt;p&gt;Fast shipping only works when “not now” is a real answer.&lt;/p&gt;

&lt;p&gt;I left out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accounts;&lt;/li&gt;
&lt;li&gt;cloud document storage;&lt;/li&gt;
&lt;li&gt;cloud template synchronization;&lt;/li&gt;
&lt;li&gt;payments and subscription tiers;&lt;/li&gt;
&lt;li&gt;team workspaces;&lt;/li&gt;
&lt;li&gt;an opaque “AI cleanup” button;&lt;/li&gt;
&lt;li&gt;a promise of perfect conversion for every source file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These features may make sense later. In the first release, they would have expanded the security surface, privacy explanation, support burden, and number of states that needed testing.&lt;/p&gt;

&lt;p&gt;The free, accountless product is not a temporary gap in front of the real product. It is the simplest complete version of the promise: open a tool, convert a document, inspect the result, and leave with a usable file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;p&gt;If I started again, I would make three changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Write the acceptance tests before the interface
&lt;/h3&gt;

&lt;p&gt;“Supports Markdown tables” is vague.&lt;/p&gt;

&lt;p&gt;“A table with alignment markers, inline emphasis, and a long cell remains readable in preview and export” is a test.&lt;/p&gt;

&lt;p&gt;Starting with the second sentence would have prevented several rounds of polishing the wrong thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build difficult sample files on day one
&lt;/h3&gt;

&lt;p&gt;A friendly demo document gives false confidence. Every converter needs a representative file and an adversarial one.&lt;/p&gt;

&lt;p&gt;For MDFold, that means wide tables, long code lines, nested lists, broken image URLs, non-Latin text, scanned PDFs, multi-column PDFs, and Word files with inconsistent styles.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Treat explanations as part of the interface
&lt;/h3&gt;

&lt;p&gt;Users need to know why a scanned PDF produces little text, why PDF background colors can depend on a print setting, and why local templates do not appear on another device.&lt;/p&gt;

&lt;p&gt;A clear limitation at the right moment prevents more frustration than another generic feature badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three principles I am keeping
&lt;/h2&gt;

&lt;p&gt;After building and revising MDFold, these are the principles I would carry into any small developer tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every format is a separate promise.&lt;/strong&gt; Define what a successful output means before implementing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy must be visible in the data flow.&lt;/strong&gt; “We care about privacy” is not a substitute for knowing where the file goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The exported artifact is the product.&lt;/strong&gt; A beautiful preview does not rescue a broken download.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first version took three days. Making it trustworthy is an ongoing process of finding vague promises and replacing them with specific behavior and repeatable checks.&lt;/p&gt;

&lt;p&gt;You can try &lt;a href="https://mdfold.com/" rel="noopener noreferrer"&gt;MDFold&lt;/a&gt; without creating an account.&lt;/p&gt;

&lt;p&gt;If you test it, I would especially value one kind of feedback: &lt;strong&gt;what file or Markdown construct produced a result you did not expect?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is more useful than “looks great,” and it gives me the next real test case.`&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
