diff --git a/lib/asciidoctor/pdf/converter.rb b/lib/asciidoctor/pdf/converter.rb index 5f8bb1a05..175a5efea 100644 --- a/lib/asciidoctor/pdf/converter.rb +++ b/lib/asciidoctor/pdf/converter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require_relative 'formatted_string' require_relative 'formatted_text' require_relative 'index_catalog' require_relative 'pdfmark' @@ -731,28 +732,42 @@ def convert_index_section node end def convert_index_list_item term, pagenum_sequence_style = nil - text = escape_xml term.name + term_fragments = term.name.fragments unless term.container? + pagenum_fragment = (parse_text %(#{DummyText}), inline_format: true)[0] if @media == 'screen' case pagenum_sequence_style when 'page' - pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| %(#{dest[:page]}) } + pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| pagenum_fragment.merge anchor: dest[:anchor], text: dest[:page] } when 'range' first_anchor_per_page = {}.tap {|accum| term.dests.each {|dest| accum[dest[:page]] ||= dest[:anchor] } } pagenums = (consolidate_ranges first_anchor_per_page.keys).map do |range| anchor = first_anchor_per_page[(range.include? '-') ? (range.partition '-')[0] : range] - %(#{range}) + pagenum_fragment.merge text: range, anchor: anchor end else # term - pagenums = term.dests.map {|dest| %(#{dest[:page]}) } + pagenums = term.dests.map {|dest| pagenum_fragment.merge text: dest[:page], anchor: dest[:anchor] } end else - pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq + pagenums = (consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq).map {|range| { text: range } } + end + pagenums.each do |pagenum| + if (prev_fragment = term_fragments[-1]).size == 1 + # NOTE: addresses a very minor kerning issue for text adjacent to the comma + if pagenum.size == 1 + term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, #{pagenum[:text]}) + next + else + term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, ) + end + else + term_fragments << ({ text: ', ' }) + end + term_fragments << pagenum end - text = %(#{text}, #{pagenums.join ', '}) end subterm_indent = @theme.description_list_description_indent - ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2 + 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 @@ -2570,17 +2585,20 @@ def convert_inline_indexterm node if scratch? visible ? node.text : '' else - # NOTE: initialize index in case converter is called before PDF is initialized - @index ||= IndexCatalog.new + unless defined? @index + # NOTE: initialize index and text formatter in case converter is called before PDF is initialized + @index = IndexCatalog.new + @text_formatter = FormattedText::Formatter.new theme: (load_theme node.document) + end # NOTE: page number (:page key) is added by InlineDestinationMarker dest = { anchor: (anchor_name = @index.next_anchor_name) } anchor = %(#{DummyText}) if visible visible_term = node.text - @index.store_primary_term (sanitize visible_term), dest + @index.store_primary_term (FormattedString.new parse_text visible_term, inline_format: [normalize: true]), dest %(#{anchor}#{visible_term}) else - @index.store_term (node.attr 'terms').map {|term| sanitize term }, dest + @index.store_term (node.attr 'terms').map {|term| FormattedString.new parse_text term, inline_format: [normalize: true] }, dest anchor end end diff --git a/lib/asciidoctor/pdf/formatted_string.rb b/lib/asciidoctor/pdf/formatted_string.rb new file mode 100644 index 000000000..f44ff5eed --- /dev/null +++ b/lib/asciidoctor/pdf/formatted_string.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class FormattedString < String + attr_reader :fragments + + def initialize fragments + super [].tap {|accum| (@fragments = fragments).each {|it| accum << it[:text] } }.join + end + + def eql? other + self == other && @fragments == other.fragments + end +end diff --git a/spec/index_spec.rb b/spec/index_spec.rb index bc97ab69a..dfbb99955 100644 --- a/spec/index_spec.rb +++ b/spec/index_spec.rb @@ -31,7 +31,8 @@ ((foo {empty} bar))(((yin - + {empty} + {empty} yang))) <<< @@ -147,6 +148,51 @@ (expect index_lines).to include 'custom behavior, 1' end + it 'should preserve text formatting in display of index term' do + pdf = to_pdf <<~'EOS', doctype: :book, analyze: true + = Document Title + + == Content + + Use the ((`return`)) keyword(((_keyword_))) to force a method to return early. + + There are cats, and then there are ((*big* cats)). + + A ((mouse _gesture_)) is a movement the software recognizes and interprets as a command. + + [index] + = Index + EOS + + (expect pdf.pages).to have_size 3 + return_entry_text = pdf.find_unique_text 'return', page_number: 3 + (expect return_entry_text[:font_name]).to eql 'mplus1mn-regular' + keyword_entry_text = pdf.find_unique_text 'keyword', page_number: 3 + (expect keyword_entry_text[:font_name]).to eql 'NotoSerif-Italic' + big_text = pdf.find_unique_text 'big', page_number: 3 + (expect big_text[:font_name]).to eql 'NotoSerif-Bold' + gesture_text = pdf.find_unique_text 'gesture', page_number: 3 + (expect gesture_text[:font_name]).to eql 'NotoSerif-Italic' + end + + it 'should not group term with and without formatting' do + pdf = to_pdf <<~'EOS', doctype: :book, analyze: true + The ((`proc`)) keyword in Ruby defines a ((proc)), which is a block of code. + + [index] + = Index + EOS + + (expect pdf.pages).to have_size 2 + proc_text = pdf.find_text %r/^proc/, page_number: 2 + (expect proc_text).to have_size 2 + (expect proc_text[0][:string]).to eql 'proc' + (expect proc_text[0][:font_name]).to eql 'mplus1mn-regular' + (expect proc_text[1][:font_name]).not_to eql 'mplus1mn-regular' + index_lines = pdf.lines pdf.find_text page_number: 2 + (expect index_lines).to eql ['Index', 'P', 'proc, 1', 'proc, 1'] + end + it 'should not add index entries inside delimited block to index twice' do pdf = to_pdf <<~'EOS', doctype: :book, analyze: true = Document Title