content_block_tools 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/content_block_tools/time_period/long_form_component.html.erb +3 -0
  3. data/app/components/content_block_tools/time_period/long_form_component.rb +24 -0
  4. data/app/components/content_block_tools/time_period/months_and_years_long_component.html.erb +3 -0
  5. data/app/components/content_block_tools/time_period/months_and_years_long_component.rb +24 -0
  6. data/app/components/content_block_tools/time_period/start_day_and_month_component.html.erb +3 -0
  7. data/app/components/content_block_tools/time_period/start_day_and_month_component.rb +23 -0
  8. data/app/components/content_block_tools/time_period/start_month_as_word_component.html.erb +3 -0
  9. data/app/components/content_block_tools/time_period/start_month_as_word_component.rb +23 -0
  10. data/app/components/content_block_tools/time_period/years_component.html.erb +3 -0
  11. data/app/components/content_block_tools/time_period/years_component.rb +24 -0
  12. data/app/components/content_block_tools/time_period/years_short_component.html.erb +3 -0
  13. data/app/components/content_block_tools/time_period/years_short_component.rb +24 -0
  14. data/app/components/content_block_tools/time_period_component.rb +49 -11
  15. data/lib/content_block_tools/content_block.rb +12 -81
  16. data/lib/content_block_tools/content_block_reference.rb +19 -15
  17. data/lib/content_block_tools/embed_code.rb +77 -0
  18. data/lib/content_block_tools/format.rb +5 -0
  19. data/lib/content_block_tools/internal_content_path.rb +49 -0
  20. data/lib/content_block_tools/normalised_date_range.rb +3 -4
  21. data/lib/content_block_tools/renderer.rb +107 -0
  22. data/lib/content_block_tools/version.rb +1 -1
  23. data/lib/content_block_tools.rb +6 -0
  24. metadata +49 -6
  25. data/app/components/content_block_tools/time_period_component.html.erb +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb61fc3df8d1dc8cb8d612db04f79372df5b136c84af620aa50727e6dd7ee17f
4
- data.tar.gz: b70351fcd23de104adfbb6d0e1c23ed3720eb8c22487ce22eda3894e4cefa908
3
+ metadata.gz: a3bf1b62951cbf2844753aff33c301958ef6bab65ed77f4b8bc3b7e3f2ec0a80
4
+ data.tar.gz: d38c2405d4acd27a334fc627c35aa22ca2436df664ad8adf0e1a55496c274dbd
5
5
  SHA512:
