defmodule Pdf.Component.Blockquote do
@moduledoc """
Blockquote component for PDF documents.
Renders an indented text block with a colored left bar,
optional background, and citation line.
## Examples
doc |> Pdf.Component.Blockquote.render({50, 700}, %{width: 400},
"The best way to predict the future is to invent it.")
doc |> Pdf.Component.Blockquote.render({50, 700}, %{
width: 400,
bar_color: {0.2, 0.5, 0.8},
cite: "— Alan Kay"
}, "The best way to predict the future is to invent it.")
"""
@default_bar_color {0.75, 0.75, 0.75}
@default_bar_width 3
@default_padding 12
@default_font "Helvetica"
@default_font_size 10
@default_color {0.25, 0.25, 0.25}
@default_cite_color {0.5, 0.5, 0.5}
@default_bg nil
@doc """
Render a blockquote at `{x, y}`.
## Style options
- `:width` — total width of the blockquote (required)
- `:bar_color` — left accent bar color (default gray)
- `:bar_width` — bar thickness (default `3`)
- `:background` — optional background color
- `:padding` — inner padding (default `12`)
- `:font` — font name (default `"Helvetica"`)
- `:font_size` — text size (default `10`)
- `:color` — text color (default dark gray)
- `:italic` — render text in italic (default `true`)
- `:cite` — optional citation line below the quote
- `:cite_color` — citation text color (default lighter gray)
"""
def render(doc, {x, y}, style \\ %{}, text) do
width = Map.get(style, :width, 400)
bar_color = Map.get(style, :bar_color, @default_bar_color)
bar_width = Map.get(style, :bar_width, @default_bar_width)
bg = Map.get(style, :background, @default_bg)
padding = Map.get(style, :padding, @default_padding)
font = Map.get(style, :font, @default_font)
font_size = Map.get(style, :font_size, @default_font_size)
color = Map.get(style, :color, @default_color)
italic = Map.get(style, :italic, true)
cite = Map.get(style, :cite)
cite_color = Map.get(style, :cite_color, @default_cite_color)
line_height = font_size * 1.5
text_x = x + bar_width + padding
# Estimate height: wrap text manually by character count
avail_w = width - bar_width - padding * 2
chars_per_line = max(trunc(avail_w / (font_size * 0.52)), 10)
lines = wrap_text(text, chars_per_line)
text_h = length(lines) * line_height
cite_h = if cite, do: line_height + 4, else: 0
total_h = padding + text_h + cite_h + padding
# Background
doc =
if bg do
doc
|> Pdf.save_state()
|> Pdf.set_fill_color(bg)
|> Pdf.rectangle({x, y - total_h}, {width, total_h})
|> Pdf.fill()
|> Pdf.restore_state()
else
doc
end
# Left bar
doc =
doc
|> Pdf.save_state()
|> Pdf.set_fill_color(bar_color)
|> Pdf.rectangle({x, y - total_h}, {bar_width, total_h})
|> Pdf.fill()
|> Pdf.restore_state()
# Text lines
font_opts = if italic, do: [italic: true], else: []
doc =
doc
|> Pdf.set_font(font, font_size, font_opts)
|> Pdf.set_fill_color(color)
{doc, last_y} =
Enum.reduce(lines, {doc, y - padding - font_size}, fn line, {d, ly} ->
d2 = Pdf.text_at(d, {text_x, ly}, line)
{d2, ly - line_height}
end)
# Citation
doc =
if cite do
doc
|> Pdf.set_font(font, font_size - 1)
|> Pdf.set_fill_color(cite_color)
|> Pdf.text_at({text_x, last_y - 4}, cite)
else
doc
end
doc
end
defp wrap_text(text, chars_per_line) do
words = String.split(text, " ")
{lines, current} =
Enum.reduce(words, {[], ""}, fn word, {lines, current} ->
candidate = if current == "", do: word, else: current <> " " <> word
if String.length(candidate) > chars_per_line and current != "" do
{[current | lines], word}
else
{lines, candidate}
end
end)
Enum.reverse(if current != "", do: [current | lines], else: lines)
end
end