Class: Asciidoctor::PDF::FormattedText::Transform

Inherits:
Object
  • Object
show all
Includes:
TextTransformer
Defined in:
lib/asciidoctor/pdf/formatted_text/transform.rb

Constant Summary collapse

LF =
?\n
ZeroWidthSpace =
?\u200b
CharEntityTable =
{ amp: '&', apos: ?', gt: '>', lt: '<', nbsp: ?\u00a0, quot: '"' }
CharRefRx =
/&(?:(#{CharEntityTable.keys.join '|'})|#(?:(\d\d\d{0,4})|x(\h\h\h{0,3})));/
HexColorRx =
/^#\h\h\h\h{0,3}$/
TextDecorationTable =
{ 'underline' => :underline, 'line-through' => :strikethrough }
ThemeKeyToFragmentProperty =
{
  'background_color' => :background_color,
  'border_color' => :border_color,
  'border_offset' => :border_offset,
  'border_radius' => :border_radius,
  'border_width' => :border_width,
  'font_color' => :color,
  'font_family' => :font,
  'font_size' => :size,
  'text_decoration_color' => :text_decoration_color,
  'text_decoration_width' => :text_decoration_width,
  'text_transform' => :text_transform,
}

Constants included from TextTransformer

TextTransformer::BareClassRx, TextTransformer::ContiguousCharsRx, TextTransformer::Hyphen, TextTransformer::LowerAlphaChars, TextTransformer::PCDATAFilterRx, TextTransformer::SmallCapsChars, TextTransformer::SoftHyphen, TextTransformer::TagFilterRx, TextTransformer::WordRx, TextTransformer::XMLMarkupRx

Instance Method Summary collapse

Methods included from TextTransformer

#capitalize_words, #capitalize_words_pcdata, #hyphenate_words, #hyphenate_words_pcdata, #lowercase_pcdata, #smallcaps, #smallcaps_pcdata, #transform_text, #uppercase_pcdata

Constructor Details

#initialize(options = {}) ⇒ Transform

Returns a new instance of Transform.



29
30
31
32
33
34
35
36
37
38
39
40
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
# File 'lib/asciidoctor/pdf/formatted_text/transform.rb', line 29

def initialize options = {}
  @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
  # TODO: add support for character spacing
  if (theme = options[:theme])
    @theme_settings = {
      button: {
        color: theme.button_font_color,
        font: theme.button_font_family,
        size: theme.button_font_size,
        styles: (to_styles theme.button_font_style),
        background_color: (button_bg_color = theme.button_background_color),
        border_width: (button_border_width = theme.button_border_width),
        border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
        border_offset: (button_border_offset = (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset),
        border_radius: button_bg_or_border && theme.button_border_radius,
        align: button_border_offset && :center,
        callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
      }.compact,
      code: {
        color: theme.codespan_font_color,
        font: theme.codespan_font_family,
        size: theme.codespan_font_size,
        styles: (to_styles theme.codespan_font_style),
        background_color: (mono_bg_color = theme.codespan_background_color),
        border_width: (mono_border_width = theme.codespan_border_width),
        border_color: mono_border_width && (theme.codespan_border_color || theme.base_border_color),
        border_offset: (mono_border_offset = (mono_bg_or_border = mono_bg_color || mono_border_width) && theme.codespan_border_offset),
        border_radius: mono_bg_or_border && theme.codespan_border_radius,
        align: mono_border_offset && :center,
        callback: mono_bg_or_border && [TextBackgroundAndBorderRenderer],
      }.compact,
      kbd: {
        color: theme.kbd_font_color,
        font: theme.kbd_font_family || theme.codespan_font_family,
        size: theme.kbd_font_size,
        styles: (to_styles theme.kbd_font_style),
        background_color: (kbd_bg_color = theme.kbd_background_color),
        border_width: (kbd_border_width = theme.kbd_border_width),
        border_color: kbd_border_width && (theme.kbd_border_color || theme.base_border_color),
        border_offset: (kbd_border_offset = (kbd_bg_or_border = kbd_bg_color || kbd_border_width) && theme.kbd_border_offset),
        border_radius: kbd_bg_or_border && theme.kbd_border_radius,
        align: kbd_border_offset && :center,
        callback: kbd_bg_or_border && [TextBackgroundAndBorderRenderer],
      }.compact,
      link: {
        color: theme.link_font_color,
        font: theme.link_font_family,
        size: theme.link_font_size,
        styles: (to_styles theme.link_font_style, theme.link_text_decoration),
        text_decoration_color: theme.link_text_decoration_color,
        text_decoration_width: theme.link_text_decoration_width,
        background_color: (link_bg_color = theme.link_background_color),
        border_offset: (link_border_offset = link_bg_color && theme.link_border_offset),
        align: link_border_offset && :center,
        callback: link_bg_color && [TextBackgroundAndBorderRenderer],
      }.compact,
      mark: {
        color: theme.mark_font_color,
        styles: (to_styles theme.mark_font_style),
        background_color: (mark_bg_color = theme.mark_background_color),
        border_offset: (mark_border_offset = mark_bg_color && theme.mark_border_offset),
        align: mark_border_offset && :center,
        callback: mark_bg_color && [TextBackgroundAndBorderRenderer],
      }.compact,
      menu: {
        color: theme.menu_font_color,
        font: theme.menu_font_family,
        size: theme.menu_font_size,
        styles: (to_styles theme.menu_font_style),
      }.compact,
    }
    @theme_settings.tap do |accum|
      roles_with_styles = [].to_set
      theme.each_pair do |key, val|
        next unless (key = key.to_s).start_with? 'role_'
        role, key = (key.slice 5, key.length).split '_', 2
        if (prop = ThemeKeyToFragmentProperty[key])
          (accum[role] ||= {})[prop] = val
          if key == 'border_width' && val && !(theme[%(role_#{role}_border_color)])
            accum[role][:border_color] = theme.base_border_color
          end
        #elsif key == 'font_kerning'
        #  unless (resolved_val = val == 'none' ? false : (val == 'normal' ? true : nil)).nil?
        #    (accum[role] ||= {})[:kerning] = resolved_val
        #  end
        elsif key == 'font_style' || key == 'text_decoration'
          roles_with_styles << role
        end
      end
      roles_with_styles.each do |role|
        (accum[role] ||= {})[:styles] = to_styles theme[%(role_#{role}_font_style)], theme[%(role_#{role}_text_decoration)]
      end
    end
    @theme_settings['line-through'] = { styles: [:strikethrough].to_set } unless @theme_settings.key? 'line-through'
    @theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
    unless @theme_settings.key? 'big'
      if (base_font_size_large = theme.base_font_size_large)
        @theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 5}em) }
      else
        @theme_settings['big'] = { size: '1.1667em' }
      end
    end
    unless @theme_settings.key? 'small'
      if (base_font_size_small = theme.base_font_size_small)
        @theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 5}em) }
      else
        @theme_settings['small'] = { size: '0.8333em' }
      end
    end
  else
    @theme_settings = {
      button: { font: 'Courier', styles: [:bold].to_set },
      code: { font: 'Courier' },
      kbd: { font: 'Courier', styles: [:italic].to_set },
      link: { color: '0000FF' },
      mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
      menu: { styles: [:bold].to_set },
      'line-through' => { styles: [:strikethrough].to_set },
      'underline' => { styles: [:underline].to_set },
      'big' => { size: '1.667em' },
      'small' => { size: '0.8333em' },
    }
  end
end

Instance Method Details

#apply(parsed, fragments = [], inherited = nil) ⇒ Object



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
# File 'lib/asciidoctor/pdf/formatted_text/transform.rb', line 154

def apply parsed, fragments = [], inherited = nil
  previous_fragment_is_text = false
  # NOTE: we use each since using inject is slower than a manual loop
  parsed.each do |node|
    case node[:type]
    when :element
      # case 1: non-void element
      if node.key? :pcdata
        # NOTE: skip element if it has no children
        if (pcdata = node[:pcdata]).empty?
          # QUESTION: should this be handled by the formatter after the transform is complete?
          if previous_fragment_is_text && ((previous_fragment_text = fragments[-1][:text]).end_with? ' ')
            fragments[-1][:text] = previous_fragment_text.chop
          end
        else
          tag_name = node[:name]
          attributes = node[:attributes]
          parent = clone_fragment inherited
          fragment = build_fragment parent, tag_name, attributes
          if tag_name == :a && fragment[:type] == :indexterm && !attributes[:visible] &&
              previous_fragment_is_text && ((previous_fragment_text = fragments[-1][:text]).end_with? ' ')
            fragments[-1][:text] = previous_fragment_text.chop
          end
          if (text_transform = fragment.delete :text_transform)
            text = (text_chunks = extract_text pcdata).join
            chars = (StringIO.new transform_text text, text_transform).each_char
            restore_text pcdata, (text_chunks.each_with_object [] do |chunk, accum|
              accum << chunk.length.times.map { chars.next }.join
            end)
          end
          # NOTE: decorate child fragments with inherited properties from this element
          apply pcdata, fragments, fragment
          previous_fragment_is_text = false
        end
      # case 2: void element
      else
        case node[:name]
        when :img
          attributes = node[:attributes]
          fragment = {
            image_path: attributes[:src],
            image_format: attributes[:format],
            # a zero-width space in the text will cause the image to be duplicated
            # NOTE: add enclosing square brackets here to avoid errors in parsing
            text: %([#{attributes[:alt].delete ZeroWidthSpace}]),
            object_id: node.object_id, # used to deduplicate if fragment gets split up
          }
          if inherited && (callback = inherited[:callback]) && (callback.include? TextBackgroundAndBorderRenderer)
            # NOTE: if we keep InlineTextAligner, it needs to skip draw_text! for image fragment
            fragment[:callback] = [TextBackgroundAndBorderRenderer, InlineImageRenderer]
            fragment.update inherited.slice :border_color, :border_offset, :border_radius, :border_width, :background_color
          else
            fragment[:callback] = [InlineImageRenderer]
          end
          attributes[:class].split.each do |class_name|
            next unless @theme_settings.key? class_name
            update_fragment fragment, @theme_settings[class_name]
            if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
              fragment[:callback] = [TextBackgroundAndBorderRenderer] | fragment[:callback]
            end
          end if attributes.key? :class
          if inherited && (link = inherited[:link])
            fragment[:link] = link
          end
          if (img_w = attributes[:width])
            fragment[:image_width] = img_w
          end
          if (img_fit = attributes[:fit])
            fragment[:image_fit] = img_fit
          end
          fragments << fragment
          previous_fragment_is_text = false
        else # :br
          if @merge_adjacent_text_nodes && previous_fragment_is_text
            fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{LF}))
          else
            fragments << { text: LF }
          end
          previous_fragment_is_text = true
        end
      end
    when :charref
      if (ref_type = node[:reference_type]) == :name
        text = CharEntityTable[node[:value]]
      elsif ref_type == :decimal
        # FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
        text = [node[:value]].pack 'U1'
      else
        # FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
        text = [(node[:value].to_i 16)].pack 'U1'
      end
      if @merge_adjacent_text_nodes && previous_fragment_is_text
        fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{text}))
      else
        fragments << (clone_fragment inherited, text: text)
      end
      previous_fragment_is_text = true
    else # :text
      if @merge_adjacent_text_nodes && previous_fragment_is_text
        fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
      else
        fragments << (clone_fragment inherited, text: node[:value])
      end
      previous_fragment_is_text = true
    end
  end
  fragments
end