6
- metadata.gz: '05793c290c8331ba99b6c5973928d5d3f3f25c585c3c21057646962a8775943b9a64f627acebab02a856ba2a1736491a67706ae24b868eeb8536461ecc7b8960'
7
- data.tar.gz: 954103c40e093cb47a6d64ebd92b4ae2fd6166a4c89cb8b9b958e7228c20e11b432dda2e6bf5b59ba25d94f374c3bc2a7fa749641c02d67fafefd26a1c337e61
6
+ metadata.gz: 6da6482464ac0f9fc7ea84dfb576c26e96115547b79a4238d1889fac7a2893615b8d44258a0c9561a10cc0ff06ff776006efb161cd59adc64641c5a6b9157202
7
+ data.tar.gz: 830911495f8f4874c1367dcd113467244b1159ea146e6b11fe108335a5d6231cb3877f809b625c9b2a14ff0000621377498e1711dc8931082ea46dc593b3e525
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_date(start_date) %> to <%= formatted_date(end_date) %>
3
+ </p>
@@ -0,0 +1,24 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class LongFormComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:, end_date:)
5
+ @start_date = start_date
6
+ @end_date = end_date
7
+ end
8
+
9
+ def render
10
+ return "" unless start_date && end_date
11
+
12
+ render_in(view_context)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :start_date, :end_date
18
+
19
+ def formatted_date(date)
20
+ Presenters::FieldPresenters::TimePeriod::DatePresenter.new(date).render
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_date(start_date) %> to <%= formatted_date(end_date) %>
3
+ </p>
@@ -0,0 +1,24 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class MonthsAndYearsLongComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:, end_date:)
5
+ @start_date = start_date
6
+ @end_date = end_date
7
+ end
8
+
9
+ def render
10
+ return "" unless start_date && end_date
11
+
12
+ render_in(view_context)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :start_date, :end_date
18
+
19
+ def formatted_date(date)
20
+ date.strftime("%B %Y")
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_date %>
3
+ </p>
@@ -0,0 +1,23 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class StartDayAndMonthComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:)
5
+ @start_date = start_date
6
+ end
7
+
8
+ def render
9
+ return "" unless start_date
10
+
11
+ render_in(view_context)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :start_date
17
+
18
+ def formatted_date
19
+ start_date.strftime("%e %B").strip
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_date %>
3
+ </p>
@@ -0,0 +1,23 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class StartMonthAsWordComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:)
5
+ @start_date = start_date
6
+ end
7
+
8
+ def render
9
+ return "" unless start_date
10
+
11
+ render_in(view_context)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :start_date
17
+
18
+ def formatted_date
19
+ start_date.strftime("%B")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_years %>
3
+ </p>
@@ -0,0 +1,24 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class YearsComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:, end_date:)
5
+ @start_date = start_date
6
+ @end_date = end_date
7
+ end
8
+
9
+ def render
10
+ return "" unless start_date && end_date
11
+
12
+ render_in(view_context)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :start_date, :end_date
18
+
19
+ def formatted_years
20
+ "#{start_date.year}-#{end_date.year}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ <p class="govuk-body">
2
+ <%= formatted_years %>
3
+ </p>
@@ -0,0 +1,24 @@
1
+ module ContentBlockTools
2
+ module TimePeriod
3
+ class YearsShortComponent < ContentBlockTools::BaseComponent
4
+ def initialize(start_date:, end_date:)
5
+ @start_date = start_date
6
+ @end_date = end_date
7
+ end
8
+
9
+ def render
10
+ return "" unless start_date && end_date
11
+
12
+ render_in(view_context)
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :start_date, :end_date
18
+
19
+ def formatted_years
20
+ "#{start_date.year}-#{end_date.strftime('%y')}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,32 +1,70 @@
1
1
  module ContentBlockTools
2
2
  class TimePeriodComponent < ContentBlockTools::BaseComponent
3
+ SUPPORTED_FORMATS = %w[
4
+ default
5
+ long_form
6
+ months_and_years_long
7
+ start_day_and_month
8
+ start_month_as_word
9
+ years
10
+ years_short
11
+ ].freeze
12
+
3
13
  def initialize(content_block:, _block_type: nil, _block_name: nil)
4
14
  @content_block = content_block
5
15
  @normalised_date_range = normalise_date_range
16
+ validate_format!
6
17
  end
7
18
 
8
- def start_date
9
- presented_date(normalised_date_range.start_date)
10
- end
11
-
12
- def end_date
13
- presented_date(normalised_date_range.end_date)
19
+ def render
20
+ format_component.render
14
21
  end
15
22
 
16
23
  private
17
24
 
18
25
  attr_reader :content_block, :normalised_date_range
19
26
 
27
+ delegate :format, to: :content_block
28
+
29
+ def format_component
30
+ case defaulted_format
31
+ when "long_form"
32
+ TimePeriod::LongFormComponent.new(start_date:, end_date:)
33
+ when "months_and_years_long"
34
+ TimePeriod::MonthsAndYearsLongComponent.new(start_date:, end_date:)
35
+ when "start_day_and_month"
36
+ TimePeriod::StartDayAndMonthComponent.new(start_date:)
37
+ when "start_month_as_word"
38
+ TimePeriod::StartMonthAsWordComponent.new(start_date:)
39
+ when "years"
40
+ TimePeriod::YearsComponent.new(start_date:, end_date:)
41
+ when "years_short"
42
+ TimePeriod::YearsShortComponent.new(start_date:, end_date:)
43
+ end
44
+ end
45
+
46
+ def defaulted_format
47
+ return "long_form" if format == Format::DEFAULT_FORMAT
48
+
49
+ format
50
+ end
51
+
52
+ def validate_format!
53
+ return if SUPPORTED_FORMATS.include?(format)
54
+
55
+ raise InvalidFormatError, "Unknown format '#{format}' for time_period"
56
+ end
57
+
20
58
  def normalise_date_range
