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

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

Constant Summary collapse

DummyText =
?\u0000
ZeroWidthSpace =
?\u200b
LF =
?\n
InnerLF =

without trailing character, use of fallback font can change line height

LF + ZeroWidthSpace
DoubleSpace =
'  '
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::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_pcdata, #transform_text, #uppercase_pcdata

Constructor Details

#initialize(options = {}) ⇒ Transform

Returns a new instance of Transform.



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

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, normalize_space: nil) ⇒ Object



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

def apply parsed, fragments = [], inherited = nil, normalize_space: nil
  previous_fragment_is_text = false
  previous_fragment_end_with_space = false
  last_node = parsed[-1]
  # 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
        unless (pcdata = node[:pcdata]).empty?
          tag_name = node[:name]
          attributes = node[:attributes]
          fragment = build_fragment (clone_fragment inherited), tag_name, attributes
          if tag_name == :a && pcdata[0][:value] == DummyText && pcdata.length == 1
            fragment[:text] = DummyText
            fragments << fragment
          else
            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, normalize_space: normalize_space
            previous_fragment_end_with_space = false
          end
          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 = previous_fragment_end_with_space = false
        else # :br
          lf = node == last_node ? LF : InnerLF
          text = @merge_adjacent_text_nodes && previous_fragment_is_text ? %(#{fragments.pop[:text]}#{lf}) : lf
          fragments << (clone_fragment inherited, text: text)
          previous_fragment_is_text = previous_fragment_end_with_space = 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
      text = %(#{fragments.pop[:text]}#{text}) if @merge_adjacent_text_nodes && previous_fragment_is_text
      fragments << (clone_fragment inherited, text: text)
      previous_fragment_is_text = true
      previous_fragment_end_with_space = false
    else # :text
      preserve_space = inherited && inherited[:preserve_space]
      unless (text = previous_fragment_end_with_space && normalize_space && !preserve_space ? node[:value].lstrip : node[:value]).empty?
        text = %(#{fragments.pop[:text]}#{text}) if @merge_adjacent_text_nodes && previous_fragment_is_text
        if normalize_space && !preserve_space && (text.include? DoubleSpace)
          text = text.tr_s ' ', ' '
        end
        fragments << (clone_fragment inherited, text: text)
        previous_fragment_is_text = true
        previous_fragment_end_with_space = text.end_with? ' '
      end
    end
  end
  fragments
end