Class: WCC::Contentful::RichTextRenderer Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/wcc/contentful/rich_text_renderer.rb

Overview

This class is abstract.

The abstract base class for rendering Rich Text. This base class implements much of the recursive logic necessary for rendering Rich Text nodes, but leaves the actual rendering of the HTML tags to the subclasses.

Subclasses can override any method to customize the rendering behavior. At a minimum they must implement the #content_tag and #concat methods to take advantage of the recursive rendering logic in the base class. The API for these methods is assumed to be equivalent to the ActionView helpers of the same name.

The canonical implementation is the WCC::Contentful::ActionViewRichTextRenderer, which uses the standard ActionView helpers as-is to render the HTML tags.

Examples:

class MyRichTextRenderer < WCC::Contentful::RichTextRenderer
  def (name, options, &block)
    # your implementation here
    # for reference of expected behavior see
    # https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag
  end

  def concat(html_string)
    # your implementation here
    # for reference of expected behavior see
    # https://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-concat
  end
end

renderer = MyRichTextRenderer.new(document)
renderer.call

Direct Known Subclasses

ActionViewRichTextRenderer

Defined Under Namespace

Classes: AbstractRendererError, NotConnectedError

Constant Summary collapse

DEFAULT_MARKS =
{
  'bold' => 'strong',
  'italic' => 'em',
  'underline' => 'u',
  'code' => 'code',
  'superscript' => 'sup',
  'subscript' => 'sub'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document, configuration: nil, store: nil, model_namespace: nil) ⇒ RichTextRenderer

Returns a new instance of RichTextRenderer.



44
45
46
47
48
49
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 44

def initialize(document, configuration: nil, store: nil, model_namespace: nil)
  @document = document
  @configuration = configuration if configuration.present?
  @store = store if store.present?
  @model_namespace = model_namespace if model_namespace.present?
end

Instance Attribute Details

#configurationObject

Returns the value of attribute configuration.



42
43
44
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 42

def configuration
  @configuration
end

#documentObject (readonly)

Returns the value of attribute document.



41
42
43
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 41

def document
  @document
end

#model_namespaceObject

Returns the value of attribute model_namespace.



42
43
44
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 42

def model_namespace
  @model_namespace
end

#storeObject

Returns the value of attribute store.



42
43
44
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 42

def store
  @store
end

Class Method Details

.call(document, *args, **kwargs) ⇒ Object



36
37
38
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 36

def call(document, *args, **kwargs)
  new(document, *args, **kwargs).call
end

Instance Method Details

#callObject



51
52
53
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 51

def call
  render.to_s
end

#renderObject



55
56
57
58
59
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 55

def render
  (:div, class: 'contentful-rich-text') do
    render_content(document.content)
  end
end


221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 221

def render_asset_hyperlink(node)
  target = resolve_target(node.data['target'])
  url = target&.dig('fields', 'file', 'url')

  render_hyperlink(
    WCC::Contentful::RichText::Hyperlink.tokenize(
      node.as_json.merge(
        'nodeType' => 'hyperlink',
        'data' => node['data'].merge({
          'uri' => url,
          'target' => target.as_json
        })
      )
    )
  )
end

#render_blockquote(node) ⇒ Object



112
113
114
115
116
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 112

def render_blockquote(node)
  (:blockquote) do
    render_content(node.content)
  end
end

#render_content(content) ⇒ Object



61
62
63
64
65
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 61

def render_content(content)
  content&.each do |node|
    concat render_node(node)
  end
end

#render_embedded_asset_block(node) ⇒ Object



267
268
269
270
271
272
273
274
275
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 267

def render_embedded_asset_block(node)
  target = resolve_target(node.data['target'])
  title = target&.dig('fields', 'title')
  url = target&.dig('fields', 'file', 'url')

  (:img, src: url, alt: title) do
    render_content(node.content)
  end
end

#render_embedded_entry_block(_node) ⇒ Object



277
278
279
280
281
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 277

def render_embedded_entry_block(_node)
  raise AbstractRendererError,
    'Entry embeds are not supported.  What should it look like? ' \
    'Please override this in your app-specific RichTextRenderer implementation.'
end

#render_embedded_entry_inline(_node) ⇒ Object



283
284
285
286
287
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 283

def render_embedded_entry_inline(_node)
  raise AbstractRendererError,
    'Inline Entry embeds are not supported.  What should it look like? ' \
    'Please override this in your app-specific RichTextRenderer implementation.'
end

#render_entry_hyperlink(node) ⇒ Object



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
263
264
265
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 238

