asciidoctor-pdf 1.5.0.beta.8 → 1.5.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +49 -0
  3. data/LICENSE.adoc +1 -1
  4. data/NOTICE.adoc +1 -1
  5. data/README.adoc +43 -47
  6. data/asciidoctor-pdf.gemspec +5 -1
  7. data/bin/asciidoctor-pdf-optimize +1 -1
  8. data/data/themes/base-theme.yml +4 -3
  9. data/data/themes/default-theme.yml +10 -5
  10. data/docs/theming-guide.adoc +286 -22
  11. data/lib/asciidoctor-pdf.rb +1 -0
  12. data/lib/asciidoctor-pdf/converter.rb +1 -0
  13. data/lib/asciidoctor-pdf/version.rb +1 -0
  14. data/lib/asciidoctor/pdf.rb +13 -2
  15. data/lib/asciidoctor/pdf/converter.rb +3962 -3955
  16. data/lib/asciidoctor/pdf/ext.rb +9 -0
  17. data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -0
  18. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +1 -0
  19. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +1 -0
  20. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +1 -0
  21. data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +18 -16
  22. data/lib/asciidoctor/pdf/ext/asciidoctor/list.rb +3 -2
  23. data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +2 -1
  24. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +3 -4
  25. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +8 -6
  26. data/lib/asciidoctor/pdf/ext/core.rb +2 -0
  27. data/lib/asciidoctor/pdf/ext/core/array.rb +1 -0
  28. data/lib/asciidoctor/pdf/ext/core/hash.rb +1 -0
  29. data/lib/asciidoctor/pdf/ext/core/numeric.rb +4 -3
  30. data/lib/asciidoctor/pdf/ext/core/object.rb +1 -0
  31. data/lib/asciidoctor/pdf/ext/core/quantifiable_stdout.rb +8 -1
  32. data/lib/asciidoctor/pdf/ext/core/regexp.rb +1 -0
  33. data/lib/asciidoctor/pdf/ext/core/string.rb +6 -7
  34. data/lib/asciidoctor/pdf/ext/pdf-core.rb +1 -0
  35. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +3 -4
  36. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +2 -1
  37. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +1 -0
  38. data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +11 -8
  39. data/lib/asciidoctor/pdf/ext/prawn-table.rb +2 -1
  40. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +9 -10
  41. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +62 -57
  42. data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +5 -3
  43. data/lib/asciidoctor/pdf/ext/prawn-templates.rb +1 -0
  44. data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
  45. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +73 -72
  46. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +814 -818
  47. data/lib/asciidoctor/pdf/ext/prawn/font/afm.rb +4 -3
  48. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +2 -1
  49. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +7 -2
  50. data/lib/asciidoctor/pdf/ext/prawn/images.rb +45 -44
  51. data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
  52. data/lib/asciidoctor/pdf/ext/rouge.rb +1 -1
  53. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +181 -149
  54. data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
  55. data/lib/asciidoctor/pdf/formatted_text.rb +2 -0
  56. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +35 -34
  57. data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +8 -7
  58. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +13 -14
  59. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +112 -133
  60. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +43 -41
  61. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +15 -14
  62. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
  63. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +46 -37
  64. data/lib/asciidoctor/pdf/formatted_text/transform.rb +371 -352
  65. data/lib/asciidoctor/pdf/index_catalog.rb +99 -95
  66. data/lib/asciidoctor/pdf/measurements.rb +51 -48
  67. data/lib/asciidoctor/pdf/optimizer.rb +34 -31
  68. data/lib/asciidoctor/pdf/pdfmark.rb +34 -33
  69. data/lib/asciidoctor/pdf/roman_numeral.rb +80 -79
  70. data/lib/asciidoctor/pdf/sanitizer.rb +38 -37
  71. data/lib/asciidoctor/pdf/temporary_path.rb +10 -9
  72. data/lib/asciidoctor/pdf/text_transformer.rb +101 -100
  73. data/lib/asciidoctor/pdf/theme_loader.rb +258 -256
  74. data/lib/asciidoctor/pdf/version.rb +5 -4
  75. metadata +55 -6
  76. data/lib/asciidoctor/pdf/ext/rouge/themes/bw.rb +0 -39
  77. data/lib/asciidoctor/pdf/ext/ttfunk.rb +0 -9
@@ -1,21 +1,22 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Asciidoctor::PDF::FormattedText
3
- module InlineTextAligner
4
- module_function
4
+ module InlineTextAligner
5
+ module_function
5
6
 
6
- def render_behind fragment
7
- document = fragment.document
8
- text = fragment.text
9
- x = fragment.left
10
- y = fragment.baseline
11
- align = fragment.format_state[:align]
12
- if align == :center || align == :right
13
- if (gap_width = fragment.width - (document.width_of text)) != 0
14
- x += gap_width * (align == :center ? 0.5 : 1)
7
+ def render_behind fragment
8
+ document = fragment.document
9
+ text = fragment.text
10
+ x = fragment.left
11
+ y = fragment.baseline
12
+ align = fragment.format_state[:align]
13
+ if align == :center || align == :right
14
+ if (gap_width = fragment.width - (document.width_of text)) != 0
15
+ x += gap_width * (align == :center ? 0.5 : 1)
16
+ end
15
17
  end