21
59
  NormalisedDateRange.new(content_block.details[:date_range])
22
60
  end
23
61
 
24
- def presented_date(date)
25
- return unless date.present?
62
+ def start_date
63
+ normalised_date_range.start_date
64
+ end
26
65
 
27
- Presenters::FieldPresenters::TimePeriod::DatePresenter.new(
28
- date,
29
- ).render
66
+ def end_date
67
+ normalised_date_range.end_date
30
68
  end
31
69
  end
32
70
  end
@@ -38,10 +38,13 @@ module ContentBlockTools
38
38
  # content_block_reference.embed_code #=> "{{embed:content_block_pension:2b92cade-549c-4449-9796-e7a3957f3a86}}"
39
39
  # content_block_reference.embed_code #=> "{{embed:content_block_contact:2b92cade-549c-4449-9796-e7a3957f3a86/field_name}}"
40
40
  # @return [String]
41
+ #
42
+ # @!attribute [r] format
43
+ # The format specifier from the embed code, used to control rendering output
44
+ # @example
45
+ # content_block.format #=> "years_short"
46
+ # @return [String]
41
47
  class ContentBlock
42
- include ActionView::Helpers::TagHelper
43
- class UnknownComponentError < StandardError; end
44
-
45
48
  CONTENT_BLOCK_PREFIX = "content_block_".freeze
46
49
 
47
50
  attr_reader :content_id, :title, :embed_code
@@ -86,22 +89,12 @@ module ContentBlockTools
86
89
  @embed_code = embed_code
87
90
  end
88
91
 
89
- # Calls the appropriate presenter class to return a HTML representation of a content
90
- # block. Defaults to {Presenters::BasePresenter}
92
+ # Renders the content block to HTML using the appropriate component or presenter
91
93
  #
92
- # @return [string] A HTML representation of the content block
94
+ # @return [String] A HTML representation of the content block
95
+ # @see Renderer
93
96
  def render