def render_entry_hyperlink(node)
  unless model_namespace.present?
    raise NotConnectedError,
      'Rendering linked entries requires a connected RichTextRenderer.  Please use the one configured in ' \
      'WCC::Contentful::Services.instance or pass a model_namespace to the RichTextRenderer constructor.'
  end

  target = resolve_target(node.data['target'])
  model_instance = model_namespace.new_from_raw(target)
  unless model_instance.respond_to?(:href)
    raise NotConnectedError,
      "Entry hyperlinks are not supported for #{model_instance.class}.  " \
      'Please ensure your model defines an #href method, or override the ' \
      '#render_entry_hyperlink method in your app-specific RichTextRenderer implementation.'
  end

  render_hyperlink(
    WCC::Contentful::RichText::Hyperlink.tokenize(
      node.as_json.merge(
        'nodeType' => 'hyperlink',
        'data' => node['data'].merge({
          'uri' => model_instance.href,
          'target' => target.as_json
        })
      )
    )
  )
end

#render_heading(node) ⇒ Object



106
107
108
109
110
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 106

def render_heading(node)
  (:"h#{node.size}") do
    render_content(node.content)
  end
end

#render_hr(_node) ⇒ Object



118
119
120
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 118

def render_hr(_node)
  (:hr)
end


212
213
214
215
216
217
218
219
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 212

def render_hyperlink(node)
  (:a,
    href: node.data['uri'],
    # External links should be target="_blank" by default
    target: ('_blank' if url_is_external?(node.data['uri']))) do
    render_content(node.content)
  end
end

#render_list_item(node) ⇒ Object



134
135
136
137
138
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 134

def render_list_item(node)
  (:li) do
    render_content(node.content)
  end
end

#render_mark(type, value) ⇒ Object



94
95
96
97
98
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 94

def render_mark(type, value)
  return value unless tag = DEFAULT_MARKS[type]

  (tag, value)
end

#render_node(node) ⇒ Object



67
68
69
70
71
72
73
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 67

def render_node(node)
  if WCC::Contentful::RichText::Heading.matches?(node.node_type)
    render_heading(node)
  else
    public_send(:"render_#{node.node_type.underscore}", node)
  end
end

#render_ordered_list(node) ⇒ Object



128
129
130
131
132
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 128

def render_ordered_list(node)
  (:ol) do
    render_content(node.content)
  end
end

#render_paragraph(node) ⇒ Object



100
101
102
103
104
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 100

def render_paragraph(node)
  (:p) do
    render_content(node.content)
  end
end

#render_table(node) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 140

def render_table(node)
  (:table) do
    # Check the first row - if it's a header row, render a <thead>
    first, *rest = node.content
    if first&.content&.all? { |cell| cell.node_type == 'table-header-cell' }
      concat(render_table_header(first))
    else
      # Otherwise, render it inside the tbody with the rest
      rest.unshift(first)
    end

    concat((:tbody) { render_content(rest) })
  end
end

#render_table_cell(node) ⇒ Object



193
194
195
196
197
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 193

def render_table_cell(node)
  (:td) do
    render_table_cell_content(node.content)
  end
end

#render_table_cell_content(content) ⇒ Object



205
206
207
208
209
210
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 205

def render_table_cell_content(content)
  # If the content is a single paragraph, render it without the <p> tag
  return render_content(content.first.content) if content.size == 1 && content.first.node_type == 'paragraph'

  render_content(content)
end

#render_table_header(table_row_node) ⇒ Object



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
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 155

def render_table_header(table_row_node)
  # roll up blank table-header-cells into the previous cell w/ colspan
  node_contents = []
  table_row_node.content.each do |node|
    if node.node_type == 'table-header-cell' &&
        node_is_blank?(node) &&
        node_contents.last&.node_type == 'table-header-cell'

      # replace the previous node with a new node with colspan + 1
      last_node = node_contents.pop
      node_contents << WCC::Contentful::RichText.tokenize(
        last_node.as_json.merge(
          'data' => (last_node['data'] || {}).merge({
            'colspan' => (last_node['data']&.try('colspan') || 1) + 1
          })
        )
      )

      # And skip adding this blank node
      next
    end

    node_contents << node
  end

  (:thead) do
    (:tr) do
      render_content(node_contents)
    end
  end
end

#render_table_header_cell(node) ⇒ Object



199
200
201
202
203
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 199

def render_table_header_cell(node)
  (:th, colspan: node.data && node.data['colspan']) do
    render_table_cell_content(node.content)
  end
end

#render_table_row(node) ⇒ Object



187
188
189
190
191
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 187

def render_table_row(node)
  (:tr) do
    render_content(node.content)
  end
end

#render_text(node) ⇒ Object



75
76
77
78
79
80
81
82
83
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 75

def render_text(node)
  return node.value unless node.marks&.any?

  node.marks.reduce(node.value) do |value, mark|
    next value unless type = mark['type']&.underscore

    render_mark(type, value)
  end
end

#render_unordered_list(node) ⇒ Object



122
123
124
125
126
# File 'lib/wcc/contentful/rich_text_renderer.rb', line 122

def render_unordered_list(node)
  (:ul) do
    render_content(node.content)
  end
end