Class: DotLeader
- Inherits:
-
Object
- Object
- DotLeader
- Defined in:
- lib/asciidoctor/dot_leader/dot_leader_pdf.rb
Instance Method Summary collapse
- #convert_inline_dot_leader(node) ⇒ Object
- #rendered_width_of_fragments(fragments, scale, roles = []) ⇒ Object
Instance Method Details
#convert_inline_dot_leader(node) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/asciidoctor/dot_leader/dot_leader_pdf.rb', line 41 def convert_inline_dot_leader(node) # warn "Node attributes: #{node.attributes.inspect}" return nil unless node.type == :dot left_side = node.attr('l_text') || '' right_side = node.attr('r_text') || '' # DEBUG: Initial context information is_column_box = defined?(::Prawn::Document::ColumnBox) && bounds.is_a?(::Prawn::Document::ColumnBox) current_column = is_column_box ? bounds.current_column : 'N/A' # log :info, %(DOT_LEADER_DEBUG: Starting - left="#{left_side}", right="#{right_side}", page=#{page_number}, cursor=#{cursor}, is_column=#{is_column_box}, current_column=#{current_column}) if left_side.empty? # Allow items only on the right left_side = ' ' end doc = node.document if doc.attr 'dot-leader-scale' scale = doc.attr 'dot-leader-scale' scale = scale.to_f else scale = 0.6 end # 2025-07-17 BYB: Create the dot_leader array; copied from ink_toc dot_leader = theme_font :toc do # TODO: we could simplify by using nested theme_font :toc_dot_leader #BYB: Original note from ink_toc if (dot_leader_font_style = @theme.toc_dot_leader_font_style&.to_sym || :normal) != font_style font_style dot_leader_font_style end font_size @theme.toc_dot_leader_font_size { font_color: @theme.toc_dot_leader_font_color || @font_color, font_style: dot_leader_font_style, font_size: font_size, levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new : (dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : nil)), text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault), width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text), # TODO: spacer gives a little bit of room between dots and page number #BYB: Original note from ink_toc spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) }, spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size), } end # log :info, %(DOT_LEADER_DEBUG: Dot leader config - text="#{dot_leader[:text]}", width=#{dot_leader[:width]}, spacer_width=#{dot_leader[:spacer_width]}) # 2025-07-17 BYB: Set toc_font_info toc_font_info = theme_font :toc do { font: font, size: @font_size } end # 2025-07-17 BYB: Set a dummy entry_level and entry_anchor entry_level = 2 entry_anchor = nil # 2025-07-17 BYB: Store the cursor position start_cursor = cursor start_dots = nil # 2025-07-17 BYB: left_side original left_side_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color # NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag left_side_fragments = text_formatter.format left_side, inherited: left_side_inherited # warn "left_side_fragments: #{left_side_fragments.inspect}" # 2025-07-17 BYB: Try doing the same for the right side right_side_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color # NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag right_side_fragments = text_formatter.format right_side, inherited: right_side_inherited line_metrics = calc_line_metrics @base_line_height # 2025-07-17 BYB: This needs to be modified right_side_placeholder_width = rendered_width_of_fragments right_side_fragments, scale, node.roles.to_a left_side_test_width = rendered_width_of_fragments left_side_fragments, scale, node.roles.to_a # log :info, %(DOT_LEADER_DEBUG: Text widths - left=#{left_side_test_width}, right=#{right_side_placeholder_width}) # 2025-07-17 Set hanging_indent to toc; potentially this could be styled hanging_indent = @theme.toc_hanging_indent start_page_number = page_number indent 0, right_side_placeholder_width do fragment_positions = [] left_side_fragments.each do |fragment| fragment_positions << (fragment_position = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new) (fragment[:callback] ||= []) << fragment_position end if right_side_placeholder_width > bounds.width log :warn, %(DOT_LEADER_DEBUG: Right side too wide; skipping: #{left_side} - right_width=#{right_side_placeholder_width}, bounds_width=#{bounds.width}) return nil end typeset_formatted_text left_side_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true break unless (last_fragment_position = fragment_positions.select(&:page_number)[-1]) start_dots = last_fragment_position.right + hanging_indent last_fragment_cursor = last_fragment_position.top + line_metrics.padding_top start_cursor = last_fragment_cursor if last_fragment_position.page_number > start_page_number || (start_cursor - last_fragment_cursor) > line_metrics.height end # log :info, %(DOT_LEADER_DEBUG: After left text - start_dots=#{start_dots}, start_cursor=#{start_cursor}) # 2025-07-17 No loop # NOTE: this will leave behind a gap where this entry would have been # break unless start_dots end_cursor = cursor move_cursor_to start_cursor # NOTE: we're guaranteed to be on the same page as the final line of the entry if dot_leader[:width] > 0 && (dot_leader[:levels] ? (dot_leader[:levels].include? entry_level.pred) : true) # 2025-07-17 BYB: Needed to calculate the right side width # right_side_width = rendered_width_of_string right_side right_side_width = right_side_placeholder_width right_side_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles } save_font do # NOTE: the same font is used for dot leaders throughout toc set_font toc_font_info[:font], dot_leader[:font_size] font_style dot_leader[:font_style] # For true vertical alignment, dots must be at consistent absolute positions across all lines # CONTEXT-AWARE GRID ALIGNMENT: # Use appropriate grid origin for each context to ensure proper alignment # while maintaining correct positioning within bounds # Calculate the absolute position where dots will start # Note: start_dots may already be absolute in some contexts (like right column) # Check if start_dots is already in absolute coordinates if start_dots > bounds.width # start_dots appears to be already absolute (larger than column width) start_dots_absolute = start_dots else # start_dots is relative to bounds, convert to absolute start_dots_absolute = start_dots + bounds.absolute_left end if defined?(::Prawn::Document::ColumnBox) && bounds.is_a?(::Prawn::Document::ColumnBox) # In column context: use UNIVERSAL grid origin (page margin) for alignment across all columns # This ensures dots align consistently across left and right columns # Get column information for debugging current_col = bounds.current_column parent_bounds = bounds.instance_variable_get(:@parent) total_columns = bounds.instance_variable_get(:@columns) || 2 # Calculate the actual column dimensions from the parent parent_width = parent_bounds.width column_width = parent_width / total_columns total_spacer_width = parent_width - (column_width * total_columns) spacer_width = total_columns > 1 ? total_spacer_width / (total_columns - 1) : 0 column_offset = current_col * (column_width + spacer_width) # UNIVERSAL GRID: Use page margin as grid origin for all columns # This ensures dots align across the entire page grid_origin = page.margins[:left] # log :info, %(DOT_LEADER_DEBUG: Column context - current_column=#{current_col}, total_columns=#{total_columns}, column_width=#{column_width}, spacer_width=#{spacer_width}, column_offset=#{column_offset}, bounds_left=#{bounds.absolute_left}, grid_origin=#{grid_origin}) else # In normal context: use the page margin as grid origin to ignore indentation # This ensures consistent alignment across all list levels grid_origin = page.margins[:left] # log :info, %(DOT_LEADER_DEBUG: Normal context - bounds_left=#{bounds.absolute_left}, page_margin=#{page.margins[:left]}, grid_origin=#{grid_origin}) end # Calculate the position relative to the grid origin for alignment start_dots_from_grid_origin = start_dots_absolute - grid_origin # log :info, %(DOT_LEADER_DEBUG: Position calculations - start_dots_absolute=#{start_dots_absolute}, start_dots_from_grid_origin=#{start_dots_from_grid_origin}) # Find the first grid position after the left text ends # Grid positions are at multiples of dot_leader[:width] from the grid origin first_dot_grid_index = (start_dots_from_grid_origin / dot_leader[:width]).ceil first_dot_position_absolute = grid_origin + (first_dot_grid_index * dot_leader[:width]) # Convert back to the local bounds coordinate system for rendering # Use universal grid but fix coordinate conversion for right column first_dot_position = first_dot_position_absolute - bounds.absolute_left # Special handling for columns beyond the first (right column in 2-column layout) # We need to recalculate using the universal grid to ensure proper alignment if defined?(::Prawn::Document::ColumnBox) && bounds.is_a?(::Prawn::Document::ColumnBox) current_col = bounds.current_column if current_col > 0 # Right column case: recalculate using universal grid # We need to find the first grid position within the column that comes AFTER the left text # Calculate the left text width to ensure dots start after it left_text_width = rendered_width_of_fragments left_side_fragments, scale, node.roles.to_a # Find the equivalent universal grid position that aligns with other columns # Calculate where text ends (without spacer first) # Use the absolute left position of the current column plus the text width text_end_no_spacer = bounds.absolute_left + left_text_width # Add the spacer to get the absolute position where dots should start text_end_absolute = text_end_no_spacer + dot_leader[:spacer_width] # Find the first universal grid position after the text ends # This ensures alignment with single column and other contexts text_end_from_grid_origin = text_end_absolute - grid_origin universal_grid_index = (text_end_from_grid_origin / dot_leader[:width]).ceil # Check if the previous grid position would still fit (with minimal spacing) # This allows us to fit one more dot if the previous grid position comes after the actual text if universal_grid_index > 0 previous_grid_position = grid_origin + ((universal_grid_index - 1) * dot_leader[:width]) # Check spacing from actual text end (not including the full spacer) spacing_from_text = previous_grid_position - text_end_no_spacer # Use previous position if it's at least 1pt after the text (minimal spacing) if spacing_from_text >= 1.0 universal_grid_index -= 1 end end # Calculate the absolute position using the universal grid adjusted_position_absolute = grid_origin + (universal_grid_index * dot_leader[:width]) first_dot_position = adjusted_position_absolute - bounds.absolute_left first_dot_position_absolute = adjusted_position_absolute # CRITICAL: Update the grid index to match the adjusted position first_dot_grid_index = universal_grid_index # log :info, %(DOT_LEADER_DEBUG: Right column adjustment - left_text_width=#{left_text_width}, text_end_absolute=#{text_end_absolute}, text_end_from_grid_origin=#{text_end_from_grid_origin}, universal_grid_index=#{universal_grid_index}, adjusted_grid_index=#{first_dot_grid_index}, adjusted_position=#{first_dot_position}) else # log :info, %(DOT_LEADER_DEBUG: Left column - no adjustment needed) end else # log :info, %(DOT_LEADER_DEBUG: Standard conversion - first_dot_position_absolute=#{first_dot_position_absolute}, bounds.absolute_left=#{bounds.absolute_left}, first_dot_position=#{first_dot_position}) end # log :info, %(DOT_LEADER_DEBUG: First dot - grid_index=#{first_dot_grid_index}, position_absolute=#{first_dot_position_absolute}, position_local=#{first_dot_position}) # Check if the first dot is too close to the left text # Calculate the actual left text width for precise spacing check left_text_width = rendered_width_of_fragments left_side_fragments, scale, node.roles.to_a space_after_left_text = first_dot_position - left_text_width dot_stripped = dot_leader[:text].rstrip dot_stripped_width = rendered_width_of_string dot_stripped # Calculate where the right text will start (in absolute coordinates) right_text_start_local = bounds.width - right_side_width - dot_leader[:spacer_width] # Convert to absolute coordinates: bounds.absolute_left already accounts for column position right_text_start_absolute = right_text_start_local + bounds.absolute_left # Find the last grid position before the right text starts (using the same grid origin) # Ensure the COMPLETE dot (start + width) fits before right text right_text_from_grid_origin = right_text_start_absolute - grid_origin # Calculate the last grid index where a complete dot would fit # A dot at position N has its right edge at N + dot_leader[:width] # The target: (last_dot_grid_index * dot_leader[:width]) + dot_leader[:width] <= right_text_from_grid_origin # Simplified: last_dot_grid_index <= (right_text_from_grid_origin / dot_leader[:width]) - 1 # Add some buffer space to ensure the dot does not touch the right text last_possible_dot_end = right_text_from_grid_origin - (1.0 * dot_stripped_width) last_dot_grid_index = (last_possible_dot_end / dot_leader[:width]).floor # log :info, %(DOT_LEADER_DEBUG: Right text - start_local=#{right_text_start_local}, start_absolute=#{right_text_start_absolute}, from_grid_origin=#{right_text_from_grid_origin}) # Calculate the number of dots that fit on the grid between left and right text num_dots = [last_dot_grid_index - first_dot_grid_index + 1, 0].max # log :info, %(DOT_LEADER_DEBUG: Last dot - last_possible_end=#{last_possible_dot_end}, last_grid_index=#{last_dot_grid_index}, num_dots=#{num_dots}) # Calculate the indent to position the first dot at the correct grid position dot_indent = first_dot_position # log :info, %(DOT_LEADER_DEBUG: Final render - dot_indent=#{dot_indent}, num_dots=#{num_dots}, dot_text="#{dot_leader[:text]}") fragment_positions = [] right_side_fragments.each do |fragment| fragment_positions << (fragment_position = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new) (fragment[:callback] ||= []) << fragment_position end # Render the dots with a proper indent to align to the grid if num_dots > 0 indent dot_indent, 0 do typeset_formatted_text [ { text: dot_leader[:text] * num_dots, color: dot_leader[:font_color] } ], line_metrics, align: :left end # log :info, %(DOT_LEADER_DEBUG: Dots rendered successfully) else log :warn, %(DOT_LEADER_DEBUG: No dots to render - num_dots=#{num_dots}) end # Move cursor back to render right text move_cursor_to start_cursor typeset_formatted_text [ dot_leader[:spacer], *right_side_fragments.map { |fragment| fragment.dup.tap do |f| f[:styles] = Array(f[:styles]) f[:styles] |= Array(right_side_font_settings[:styles]) if right_side_font_settings[:styles] f[:font] ||= right_side_font_settings[:font] if right_side_font_settings[:font] f[:size] ||= right_side_font_settings[:size] if right_side_font_settings[:size] f[:color] ||= right_side_font_settings[:color] if right_side_font_settings[:color] f[:anchor] = entry_anchor if entry_anchor end } ], line_metrics, align: :right end else # log :info, %(DOT_LEADER_DEBUG: Skipping dot rendering - width=#{dot_leader[:width]}, levels_check=#{dot_leader[:levels] ? (dot_leader[:levels].include? entry_level.pred) : true}) typeset_formatted_text [{ text: right_side, color: @font_color, anchor: entry_anchor }], line_metrics, align: :right end move_cursor_to end_cursor # log :info, %(DOT_LEADER_DEBUG: Completed - final_cursor=#{cursor}) # Return nil to avoid numbers being printed out nil end |
#rendered_width_of_fragments(fragments, scale, roles = []) ⇒ Object
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/asciidoctor/dot_leader/dot_leader_pdf.rb', line 9 def rendered_width_of_fragments(fragments, scale, roles = []) fragments.sum do |fragment| styles = fragment[:styles] || Set.new # Use fragment's size if specified (for roles like .small, .big), otherwise use current font size size = fragment[:size] || font_size size *= scale if styles.include?(:subscript) || styles.include?(:superscript) font_style = if styles.include?(:bold) && styles.include?(:italic) :bold_italic elsif styles.include?(:bold) :bold elsif styles.include?(:italic) :italic else :normal end font_name = fragment[:font] || font_family width = nil # Handle inline image width if fragment[:image_format] # Use numeric value if it's a string image_width = fragment[:image_width] return image_width.to_f if image_width end font font_name, style: font_style, size: size do width = rendered_width_of_string(fragment[:text]) end width end end |