Building a DOCX to Markdown Converter with Node.js
Tags: markdown, converter, node, tutorial
Published: 2024-06-30 00:42:56

Welcome to a step-by-step guide on building a powerful DOCX to Markdown converter using Node.js. This project is a great way to learn about file manipulation, command-line interfaces, and converting document formats. By the end of this series, you'll have a tool that not only converts DOCX files to Markdown but also extracts images and formats tables. Let's dive in!

## Table of Contents

1. [Introduction](#introduction)
2. [Setting Up the Project](#setting-up-the-project)
3. [Basic DOCX to HTML Conversion](#basic-docx-to-html-conversion)
4. [Converting HTML to Markdown](#converting-html-to-markdown)
5. [Extracting Images](#extracting-images)
6. [Formatting Tables](#formatting-tables)
7. [Conclusion](#conclusion)

## Introduction

Markdown is a lightweight markup language with plain text formatting syntax. It's widely used for documentation due to its simplicity and readability. However, many documents are created in DOCX format, especially in corporate environments. Converting these documents to Markdown can be tedious if done manually. This is where our converter comes in handy.

## Setting Up the Project

First, let's create a new directory for our project and initialize it with `npm`.

```sh
mkdir docx-to-md-converter
cd docx-to-md-converter
npm init -y
```

Next, we'll install the necessary dependencies. We'll use `mammoth` for converting DOCX to HTML, `turndown` for converting HTML to Markdown, `commander` for building the CLI, and `uuid` for unique image names.

```sh
npm install mammoth turndown commander uuid
```

Create a new file named `index.js` in your project directory. This will be the main file for our converter.

```sh
touch index.js
```

## Basic DOCX to HTML Conversion

Let's start by writing a simple script to convert DOCX files to HTML. We'll use the `mammoth` library for this.

Open `index.js` and add the following code:

```javascript
#!/usr/bin/env node

import * as fs from 'fs';
import * as path from 'path';
import * as mammoth from 'mammoth';
import { program } from 'commander';

program
  .version('1.0.0')
  .description('Convert DOCX to HTML')
  .argument('<input>', 'Input DOCX file')
  .argument('[output]', 'Output HTML file (default: same as input with .html extension)')
  .action(async (input, output) => {
    try {
      await convertDocxToHtml(input, output);
    } catch (error) {
      console.error('Error:', error);
      process.exit(1);
    }
  });

program.parse(process.argv);

async function convertDocxToHtml(inputFile, outputFile) {
  if (!outputFile) {
    outputFile = path.join(path.dirname(inputFile), `${path.basename(inputFile, '.docx')}.html`);
  }

  const result = await mammoth.convertToHtml({ path: inputFile });
  await fs.promises.writeFile(outputFile, result.value);
  console.log(`Conversion complete. Output saved to ${outputFile}`);
}
```

This script uses `commander` to parse command-line arguments, `mammoth` to convert DOCX to HTML, and `fs` to write the output to a file. To make this script executable, add the following line at the top of `index.js`:

```sh
#!/usr/bin/env node
```

Make sure the script has execute permissions:

```sh
chmod +x index.js
```

Now you can run the script to convert a DOCX file to HTML:

```sh
node index.js example.docx example.html
```

## Converting HTML to Markdown

Next, we'll add the functionality to convert HTML to Markdown using `turndown`.

First, install `turndown`:

```sh
npm install turndown
```

Update `index.js` to include the HTML to Markdown conversion:

```javascript
#!/usr/bin/env node

import * as fs from 'fs';
import * as path from 'path';
import * as mammoth from 'mammoth';
import TurndownService from 'turndown';
import { program } from 'commander';

program
  .version('1.0.0')
  .description('Convert DOCX to Markdown')
  .argument('<input>', 'Input DOCX file')
  .argument('[output]', 'Output Markdown file (default: same as input with .md extension)')
  .action(async (input, output) => {
    try {
      await convertDocxToMarkdown(input, output);
    } catch (error) {
      console.error('Error:', error);
      process.exit(1);
    }
  });

program.parse(process.argv);

async function convertDocxToMarkdown(inputFile, outputFile) {
  if (!outputFile) {
    outputFile = path.join(path.dirname(inputFile), `${path.basename(inputFile, '.docx')}.md`);
  }

  const result = await mammoth.convertToHtml({ path: inputFile });
  const turndownService = new TurndownService();
  const markdown = turndownService.turndown(result.value);

  await fs.promises.writeFile(outputFile, markdown);
  console.log(`Conversion complete. Output saved to ${outputFile}`);
}
```

Now you can convert DOCX files to Markdown:

```sh
node index.js example.docx example.md
```

## Extracting Images

DOCX files often contain images that we need to handle. We'll extract these images and save them to a folder, updating the image links in the Markdown file.

Update `index.js` to include image extraction:

```javascript
#!/usr/bin/env node

import * as fs from 'fs';
import * as path from 'path';
import * as mammoth from 'mammoth';
import TurndownService from 'turndown';
import { program } from 'commander';
import { v4 as uuidv4 } from 'uuid';

program
  .version('1.0.0')
  .description('Convert DOCX to Markdown with image extraction')
  .argument('<input>', 'Input DOCX file')
  .argument('[output]', 'Output Markdown file (default: same as input with .md extension)')
  .action(async (input, output) => {
    try {
      await convertDocxToMarkdown(input, output);
    } catch (error) {
      console.error('Error:', error);
      process.exit(1);
    }
  });

program.parse(process.argv);

async function convertDocxToMarkdown(inputFile, outputFile) {
  if (!outputFile) {
    outputFile = path.join(path.dirname(inputFile), `${path.basename(inputFile, '.docx')}.md`);
  }

  const imageDir = path.join(path.dirname(outputFile), 'images');
  if (!fs.existsSync(imageDir)) {
    fs.mkdirSync(imageDir, { recursive: true });
  }

  const result = await mammoth.convertToHtml({ path: inputFile }, {
    convertImage: mammoth.images.imgElement(async (image) => {
      const buffer = await image.read();
      const extension = image.contentType.split('/')[1];
      const imageName = `image-${uuidv4()}.${extension}`;
      const imagePath = path.join(imageDir, imageName);
      await fs.promises.writeFile(imagePath, buffer);
      return { src: `images/${imageName}` };
    })
  });

  const turndownService = new TurndownService();
  const markdown = turndownService.turndown(result.value);

  await fs.promises.writeFile(outputFile, markdown);
  console.log(`Conversion complete. Output saved to ${outputFile}`);
}
```

Now, images will be extracted and saved in an `images` folder, and the Markdown file will contain the correct links to these images.

## Formatting Tables

The final feature we'll add is table formatting. DOCX files often contain tables that need to be correctly formatted in Markdown.

Update `index.js` to include table formatting:

```javascript
#!/usr/bin/env node

import * as fs from 'fs';
import * as path from 'path';
import * as mammoth from 'mammoth';
import TurndownService from 'turndown';
import { program } from 'commander';
import { v4 as uuidv4 } from 'uuid';

program
  .version('1.0.0')
  .description('Convert DOCX to Markdown with image extraction and table formatting')
  .argument('<input>', 'Input DOCX file')
  .argument('[output]', 'Output Markdown file (default: same as input with .md extension)')
  .action(async (input, output) => {
    try {
      await convertDocxToMarkdown(input, output);
    } catch (error) {
      console.error('Error:', error);
      process.exit(1);
    }
  });

program.parse(process.argv);

function createMarkdownTable(table) {
  const rows = Array.from(table.rows);
  if (rows.length === 0) return '';

  const headers = Array.from(rows[0].cells).map(cell => cell.textContent?.trim() || '');
  const markdownRows = rows.slice(1).map(row => 
    Array.from(row.cells).map(cell => cell.textContent?.trim() || '')
  );

  let markdown = '| ' + headers.join(' | ') + ' |\n';
  markdown += '| ' + headers.map(() => '---').join(' | ') + ' |\n';
  markdownRows.forEach(row => {
    markdown += '| ' + row.join(' | ') + ' |\n

';
  });

  return markdown;
}

async function convertDocxToMarkdown(inputFile, outputFile) {
  if (!outputFile) {
    outputFile = path.join(path.dirname(inputFile), `${path.basename(inputFile, '.docx')}.md`);
  }

  const imageDir = path.join(path.dirname(outputFile), 'images');
  if (!fs.existsSync(imageDir)) {
    fs.mkdirSync(imageDir, { recursive: true });
  }

  const result = await mammoth.convertToHtml({ path: inputFile }, {
    convertImage: mammoth.images.imgElement(async (image) => {
      const buffer = await image.read();
      const extension = image.contentType.split('/')[1];
      const imageName = `image-${uuidv4()}.${extension}`;
      const imagePath = path.join(imageDir, imageName);
      await fs.promises.writeFile(imagePath, buffer);
      return { src: `images/${imageName}` };
    })
  });

  let html = result.value;

  const turndownService = new TurndownService();

  turndownService.addRule('table', {
    filter: 'table',
    replacement: function(content, node) {
      return '\n\n' + createMarkdownTable(node) + '\n\n';
    }
  });

  const markdown = turndownService.turndown(html);

  await fs.promises.writeFile(outputFile, markdown);
  console.log(`Conversion complete. Output saved to ${outputFile}`);
}
```

## Conclusion

In this blog post, we built a DOCX to Markdown converter step by step, adding features like image extraction and table formatting. This tool demonstrates the power and flexibility of Node.js for handling file manipulations and conversions. 

The source code for this project is available on [GitHub](https://github.com/f5serge/docx-to-md-converter), where you can find the latest updates, contribute to the project, and explore further enhancements.

Thank you for following along with this guide. Happy coding!