94
- content_tag(
95
- base_tag,
96
- content,
97
- class: %W[content-block content-block--#{document_type}],
98
- data: {
99
- content_block: "",
100
- document_type: document_type,
101
- content_id: content_id,
102
- embed_code: embed_code,
103
- },
104
- )
97
+ Renderer.new(self).render
105
98
  end
106
99
 
107
100
  def details
@@ -112,70 +105,8 @@ module ContentBlockTools
112
105
  @document_type.delete_prefix(CONTENT_BLOCK_PREFIX)
113
106
  end
114
107
 
115
- private
116
-
117
- def base_tag
118
- rendering_block? ? :div : :span
119
- end
120
-
121
- def content
122
- field_names.present? ? field_or_block_content : component.new(content_block: self).render
123
- rescue UnknownComponentError
124
- title
125
- end
126
-
127
- def field_or_block_content
128
- content = details.dig(*field_names)
129
-
130
- case content
131
- when String
132
- field_presenter(field_names.last).new(content).render
133
- when Hash
134
- if embedded_object_in_one_to_one_relationship?
135
- field_presenter(field_names.last).new(content).render
136
- else
137
- component.new(content_block: self, block_type: field_names.first, block_name: field_names.last).render
138
- end
139
- else
140
- ContentBlockTools.logger.warn("Content not found for content block #{content_id} and fields #{field_names}")
141
- embed_code
142
- end
143
- end
144
-
145
- def embedded_object_in_one_to_one_relationship?
146
- field_names.one?
147
- end
148
-
149
- def rendering_block?
150
- !field_names.present? || details.dig(*field_names).is_a?(Hash)
151
- end
152
-
153
- def component
154
- "ContentBlockTools::#{document_type.camelize}Component".constantize
155
- rescue NameError
156
- raise UnknownComponentError
157
- end
158
-
159
- def field_presenter(field)
160
- "ContentBlockTools::Presenters::FieldPresenters::#{document_type.camelize}::#{field.to_s.camelize}Presenter".constantize
161
- rescue NameError
162
- ContentBlockTools::Presenters::FieldPresenters::BasePresenter
163
- end
164
-
165
- def field_names
166
- @field_names ||= begin
167
- embed_code_match = ContentBlockReference::EMBED_REGEX.match(embed_code)
168
- if embed_code_match.present?
169
- all_fields = embed_code_match[4]&.reverse&.chomp("/")&.reverse
170
- all_fields&.split("/")&.map do |item|
171
- is_number?(item) ? item.to_i : item.to_sym
172
- end
173
- end
174
- end
175
- end
176
-
177
- def is_number?(item)
178
- Float(item, exception: false)
108
+ def format
109
+ EmbedCode.new(embed_code).format
179
110
  end
180
111
  end
181
112
  end
@@ -40,10 +40,10 @@ module ContentBlockTools
40
40
  UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
41
41
  # The regex used to find content ID aliases
42
42
  CONTENT_ID_ALIAS_REGEX = /[a-z0-9\-–—]+/
43
- # The regex to find optional field names after the UUID, begins with '/'
44
- FIELD_REGEX = /(\/[a-z0-9_\-–—\/]*)?/
43
+ # The regex to find optional internal content path after the UUID, begins with '/'
44
+ INTERNAL_CONTENT_PATH_REGEX = /(?<internal_content_path>\/[a-z0-9_\-–—\/]*)?/
45
45
  # The regex used when scanning a document using {ContentBlockTools::ContentBlockReference.find_all_in_document}
46
- EMBED_REGEX = /({{embed:(#{SUPPORTED_DOCUMENT_TYPES.join('|')}):(#{UUID_REGEX}|#{CONTENT_ID_ALIAS_REGEX})#{FIELD_REGEX}}})/
46
+ EMBED_REGEX = /(?<embed_code>{{embed:(?<document_type>#{SUPPORTED_DOCUMENT_TYPES.join('|')}):(?<identifier>#{UUID_REGEX}|#{CONTENT_ID_ALIAS_REGEX})#{INTERNAL_CONTENT_PATH_REGEX}}})/
47
47
 
48
48
  # Returns if the identifier is an alias
49
49
  #
@@ -73,8 +73,8 @@ module ContentBlockTools
73
73
  #
74
74
  # @return [Array<ContentBlockReference>] An array of content block references
75
75
  def find_all_in_document(document)
76
- document.scan(EMBED_REGEX).map do |match_data|
77
- ContentBlockReference.from_match_data(match_data)
76
+ document.to_enum(:scan, EMBED_REGEX).map do
77
+ ContentBlockReference.from_match_data(Regexp.last_match)
78
78
  end
79
79
  end
80
80
 
@@ -97,7 +97,7 @@ module ContentBlockTools
97
97
  match_data = embed_code.match(/^#{EMBED_REGEX}$/)
98
98
  raise InvalidEmbedCodeError unless match_data
99
99
 
100
- ContentBlockReference.from_match_data(match_data.captures)
100
+ ContentBlockReference.from_match_data(match_data)
101
101
  end
102
102
 
103
103
  # Converts match data from a regex scan into a ContentBlockReference object
@@ -107,8 +107,8 @@ module ContentBlockTools
107
107
  # by replacing en/em dashes with double/triple dashes (which can occur due to Kramdown's
108
108
  # markdown parsing) before creating the object.
109
109
  #
110
- # @param match_data [MatchData, Array] the match data from scanning with {EMBED_REGEX}
111
- # Expected to contain: [full_match, document_type, identifier, field]
110
+ # @param match_data [MatchData] the match data from scanning with {EMBED_REGEX}
111
+ # Expected named captures: embed_code, document_type, identifier, internal_content_path
112
112
  # @example Creating from match data
113
113
  # match_data = "{{embed:content_block_pension:2b92cade-549c-4449-9796-e7a3957f3a86}}".match(EMBED_REGEX)
114
114
  # ContentBlockReference.from_match_data(match_data)
@@ -121,7 +121,11 @@ module ContentBlockTools
121
121
  def from_match_data(match_data)
122
122
  match = prepare_match(match_data)
123
123
  ContentBlockTools.logger.info("Found Content Block Reference: #{match}")
124
- ContentBlockReference.new(document_type: match[1], identifier: match[2], embed_code: match[0])
124
+ ContentBlockReference.new(
125
+ document_type: match[:document_type],
126
+ identifier: match[:identifier],
127
+ embed_code: match[:embed_code],
128
+ )
125
129
  end
126
130
 
127
131
  private
@@ -130,12 +134,12 @@ module ContentBlockTools
130
134
  # because Kramdown (the markdown parser that Govspeak is based on) replaces double dashes with en dashes and
131
135
  # triple dashes with em dashes
132
136
  def prepare_match(match)
133
- [
134
- match[0],
135
- match[1],
136
- replace_dashes(match[2]),
137
- match[3],
138
- ]
137
+ {
138
+ embed_code: match[:embed_code],
139
+ document_type: match[:document_type],
140
+ identifier: replace_dashes(match[:identifier]),
141
+ internal_content_path: match[:internal_content_path],
142
+ }
139
143
  end
140
144
 
141
145
  def replace_dashes(value)
@@ -0,0 +1,77 @@
1
+ module ContentBlockTools
2
+ # Parses and represents an embed code string
3
+ #
4
+ # An embed code identifies a content block and optionally specifies which
5
+ # fields to render. The format is:
6
+ # {{embed:block_type:identifier}}
7
+ # {{embed:block_type:identifier/field1/nested_field2}}
8
+ # {{embed:block_type:identifier|format}}
9
+ # {{embed:block_type:identifier/field1|format}}
10
+ #
11
+ # @example Basic embed code
12
+ # embed_code = EmbedCode.new("{{embed:content_block_contact:main-office}}")
13
+ # embed_code.internal_content_path.present? #=> false
14
+ #
15
+ # @example Embed code with field path
16
+ # embed_code = EmbedCode.new("{{embed:content_block_contact:main-office/email_addresses/main}}")
17
+ # embed_code.internal_content_path.path #=> [:email_addresses, :main]
18
+ #
19
+ # @example Embed code with format
20
+ # embed_code = EmbedCode.new("{{embed:content_block_time_period:tax-year#years_short}}")
21
+ # embed_code.format #=> "years_short"
22
+ #
23
+ class EmbedCode
24
+ FORMAT_REGEX = /\#(?<format>[^}#]+)}}$/
25
+
26
+ def initialize(embed_code_string)
27
+ @embed_code_string = embed_code_string
28
+ end
29
+
30
+ # Returns the internal content path for this embed code
31
+ #
32
+ # @return [InternalContentPath] The path to internal content
33
+ def internal_content_path
34
+ @internal_content_path ||= InternalContentPath.new(parse_path_segments)
35
+ end
36
+
37
+ # Returns the format specifier from the embed code
38
+ #
39
+ # @return [String] The format name, or Format::DEFAULT_FORMAT if none specified
40
+ def format
41
+ @format ||= parse_format
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :embed_code_string
47
+
48
+ def parse_path_segments
49
+ match = ContentBlockReference::EMBED_REGEX.match(embed_code_string)
50
+ return [] unless match
51
+
52
+ all_segments = strip_leading_slash(match[:internal_content_path])
53
+ return [] if all_segments.nil?
54
+
55
+ all_segments.split("/").map { |item| convert_segment(item) }
56
+ end
57
+
58
+ def strip_leading_slash(path)
59
+ path&.reverse&.chomp("/")&.reverse
60
+ end
61
+
62
+ def convert_segment(item)
63
+ numeric?(item) ? item.to_i : item.to_sym
64
+ end
65
+
66
+ def parse_format
67
+ match = FORMAT_REGEX.match(embed_code_string)
68
+ return Format::DEFAULT_FORMAT unless match
69
+
70
+ match[:format]
71
+ end
72
+
73
+ def numeric?(item)
74
+ Float(item, exception: false)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ module ContentBlockTools
2
+ module Format
3
+ DEFAULT_FORMAT = "default".freeze
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ module ContentBlockTools
2
+ # Represents the path to internal content within a content block
3
+ #
4
+ # An internal content path identifies a specific piece of content within a
5
+ # content block's details hash. The path is used with `dig` to traverse the
6
+ # nested structure.
7
+ #
8
+ # @example Path to a block
9
+ # path = InternalContentPath.new([:email_addresses, :main])
10
+ # path.present? #=> true
11
+ # path.singular? #=> false
12
+ # path.block_type #=> :email_addresses
13
+ # path.block_name #=> :main
14
+ #
15
+ # @example Path to a field in a one-to-one relationship
16
+ # path = InternalContentPath.new([:date_range])
17
+ # path.singular? #=> true
18
+ # path.block_type #=> :date_range
19
+ # path.block_name #=> :date_range
20
+ #
21
+ # @example Empty path (render the whole block)
22
+ # path = InternalContentPath.new([])
23
+ # path.present? #=> false
24
+ # path.block_type #=> nil
25
+ #
26
+ class InternalContentPath
27
+ attr_reader :path
28
+
29
+ def initialize(path)
30
+ @path = path
31
+ end
32
+
33
+ def present?
34
+ path.any?
35
+ end
36
+
37
+ def singular?
38
+ path.one?
39
+ end
40
+
41
+ def block_type
42
+ path.first
43
+ end
44
+
45
+ def block_name
46
+ path.last
47
+ end
48
+ end
49
+ end
@@ -54,10 +54,9 @@ module ContentBlockTools
54
54
  end
55
55
 
56
56
  def parse_iso8601_format(datetime_string)
57
- result = Time.zone.parse(datetime_string)
58
- raise ParseError, "Invalid ISO 8601 format: #{datetime_string.inspect}" if result.nil?
59
-
60
- result
57
+ Time.parse(datetime_string)
58
+ rescue ArgumentError
59
+ raise ParseError, "Invalid ISO 8601 format: #{datetime_string.inspect}"
61
60
  end
62
61
  end
63
62
  end
@@ -0,0 +1,107 @@
1
+ module ContentBlockTools
2
+ # Renders a ContentBlock to HTML
3
+ #
4
+ # This class encapsulates the logic for rendering a content block,
5
+ # including determining the appropriate component or presenter to use
6
+ # and wrapping the result in the standard content block markup.
7
+ #
8
+ # @example
9
+ # content_block = ContentBlock.new(...)
10
+ # html = Renderer.new(content_block).render
11
+ #
12
+ class Renderer
13
+ include ActionView::Helpers::TagHelper
14
+
15
+ class UnknownComponentError < StandardError; end
16
+
17
+ def initialize(content_block)
18
+ @content_block = content_block
19
+ end
20
+
21
+ # Renders the content block to HTML
22
+ #
23
+ # @return [String] HTML representation of the content block
24
+ def render
25
+ content_tag(
26
+ base_tag,
27
+ content,
28
+ class: %W[content-block content-block--#{content_block.document_type}],
29
+ data: {
30
+ content_block: "",
31
+ document_type: content_block.document_type,
32
+ content_id: content_block.content_id,
33
+ embed_code: content_block.embed_code,
34
+ },
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :content_block
41
+
42
+ def base_tag
43
+ rendering_block? ? :div : :span
44
+ end
45
+
46
+ def content
47
+ internal_content_path.present? ? field_or_block_content : component.new(content_block:).render
48
+ rescue UnknownComponentError
49
+ content_block.title
50
+ end
51
+
52
+ def field_or_block_content
53
+ field_content = content_block.details.dig(*internal_content_path.path)
54
+
55
+ case field_content
56
+ when String
57
+ field_presenter(internal_content_path.block_name).new(field_content).render
58
+ when Hash
59
+ render_hash_content(field_content)
60
+ else
61
+ log_content_not_found
62
+ content_block.embed_code
63
+ end
64
+ end
65
+
66
+ def render_hash_content(field_content)
67
+ if internal_content_path.singular?
68
+ field_presenter(internal_content_path.block_name).new(field_content).render
69
+ else
70
+ component.new(
71
+ content_block:,
72
+ block_type: internal_content_path.block_type,
73
+ block_name: internal_content_path.block_name,
74
+ ).render
75
+ end
76
+ end
77
+
78
+ def log_content_not_found
79
+ ContentBlockTools.logger.warn(
80
+ "Content not found for content block #{content_block.content_id} " \
81
+ "and fields #{internal_content_path.path}",
82
+ )
83
+ end
84
+
85
+ def rendering_block?
86
+ !internal_content_path.present? || content_block.details.dig(*internal_content_path.path).is_a?(Hash)
87
+ end
88
+
89
+ def component
90
+ "ContentBlockTools::#{content_block.document_type.camelize}Component".constantize
91
+ rescue NameError
92
+ raise UnknownComponentError
93
+ end
94
+
95
+ def field_presenter(field)
96
+ presenter_class_name = "ContentBlockTools::Presenters::FieldPresenters::" \
97
+ "#{content_block.document_type.camelize}::#{field.to_s.camelize}Presenter"
98
+ presenter_class_name.constantize
99
+ rescue NameError
100
+ ContentBlockTools::Presenters::FieldPresenters::BasePresenter
101
+ end
102
+
103
+ def internal_content_path
104
+ @internal_content_path ||= EmbedCode.new(content_block.embed_code).internal_content_path
105
+ end
106
+ end
107
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ContentBlockTools
4
- VERSION = "1.11.0"
4
+ VERSION = "1.12.0"
5
5
  end
@@ -18,7 +18,11 @@ require "content_block_tools/presenters/field_presenters/time_period/end_present
18
18
 
19
19
  require "content_block_tools/content_block"
20
20
  require "content_block_tools/content_block_reference"
21
+ require "content_block_tools/embed_code"
22
+ require "content_block_tools/format"
23
+ require "content_block_tools/internal_content_path"
21
24
  require "content_block_tools/normalised_date_range"
25
+ require "content_block_tools/renderer"
22
26
 
23
27
  require "content_block_tools/engine"
24
28
 
@@ -27,11 +31,13 @@ require "content_block_tools/version"
27
31
  module ContentBlockTools
28
32
  class Error < StandardError; end
29
33
  class InvalidEmbedCodeError < StandardError; end
34
+ class InvalidFormatError < StandardError; end
30
35
 
31
36
  module Presenters; end
32
37
 
33
38
  module Components
34
39
  module Contacts; end
40
+ module TimePeriod; end
35
41
  end
36
42
 
37
43
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: content_block_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.0
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GOV.UK Dev
@@ -9,20 +9,48 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: cucumber
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '9.2'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '9.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: pry-byebug
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: rake
14
42
  requirement: !ruby/object:Gem::Requirement
15
43
  requirements:
16
44
  - - '='
17
45
  - !ruby/object:Gem::Version
18
- version: 13.3.1
46
+ version: 13.4.2
19
47
  type: :development
20
48
  prerelease: false
21
49
  version_requirements: !ruby/object:Gem::Requirement
22
50
  requirements:
23
51
  - - '='
24
52
  - !ruby/object:Gem::Version
25
- version: 13.3.1
53
+ version: 13.4.2
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: rspec-html-matchers
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -57,14 +85,14 @@ dependencies:
57
85
  requirements:
58
86
  - - '='
59
87
  - !ruby/object:Gem::Version
60
- version: 5.2.0
88
+ version: 5.2.1
61
89
  type: :development
62
90
  prerelease: false
63
91
  version_requirements: !ruby/object:Gem::Requirement
64
92
  requirements:
65
93
  - - '='
66
94
  - !ruby/object:Gem::Version
67
- version: 5.2.0
95
+ version: 5.2.1
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: simplecov
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -180,14 +208,28 @@ files:
180
208
  - app/components/content_block_tools/contacts/email_address_component.rb
181
209
  - app/components/content_block_tools/contacts/telephone_component.html.erb
182
210
  - app/components/content_block_tools/contacts/telephone_component.rb
183
- - app/components/content_block_tools/time_period_component.html.erb
211
+ - app/components/content_block_tools/time_period/long_form_component.html.erb
212
+ - app/components/content_block_tools/time_period/long_form_component.rb
213
+ - app/components/content_block_tools/time_period/months_and_years_long_component.html.erb
214
+ - app/components/content_block_tools/time_period/months_and_years_long_component.rb
215
+ - app/components/content_block_tools/time_period/start_day_and_month_component.html.erb
216
+ - app/components/content_block_tools/time_period/start_day_and_month_component.rb
217
+ - app/components/content_block_tools/time_period/start_month_as_word_component.html.erb
218
+ - app/components/content_block_tools/time_period/start_month_as_word_component.rb
219
+ - app/components/content_block_tools/time_period/years_component.html.erb
220
+ - app/components/content_block_tools/time_period/years_component.rb
221
+ - app/components/content_block_tools/time_period/years_short_component.html.erb
222
+ - app/components/content_block_tools/time_period/years_short_component.rb
184
223
  - app/components/content_block_tools/time_period_component.rb
185
224
  - lib/content_block_tools.rb
186
225
  - lib/content_block_tools/content_block.rb
187
226
  - lib/content_block_tools/content_block_reference.rb
227
+ - lib/content_block_tools/embed_code.rb
188
228
  - lib/content_block_tools/engine.rb
229
+ - lib/content_block_tools/format.rb
189
230
  - lib/content_block_tools/helpers/govspeak.rb
190
231
  - lib/content_block_tools/helpers/override_classes.rb
232
+ - lib/content_block_tools/internal_content_path.rb
191
233
  - lib/content_block_tools/normalised_date_range.rb
192
234
  - lib/content_block_tools/presenters/field_presenters/base_presenter.rb
193
235
  - lib/content_block_tools/presenters/field_presenters/contact/email_presenter.rb
@@ -196,6 +238,7 @@ files:
196
238
  - lib/content_block_tools/presenters/field_presenters/time_period/end_presenter.rb
197
239
  - lib/content_block_tools/presenters/field_presenters/time_period/start_presenter.rb
198
240
  - lib/content_block_tools/presenters/field_presenters/time_period/time_presenter.rb
241
+ - lib/content_block_tools/renderer.rb
199
242
  - lib/content_block_tools/version.rb
200
243
  - node_modules/govuk-frontend/README.md
201
244
  - node_modules/govuk-frontend/dist/govuk-prototype-kit/functions.js
@@ -1,8 +0,0 @@
1
- <% if start_date.present? && end_date.present? %>
2
- <p class="govuk-body">
3
- <%= start_date %>
4
- to
5
- <%= end_date %>
6
- </p>
7
- <% end %>
8
-