defmodule Pdf.Component.TOC do
@moduledoc """
Table of Contents component for PDF documents.
Renders a list of entries with titles, optional dot leaders,
and right-aligned page numbers.
## Examples
doc |> Pdf.Component.TOC.render({50, 700}, %{width: 450}, [
%{title: "Introduction", page: 1},
%{title: "Getting Started", page: 3, level: 1},
%{title: "Installation", page: 3, level: 2},
%{title: "Configuration", page: 5, level: 2},
%{title: "Advanced Usage", page: 10, level: 1}
])
"""
@default_font "Helvetica"
@default_font_size 10
@default_color {0.1, 0.1, 0.1}
@default_page_color {0.4, 0.4, 0.4}
@default_dot_color {0.7, 0.7, 0.7}
@default_line_height 18
@default_indent 20
@doc """
Render a table of contents at `{x, y}`.
## Style options
- `:width` — total width (default `450`)
- `:font` — font name (default `"Helvetica"`)
- `:font_size` — base text size (default `10`)
- `:color` — title text color
- `:page_color` — page number color
- `:dot_color` — dot leader color
- `:line_height` — row spacing (default `18`)
- `:indent` — indentation per level (default `20`)
- `:dots` — show dot leaders (default `true`)
## Entries format
List of maps: `%{title: "Section", page: 1, level: 1}`
Level defaults to `1`. Level `0` renders bold (chapter heading).
"""
def render(doc, {x, y}, style \\ %{}, entries) do
width = Map.get(style, :width, 450)
font = Map.get(style, :font, @default_font)
font_size = Map.get(style, :font_size, @default_font_size)
color = Map.get(style, :color, @default_color)
page_color = Map.get(style, :page_color, @default_page_color)
dot_color = Map.get(style, :dot_color, @default_dot_color)
line_height = Map.get(style, :line_height, @default_line_height)
indent = Map.get(style, :indent, @default_indent)
dots = Map.get(style, :dots, true)
entries
|> Enum.with_index()
|> Enum.reduce(doc, fn {entry, i}, d ->
level = Map.get(entry, :level, 1)
title = Map.get(entry, :title, "")
page = Map.get(entry, :page, "")
page_str = "#{page}"
ey = y - i * line_height
entry_x = x + (level - 0) * indent
is_heading = level == 0
fs = if is_heading, do: font_size + 1, else: font_size
font_opts = if is_heading, do: [bold: true], else: []
# Title
d =
d
|> Pdf.set_font(font, fs, font_opts)
|> Pdf.set_fill_color(color)
|> Pdf.text_at({entry_x, ey}, title)
# Page number (right-aligned)
page_w = String.length(page_str) * fs * 0.55
page_x = x + width - page_w
d =
d
|> Pdf.set_font(font, fs)
|> Pdf.set_fill_color(page_color)
|> Pdf.text_at({page_x, ey}, page_str)
# Dot leaders
if dots and not is_heading do
title_w = String.length(title) * fs * 0.52
dot_start = entry_x + title_w + 4
dot_end = page_x - 4
dot_spacing = 4
if dot_end > dot_start do
dots_count = trunc((dot_end - dot_start) / dot_spacing)
dot_text = String.duplicate(". ", dots_count)
d
|> Pdf.set_font(font, fs - 2)
|> Pdf.set_fill_color(dot_color)
|> Pdf.text_at({dot_start, ey}, dot_text)
else
d
end
else
d
end
end)
end
end