Editing Microsoft Word documents with Elixir

Hello Everyone,

I am trying to modify or edit a Microsoft Word Document with Elixir.

The idea is to have a Word Document as a Template, open it with Elixir and create a new document based on information I have from my database.

Can you please suggest a library?

Thanks in advance for your help,

Best regards,

1 Like

I did a review a short while ago and came up short with Elixir libraries for Word, at least actively maintained. With Microsoft’s OOXML, I really think you want actively maintained.

Better luck in Erlang land perhaps?

Luckily there are plenty of options outside of the BEAM ecosystem at least.

At the end of the day, Word files are zipped XML-packages ready to be explored.

1 Like

I’m currently working on a project that involves uploading .docx files and generating fillable PDFs. Initially, I tried converting the .docx files into HTML or .heex files to create PDFs, but that approach proved cumbersome. Instead, I decided to use the .docx files directly as templates and then convert them into PDFs.

For the templating part using .docx files, here’s my solution: I unzip the Word file, replace the placeholders in the word/document.xml file, and then generate a new .docx file. It’s a straightforward method, and it works well enough. One key detail to note is that placeholders must be written as placeholder or PLACEHOLDER. If they’re formatted differently, they might get split across multiple OOXML runs, making them impossible to replace consistently.

defmodule WordPlaceholderUpdater do
  @moduledoc """
  A module to update placeholders in Word (.docx) files.
  Placeholders should be in the format PLACEHOLDERNAME
  outerwise the placeholder text can be in diffrent runs.
  """

  @doc """
  Updates placeholders in a Word document with provided values.

  ## Parameters
    - input_path: Path to the input .docx file
    - output_path: Path where the modified .docx will be saved
    - replacements: Map of placeholder names to their replacement values
  """
  def update_placeholders(input_path, output_path, replacements) do
    temp_dir = "/tmp/word_update_#{:erlang.unique_integer()}"

    case File.mkdir(temp_dir) do
      {:error, reason} ->
        {:error, "Failed to create temp directory: #{reason}"}

      :ok ->
        result =
          with :ok <- extract_docx(input_path, temp_dir),
               :ok <- update_document_xml(temp_dir, replacements),
               :ok <- create_new_docx(temp_dir, output_path) do
            :ok
          end

        # Cleanup happens regardless of success or failure
        cleanup(temp_dir)

        case result do
          :ok -> :ok
          {:error, reason} -> {:error, reason}
        end
    end
  end

  defp extract_docx(input_path, temp_dir) do
    case :zip.extract(String.to_charlist(input_path), [{:cwd, String.to_charlist(temp_dir)}]) do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, "Failed to extract docx: #{reason}"}
    end
  end

  defp update_document_xml(temp_dir, replacements) do
    doc_path = Path.join(temp_dir, "word/document.xml")

    case File.read(doc_path) do
      {:ok, content} ->
        updated_content = replace_placeholders(content, replacements)
        File.write("test_out.xml", updated_content)
        File.write(doc_path, updated_content)

      {:error, reason} ->
        {:error, "Failed to read document.xml: #{reason}"}
    end
  end

  defp replace_placeholders(content, replacements) do
    Enum.reduce(replacements, content, fn {key, value}, acc ->
      Regex.replace(~r/#{key}/, acc, value)
    end)
  end

  defp create_new_docx(temp_dir, output_path) do
    file_list =
      temp_dir
      |> File.ls!()
      |> Enum.map(&String.to_charlist/1)

    case :zip.create(String.to_charlist(output_path), file_list, [
           {:cwd, String.to_charlist(temp_dir)}
         ]) do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, "Failed to create new docx: #{reason}"}
    end
  end

  defp cleanup(temp_dir) do
    File.rm_rf(temp_dir)
  end
end

5 Likes