Convert HTML to RTF and Markdown with JavaScript in React

HTML is the universal way to describe a web page, but its downstream consumers are often not browsers: when content is delivered to traditional office systems, RTF offers the best compatibility; when content enters a documentation site or a code repository, Markdown is the maintainable form. The two put different demands on the source file—RTF has to keep fonts, colors and table layout, while Markdown keeps only structural information such as heading levels, lists, tables, links and images.

This article uses Spire.Doc for JavaScript to cover two features:

For installation and project setup, refer to Integrating Spire.Doc for JavaScript in a React Project. The examples below assume that Spire.Doc is installed and the WebAssembly module is initialized.


Convert HTML to RTF

RTF describes fonts, paragraphs and tables with plain-text markup, and almost every word processor can open it. Once HTML has been converted to RTF, the content can still be edited on systems without Office components installed, and it can serve as a rich-text source for an email body.

The source file in this section is a release notes page, Report.html, which contains a dark title bar, KPI cards, a version comparison table, a callout box and a chart, covering the common web layouts. After conversion, the title bar background, card shading, table borders and text colors are all written into the RTF.

The specific steps are as follows:

  1. Get the Spire.Doc WASM module and confirm that it is ready.
  2. Load the font files, Report.html and the chart file into the virtual file system (VFS).
  3. Load the document as HTML with lenient validation.
  4. Save the document to the VFS with FileFormat.Rtf.
  5. Read the RTF bytes from the VFS and trigger a browser download.

The code for converting HTML to RTF is as follows:

import React from 'react';

function App() {
  const htmlToRtf = async () => {
    // Get the Spire.Doc WASM module
    const docModule = window.wasmModule?.spiredoc;

    // Check if the module is ready
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the fonts and the source files
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('arialbd.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('Report.html', '', `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS('report-chart.png', '', `${process.env.PUBLIC_URL}/data/`);

    // Load the document as HTML
    const doc = new docModule.Document();
    doc.LoadFromFile({
      fileName: 'Report.html',
      fileFormat: docModule.FileFormat.Html,
      validationType: docModule.XHTMLValidationType.None,
    });
    doc.HtmlBaseUrl = '/';

    // Save in RTF format
    const outputFileName = 'Report.rtf';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Rtf });

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/rtf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);

    doc.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>HTML to RTF</h1>
      <button onClick={htmlToRtf}>Generate</button>
    </div>
  );
}

export default App;

Opened in Word, the generated RTF keeps the title bar, the cards and the table backgrounds:

Convert HTML to RTF: the title bar, card shading and table backgrounds are kept in Word

Which Styles Survive and Which Are Lost

Inline styles and stylesheets in the HTML map to character and paragraph attributes in RTF, but not every CSS rule has an equivalent:

Style category Conversion result
Font, font size, text color Kept
Bold, italic, underline, strikethrough Kept
Paragraph alignment, indentation, line spacing Kept
Table borders, cell shading, column widths Kept
Images and hyperlinks Kept; images are embedded as data
flex, grid, absolute positioning, floats Not kept; content is reflowed in document order

Multi-column pages that depend on layout usually become a single column in RTF; decorative elements on the page (shadows, rounded corners, gradients) degrade to solid colors or are dropped.

Images and File Size

Images are written into the RTF as hexadecimal data, which increases the file size; the same image is also attached as a metafile copy for compatibility with older readers. In the sample, a PNG chart of about 48 KB pushes the RTF to about 2 MB. Relative paths in the source are resolved against the directory given by HtmlBaseUrl; images that are cross-origin and unreachable are skipped, leaving a gap in the output. When there are many images, compress or crop them in the HTML before converting.


Convert HTML to Markdown

Markdown suits long-term maintenance: plain text can be diffed, reviewed and hosted in a version repository. Converting HTML to Markdown restores the content from its rendered result to a structural description, and heading levels, lists, tables, links and images are the main things kept.

The source file in this section is an upgrade guide page, Guide.html, made up of headings, paragraphs, ordered and unordered lists, a validation table, a flow image and document links. Unlike the RTF sample, this source does not use layout tables, so the exported Markdown has a more regular structure.

The specific steps are as follows:

  1. Get the WASM module, then load the fonts, Guide.html and the image into the VFS.
  2. Load the document as HTML.
  3. Set MarkdownExportOptions.ImagesFolder to choose the directory the images are written to.
  4. Save the document with FileFormat.Markdown.
  5. Read the Markdown text from the VFS and trigger a browser download.

The code for converting HTML to Markdown is as follows:

import React from 'react';

