From eb984e3385455015a75ceda1e1fc8491b90da480 Mon Sep 17 00:00:00 2001 From: Dan Allen Date: Mon, 18 May 2015 00:04:31 -0600 Subject: [PATCH] resolves #110 fix xrefs to non-section content - introduce add_dest_for_block for adding xref targets (i.e, destinations) - call add_dest_for_block in each block handler - add support in parser for to set inline destinations - handle render_behind callback to add inline destinations - enable inline ref and bibref targets --- lib/asciidoctor-pdf/converter.rb | 57 +++++++++++++++---- .../prawn_ext/formatted_text/transform.rb | 22 +++++++ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/lib/asciidoctor-pdf/converter.rb b/lib/asciidoctor-pdf/converter.rb index 8a468ef47..9637affb6 100644 --- a/lib/asciidoctor-pdf/converter.rb +++ b/lib/asciidoctor-pdf/converter.rb @@ -37,6 +37,7 @@ def self.unicode_char number TabSpaces = ' ' * 4 NoBreakSpace = unicode_char 0x00a0 NarrowNoBreakSpace = unicode_char 0x202f + ZeroWidthSpace = unicode_char 0x200b HairSpace = unicode_char 0x200a DotLeader = %(#{HairSpace}.) EmDash = unicode_char 0x2014 @@ -259,9 +260,7 @@ def convert_section sect, opts = {} end # QUESTION should we store page_start & destination in internal map? sect.set_attr 'page_start', page_number - dest_y = at_page_top? ? page_height : y - sect.set_attr 'destination', (sect_destination = (dest_xyz 0, dest_y)) - add_dest sect.id, sect_destination + add_dest_for_block sect sect.chapter? ? (layout_chapter_title sect, title) : (layout_heading title) end @@ -270,12 +269,14 @@ def convert_section sect, opts = {} end def convert_floating_title node + add_dest_for_block node if node.id theme_font :heading, level: (node.level + 1) do layout_heading node.title end end def convert_abstract node + add_dest_for_block node if node.id pad_box @theme.abstract_padding do theme_font :abstract do # FIXME control first_line_options using theme @@ -313,6 +314,7 @@ def convert_preamble node # TODO add prose around image logic (use role to add special logic for headshot) def convert_paragraph node + add_dest_for_block node if node.id is_lead = false prose_opts = {} node.roles.each do |role| @@ -345,6 +347,7 @@ def convert_paragraph node # FIXME alignment of content is off def convert_admonition node + add_dest_for_block node if node.id #move_down @theme.block_margin_top unless at_page_top? theme_margin :block, :top icons = node.document.attr? 'icons', 'font' @@ -391,6 +394,7 @@ def convert_admonition node end def convert_example node + add_dest_for_block node if node.id #move_down @theme.block_margin_top unless at_page_top? theme_margin :block, :top keep_together do |box_height = nil| @@ -421,14 +425,17 @@ def convert_open node if node.blocks.size == 1 && node.blocks.first.style == 'abstract' convert_abstract node.blocks.first else + add_dest_for_block node if node.id convert_content_for_block node end else + add_dest_for_block node if node.id convert_content_for_block node end end def convert_quote_or_verse node + add_dest_for_block node if node.id border_width = @theme.blockquote_border_width #move_down @theme.block_margin_top unless at_page_top? theme_margin :block, :top @@ -466,6 +473,7 @@ def convert_quote_or_verse node alias :convert_verse :convert_quote_or_verse def convert_sidebar node + add_dest_for_block node if node.id #move_down @theme.block_margin_top unless at_page_top? theme_margin :block, :top keep_together do |box_height = nil| @@ -495,6 +503,7 @@ def convert_sidebar node end def convert_colist node + add_dest_for_block node if node.id # HACK undo the margin below the listing move_up ((@theme.block_margin_bottom || @theme.vertical_rhythm) * 0.5) @list_numbers ||= [] @@ -508,6 +517,7 @@ def convert_colist node end def convert_dlist node + add_dest_for_block node if node.id node.items.each do |terms, desc| terms = [*terms] # NOTE don't orphan the terms, allow for at least one line of content @@ -525,6 +535,7 @@ def convert_dlist node end def convert_olist node + add_dest_for_block node if node.id @list_numbers ||= [] list_number = case node.style when 'arabic' @@ -554,6 +565,7 @@ def convert_olist node # TODO implement checklist def convert_ulist node + add_dest_for_block node if node.id bullet_type = if (style = node.style) case style when 'bibliography' @@ -629,6 +641,7 @@ def convert_content_for_list_item node end def convert_image node + add_dest_for_block node if node.id #move_down @theme.block_margin_top unless at_page_top? theme_margin :block, :top target = node.attr 'target' @@ -699,6 +712,7 @@ def convert_image node end def convert_listing_or_literal node + add_dest_for_block node if node.id # HACK disable built-in syntax highlighter; must be done before calling node.content! if (node.style == 'source') node.subs.delete :highlight @@ -771,6 +785,7 @@ def convert_listing_or_literal node alias :convert_literal :convert_listing_or_literal def convert_table node + add_dest_for_block node if node.id num_rows = 0 num_cols = node.columns.size table_header = false @@ -971,15 +986,11 @@ def convert_inline_anchor node end end when :ref - # FIXME add destination to PDF document - #target = node.target - #%() - '' + # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment + %(#{ZeroWidthSpace}) when :bibref - # FIXME add destination to PDF document - #target = node.target - #%([#{target}]) - %([#{node.target}]) + # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment + %(#{ZeroWidthSpace}[#{target}]) else warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect}) end @@ -1052,7 +1063,7 @@ def convert_inline_quoted node quoted_text = %(#{open}#{node.text}#{close}) end - node.id ? %(#{quoted_text}) : quoted_text + node.id ? %(#{ZeroWidthSpace}#{quoted_text}) : quoted_text end def layout_title_page doc @@ -1469,6 +1480,28 @@ def sanitize string .strip end + # If the node passed as the first argument has an id, add a named destination + # to the document equivalent to the node id at the current y position. If the + # node does not have an id, do nothing. + # + # If the node is a section, and the current y position is the top of the + # page, set the position equal to the page height to improve the navigation + # experience. + def add_dest_for_block node + if !scratch? && (id = node.id) + dest_y = if node.context == :section && at_page_top? + page_height + else + y + end + # QUESTION should this attribute be named pdf-destination instead? + # FIXME destination is not always the left margin; use x value of current bounding box + node.set_attr 'destination', (node_dest = (dest_xyz 0, dest_y)) + add_dest id, node_dest + end + nil + end + def resolve_imagesdir doc @imagesdir ||= begin imagesdir = (doc.attr 'imagesdir', '.').chomp '/' diff --git a/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb b/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb index a20855d2b..38671106a 100644 --- a/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb +++ b/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb @@ -133,6 +133,10 @@ def build_fragment(fragment, tag_name = nil, attrs = {}) if !fragment[:local] && (value = attrs[:local]) fragment[:local] = value end + if !fragment[:name] && (value = attrs[:name]) + fragment[:name] = value + fragment[:callback] = InlineDestinationMarker.instance + end fragment[:color] ||= @link_font_color when :sub styles << :subscript @@ -174,5 +178,23 @@ def build_fragment(fragment, tag_name = nil, attrs = {}) fragment end end + +class InlineDestinationMarker + @__instance__ = new + + def self.instance + @__instance__ + end + + def render_behind fragment + unless (doc = fragment.instance_variable_get :@document).scratch? + if (name = (fragment.instance_variable_get :@format_state)[:name]) + # get precise position of the reference + dest_rect = fragment.absolute_bounding_box + doc.add_dest name, (doc.dest_xyz dest_rect.first, dest_rect.last) + end + end + end +end end end