18
+ document.draw_text! text, at: [x, y]
19
+ fragment.conceal
16
20
  end
17
- document.draw_text! text, at: [x, y]
18
- fragment.conceal
19
21
  end
20
22
  end
21
- end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module PDF
5
+ module FormattedText
6
+ module SourceWrap
7
+ NoBreakSpace = ?\u00a0
8
+
9
+ def wrap array
10
+ initialize_wrap array
11
+ stop = nil
12
+ highlight_line = nil
13
+ unconsumed = @arranger.unconsumed
14
+ if (linenum_fragment = unconsumed[0] || {})[:linenum]
15
+ linenum_spacer = { text: NoBreakSpace + (' ' * (linenum_fragment[:text].length - 1)) }
16
+ end
17
+ until stop
18
+ if linenum_spacer && (first_fragment = unconsumed[0])
19
+ if first_fragment[:linenum]
20
+ highlight_line = (second_fragment = unconsumed[1]) && (second_fragment[:highlight]) ? second_fragment.dup : nil
21
+ else
22
+ first_fragment[:text] = first_fragment[:text].lstrip
23
+ @arranger.unconsumed.unshift highlight_line if highlight_line
24
+ @arranger.unconsumed.unshift linenum_spacer.dup
25
+ end
26
+ end
27
+ @line_wrap.wrap_line document: @document, kerning: @kerning, width: available_width, arranger: @arranger, disable_wrap_by_char: @disable_wrap_by_char
28
+ if enough_height_for_this_line?
29
+ move_baseline_down
30
+ print_line
31
+ else
32
+ stop = true
33
+ end
34
+ stop ||= @single_line || @arranger.finished?
35
+ end
36
+ @text = @printed_lines.join ?\n
37
+ @everything_printed = @arranger.finished?
38
+ @arranger.unconsumed
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,46 +1,55 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Asciidoctor::Pdf::FormattedText
3
- module TextBackgroundAndBorderRenderer
4
- module_function
4
+ module TextBackgroundAndBorderRenderer
5
+ module_function
5
6
 
6
- # render_behind is called before the text is printed
7
- def render_behind fragment
8
- return if (pdf = fragment.document).scratch?
9
- data = fragment.format_state
10
- if (border_offset = data[:border_offset])
11
- at = [fragment.left - border_offset, fragment.top + border_offset]
12
- width = fragment.width + border_offset * 2
13
- height = fragment.height + border_offset * 2
14
- else
15
- at = fragment.top_left
16
- width = fragment.width
17
- height = fragment.height
18
- end
19
- border_radius = data[:border_radius]
20
- if (background_color = data[:background_color])
21
- prev_fill_color = pdf.fill_color
22
- pdf.fill_color background_color
23
- if border_radius
24
- pdf.fill_rounded_rectangle at, width, height, border_radius
7
+ DummyText = ?\u0000
8
+
9
+ # render_behind is called before the text is printed
10
+ def render_behind fragment
11
+ return if (pdf = fragment.document).scratch?
12
+ data = fragment.format_state
13
+ if data[:inline_block]
14
+ padding = (height = fragment.line_height) - fragment.height
15
+ at = [fragment.left, fragment.top + padding * 0.5]
16
+ width = data[:extend] ? (pdf.bounds.width - fragment.left) : fragment.width
17
+ fragment.conceal if fragment.text == DummyText
18
+ elsif (border_offset = data[:border_offset])
19
+ at = [fragment.left, fragment.top + border_offset]
20
+ width = fragment.width
21
+ height = fragment.height + border_offset * 2
25
22
  else
26
- pdf.fill_rectangle at, width, height
23
+ at = fragment.top_left
24
+ width = fragment.width
25
+ height = fragment.height
27
26
  end
28
- pdf.fill_color prev_fill_color
29
- end
30
- if (border_width = data[:border_width])
31
- border_color = data[:border_color]
32
- prev_stroke_color = pdf.stroke_color
33
- prev_line_width = pdf.line_width
34
- pdf.stroke_color border_color
35
- pdf.line_width border_width
36
- if border_radius
37
- pdf.stroke_rounded_rectangle at, width, height, border_radius
38
- else
39
- pdf.stroke_rectangle at, width, height
27
+ border_radius = data[:border_radius]
28
+ if (background_color = data[:background_color])
29
+ prev_fill_color = pdf.fill_color
30
+ pdf.fill_color background_color
31
+ if border_radius
32
+ pdf.fill_rounded_rectangle at, width, height, border_radius
33
+ else
34
+ pdf.fill_rectangle at, width, height
35
+ end
36
+ pdf.fill_color prev_fill_color
37
+ end
38
+ if (border_width = data[:border_width])
39
+ border_color = data[:border_color]
40
+ prev_stroke_color = pdf.stroke_color
41
+ prev_line_width = pdf.line_width
42
+ pdf.stroke_color border_color
43
+ pdf.line_width border_width
44
+ if border_radius
45
+ pdf.stroke_rounded_rectangle at, width, height, border_radius
46
+ else
47
+ pdf.stroke_rectangle at, width, height
48
+ end
49
+ pdf.stroke_color prev_stroke_color
50
+ pdf.line_width prev_line_width
40
51
  end
