Extended Converter Use Cases
This page presents common use cases that can be accomplished by extending and customizing the converter. In Create an Extended Converter, we were just biting around the edges of what you can do with an extended converter. This page gets into more realistic use cases. Each section introduces a different use case and presents the code for an extended converter you can use as a starting point.
The extended converter can access predefined or custom theme keys via the theme accessor.
The segments in a key are always separated by an underscore character (e.g., theme.title_page_font_color ).
Consulting the value of theme keys allows the extra behavior provided by the extended converter to be styled using the theme.
|
Custom thematic break
One of the simplest ways to extend the converter is to make a thematic break.
For this case, we’ll override the convert handler method for a thematic break, which is convert_thematic_break
.
The thematic break only consists of line graphics, no text.
That means we can make use of graphics fill and stroke methods provided by Asciidoctor PDF or Prawn.
class PDFConverterCustomThematicBreak < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def convert_thematic_break node
theme_margin :thematic_break, :top
stroke_horizontal_rule 'FF0000', line_width: 0.5, line_style: :solid
move_down 1
stroke_horizontal_rule 'FF0000', line_width: 1, line_style: :solid
move_down 1
stroke_horizontal_rule 'FF0000', line_width: 0.5, line_style: :solid
theme_margin :thematic_break, ((block_next = next_enclosed_block node) ? :bottom : :top), block_next || true
end
end
The return value of the convert handler method for a block node is ignored, which is why there’s no clear return value in this override. If this were a convert handler method for an inline node, a return value would be required, which becomes the text to render.
Custom title page
Every title page is as unique as the work itself.
That’s why Asciidoctor PDF gives you the ability to customize the title page by overriding the ink_title_page
method in an extended converter.
The ink_title_page
method is called after the title page has been created and the background applied, so it can focus on writing content.
In this method, you can choose to honor the title-page
settings from the theme, or go your own way.
The one rule is that this method must not start a new page.
Let’s create a custom title page that shows the document title and subtitle between two lines in the top half and a logo in the bottom half.
class PDFConverterCustomTitlePage < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def ink_title_page doc
move_cursor_to page_height * 0.75
theme_font :title_page do
stroke_horizontal_rule '2967B2', line_width: 1.5, line_style: :double
move_down 10
doctitle = doc.doctitle partition: true
theme_font :title_page_title do
ink_prose doctitle.main, align: :center, color: theme.base_font_color, line_height: 1, margin: 0
end
if (subtitle = doctitle.subtitle)
theme_font :title_page_subtitle do
move_down 10
ink_prose subtitle, align: :center, margin: 0
move_down 10
end
end
stroke_horizontal_rule '2967B2', line_width: 1.5, line_style: :double
move_cursor_to page_height * 0.5
convert ::Asciidoctor::Block.new doc, :image,
content_model: :empty,
attributes: { 'target' => 'sample-logo.jpg', 'pdfwidth' => '1.5in', 'align' => 'center' },
pinned: true
end
end
end
The methods move_cursor_to
and move_cursor
advance the cursor on the page where the next content will be written.
The method theme_font
applies the font from the specified category in the theme (with hyphens in the category name replaced by underscores).
The method stroke_horizontal_rule
draws a horizontal line using the specified color and line width.
The method ink_prose
is provided by Asciidoctor PDF to make writing text to the page easier.
Finally, the method convert
will convert and render the Asciidoctor node that is passed to it, in this case a block image.
Custom part title
A common need is to add extra styling to the title page for a part in a multi-part book.
Since this is a specialized section element, there’s a dedicated method named ink_part_title
that you can override.
The converter already allocates a dedicated page for the part title (so there’s no need to worry about doing that).
The extended converter can override the method that inks the part title to add extra decoration or content to that page.
Let’s customize the part title page by making the background orange, making the font white, aligning the title to the right, adding a line below it, and switching off the running content.
class PDFConverterCustomPartTitle < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def ink_part_title node, title, opts = {}
fill_absolute_bounds 'E64C3D'
move_down cursor * 0.25
indent bounds.width * 0.5 do
ink_prose title, line_height: 1.3, color: 'FFFFFF', inline_format: true, align: :right, size: 42, margin: 0
end
indent bounds.width * 0.33 do
move_down 12
stroke_horizontal_rule 'FFFFFF', line_width: 3
end
page.imported
end
end
The method ink_prose
is provided by Asciidoctor PDF to make writing text to the page easier.
If you wanted, you could just use the low-level text
method provided by Prawn.
It’s also possible to override the start_new_part method if all you want to do is called page.imported to turn off the running content.
|
To find all the available methods to override, consult the API docs.
Custom chapter title
A similar need is to add extra styling to the title of a chapter, or to place it on a page by itself. The extended converter can override the method that inks the chapter title to add extra decoration or content to that page, then insert a page break afterwards.
class PDFConverterCustomChapterTitle < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def ink_chapter_title node, title, opts = {}
move_down cursor * 0.25
ink_heading title, (opts.merge align: :center, text_transform: :uppercase)
stroke_horizontal_rule 'DDDDDD', line_width: 2
move_down theme.block_margin_bottom
theme_font :base do
layout_prose 'Custom text here, maybe a chapter preamble.'
end
start_new_page
end
end
It’s also possible to override the start_new_chapter method if all you want to do is called page.imported to turn off the running content.
|
Chapter image
As another way to customize the chapter title, you may want to add an image above the chapter title if specified. Once again, the extended converter can override the method that inks the chapter title and use it as an opportunity to insert an image.
class PDFConverterChapterImage < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def ink_chapter_title sect, title, opts
if (image_path = sect.attr 'image')
image_attrs = { 'target' => image_path, 'pdfwidth' => '1in' }
image_block = ::Asciidoctor::Block.new sect.document, :image, content_model: :empty, attributes: image_attrs
convert_image image_block, relative_to_imagesdir: true, pinned: true
end
super
end
end
The path to the image is controlled using the image
block attribute on the chapter.
[image=gears.png]
== Chapter Title
Per chapter TOC
In addition to (or instead of) a TOC for the whole book, you may want to insert a TOC per chapter immediately following the chapter title.
Inserting a TOC into the PDF is a two-step process.
First, you need to allocate the space for the chapter TOC using the allocate_toc
method.
Then, you need to come back and ink the TOC after the chapter has been rendered using the ink_toc
method.
class PDFConverterChapterTOC < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def convert_section sect, opts = {}
result = super
if (toc_extent = sect.attr 'pdf-toc-extent')
levels = (sect.document.attr 'chapter-toclevels', 1).to_i + 1
page_numbering_offset = @index.start_page_number - 1
float do
ink_toc sect, levels, toc_extent.from.page, toc_extent.from.cursor, page_numbering_offset
end
end
result
end
def ink_chapter_title sect, title, opts
super
if ((doc = sect.document).attr? 'chapter-toc') && (levels = (doc.attr 'chapter-toclevels', 1).to_i + 1) > 1
theme_font :base do
sect.set_attr 'pdf-toc-extent', (allocate_toc sect, levels, cursor, false)
end
end
end
end
The chapter TOC can is activated by setting the chapter-toc
attribute and the depth of the TOC is controlled using the chapter-toclevels
attribute.
For example:
= Book Title
:chapter-toc:
:chapter-toclevels: 2
License page
Let’s so you want to insert a license page into your documents, but you don’t want to have to put a block macro for it in the document source. You can use an extended converter to add new pages to the body of the document.
Let’s consider the case of reading the license text from a file and inserting it into the first page of the body.
class PDFConverterLicensePage < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def traverse node
return super unless node.context == :document
start_new_page unless at_page_top?
theme_font :heading, level: 2 do
ink_heading 'License', level: 2
end
license_text = File.read 'LICENSE'
theme_font :code do
ink_prose license_text, normalize: false, align: :left, color: theme.base_font_color
end
start_new_page
super
end
end
The method start_new_page
will create a new page in the document.
The ink_prose
method provides a normalize
option.
When this option is false, it will preserve the newlines in the content, which is what we want in the case of license text.
You may want to take this a bit further and allow the location of the license file to be configurable.
Paragraph numbering
To help with content auditing or coorelation, you may want to add a number in front of each paragraph.
You can do this first by assigning a number to each paragraph in the document in the init_pdf
method.
Then, you can add this number in the left margin at the start of each paragraph by overriding the convert_paragraph
method.
class PDFConverterNumberedParagraphs < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def init_pdf doc
doc
.find_by(context: :paragraph) {|candidate| [:document, :section].include? candidate.parent.context }
.each_with_index {|paragraph, idx| paragraph.set_attr 'number', idx + 1 }
super
end
def convert_paragraph node
if (paragraph_number = node.attr 'number')
float do
label = %(#{paragraph_number}.#{::Prawn::Text::NBSP})
label_width = rendered_width_of_string label
bounding_box [-label_width, cursor], width: label_width do
ink_prose label, color: 'CCCCCC', align: :right, margin: 0, single_line: true
end
end
end
super
end
end
Change bars
If you have a preprocessor that adds change metadata to the content, you can use an extended converter to draw change bars to add a visual indicator in the rendered output.
class PDFConverterChangeBars < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def convert_paragraph node
start_cursor = cursor
super
if node.role? 'changed'
float do
bounding_box [bounds.left - 4, start_cursor], width: 2, height: (start_cursor - cursor) do
fill_bounds 'FF0000'
end
end
end
end
end
This converter will look for paragraphs like this one:
[.changed]
This line has been changed.
Avoid break after heading
This functionality is already provided by the converter if you set the breakable
option on section title or discrete heading.
The code is presented here both to explain how it works and to use to make this behavior automatic.
If an in-flow heading is followed by content that doesn’t fit on the current page, and the breakable
option is not set on the heading, the converter will orphan the heading on the current page.
You can fix this behavior by overriding the arrange_heading
method in an extended converter.
This extended converter takes this opportunity to use dry_run
to make an attempt to write content in the remaining space on the page after the heading.
If no content is written, it advances to the next page before inking the heading (and its corresponding anchor).
class PDFConverterAvoidBreakAfterSectionTitle < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def arrange_heading node, title, opts
return if y >= page_height / 3 (1)
orphaned = nil
dry_run single_page: true do (2)
start_page = page
theme_font :heading, level: opts[:level] do
if opts[:part]
ink_part_title node, title, opts (3)
elsif opts[:chapterlike]
ink_chapter_title node, title, opts (3)
else
ink_general_heading node, title, opts (3)
end
end
if page == start_page
page.tare_content_stream
orphaned = stop_if_first_page_empty do (4)
if node.context == :section
traverse node
else # discrete heading
convert (siblings = node.parent.blocks)[(siblings.index node).next]
end
end
end
end
advance_page if orphaned (5)
nil
end
end
1 | An optional optimization to skip this logic if the cursor is above the bottom third of the page. |
2 | Initiate a dry run up to the end of the current page. |
3 | Render the heading as normal. |
4 | Proceed with converting content until the end of the page is reached. Returns true if content is written, false otherwise. |
5 | Start new page before rendering heading if orphaned. |
Additional TOC entries
By default, the table of contents (TOC) only includes section references.
If you want to include additional entries in the TOC, or to filter the sections that are included, you can extend the converter and override the get_entries_for_toc
method.
This method is invoked for each parent entry in the TOC, starting from the document.
class PDFConverterAdditionalTOCEntries < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def get_entries_for_toc node
return super if node.context == :document
node.blocks.select do |candidate|
candidate.context == :section ||
(candidate.id && (candidate.title? || candidate.reftext?))
end
end
end
The depth of the TOC is automatically controlled by the toclevels
attributes.
Once this limit is reached, the converter will not call get_entries_for_toc
for that parent (as none of its children will be included in the TOC).
Narrow TOC
Let’s say you want to make the content on the TOC page(s) really narrow.
You can do so by overridding the ink_toc
method and squeezing the margins by applying extra indentation.
class PDFConverterNarrowTOC < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def ink_toc *_args
indent 100, 100 do
super
end
end
end
Indent block image
If you want all (or some) block images to be indented by an amount specified in the theme, you can override the convert handler method for block images, convert_image
, and call super within an indented context.
class PDFConverterImageIndent < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def convert_image node
if (image_indent = theme.image_indent)
indent(*Array(image_indent)) { super }
else
super
end
end
end
The indent
DSL method adds padding to either side of the content area, delegates to the specified code block, then shaves it back off.
This converter works when a custom theme defines the image-indent
key, as follows:
extends: default
image:
indent: [0.5in, 0]
Wrap code blocks around an image float
Asciidoctor PDF provides basic support for image floats. It will wrap paragraph text on the opposing side of the float. However, if it encounters a non-paragraph, the converter will clear the float and continue positioning content below the image.
As a companion to this basics support, the converter provides a framework for broadening support for float wrapping.
We can take advantage of this framework in an extended converter.
By extending the converter and overriding the supports_float_wrapping?
as well as the convert handler for the block you want to enlist (e.g., convert_code
), you can arrange additional content into the empty space adjacent to the floated image.
In the following example, code (listing and literal) blocks are included in the float wrapping.
class PDFConverterCodeFloatWrapping < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def supports_float_wrapping? node
%i(paragraph listing literal).include? node.context
end
def convert_code node
return super unless (float_box = @float_box ||= nil)
indent(float_box[:left] - bounds.left, bounds.width - float_box[:right]) { super }
@float_box = nil unless page_number == float_box[:page] && cursor > float_box[:bottom]
end
end
You can configure the gap next to and below the image using the image-float-gap
key in the theme.
extends: default
image:
float-gap: [12, 6]
Multiple columns
Asciidoctor PDF does not yet provide multi-column support, where the body of the article is arranged into multiple columns. However, the converter does provide the foundation for supporting a multi-column layout. We can tap into that foundation using an extended converter.
The trick is to intercept the traverse
method and enclose the call in a column box using the column_box
method.
The traverse
method is called to render the body, accepting the document as the sole argument.
Since this method is also called for other blocks, we’ll need to filter out those calls by looking for the :document
context.
class PDFConverterColumns < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'
def traverse node
if node.context == :document &&
(columns = ColumnBox === bounds ? 1 : theme.base_columns || 1) > 1
column_box [bounds.left, cursor],
columns: columns,
width: bounds.width,
reflow_margins: true,
spacer: theme.base_column_gap do
super
end
else
super
end
end
end
You may encounter some quirks when using this extended converter. It’s not yet a perfect solution. For example, it does not handle the index section correctly. You may have to play around with the code to get the desired result. |
You can configure the number of columns and the gap between the columns in the theme file as follows:
extends: default
base:
columns: 2
column-gap: 12
Access page number from inline macro
Although not an extended converter, this use case uses information from the converter in much the same way. In this case, we’re interested in retrieving the page number and inserting it into the content.
Let’s create an inline macro named pagenum
that inserts the current page number into the document when the macro is converted.
Asciidoctor::Extensions.register do
inline_macro :pagenum do
format :short
process do |parent|
create_inline parent, :quoted, parent.document.converter.page_number.to_s
end
end
end
Here’s how this macro would be used.
= Document Title
:doctype: book
You're looking at page number pagenum:[].
Resources
To find even more examples of how to override the behavior of the converter, refer to the extended converter in the InfoQ Mini-Book template.