diff options
| author | Dan Allen <dan.j.allen@gmail.com> | 2022-06-10 15:07:33 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-10 15:07:33 -0600 |
| commit | f4bd057f69b3d72da73bf20e31fca7a2bfedbf3a (patch) | |
| tree | eee20f12c66dad23f8f57c8e5e3b2f97f5cc5951 | |
| parent | 8d25b995f19cd8ada4b692227e59bf7c33dc3868 (diff) | |
resolves #327 arrange body of article or manpage doctype into multiple columns if page-columns key is set in theme (PR #2232)
| -rw-r--r-- | CHANGELOG.adoc | 4 | ||||
| -rw-r--r-- | docs/modules/extend/examples/pdf-converter-columns.rb | 18 | ||||
| -rw-r--r-- | docs/modules/extend/pages/use-cases.adoc | 34 | ||||
| -rw-r--r-- | docs/modules/theme/pages/page.adoc | 23 | ||||
| -rw-r--r-- | lib/asciidoctor/pdf/converter.rb | 69 | ||||
| -rw-r--r-- | spec/arrange_block_spec.rb | 26 | ||||
| -rw-r--r-- | spec/index_spec.rb | 21 | ||||
| -rw-r--r-- | spec/manpage_spec.rb | 27 | ||||
| -rw-r--r-- | spec/page_spec.rb | 171 |
9 files changed, 306 insertions, 87 deletions
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 7a51da10..58da4fb4 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,10 @@ For a detailed view of what has changed, refer to the {url-repo}/commits/main[co Enhancements:: +* arrange body of article or manpage doctype into multiple columns if `page-columns` key is set in theme (#327) +* allow column gap to be specified using `page-column-gap` key (#327) +* introduce `convert_index_categories` method to handle rendering of categories for index inside column box (#327) +* rename `convert_index_list` method to `convert_index_term` to make its purpose more clear (#327) * add `save_theme` helper to work with a copy of the theme within a scope (#2196) * add support for `scale` attribute or `iw` unit on `pdfwidth` attribute on image macros (#1933) * add backlink from bibref on bibliography entry to first reference to that entry in the document (#1737) diff --git a/docs/modules/extend/examples/pdf-converter-columns.rb b/docs/modules/extend/examples/pdf-converter-columns.rb deleted file mode 100644 index b6231bcc..00000000 --- a/docs/modules/extend/examples/pdf-converter-columns.rb +++ /dev/null @@ -1,18 +0,0 @@ -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 diff --git a/docs/modules/extend/pages/use-cases.adoc b/docs/modules/extend/pages/use-cases.adoc index 014a29ff..9daa9f2f 100644 --- a/docs/modules/extend/pages/use-cases.adoc +++ b/docs/modules/extend/pages/use-cases.adoc @@ -270,34 +270,24 @@ image: == 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. +Starting with Asciidoctor PDF 2.1, this converter provides built-in support for multiple columns. +This feature is available when the doctype is article or manpage, but not book. +The columns get applied to the body of the document, which excludes the document title and TOC, if present. -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. +The Asciidoctor PDF converter also provides the framework for making multi-column layouts in an extended converter. +This framework is accessible via the helper method `column_box`. +To make a muti-column layout, you put statements that ink content inside a code block and pass it to the `column_box` method as follows: -.Extended converter that arranges the body into columns [,ruby] ---- -include::example$pdf-converter-columns.rb[] +column_box [bounds.left, cursor], columns: 2, width: bounds.width, reflow_margins: true do + ink_prose 'left column' + bounds.move_past_bottom + ink_prose 'right column' +end ---- -WARNING: 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: - -[,yaml] ----- -extends: default -base: - columns: 2 - column-gap: 12 ----- +If you want a multi-column layout for a specific chapter or section, you can override the `traverse` method, look for the section you want to arrange, and wrap the call to `super` in a `column_box` enclosure. == Theme table using custom role diff --git a/docs/modules/theme/pages/page.adoc b/docs/modules/theme/pages/page.adoc index 64df40f7..97c5852c 100644 --- a/docs/modules/theme/pages/page.adoc +++ b/docs/modules/theme/pages/page.adoc @@ -39,6 +39,21 @@ page: recto: image:page-bg-recto.png[] verso: image:page-bg-verso.png[] +|columns +|Integer + +(default: _not set_) +|[source] +page: + columns: 2 + +|column_gap +|xref:measurement-units.adoc[Measurement] + +(default: _$base-font-size_) +|[source] +page: + columns: 2 + columns_gap: 12 + |xref:images.adoc#foreground[foreground-image] |xref:images.adoc#specify[image macro] {vbar} xref:images.adoc#specify[path] + (default: _not set_) @@ -111,6 +126,14 @@ If no cover is specified, the recto margin is not applied to the title page. To apply the recto margin to the title page, but not include a cover, assign the value `~` to the `front-cover-image` and `back-cover-image` attributes. See xref:cover.adoc[] for information about the `cover` category keys. +[#columns] +=== Columns + +The columns are only applied to the body of a document with the article or manpage doctype. +The body of the document begins after the document title and TOC, if present. + +If the columns are set on the index section using the `index-columns` key, they will be ignored when `page-columns` is set. + [#numbering] == page-numbering diff --git a/lib/asciidoctor/pdf/converter.rb b/lib/asciidoctor/pdf/converter.rb index 208a1579..3136dff2 100644 --- a/lib/asciidoctor/pdf/converter.rb +++ b/lib/asciidoctor/pdf/converter.rb @@ -171,7 +171,8 @@ module Asciidoctor # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF ink_cover_page doc, :front has_front_cover = page_number > marked_page_number - if (has_title_page = (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page')) && (start_title_page doc)) + doctype = doc.doctype + if (has_title_page = (title_page_on = doctype == 'book' || (doc.attr? 'title-page')) && (start_title_page doc)) # NOTE: the base font must be set before any content is written to the main or scratch document font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style if perform_on_single_page { ink_title_page doc } @@ -280,12 +281,19 @@ module Asciidoctor doc.set_attr 'pdf-anchor', (derive_anchor_from_id doc.id, 'top') doc.set_attr 'pdf-page-start', page_number - convert_section generate_manname_section doc if doc.doctype == 'manpage' && (doc.attr? 'manpurpose') - - traverse doc - - # NOTE: for a book, these are leftover footnotes; for an article this is everything - outdent_section { ink_footnotes doc } + if doctype == 'book' || (columns = @theme.page_columns || 1) < 2 + convert_section generate_manname_section doc if doctype == 'manpage' && (doc.attr? 'manpurpose') + traverse doc + # NOTE: for a book, these are leftover footnotes; for an article this is everything + outdent_section { ink_footnotes doc } + else + column_box [bounds.left, cursor], columns: columns, width: bounds.width, reflow_margins: true, spacer: @theme.page_column_gap do + convert_section generate_manname_section doc if doctype == 'manpage' && (doc.attr? 'manpurpose') + traverse doc + # NOTE: for a book, these are leftover footnotes; for an article this is everything + outdent_section { ink_footnotes doc } + end + end if (toc_extent = @toc_extent) if title_page_on && !insert_toc @@ -710,28 +718,35 @@ module Asciidoctor end def convert_index_section node - space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A')) - pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style' - end_cursor = nil - column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do - @index.categories.each do |category| - bounds.move_past_bottom if space_needed_for_category > cursor - ink_prose category.name, - align: :left, - inline_format: false, - margin_bottom: @theme.description_list_term_spacing, - style: @theme.description_list_term_font_style&.to_sym - category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style } - @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom) - end - end_cursor = cursor if bounds.current_column == 0 - end - # Q: could we move this logic into column_box? - move_cursor_to end_cursor if end_cursor + if ColumnBox === bounds || (columns = @theme.index_columns || 1) < 2 + convert_index_categories @index.categories, (node.document.attr 'index-pagenum-sequence-style') + else + end_cursor = nil + column_box [bounds.left, cursor], columns: columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do + convert_index_categories @index.categories, (node.document.attr 'index-pagenum-sequence-style') + end_cursor = cursor if bounds.current_column == 0 + end + # Q: could we move this logic into column_box? + move_cursor_to end_cursor if end_cursor + end nil end - def convert_index_list_item term, pagenum_sequence_style = nil + def convert_index_categories categories, pagenum_sequence_style = nil + space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A')) + categories.each do |category| + bounds.move_past_bottom if space_needed_for_category > cursor + ink_prose category.name, + align: :left, + inline_format: false, + margin_bottom: @theme.description_list_term_spacing, + style: @theme.description_list_term_font_style&.to_sym + category.terms.each {|term| convert_index_term term, pagenum_sequence_style } + @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom) + end + end + + def convert_index_term term, pagenum_sequence_style = nil term_fragments = term.name.fragments unless term.container? pagenum_fragment = (parse_text %(<a>#{DummyText}</a>), inline_format: true)[0] @@ -770,7 +785,7 @@ module Asciidoctor typeset_formatted_text term_fragments, (calc_line_metrics @base_line_height), align: :left, color: @font_color, hanging_indent: subterm_indent * 2 indent subterm_indent do term.subterms.each do |subterm| - convert_index_list_item subterm, pagenum_sequence_style + convert_index_term subterm, pagenum_sequence_style end end unless term.leaf? end diff --git a/spec/arrange_block_spec.rb b/spec/arrange_block_spec.rb index 729c1b9c..ac57252e 100644 --- a/spec/arrange_block_spec.rb +++ b/spec/arrange_block_spec.rb @@ -1856,16 +1856,9 @@ describe 'Asciidoctor::PDF::Converter#arrange_block' do end it 'should fill extent when block is advanced to next column' do - source_file = doc_file 'modules/extend/examples/pdf-converter-columns.rb' - source_lines = (File.readlines source_file).select {|l| l == ?\n || (l.start_with? ' ') } - ext_class = create_class Asciidoctor::Converter.for 'pdf' - backend = %(pdf#{ext_class.object_id}) - source_lines[0] = %( register_for '#{backend}'\n) - ext_class.class_eval source_lines.join, source_file - pdf_theme.update \ - base_columns: 2, - base_column_gap: 12, + page_columns: 2, + page_column_gap: 12, code_border_radius: 0, code_border_width: 0, code_background_color: 'EFEFEF' @@ -1879,7 +1872,7 @@ describe 'Asciidoctor::PDF::Converter#arrange_block' do $ asciidoctor-pdf -r asciidoctor-mathematical -a mathematical-format=svg sample.adoc .... EOS - pdf = to_pdf input, backend: backend, pdf_theme: pdf_theme, analyze: true + pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true pages = pdf.pages (expect pages).to have_size 1 gs = (pdf.extract_graphic_states pages[0][:raw_content])[1] @@ -1888,14 +1881,7 @@ describe 'Asciidoctor::PDF::Converter#arrange_block' do end it 'should correctly compute to cursor value on extent when column_box starts below top of page' do - source_file = doc_file 'modules/extend/examples/pdf-converter-columns.rb' - source_lines = (File.readlines source_file).select {|l| l == ?\n || (l.start_with? ' ') } - ext_class = create_class Asciidoctor::Converter.for 'pdf' - backend = %(pdf#{ext_class.object_id}) - source_lines[0] = %( register_for '#{backend}'\n) - ext_class.class_eval source_lines.join, source_file - - pdf_theme.update base_columns: 2, base_column_gap: 12, admonition_column_rule_color: '0000FF' + pdf_theme.update page_columns: 2, page_column_gap: 12, admonition_column_rule_color: '0000FF' pdf = with_content_spacer 10, 400 do |spacer_path| input = <<~EOS @@ -1912,10 +1898,10 @@ describe 'Asciidoctor::PDF::Converter#arrange_block' do ==== EOS - pdf = to_pdf input, backend: backend, pdf_theme: pdf_theme, analyze: true + pdf = to_pdf input, pdf_theme: pdf_theme, analyze: true pages = pdf.pages (expect pages).to have_size 1 - lines = (to_pdf input, backend: backend, pdf_theme: pdf_theme, analyze: :line).lines + lines = (to_pdf input, pdf_theme: pdf_theme, analyze: :line).lines column_rules = lines.select {|it| it[:color] == '0000FF' } (expect column_rules).to have_size 2 (expect column_rules[0][:from][:x]).to be < column_rules[1][:from][:x] diff --git a/spec/index_spec.rb b/spec/index_spec.rb index 3a8bf3c0..13cdc112 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -818,6 +818,27 @@ describe 'Asciidoctor::PDF::Converter - Index' do (expect category_l_text[:x]).to be > category_a_text[:x] end + it 'should ignore index columns if columns are set on page' do + pdf = to_pdf <<~EOS, pdf_theme: { page_columns: 2, index_columns: 3 }, analyze: true + = Document Title + :notitle: + + #{('a'..'z').map {|it| %(((#{it}-keyword))((#{it}-term))) }.join} + + [index] + == Index + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + category_g_text = (pdf.find_text 'A')[0] + category_s_text = (pdf.find_text 'L')[0] + category_t_text = (pdf.find_text 'W')[0] + (expect category_g_text[:page_number]).to be 1 + (expect category_g_text[:x]).to eql 48.24 + (expect category_s_text[:x]).to be > midpoint + (expect category_t_text[:x]).to eql 48.24 + end + it 'should not allocate space for anchor if font is missing glyph for null character' do pdf_theme = { extends: 'default', diff --git a/spec/manpage_spec.rb b/spec/manpage_spec.rb index 5d832e23..a3e320e1 100644 --- a/spec/manpage_spec.rb +++ b/spec/manpage_spec.rb @@ -93,4 +93,31 @@ describe 'Asciidoctor::PDF::Converter - Manpage' do (expect name_title_text[:font_size]).to be 22 (expect pdf.lines).to include 'cmd - does stuff' end + + it 'should arrange body of manpage into columns if specified in theme' do + pdf = to_pdf <<~'EOS', doctype: :manpage, pdf_theme: { page_columns: 2 }, analyze: true + = cmd(1) + + == Name + + cmd - does stuff + + == Synopsis + + *cmd* [_OPTION_]... _FILE_... + + <<< + + == Options + + *-v*:: Prints the version. + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + name_text = pdf.find_unique_text 'Name' + options_text = pdf.find_unique_text 'Options' + (expect name_text[:x]).to eql 48.24 + (expect options_text[:x]).to be > midpoint + (expect name_text[:y]).to eql options_text[:y] + end end diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 62ec84e2..be80b9be 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -538,6 +538,177 @@ describe 'Asciidoctor::PDF::Converter - Page' do end end + context 'Columns' do + it 'should ignore columns for book doctype' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2 }, analyze: true + = Document Title + :doctype: book + :notitle: + + [.text-right] + first page + + <<< + + second page + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 2 + (expect (pdf.find_unique_text 'first page')[:page_number]).to eql 1 + (expect (pdf.find_unique_text 'first page')[:x]).to be > midpoint + (expect (pdf.find_unique_text 'second page')[:page_number]).to eql 2 + end + + it 'should ignore columns if less than 2' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 1 }, analyze: true + = Document Title + :notitle: + + first page + + <<< + + second page + EOS + + (expect pdf.pages).to have_size 2 + (expect (pdf.find_unique_text 'first page')[:page_number]).to eql 1 + (expect (pdf.find_unique_text 'second page')[:page_number]).to eql 2 + end + + it 'should arrange article body into columns' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2 }, analyze: true + first column + + <<< + + second column + + <<< + + [.text-right] + first column again + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 2 + (expect (pdf.find_unique_text 'first column')[:page_number]).to eql 1 + (expect (pdf.find_unique_text 'second column')[:page_number]).to eql 1 + (expect (pdf.find_unique_text 'second column')[:x]).to be > midpoint + (expect (pdf.find_unique_text 'first column again')[:page_number]).to eql 2 + (expect (pdf.find_unique_text 'first column again')[:x]).to be < midpoint + end + + it 'should put footnotes at bottom of last column with content' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2 }, analyze: true + first columnfootnote:[This page has two columns.] + + <<< + + second column + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 1 + (expect (pdf.find_unique_text 'second column')[:x]).to be > midpoint + right_column_text = pdf.text.select {|it| it[:x] > midpoint } + right_column_lines = pdf.lines right_column_text + (expect right_column_lines).to have_size 2 + (expect right_column_lines[-1]).to eql '[1] This page has two columns.' + end + + it 'should place document title outside of column box' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2 }, analyze: true + = Article Title Goes Here + + first column + + <<< + + second column + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 1 + title_text = pdf.find_unique_text 'Article Title Goes Here' + (expect title_text[:x]).to be < midpoint + (expect title_text[:x] + title_text[:width]).to be > midpoint + (expect (pdf.find_unique_text 'second column')[:x]).to be > midpoint + end + + it 'should place TOC outside of column box' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2 }, analyze: true + = Article Title Goes Here + :toc: + + == First Column + + <<< + + == Second Column + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 1 + first_column_text = (pdf.find_text 'First Column').sort_by {|it| -it[:y] } + second_column_text = (pdf.find_text 'Second Column').sort_by {|it| -it[:y] } + (expect first_column_text[0][:x]).to eql 48.24 + (expect first_column_text[1][:x]).to eql 48.24 + (expect second_column_text[0][:x]).to eql 48.24 + (expect second_column_text[1][:x]).to be > midpoint + dots_text = pdf.text.select {|it| it[:string].include? '.' } + dots_text.each do |it| + (expect it[:x]).to be < midpoint + (expect it[:x] + it[:width]).to be > midpoint + end + end + + it 'should allow theme to control number of columns' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 4 }, analyze: true + one + + <<< + + two + + <<< + + three + + <<<< + + four + EOS + + midpoint = (get_page_size pdf)[0] * 0.5 + (expect pdf.pages).to have_size 1 + one_text = pdf.find_unique_text 'one' + two_text = pdf.find_unique_text 'two' + three_text = pdf.find_unique_text 'three' + four_text = pdf.find_unique_text 'four' + (expect two_text[:x]).to be > one_text[:x] + (expect two_text[:x]).to be < midpoint + (expect four_text[:x]).to be > three_text[:x] + (expect three_text[:x]).to be > midpoint + end + + it 'should allow theme to control column gap' do + pdf = to_pdf <<~'EOS', pdf_theme: { page_columns: 2, page_column_gap: 12 }, analyze: :image + image::square.png[pdfwidth=100%] + + <<< + + image::square.png[pdfwidth=100%] + EOS + + images = pdf.images + (expect images).to have_size 2 + column_gap = (images[1][:x] - (images[0][:x] + images[0][:width])).to_f + (expect column_gap).to eql 12.0 + end + end + context 'Background' do it 'should set page background to white if value is not defined or transparent', visual: true do [nil, 'transparent'].each do |bg_color| |