41
- pdf.stroke_color prev_stroke_color
42
- pdf.line_width prev_line_width
52
+ nil
43
53
  end
44
54
  end
45
55
  end
46
- end
@@ -1,375 +1,394 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Asciidoctor
3
- module PDF
4
- module FormattedText
5
- class Transform
6
- LF = ?\n
7
- ZeroWidthSpace = ?\u200b
8
- CharEntityTable = {
9
- amp: ?&,
10
- apos: ?',
11
- gt: ?>,
12
- lt: ?<,
13
- nbsp: ?\u00a0,
14
- quot: ?",
15
- }
16
- CharRefRx = /&(?:(#{CharEntityTable.keys.join ?|})|#(?:(\d\d\d{0,4})|x([a-f\d][a-f\d][a-f\d]{0,3})));/
17
- TextDecorationTable = { 'underline' => :underline, 'line-through' => :strikethrough }
18
- ThemeKeyToFragmentProperty = {
19
- 'background_color' => :background_color,
20
- 'border_color' => :border_color,
21
- 'border_offset' => :border_offset,
22
- 'border_radius' => :border_radius,
23
- 'border_width' => :border_width,
24
- 'font_color' => :color,
25
- 'font_family' => :font,
26
- 'font_size' => :size,
27
- 'text_decoration_color' => :text_decoration_color,
28
- 'text_decoration_width' => :text_decoration_width,
29
- }
30
- #DummyText = ?\u0000
4
+ module PDF
5
+ module FormattedText
6
+ class Transform
7
+ LF = ?\n
8
+ ZeroWidthSpace = ?\u200b
9
+ CharEntityTable = { amp: ?&, apos: ?', gt: ?>, lt: ?<, nbsp: ?\u00a0, quot: ?" }
10
+ CharRefRx = /&(?:(#{CharEntityTable.keys.join ?|})|#(?:(\d\d\d{0,4})|x([a-f\d][a-f\d][a-f\d]{0,3})));/
11
+ HexColorRx = /^#[a-fA-F0-9]{6}$/
12
+ TextDecorationTable = { 'underline' => :underline, 'line-through' => :strikethrough }
13
+ ThemeKeyToFragmentProperty = {
14
+ 'background_color' => :background_color,
15
+ 'border_color' => :border_color,
16
+ 'border_offset' => :border_offset,
17
+ 'border_radius' => :border_radius,
18
+ 'border_width' => :border_width,
19
+ 'font_color' => :color,
20
+ 'font_family' => :font,
21
+ 'font_size' => :size,
22
+ 'text_decoration_color' => :text_decoration_color,
23
+ 'text_decoration_width' => :text_decoration_width,
24
+ }
25
+ #DummyText = ?\u0000
31
26
 
32
- def initialize(options = {})
33
- @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
34
- # TODO add support for character spacing
35
- if (theme = options[:theme])
36
- @theme_settings = {
37
- button: {
38
- color: theme.button_font_color,
39
- font: theme.button_font_family,
40
- size: theme.button_font_size,
41
- styles: (to_styles theme.button_font_style),
42
- background_color: (button_bg_color = theme.button_background_color),
43
- border_width: (button_border_width = theme.button_border_width),
44
- border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
45
- border_offset: (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset,
46
- border_radius: button_bg_or_border && theme.button_border_radius,
47
- callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
48
- }.compact,
49
- code: {
50
- color: theme.literal_font_color,
51
- font: theme.literal_font_family,
52
- size: theme.literal_font_size,
53
- styles: (to_styles theme.literal_font_style),
54
- background_color: (monospaced_bg_color = theme.literal_background_color),
55
- border_width: (monospaced_border_width = theme.literal_border_width),
56
- border_color: monospaced_border_width && (theme.literal_border_color || theme.base_border_color),
57
- border_offset: (monospaced_bg_or_border = monospaced_bg_color || monospaced_border_width) && theme.literal_border_offset,
58
- border_radius: monospaced_bg_or_border && theme.literal_border_radius,
59
- callback: monospaced_bg_or_border && [TextBackgroundAndBorderRenderer],
60
- }.compact,
61
- key: {
62
- color: theme.key_font_color,
63
- font: theme.key_font_family || theme.literal_font_family,
64
- size: theme.key_font_size,
65
- styles: (to_styles theme.key_font_style),
66
- background_color: (key_bg_color = theme.key_background_color),
67
- border_width: (key_border_width = theme.key_border_width),
68
- border_color: key_border_width && (theme.key_border_color || theme.base_border_color),
69
- border_offset: (key_bg_or_border = key_bg_color || key_border_width) && theme.key_border_offset,
70
- border_radius: key_bg_or_border && theme.key_border_radius,
71
- callback: key_bg_or_border && [TextBackgroundAndBorderRenderer],
72
- }.compact,
73
- link: {
74
- color: theme.link_font_color,
75
- font: theme.link_font_family,
76
- size: theme.link_font_size,
77
- styles: (to_styles theme.link_font_style, theme.link_text_decoration),
78
- text_decoration_color: theme.link_text_decoration_color,
79
- text_decoration_width: theme.link_text_decoration_width,
80
- }.compact,
81
- mark: {
82
- color: theme.mark_font_color,
83
- styles: (to_styles theme.mark_font_style),
84
- background_color: (mark_bg_color = theme.mark_background_color),
85
- border_offset: mark_bg_color && theme.mark_border_offset,
86
- callback: mark_bg_color && [TextBackgroundAndBorderRenderer],
87
- }.compact,
88
- }
89
- revise_roles = [].to_set
90
- theme.each_pair.reduce @theme_settings do |accum, (key, val)|
91
- if (key = key.to_s).start_with? 'role_'
92
- role, key = (key.slice 5, key.length).split '_', 2
93
- if (prop = ThemeKeyToFragmentProperty[key])
94
- (accum[role] ||= {})[prop] = val
95
- elsif key == 'font_style' || key == 'text_decoration'
96
- revise_roles << role
27
+ def initialize options = {}
28
+ @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
29
+ # TODO: add support for character spacing
30
+ if (theme = options[:theme])
31
+ @theme_settings = {
32
+ button: {
33
+ color: theme.button_font_color,
34
+ font: theme.button_font_family,
35
+ size: theme.button_font_size,
36
+ styles: (to_styles theme.button_font_style),
37
+ background_color: (button_bg_color = theme.button_background_color),
38
+ border_width: (button_border_width = theme.button_border_width),
39
+ border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
40
+ border_offset: (button_border_offset = (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset),
41
+ border_radius: button_bg_or_border && theme.button_border_radius,
42
+ align: button_border_offset && :center,
43
+ callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
44
+ }.compact,
45
+ code: {
46
+ color: theme.literal_font_color,
47
+ font: theme.literal_font_family,
48
+ size: theme.literal_font_size,
49
+ styles: (to_styles theme.literal_font_style),
50
+ background_color: (mono_bg_color = theme.literal_background_color),
51
+ border_width: (mono_border_width = theme.literal_border_width),
52
+ border_color: mono_border_width && (theme.literal_border_color || theme.base_border_color),
53
+ border_offset: (mono_border_offset = (mono_bg_or_border = mono_bg_color || mono_border_width) && theme.literal_border_offset),
54
+ border_radius: mono_bg_or_border && theme.literal_border_radius,
55
+ align: mono_border_offset && :center,
56
+ callback: mono_bg_or_border && [TextBackgroundAndBorderRenderer],
57
+ }.compact,
58
+ key: {
59
+ color: theme.key_font_color,
60
+ font: theme.key_font_family || theme.literal_font_family,
61
+ size: theme.key_font_size,
62
+ styles: (to_styles theme.key_font_style),
63
+ background_color: (key_bg_color = theme.key_background_color),
64
+ border_width: (key_border_width = theme.key_border_width),
65
+ border_color: key_border_width && (theme.key_border_color || theme.base_border_color),
66
+ border_offset: (key_border_offset = (key_bg_or_border = key_bg_color || key_border_width) && theme.key_border_offset),
67
+ border_radius: key_bg_or_border && theme.key_border_radius,
68
+ align: key_border_offset && :center,
69
+ callback: key_bg_or_border && [TextBackgroundAndBorderRenderer],
70
+ }.compact,
71
+ link: {
72
+ color: theme.link_font_color,
73
+ font: theme.link_font_family,
74
+ size: theme.link_font_size,
75
+ styles: (to_styles theme.link_font_style, theme.link_text_decoration),
76
+ text_decoration_color: theme.link_text_decoration_color,
77
+ text_decoration_width: theme.link_text_decoration_width,
78
+ }.compact,
79
+ mark: {
80
+ color: theme.mark_font_color,
81
+ styles: (to_styles theme.mark_font_style),
82
+ background_color: (mark_bg_color = theme.mark_background_color),
83
+ border_offset: (mark_border_offset = mark_bg_color && theme.mark_border_offset),
84
+ align: mark_border_offset && :center,
85
+ callback: mark_bg_color && [TextBackgroundAndBorderRenderer],
86
+ }.compact,
87
+ }
88
+ revise_roles = [].to_set
89
+ theme.each_pair.each_with_object @theme_settings do |(key, val), accum|
90
+ next unless (key = key.to_s).start_with? 'role_'
91
+ role, key = (key.slice 5, key.length).split '_', 2
92
+ if (prop = ThemeKeyToFragmentProperty[key])
93
+ (accum[role] ||= {})[prop] = val
94
+ #elsif key == 'font_kerning'
95
+ # unless (resolved_val = val == 'none' ? false : (val == 'normal' ? true : nil)).nil?
96
+ # (accum[role] ||= {})[:kerning] = resolved_val
97
+ # end
98
+ elsif key == 'font_style' || key == 'text_decoration'
99
+ revise_roles << role
100
+ end
101
+ end
102
+ revise_roles.each_with_object @theme_settings do |role, accum|
103
+ (accum[role] ||= {})[:styles] = to_styles theme[%(role_#{role}_font_style)], theme[%(role_#{role}_text_decoration)]
104
+ end
105
+ @theme_settings['line-through'] = { styles: [:strikethrough].to_set } unless @theme_settings.key? 'line-through'
106
+ @theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
107
+ unless @theme_settings.key? 'big'
108
+ if (base_font_size_large = theme.base_font_size_large)
109
+ @theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 4}em) }
110
+ else
111
+ @theme_settings['big'] = { size: '1.1667em' }
112
+ end
113
+ end
114
+ unless @theme_settings.key? 'small'
115
+ if (base_font_size_small = theme.base_font_size_small)
116
+ @theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 4}em) }
117
+ else
118
+ @theme_settings['small'] = { size: '0.8333em' }
119
+ end
120
+ end
121
+ else
122
+ @theme_settings = {
123
+ button: { font: 'Courier', styles: [:bold].to_set },
124
+ code: { font: 'Courier' },
125
+ key: { font: 'Courier', styles: [:italic].to_set },
126
+ link: { color: '0000FF' },
127
+ mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
128
+ 'line-through' => { styles: [:strikethrough].to_set },
129
+ 'underline' => { styles: [:underline].to_set },
130
+ 'big' => { size: '1.667em' },
131
+ 'small' => { size: '0.8333em' },
132
+ }
97
133
  end
98
134
  end
99
- accum
100
- end
101
- revise_roles.reduce @theme_settings do |accum, role|
102
- (accum[role] ||= {})[:styles] = to_styles theme[%(role_#{role}_font_style)], theme[%(role_#{role}_text_decoration)]
103
- accum
104
- end
105
- @theme_settings['line-through'] = { styles: [:strikethrough].to_set } unless @theme_settings.key? 'line-through'
106
- @theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
107
- unless @theme_settings.key? 'big'
108
- if (base_font_size_large = theme.base_font_size_large)
109
- @theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 4}em) }
110
- else
111
- @theme_settings['big'] = { size: '1.1667em' }
112
- end
113
- end
114
- unless @theme_settings.key? 'small'
115
- if (base_font_size_small = theme.base_font_size_small)
116
- @theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 4}em) }
117
- else
118
- @theme_settings['small'] = { size: '0.8333em' }
119
- end
120
- end
121
- else
122
- @theme_settings = {
123
- button: { font: 'Courier', styles: [:bold].to_set },
124
- code: { font: 'Courier' },
125
- key: { font: 'Courier', styles: [:italic].to_set },
126
- link: { color: '0000FF' },
127
- mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
128
- 'line-through' => { styles: [:strikethrough].to_set },
129
- 'underline' => { styles: [:underline].to_set },
130
- 'big' => { size: '1.667em' },
131
- 'small' => { size: '0.8333em' },
132
- }
133
- end
134
- end
135
135
 
