diff --git a/data/themes/default-theme.yml b/data/themes/default-theme.yml index a6194b0d7..a173b4630 100644 --- a/data/themes/default-theme.yml +++ b/data/themes/default-theme.yml @@ -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 diff --git a/examples/chronicles.adoc b/examples/chronicles.adoc index 6470090f0..ba96dcf1d 100644 --- a/examples/chronicles.adoc +++ b/examples/chronicles.adoc @@ -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 greetings = new ArrayList(); void receive(@Observes String greet) { - this.greet = greet; + this.greetings.add(greet); } - @Override - public String getGreet() { - return greet; + @GET @Produces("application/json") + public List 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 <>. diff --git a/examples/chronicles.pdf b/examples/chronicles.pdf index 068c9897c..75e142b01 100644 Binary files a/examples/chronicles.pdf and b/examples/chronicles.pdf differ diff --git a/lib/asciidoctor-pdf/converter.rb b/lib/asciidoctor-pdf/converter.rb index e22bca518..e881dfc34 100644 --- a/lib/asciidoctor-pdf/converter.rb +++ b/lib/asciidoctor-pdf/converter.rb @@ -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' @@ -67,6 +67,7 @@ def self.unicode_char number # CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?(?=(?: ?\\?)*$)/ ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/ + LineScanRx = /\n|.+/ def initialize backend, opts super @@ -864,7 +865,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! @@ -873,13 +874,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' @@ -897,7 +896,11 @@ 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 @@ -905,6 +908,12 @@ def convert_listing_or_literal node #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 @@ -927,7 +936,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 @@ -962,6 +973,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 @@ -2004,6 +2018,73 @@ 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 + 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 diff --git a/lib/asciidoctor-pdf/core_ext.rb b/lib/asciidoctor-pdf/core_ext.rb new file mode 100644 index 000000000..1a63df710 --- /dev/null +++ b/lib/asciidoctor-pdf/core_ext.rb @@ -0,0 +1,3 @@ +require_relative 'core_ext/array' +require_relative 'core_ext/numeric' +#require_relative 'core_ext/ostruct' diff --git a/lib/asciidoctor-pdf/core_ext/array.rb b/lib/asciidoctor-pdf/core_ext/array.rb index e1ac36d75..8bb7f502e 100644 --- a/lib/asciidoctor-pdf/core_ext/array.rb +++ b/lib/asciidoctor-pdf/core_ext/array.rb @@ -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 diff --git a/lib/asciidoctor-pdf/core_ext/numeric.rb b/lib/asciidoctor-pdf/core_ext/numeric.rb new file mode 100644 index 000000000..3d246652e --- /dev/null +++ b/lib/asciidoctor-pdf/core_ext/numeric.rb @@ -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 diff --git a/lib/asciidoctor-pdf/sanitizer.rb b/lib/asciidoctor-pdf/sanitizer.rb index a3351ae2a..7854ffdf4 100644 --- a/lib/asciidoctor-pdf/sanitizer.rb +++ b/lib/asciidoctor-pdf/sanitizer.rb @@ -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 }