Structured document model

Copy Markdown View Source

ExAnydoc.to_document/2 exposes anydoc's information-preserving representation when Markdown alone is not enough. It retains blocks, inline styles, notes, and embedded assets.

bytes = File.read!("report.docx")

{:ok, %ExAnydoc.Document{} = document} =
  ExAnydoc.to_document(bytes)

Traverse text

Blocks are recursive tagged tuples. A basic traversal can collect paragraph and heading text like this:

defmodule DocumentText do
  def from_blocks(blocks), do: Enum.flat_map(blocks, &from_block/1)

  defp from_block({:heading, %{content: content}}), do: from_inlines(content)
  defp from_block({:paragraph, content}), do: from_inlines(content)
  defp from_block({:block_quote, blocks}), do: from_blocks(blocks)
  defp from_block(_other), do: []

  defp from_inlines(inlines) do
    Enum.flat_map(inlines, fn
      {:text, %{text: text}} -> [text]
      {:link, %{content: content}} -> from_inlines(content)
      :line_break -> ["\n"]
      _other -> []
    end)
  end
end

Lists, table cells, block quotes, and notes can themselves contain blocks. A production traversal should recurse into the variants it needs. The complete shape of every variant is documented in ExAnydoc.Document types.

Tables

A table contains a rectangular grid. An {:origin, cell} slot owns the cell content and span. A {:covered, coordinates} slot points back to the origin of a merged cell. header_rows identifies the leading header rows and kind is either :data or :layout.

Assets

An image can reference {:asset, id}. Find the matching entry in document.assets to access its media type and original bytes:

asset = Enum.find(document.assets, &(&1.id == asset_id))
File.write!("image.bin", asset.bytes)

Asset bytes are held in BEAM binaries. Account for their size when processing large or untrusted documents and do not persist them without validating their media type and contents.