136
- def apply(parsed, fragments = [], inherited = nil)
137
- previous_fragment_is_text = false
138
- # NOTE we use each since using inject is slower than a manual loop
139
- parsed.each do |node|
140
- case node[:type]
141
- when :element
142
- # case 1: non-void element
143
- if node.key?(:pcdata)
144
- unless (pcdata = node[:pcdata]).empty?
145
- tag_name = node[:name]
146
- attributes = node[:attributes]
147
- parent = clone_fragment inherited
148
- # NOTE decorate child fragments with inherited properties from this element
149
- apply(pcdata, fragments, (build_fragment parent, tag_name, attributes))
150
- previous_fragment_is_text = false
151
- # NOTE skip element if it has no children
152
- #else
153
- # # NOTE handle an empty anchor element (i.e., <a ...></a>)
154
- # if (tag_name = node[:name]) == :a
155
- # seed = clone_fragment inherited, text: DummyText
156
- # fragments << build_fragment(seed, tag_name, node[:attributes])
157
- # previous_fragment_is_text = false
158
- # end
136
+ def apply parsed, fragments = [], inherited = nil
137
+ previous_fragment_is_text = false
138
+ # NOTE we use each since using inject is slower than a manual loop
139
+ parsed.each do |node|
140
+ case node[:type]
141
+ when :element
142
+ # case 1: non-void element
143
+ if node.key? :pcdata
144
+ # NOTE skip element if it has no children
145
+ if (pcdata = node[:pcdata]).empty?
146
+ ## NOTE handle an empty anchor element (i.e., <a ...></a>)
147
+ #if (tag_name = node[:name]) == :a
148
+ # seed = clone_fragment inherited, text: DummyText
149
+ # fragments << build_fragment(seed, tag_name, node[:attributes])
150
+ # previous_fragment_is_text = false
151
+ #end
152
+ else
153
+ tag_name = node[:name]
154
+ attributes = node[:attributes]
155
+ parent = clone_fragment inherited
156
+ # NOTE decorate child fragments with inherited properties from this element
157
+ apply pcdata, fragments, (build_fragment parent, tag_name, attributes)
158
+ previous_fragment_is_text = false
159
+ end
160
+ # case 2: void element
161
+ else
162
+ case node[:name]
163
+ when :br
164
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
165
+ fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{LF}))
166
+ else
167
+ fragments << { text: LF }
168
+ end
169
+ previous_fragment_is_text = true
170
+ when :img
171
+ attributes = node[:attributes]
172
+ fragment = {
173
+ image_path: attributes[:tmp] == 'true' ? attributes[:src].extend(TemporaryPath) : attributes[:src],
174
+ image_format: attributes[:format],
175
+ # a zero-width space in the text will cause the image to be duplicated
176
+ text: (attributes[:alt].delete ZeroWidthSpace),
177
+ callback: [InlineImageRenderer],
178
+ }
179
+ if inherited && (link = inherited[:link])
180
+ fragment[:link] = link
181
+ end
182
+ if (img_w = attributes[:width])
183
+ fragment[:image_width] = img_w
184
+ end
185
+ if (img_fit = attributes[:fit])
186
+ fragment[:image_fit] = img_fit
187
+ end
188
+ fragments << fragment
189
+ previous_fragment_is_text = false
190
+ end
191
+ end
192
+ when :text
193
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
194
+ fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
195
+ else
196
+ fragments << (clone_fragment inherited, text: node[:value])
197
+ end
198
+ previous_fragment_is_text = true
199
+ when :charref
200
+ if (ref_type = node[:reference_type]) == :name
201
+ text = CharEntityTable[node[:value]]
202
+ elsif ref_type == :decimal
203
+ # FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
204
+ text = [node[:value]].pack 'U1'
205
+ else
206
+ # FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
207
+ text = [(node[:value].to_i 16)].pack 'U1'
208
+ end
209
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
210
+ fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{text}))
211
+ else
212
+ fragments << (clone_fragment inherited, text: text)
213
+ end
214
+ previous_fragment_is_text = true
215
+ end
159
216
  end