function App() {
  const htmlToMarkdown = async () => {
    // Get the Spire.Doc WASM module
    const docModule = window.wasmModule?.spiredoc;

    // Check if the module is ready
    if (!docModule) {
      alert('Spire.Doc is not ready yet');
      return;
    }

    // Load the fonts and the source files
    await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('arialbd.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
    await window.spire.FetchFileToVFS('Guide.html', '', `${process.env.PUBLIC_URL}/data/`);
    await window.spire.FetchFileToVFS('guide-flow.png', '', `${process.env.PUBLIC_URL}/data/`);

    // Load the document as HTML
    const doc = new docModule.Document();
    doc.LoadFromFile({
      fileName: 'Guide.html',
      fileFormat: docModule.FileFormat.Html,
      validationType: docModule.XHTMLValidationType.None,
    });
    doc.HtmlBaseUrl = '/';

    // Write images into the images directory in the VFS; the Markdown references them by relative path
    doc.MarkdownExportOptions.ImagesFolder = 'images';

    // Save in Markdown format
    const outputFileName = 'Guide.md';
    doc.SaveToFile({ fileName: outputFileName, fileFormat: docModule.FileFormat.Markdown });

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'text/markdown' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);

    doc.Dispose();
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>HTML to Markdown</h1>
      <button onClick={htmlToMarkdown}>Generate</button>
    </div>
  );
}

export default App;

The generated Markdown is shown below in a reader that supports tables:

Convert HTML to Markdown: the exported Markdown rendered in a reader that supports tables

Image Paths and Encoding

MarkdownExportOptions offers two ways to handle images: write them as separate files, or encode them into the Markdown itself. The differences are as follows:

Setting Reference in the Markdown When to use
Not set ![](/Guide_001.png), with the image written to the VFS root Package the result right after export
ImagesFolder = 'images' ![](images/Guide_001.png), with the image written to the VFS images directory Commit to a code repository or a documentation site
ImagesAsBase64 = true ![](data:image/png;base64,...), with no separate image file Single-file distribution, copy and paste

In directory mode, images are named after the output file plus a sequence number (outputname_001.png) and stay in the VFS alongside the Markdown; when downloading, take the Markdown and the images directory together:

// Read the image from the export directory and package it with the Markdown for download
const imageArray = window.dotnetRuntime.Module.FS.readFile('images/Guide_001.png');

ImagesAsBase64 costs about a third more file size, so it is best avoided when there are many images. If directory mode is used but only the Markdown file is downloaded, the reader shows a missing-image placeholder.

What Cannot Be Expressed in Markdown

Markdown's syntax coverage is limited, so some HTML semantics keep only the text after conversion, not the form:

HTML element Markdown output
Headings h1–h6 #–######; bold on the original heading is kept as **text**
Ordered and unordered lists List items starting with 1. and -
Tables Pipe tables, with header cells bold
blockquote A plain paragraph, without the > marker
Code blocks and inline code Plain text, without fences or backticks
Colors, background colors, alignment Not emitted
iframe, scripts, complex merged cells Ignored or simplified during conversion

Body-style content (articles, manuals, help pages) converts best; interactive pages (dashboards, forms) are poor candidates.

Choosing Between the Two Directions

Goal Format Reason
Deliver to office software for further editing RTF Keeps character and paragraph styles and works with legacy systems
Publish to a documentation site or code repository Markdown Plain text, diffable, hostable

Both conversions use the same Document as the intermediate model and share identical loading logic; only the save format and the export options differ. For batch processing, load once, export RTF first, then Markdown, and dispose of the instance at the end.


FAQ

The font files cannot be found when converting HTML

Cause: Font information in the HTML is parsed at load time; when the VFS holds no font file at all, LoadFromFile throws Cannot find any fonts in specified font sources.

Solution: Before loading, copy the project's font file into the VFS /Library/Fonts/ directory:

await window.spire.FetchFileToVFS('arial.ttf', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);

The font file has to be supplied by you and placed in the project's public/font/ directory; the page does not necessarily ship a file with that name.

Why do exported Markdown headings contain **

Cause: h1–h6 in HTML display in bold by default, so the converted heading paragraph carries bold character formatting, and Markdown export writes it as **text**, producing ## **Heading**.

Solution: This is normal format preservation. If headings should not be bold, strip ** from the heading lines after export, or set font-weight: normal on the headings in the source HTML before converting.

Where are the image files referenced by the Markdown

Images are written to the VFS root by default, named after the output file name and a sequence number, for example Guide_001.png. With ImagesFolder set, images go into the specified subdirectory and the references in the Markdown become relative paths. Either way, the images have to be read from the VFS and delivered together with the Markdown.

Why is the exported RTF much larger than the original HTML

RTF stores images as hexadecimal text, so the same image takes about twice the bytes of the original file; in addition, a metafile copy is attached for compatibility with older readers. In the sample, a chart of about 48 KB pushes the RTF to about 2 MB. The more images there are and the higher their resolution, the larger the growth.

Why are colors and backgrounds lost in the Markdown

Markdown is structural plain text and has no syntax for colors or shading. Conversion does not emit font colors, cell shading or alignment; it keeps only structure such as headings, lists, tables, links and images. When that layout has to be preserved, use RTF or PDF as the output format instead.


Get a Free License

Spire.Doc for JavaScript offers a 30-day full-featured free trial license with no functional limitations. Apply here to evaluate before purchasing.