Skip to content

Commit

Permalink
resolves #185 autofit font size in listing & literal blocks
Browse files Browse the repository at this point in the history
- autofit (reduce font size) to prevent wrapping in listing & literal blocks
- prevent font size from being reduced more than *_font_size_min
- activate autofit if autofit option is specified on block
- activate autofit if autofit-option attribute is specified on document
- add autofit example to chronicles.adoc
- add additional helper methods to core_ext and sanitizer
  • Loading branch information
mojavelinux committed Jun 13, 2015
1 parent b27e700 commit 3e3fa7b
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 20 deletions.
1 change: 1 addition & 0 deletions data/themes/default-theme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ base:
line_height: $base_line_height_length / $base_font_size
font_size_large: round($base_font_size * 1.25)
font_size_small: round($base_font_size * 0.85)
font_size_min: $base_font_size * 0.75
font_style: normal
align: justify
border_radius: 4
Expand Down
22 changes: 13 additions & 9 deletions examples/chronicles.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -268,31 +268,35 @@ code {
```

Where could we go without some Java?
Naturally, some autosizing is necessary.

```java
[source%autofit,java]
----
package org.javaee7.cdi.events;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.event.Observes;
import java.io.Serializable;
import java.util.*;
import javax.ws.rs.*;
/**
* @author The Duke
*/
@SessionScoped
public class GreetingReceiver implements EventReceiver, Serializable {
private String greet = "Willkommen";
public class GreetingReceiver implements EventReceiver, java.io.Serializable {
private List<String> greetings = new ArrayList<String>();
void receive(@Observes String greet) {
this.greet = greet;
this.greetings.add(greet);
}
@Override
public String getGreet() {
return greet;
@GET @Produces("application/json")
public List<String> listAll(@QueryParam("start") Integer start, @QueryParam("max") Integer max) {
// TODO implement filtering by start and max
this.greetings;
}
}
```
----

We already showed you an XML example in <<ravages>>.

Expand Down
Binary file modified examples/chronicles.pdf
Binary file not shown.
97 changes: 90 additions & 7 deletions lib/asciidoctor-pdf/converter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# encoding: UTF-8
# TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
require_relative 'core_ext/array'
require_relative 'core_ext'
require 'prawn'
require 'prawn-svg'
require 'prawn/table'
Expand Down Expand Up @@ -62,6 +62,7 @@ def self.unicode_char number
# CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core
CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*$)/
ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
LineScanRx = /\n|.+/

def initialize backend, opts
super
Expand Down Expand Up @@ -846,7 +847,7 @@ def convert_image node
unlink_tmp_file image_path
end

# TODO shrink text if it's too wide to fit in the bounding box
# QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
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!
Expand All @@ -855,13 +856,11 @@ def convert_listing_or_literal node
highlighter = node.document.attr 'source-highlighter'
# NOTE the source highlighter logic below handles the callouts and highlight subs
prev_subs = subs.dup
subs.delete :callouts
subs.delete :highlight
subs.delete_all :highlight, :callouts
else
highlighter = nil
prev_subs = nil
end
# FIXME source highlighter freaks out about the non-breaking space characters; does it?
source_string = preserve_indentation node.content
source_chunks = case highlighter
when 'coderay'
Expand All @@ -879,14 +878,24 @@ def convert_listing_or_literal node
conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
else
# NOTE only format if we detect a need
(source_string =~ BuiltInEntityCharOrTagRx) ? (text_formatter.format source_string) : [{ text: source_string }]
if source_string =~ BuiltInEntityCharOrTagRx
text_formatter.format source_string
else
[{ text: source_string }]
end
end

node.subs.replace prev_subs if prev_subs

#move_down @theme.block_margin_top unless at_page_top?
theme_margin :block, :top

if (node.option? 'autofit') || (node.document.attr? 'autofit-option')
adjusted_font_size = theme_font_size_autofit source_chunks, :code
else
adjusted_font_size = nil
end

keep_together do |box_height = nil|
caption_height = node.title? ? (layout_caption node) : 0
theme_font :code do
Expand All @@ -909,7 +918,9 @@ def convert_listing_or_literal node
end

pad_box @theme.code_padding do
typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height), color: (@theme.code_font_color || @font_color)
typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
color: (@theme.code_font_color || @font_color),
size: adjusted_font_size
end
end
end
Expand Down Expand Up @@ -944,6 +955,9 @@ def extract_conums string
end

# Restore the conums into the Array of formatted text fragments
#--
# QUESTION can this be done more efficiently?
# QUESTION can we reuse arrange_fragments_by_line?
def restore_conums fragments, conum_mapping
lines = []
line_num = 0
Expand Down Expand Up @@ -1985,6 +1999,75 @@ def theme_font category, opts = {}
@text_transform = prev_transform if transform
end

# Calculate the font size (down to the minimum font size) that would allow
# all the specified fragments to fit in the available width without wrapping lines.
#
# Return the calculated font size if an adjustment is necessary or nil if no
# font size adjustment is necessary.
def theme_font_size_autofit fragments, category
arranger = arrange_fragments_by_line fragments
adjusted_font_size = nil
theme_font category do
# NOTE finalizing the line here generates fragments using current font settings
arranger.finalize_line
actual_width = width_of_fragments arranger.fragments
unless ::Array === (padding = @theme[%(#{category}_padding)])
padding = [padding] * 4
end
bounds.add_left_padding(p_left = padding[3] || 0)
bounds.add_right_padding(p_right = padding[1] || 0)
if actual_width > bounds.width
adjusted_font_size = ((bounds.width * font_size).to_f / actual_width).with_precision 4
if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
adjusted_font_size = min
end
else
adjusted_font_size = nil
end
bounds.subtract_left_padding p_left
bounds.subtract_right_padding p_right
end
adjusted_font_size
end

# Arrange fragments by line in an arranger and return an unfinalized arranger.
#
# Finalizing the arranger is deferred since it must be done in the context of
# the global font settings you want applied to each fragment.
def arrange_fragments_by_line fragments, opts = {}
arranger = ::Prawn::Text::Formatted::Arranger.new self
by_line = arranger.consumed = []
fragments.each do |fragment|
if (txt = fragment[:text]) == EOL
by_line << fragment
elsif txt.include? EOL
txt.scan(LineScanRx) do |line|
by_line << fragment.merge(text: line)
end
else
by_line << fragment
end
end
arranger
end

# Calculate the width that is needed to print all the
# fragments without wrapping any lines.
#
# This method assumes endlines are represented as discrete entries in the
# fragments array.
def width_of_fragments fragments
line_widths = [0]
fragments.each do |fragment|
if fragment.text == EOL
line_widths << 0
else
line_widths[-1] += fragment.width
end
end
line_widths.max
end

# TODO document me, esp the first line formatting functionality
def typeset_text string, line_metrics, opts = {}
move_down line_metrics.padding_top
Expand Down
3 changes: 3 additions & 0 deletions lib/asciidoctor-pdf/core_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative 'core_ext/array'
require_relative 'core_ext/numeric'
#require_relative 'core_ext/ostruct'
14 changes: 10 additions & 4 deletions lib/asciidoctor-pdf/core_ext/array.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
class Array
def to_h
Hash[to_a]
end unless respond_to? :to_h
end if RUBY_VERSION < '2.1.0'
if RUBY_VERSION < '2.1.0'
def to_h
Hash[to_a]
end unless respond_to? :to_h
end

def delete_all *entries
entries.map {|entry| delete entry }.compact
end unless respond_to? :delete_all
end
11 changes: 11 additions & 0 deletions lib/asciidoctor-pdf/core_ext/numeric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Numeric
def with_precision precision
precision = precision.to_i
if precision > 0
factor = 10 ** precision
(self * factor).truncate / factor.to_f
else
self.truncate
end
end unless respond_to? :with_precision
end
10 changes: 10 additions & 0 deletions lib/asciidoctor-pdf/sanitizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ def sanitize string
.gsub(BuiltInEntityCharRx, BuiltInEntityChars)
end

def unescape_all string
string
.gsub(NumericCharRefRx) { [$1.to_i].pack('U*') }
.gsub(BuiltInEntityCharRx, BuiltInEntityChars)
end

def unescape_builtin string
string.gsub(BuiltInEntityCharRx, BuiltInEntityChars)
end

def upcase_pcdata string
if BuiltInEntityCharOrTagRx =~ string
string.gsub(SegmentPcdataRx) { $2 ? $2.upcase : $1 }
Expand Down

0 comments on commit 3e3fa7b

Please sign in to comment.