160
- # case 2: void element
161
- else
162
- case node[:name]
163
- when :br
164
- if @merge_adjacent_text_nodes && previous_fragment_is_text
165
- fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{LF}))
166
- else
167
- fragments << { text: LF }
217
+ fragments
218
+ end
219
+
220
+ def build_fragment fragment, tag_name, attrs = {}
221
+ styles = (fragment[:styles] ||= ::Set.new)
222
+ case tag_name
223
+ when :strong
224
+ styles << :bold
225
+ when :em
226
+ styles << :italic
227
+ when :button, :code, :key, :mark
228
+ update_fragment fragment, @theme_settings[tag_name]
229
+ when :color
230
+ if (rgb = attrs[:rgb])
231
+ case rgb.chr
232
+ when '#'
233
+ fragment[:color] = rgb.slice 1, rgb.length
234
+ when '['
235
+ # treat value as CMYK array (e.g., "[50, 100, 0, 0]")
236
+ fragment[:color] = rgb.slice(1, rgb.length).chomp(']').split(', ').map(&:to_i)
237
+ # ...or we could honor an rgb array too
238
+ #case (vals = rgb.slice(1, rgb.length).chomp(']').split(', ')).size
239
+ #when 4
240
+ # fragment[:color] = vals.map(&:to_i)
241
+ #when 3
242
+ # fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
243
+ #end
244
+ else
245
+ fragment[:color] = rgb
246
+ end
247
+ # QUESTION should we even support r,g,b and c,m,y,k as individual values?
248
+ elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
249
+ fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
250
+ elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
251
+ fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
168
252
  end
169
- previous_fragment_is_text = true
170
- when :img
171
- attributes = node[:attributes]
172
- fragment = {
173
- image_path: attributes[:tmp] == 'true' ? attributes[:src].extend(TemporaryPath) : attributes[:src],
174
- image_format: attributes[:format],
175
- # a zero-width space in the text will cause the image to be duplicated
176
- text: (attributes[:alt].delete ZeroWidthSpace),
177
- callback: [InlineImageRenderer],
178
- }
179
- if inherited && (link = inherited[:link])
180
- fragment[:link] = link
253
+ when :font
254
+ if (value = attrs[:name])
255
+ fragment[:font] = value
181
256
  end
182
- if (img_w = attributes[:width])
183
- fragment[:image_width] = img_w
257
+ if (value = attrs[:size])
258
+ # FIXME: can we make this comparison more robust / accurate?
259
+ if (f_value = value.to_f).to_s == value || value.to_i.to_s == value
260
+ fragment[:size] = f_value
261
+ elsif value != '1em'
262
+ fragment[:size] = value
263
+ end
184
264
  end
185
- fragments << fragment
186
- previous_fragment_is_text = false
265
+ # NOTE width is used for font-based icons
266
+ if (value = attrs[:width])
267
+ fragment[:width] = value
268
+ fragment[:align] = :center
269
+ fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
270
+ end
271
+ #if (value = attrs[:character_spacing])
272
+ # fragment[:character_spacing] = value.to_f
273
+ #end
274
+ when :a
275
+ visible = true
276
+ # a element can have no attributes, so short-circuit if that's the case
277
+ unless attrs.empty?
278
+ # NOTE href, anchor, and name are mutually exclusive; nesting is not supported
279
+ if (value = attrs[:anchor])
280
+ fragment[:anchor] = value
281
+ elsif (value = attrs[:href])
282
+ fragment[:link] = (value.include? ';') ? (value.gsub CharRefRx do
283
+ $1 ? CharEntityTable[$1.to_sym] : [$2 ? $2.to_i : ($3.to_i 16)].pack('U1')
284
+ end) : value
285
+ elsif (value = attrs[:id] || attrs[:name])
286
+ # NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
287
+ fragment = { name: value, callback: [InlineDestinationMarker] }
288
+ if (type = attrs[:type])
289
+ fragment[:type] = type.to_sym
290
+ end
291
+ visible = nil
292
+ end
293
+ end
294
+ update_fragment fragment, @theme_settings[:link] if visible
295
+ when :sub
296
+ styles << :subscript
297
+ when :sup
298
+ styles << :superscript
299
+ when :del
300
+ styles << :strikethrough
301
+ when :span
302
+ # NOTE spaces in style value are superfluous for our purpose; split drops record after trailing ;
303
+ attrs[:style].tr(' ', '').split(';').each do |style|
304
+ pname, pvalue = style.split ':', 2
305
+ # TODO: text-transform
306
+ case pname
307
+ when 'color'
308
+ # TODO: check whether the value is a valid hex color?
309
+ case pvalue.length
310
+ when 6
311
+ fragment[:color] = pvalue
312
+ when 7
313
+ fragment[:color] = pvalue.slice 1, 6 if pvalue.start_with? '#'
314
+ end
315
+ # QUESTION should we support the 3 character form?
316
+ #when 3
317
+ # fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
318
+ #when 4
319
+ # fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
320
+ when 'font-weight'
321
+ styles << :bold if pvalue == 'bold'
322
+ when 'font-style'
323
+ styles << :italic if pvalue == 'italic'
324
+ when 'align', 'text-align'
325
+ fragment[:align] = pvalue.to_sym
326
+ fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
327
+ when 'width'
328
+ # NOTE implicitly activates inline-block behavior
329
+ fragment[:width] = pvalue
330
+ when 'background-color' # background-color needed to support syntax highlighters
331
+ if (pvalue.start_with? '#') && (HexColorRx.match? pvalue)
332
+ fragment[:background_color] = pvalue.slice 1, pvalue.length
333
+ fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
334
+ end
335
+ end
336
+ end if attrs.key? :style
187
337
  end
338
+ # TODO: we could limit to select tags, but doesn't seem to really affect performance
339
+ attrs[:class].split.each do |class_name|
340
+ next unless @theme_settings.key? class_name
341
+ update_fragment fragment, @theme_settings[class_name]
342
+ if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
343
+ fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
344
+ fragment[:align] = :center if fragment[:border_offset]
345
+ end
346
+ end if attrs.key? :class
347
+ fragment.delete :styles if styles.empty?
348
+ fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner] if fragment.key? :align
349
+ fragment
188
350
  end
189
- when :text
190
- if @merge_adjacent_text_nodes && previous_fragment_is_text
191
- fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
192
- else
193
- fragments << (clone_fragment inherited, text: node[:value])
194
- end
195
- previous_fragment_is_text = true
196
- when :charref
197
- if (ref_type = node[:reference_type]) == :name
198
- text = CharEntityTable[node[:value]]
199
- elsif ref_type == :decimal
200
- # FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
201
- text = [node[:value]].pack('U1')
202
- else
203
- # FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
204
- text = [(node[:value].to_i 16)].pack('U1')
205
- end
206
- if @merge_adjacent_text_nodes && previous_fragment_is_text
207
- fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{text}))
208
- else
209
- fragments << (clone_fragment inherited, text: text)
210
- end
211
- previous_fragment_is_text = true
212
- end
213
- end
214
- fragments
215
- end
216
351
 
217
- def build_fragment(fragment, tag_name, attrs = {})
218
- styles = (fragment[:styles] ||= ::Set.new)
219
- case tag_name
220
- when :strong
221
- styles << :bold
222
- when :em
223
- styles << :italic
224
- when :button, :code, :key, :mark
225
- fragment.update(@theme_settings[tag_name]) {|k, oval, nval| k == :styles ? (nval ? oval.merge(nval) : oval.clear) : (k == :callback ? oval.union(nval) : nval) }
226
- when :color
227
- if (rgb = attrs[:rgb])
228
- case rgb.chr
229
- when '#'
230
- fragment[:color] = rgb[1..-1]
231
- when '['
232
- # treat value as CMYK array (e.g., "[50, 100, 0, 0]")
233
- fragment[:color] = rgb[1..-1].chomp(']').split(', ').map(&:to_i)
234
- # ...or we could honor an rgb array too
235
- #case (vals = rgb[1..-1].chomp(']').split(', ')).size
236
- #when 4
237
- # fragment[:color] = vals.map(&:to_i)
238
- #when 3
239
- # fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
240
- #end
241
- else
242
- fragment[:color] = rgb
243
- end
244
- # QUESTION should we even support r,g,b and c,m,y,k as individual values?
245
- elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
246
- fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
247
- elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
248
- fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
249
- end
250
- when :font
251
- if (value = attrs[:name])
252
- fragment[:font] = value
253
- end
254
- if (value = attrs[:size])
255
- # FIXME can we make this comparison more robust / accurate?
256
- if %(#{f_value = value.to_f}) == value || %(#{value.to_i}) == value
257
- fragment[:size] = f_value
258
- elsif value != '1em'
259
- fragment[:size] = value
260
- end
261
- end
262
- if (value = attrs[:width])
263
- fragment[:width] = value
264
- if (value = attrs[:align])
265
- fragment[:align] = value.to_sym
266
- fragment[:callback] = ((fragment[:callback] ||= []) << InlineTextAligner).uniq
267
- end
268
- end
269
- #if (value = attrs[:character_spacing])
270
- # fragment[:character_spacing] = value.to_f
271
- #end
272
- when :a
273
- visible = true
274
- # a element can have no attributes, so short-circuit if that's the case
275
- unless attrs.empty?
276
- # NOTE href, anchor, and name are mutually exclusive; nesting is not supported
277
- if (value = attrs[:anchor])
278
- fragment[:anchor] = value
279
- elsif (value = attrs[:href])
280
- fragment[:link] = value.include?(';') ? value.gsub(CharRefRx) {
281
- $1 ? CharEntityTable[$1.to_sym] : [$2 ? $2.to_i : ($3.to_i 16)].pack('U1')
282
- } : value
283
- elsif (value = attrs[:name])
284
- # NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
285
- fragment[:name] = value
286
- if (type = attrs[:type])
287
- fragment[:type] = type.to_sym
352
+ def clone_fragment fragment, append = nil
353
+ if fragment
354
+ fragment = fragment.dup
355
+ fragment[:styles] = fragment[:styles].dup if fragment.key? :styles
356
+ fragment[:callback] = fragment[:callback].dup if fragment.key? :callback
357
+ else
358
+ fragment = {}
288
359
  end
289
- fragment[:callback] = ((fragment[:callback] ||= []) << InlineDestinationMarker).uniq
290
- visible = false
360
+ fragment.update append if append
361
+ fragment
291
362
  end
292
- end
293
- fragment.update(@theme_settings[:link]) {|k, oval, nval| k == :styles ? (nval ? oval.merge(nval) : oval.clear) : nval } if visible
294
- when :sub
295
- styles << :subscript
296
- when :sup
297
- styles << :superscript
298
- when :del
299
- styles << :strikethrough
300
- when :span
301
- # NOTE spaces in style attribute value are superfluous, for our purpose; split drops record after trailing ;
302
- attrs[:style].tr(' ', '').split(';').each do |style|
303
- pname, pvalue = style.split(':', 2)
304
- case pname
305
- when 'color'
306
- # QUESTION should we check whether the value is a valid hex color?
307
- unless fragment[:color]
308
- case pvalue.length
309
- when 6
310
- fragment[:color] = pvalue
311
- when 7
312
- fragment[:color] = pvalue.slice(1, 6) if pvalue.start_with?('#')
313
- # QUESTION should we support the 3 character form?
314
- #when 3
315
- # fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
316
- #when 4
317
- # fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
318
- end
319
- end
320
- when 'font-weight'
321
- if pvalue == 'bold'
322
- styles << :bold
363
+
364
+ def to_styles font_style, text_decoration = nil
365
+ case font_style
366
+ when 'bold'
367
+ styles = [:bold].to_set
368
+ when 'italic'
369
+ styles = [:italic].to_set
370
+ when 'bold_italic'
371
+ styles = [:bold, :italic].to_set
323
372
  end
324
- when 'font-style'
325
- if pvalue == 'italic'
326
- styles << :italic
373
+ if (style = TextDecorationTable[text_decoration])
374
+ styles ? (styles << style) : [style].to_set
375
+ else
376
+ styles
327
377
  end
328
- # TODO text-transform
329
378
  end
330
- end if attrs.key?(:style)
331
- end
332
- # TODO we could limit to select tags, but doesn't seem to really affect performance
333
- attrs[:class].split.each do |class_name|
334
- if @theme_settings.key? class_name
335
- fragment.update(@theme_settings[class_name]) {|k, oval, nval| k == :styles ? (nval ? oval.merge(nval) : oval.clear) : nval }
336
- if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
337
- fragment[:callback] = ((fragment[:callback] || []) << TextBackgroundAndBorderRenderer).uniq
379
+
380
+ def update_fragment fragment, props
381
+ fragment.update props do |k, oval, nval|
382
+ if k == :styles
383
+ nval ? (oval.merge nval) : oval.clear
384
+ elsif k == :callback
385
+ oval | nval
386
+ else
387
+ nval
388
+ end
389
+ end
338
390
  end
339
391
  end
340
- end if attrs.key?(:class)
341
- fragment.delete(:styles) if styles.empty?
342
- fragment
343
- end
344
-
345
- def clone_fragment fragment, append = nil
346
- if fragment
347
- fragment = fragment.dup
348
- fragment[:styles] = fragment[:styles].dup if fragment.key? :styles
349
- fragment[:callback] = fragment[:callback].dup if fragment.key? :callback
350
- else
351
- fragment = {}
352
392
  end
353
- fragment.update append if append
354
- fragment
355
393
  end
356
-
357
- def to_styles(font_style, text_decoration = nil)
358
- case font_style
359
- when 'bold'
360
- styles = [:bold].to_set
361
- when 'italic'
362
- styles = [:italic].to_set
363
- when 'bold_italic'
364
- styles = [:bold, :italic].to_set
365
- end
366
- if (style = TextDecorationTable[text_decoration])
367
- styles ? (styles << style) : [style].to_set
368
- else
369
- styles
370
- end
371
- end
372
- end
373
- end
374
- end
375
394
  end