svg_conform 0.1.6 → 0.1.8

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base_remediation"
4
+ require "set"
4
5
 
5
6
  module SvgConform
6
7
  module Remediations
@@ -105,6 +106,11 @@ module SvgConform
105
106
  remove_node(node)
106
107
  end
107
108
 
109
+ # Clean up unused namespace declarations
110
+ # Note: Moxml/Nokogiri don't support removing namespace declarations directly,
111
+ # so we mark the document for post-processing during serialization
112
+ cleanup_unused_namespace_declarations(document)
113
+
108
114
  changes
109
115
  end
110
116
 
@@ -146,6 +152,61 @@ module SvgConform
146
152
  # Same logic as find_namespace_uri_for_prefix
147
153
  find_namespace_uri_for_prefix(node, prefix)
148
154
  end
155
+
156
+ def cleanup_unused_namespace_declarations(document)
157
+ # Find which namespace prefixes are still in use
158
+ used_prefixes = find_used_namespace_prefixes(document)
159
+
160
+ # Get the root element
161
+ root = document.respond_to?(:root) ? document.root : document
162
+ return unless root.respond_to?(:namespace_definitions)
163
+
164
+ # Get disallowed namespace prefixes
165
+ disallowed_prefixes = root.namespace_definitions.filter_map do |ns|
166
+ prefix = ns.respond_to?(:prefix) ? ns.prefix : nil
167
+ # Extract namespace URI from Moxml::Namespace object
168
+ uri = ns.respond_to?(:uri) ? ns.uri : nil
169
+ next if prefix.nil? || prefix.empty? # Skip default namespace
170
+ next if allowed_namespaces.include?(uri) # Keep allowed namespaces
171
+
172
+ next if used_prefixes.include?(prefix) # Keep prefixes still in use
173
+
174
+ prefix
175
+ end
176
+
177
+ # Store the prefixes to remove on the document for later serialization
178
+ # Try to store on Document wrapper first, fallback to moxml_document
179
+ target = document.respond_to?(:instance_variable_set) ? document : root.document
180
+ target&.instance_variable_set(:@unused_namespace_prefixes,
181
+ disallowed_prefixes)
182
+ end
183
+
184
+ def find_used_namespace_prefixes(document)
185
+ used_prefixes = Set.new(["xml"]) # xml prefix is always reserved
186
+
187
+ document.traverse do |node|
188
+ next unless node.respond_to?(:name)
189
+
190
+ # Check element name for namespace prefix
191
+ if node.name.include?(":")
192
+ prefix = node.name.split(":").first
193
+ used_prefixes << prefix
194
+ end
195
+
196
+ # Check attributes for namespace prefixes
197
+ if node.respond_to?(:attributes)
198
+ node.attributes.each do |attr|
199
+ attr_name = attr.respond_to?(:name) ? attr.name : attr.to_s
200
+ if attr_name.include?(":")
201
+ prefix = attr_name.split(":").first
202
+ used_prefixes << prefix
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ used_prefixes
209
+ end
149
210
  end
150
211
  end
151
212
  end
@@ -1,9 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../node_helpers"
4
+ require_relative "../interfaces/requirement_interface"
5
+
3
6
  module SvgConform
4
7
  module Requirements
5
8
  # Base class for all validation requirements
6
9
  class BaseRequirement < Lutaml::Model::Serializable
10
+ include SvgConform::NodeHelpers
11
+ include SvgConform::Interfaces::RequirementInterface
12
+
7
13
  attribute :id, :string
8
14
  attribute :description, :string
9
15
  attribute :type, :string, polymorphic_class: true, default: -> {
@@ -65,48 +71,8 @@ module SvgConform
65
71
  true
66
72
  end
67
73
 
68
- # Helper method to check if a node is an element
69
- def element?(node)
70
- node.respond_to?(:name) && !node.name.nil?
71
- end
72
-
73
- # Helper method to check if a node is text
74
- def text?(node)
75
- node.respond_to?(:text?) && node.text?
76
- end
77
-
78
- # Helper method to get attribute value
79
- def get_attribute(node, name)
80
- return nil unless node.respond_to?(:attribute)
81
-
82
- attr = node.attribute(name)
83
- attr&.value
84
- end
85
-
86
- # Helper method to set attribute value
87
- def set_attribute(node, name, value)
88
- return false unless node.respond_to?(:set_attribute)
89
-
90
- node.set_attribute(name, value)
91
- true
92
- end
93
-
94
- # Helper method to remove attribute
95
- def remove_attribute(node, name)
96
- return false unless node.respond_to?(:remove_attribute)
97
-
98
- node.remove_attribute(name)
99
- true
100
- end
101
-
102
- # Helper method to check if attribute exists
103
- def has_attribute?(node, name)
104
- return false unless node.respond_to?(:attribute)
105
-
106
- !node.attribute(name).nil?
107
- end
108
-
109
74
  # Helper method to get all attributes
75
+ # Note: Other attribute helpers are included via NodeHelpers module
110
76
  def get_attributes(node)
111
77
  return {} unless node.respond_to?(:attributes)
112
78
 
@@ -117,6 +83,20 @@ module SvgConform
117
83
  end
118
84
  end
119
85
 
86
+ # Shared helper for skipping attribute validation on structurally invalid nodes
87
+ # Used by both DOM and SAX validation
88
+ def skip_attribute_validation?(node_or_element, context)
89
+ # For DOM mode, also check if it's an element
90
+ return true if element?(node_or_element) && context.node_structurally_invalid?(node_or_element)
91
+
92
+ # For SAX mode (ElementProxy always has this method)
93
+ if node_or_element.respond_to?(:path_id)
94
+ return context.node_structurally_invalid?(node_or_element)
95
+ end
96
+
97
+ false
98
+ end
99
+
120
100
  def to_s
121
101
  "#{@id}: #{@description}"
122
102
  end
@@ -26,70 +26,30 @@ module SvgConform
26
26
  def check(node, context)
27
27
  return unless element?(node)
28
28
 
29
- # Skip attribute validation for structurally invalid nodes (e.g., wrong parent-child)
30
- return if context.node_structurally_invalid?(node)
31
-
32
- # Check color-related attributes
33
- color_attributes = %w[fill stroke color stop-color flood-color
34
- lighting-color]
35
-
36
- color_attributes.each do |attr_name|
37
- value = get_attribute(node, attr_name)
38
- next if value.nil? || value.empty?
39
-
40
- next if valid_color?(value)
41
-
42
- context.add_error(
43
- requirement_id: id,
44
- message: "Color '#{value}' in attribute '#{attr_name}' is not allowed in this profile",
45
- node: node,
46
- severity: :error,
47
- data: {
48
- attribute: attr_name,
49
- value: value,
50
- element: node.name,
51
- },
52
- )
53
- end
54
-
55
- # Check style attribute for color properties
56
- style_value = get_attribute(node, "style")
57
- return unless style_value
58
-
59
- styles = parse_style(style_value)
60
- color_properties = %w[fill stroke color stop-color flood-color
61
- lighting-color]
62
-
63
- color_properties.each do |prop|
64
- value = styles[prop]
65
- next if value.nil? || value.empty?
66
-
67
- next if valid_color?(value)
68
-
69
- context.add_error(
70
- requirement_id: id,
71
- message: "Color '#{value}' in style property '#{prop}' is not allowed in this profile",
72
- node: node,
73
- severity: :error,
74
- data: {
75
- attribute: prop,
76
- value: value,
77
- element: node.name,
78
- },
79
- )
80
- end
29
+ validate_colors(node, context)
81
30
  end
82
31
 
83
32
  def validate_sax_element(element, context)
33
+ # ElementProxy always represents an element, so no element? check needed
34
+ validate_colors(element, context)
35
+ end
36
+
37
+ private
38
+
39
+ # Shared validation logic for both DOM and SAX modes
40
+ def validate_colors(node_or_element, context)
84
41
  # Skip attribute validation for structurally invalid nodes
85
- return if context.node_structurally_invalid?(element)
42
+ return if skip_attribute_validation?(node_or_element, context)
43
+
44
+ # Get element name
45
+ element_name = node_or_element.name
86
46
 
87
47
  # Check color-related attributes
88
48
  color_attributes = %w[fill stroke color stop-color flood-color
89
49
  lighting-color]
90
50
 
91
51
  color_attributes.each do |attr_name|
92
- value = element.raw_attributes[attr_name]
52
+ value = get_attribute(node_or_element, attr_name)
93
53
  next if value.nil? || value.empty?
94
54
 
95
55
  next if valid_color?(value)
@@ -97,18 +57,18 @@ module SvgConform
97
57
  context.add_error(
98
58
  requirement_id: id,
99
59
  message: "Color '#{value}' in attribute '#{attr_name}' is not allowed in this profile",
100
- node: element,
60
+ node: node_or_element,
101
61
  severity: :error,
102
62
  data: {
103
63
  attribute: attr_name,
104
64
  value: value,
105
- element: element.name,
65
+ element: element_name,
106
66
  },
107
67
  )
108
68
  end
109
69
 
110
70
  # Check style attribute for color properties
111
- style_value = element.raw_attributes["style"]
71
+ style_value = get_attribute(node_or_element, "style")
112
72
  return unless style_value
113
73
 
114
74
  styles = parse_style(style_value)
@@ -124,19 +84,17 @@ module SvgConform
124
84
  context.add_error(
125
85
  requirement_id: id,
126
86
  message: "Color '#{value}' in style property '#{prop}' is not allowed in this profile",
127
- node: element,
87
+ node: node_or_element,
128
88
  severity: :error,
129
89
  data: {
130
90
  attribute: prop,
131
91
  value: value,
132
- element: element.name,
92
+ element: element_name,
133
93
  },
134
94
  )
135
95
  end
136
96
  end
137
97
 
138
- private
139
-
140
98
  def valid_color?(color)
141
99
  # First check if threshold-based validation is enabled
142
100
  if black_and_white_threshold
@@ -27,49 +27,39 @@ module SvgConform
27
27
  def check(node, context)
28
28
  return unless element?(node)
29
29
 
30
- # Check font-family attribute only (not style properties)
31
- # Style properties are handled by StylePromotionRequirement to avoid duplication
32
- font_family = get_attribute(node, "font-family")
33
- return unless font_family
34
-
35
- if svgcheck_compatibility
36
- check_font_family_svgcheck_mode(node, context, font_family,
37
- "font-family")
38
- elsif !valid_font_family?(font_family)
39
- context.add_error(
40
- requirement_id: id,
41
- message: "Font family '#{font_family}' is not allowed in this profile",
42
- node: node,
43
- severity: :error,
44
- data: { attribute: "font-family", value: font_family },
45
- )
46
- end
30
+ validate_font_family(node, context)
47
31
  end
48
32
 
49
33
  def validate_sax_element(element, context)
50
- # Skip if parent is structurally invalid
34
+ # Skip if parent is structurally invalid (SAX-specific: DOM handles this via should_check_node?)
51
35
  return if element.parent && context.node_structurally_invalid?(element.parent)
52
36
 
53
- # Check font-family attribute only
54
- font_family = element.raw_attributes["font-family"]
37
+ validate_font_family(element, context)
38
+ end
39
+
40
+ private
41
+
42
+ # Shared validation logic for both DOM and SAX modes
43
+ def validate_font_family(node_or_element, context)
44
+ # Check font-family attribute only (not style properties)
45
+ # Style properties are handled by StylePromotionRequirement to avoid duplication
46
+ font_family = get_attribute(node_or_element, "font-family")
55
47
  return unless font_family
56
48
 
57
49
  if svgcheck_compatibility
58
- check_font_family_svgcheck_mode(element, context, font_family,
50
+ check_font_family_svgcheck_mode(node_or_element, context, font_family,
59
51
  "font-family")
60
52
  elsif !valid_font_family?(font_family)
61
53
  context.add_error(
62
54
  requirement_id: id,
63
55
  message: "Font family '#{font_family}' is not allowed in this profile",
64
- node: element,
56
+ node: node_or_element,
65
57
  severity: :error,
66
58
  data: { attribute: "font-family", value: font_family },
67
59
  )
68
60
  end
69
61
  end
70
62
 
71
- private
72
-
73
63
  def check_font_family_svgcheck_mode(node, context, font_family_value,
74
64
  attribute_name)
75
65
  # Check if the font family value is valid according to svgcheck
@@ -57,7 +57,7 @@ module SvgConform
57
57
 
58
58
  # Validate collected style elements
59
59
  @collected_style_elements.each do |element|
60
- check_style_element_sax(element, context)
60
+ check_style_element(element, context)
61
61
  end
62
62
 
63
63
  # Reset for next validation
@@ -79,17 +79,22 @@ module SvgConform
79
79
  # Style elements handled in deferred validation (need text content)
80
80
  # Already collected in collect_sax_data
81
81
  when "link"
82
- check_link_element_sax(element, context) if check_link_elements
82
+ check_link_element(element, context) if check_link_elements
83
83
  else
84
- check_style_attribute_sax(element, context) if check_style_attributes
84
+ check_style_attribute(element, context) if check_style_attributes
85
85
  end
86
86
  end
87
87
 
88
88
  private
89
89
 
90
- def check_style_element(node, context)
90
+ def check_style_element(node_or_element, context)
91
91
  # Check for @import rules in style elements
92
- content = node.text || ""
92
+ # Handle both DOM nodes (node.text) and SAX elements (element.text_content)
93
+ content = if node_or_element.respond_to?(:text_content)
94
+ node_or_element.text_content
95
+ else
96
+ node_or_element.text || ""
97
+ end
93
98
 
94
99
  if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
95
100
  url = ::Regexp.last_match(1)
@@ -97,7 +102,7 @@ module SvgConform
97
102
  context.add_error(
98
103
  requirement_id: id,
99
104
  message: "External CSS import not allowed: #{url}",
100
- node: node,
105
+ node: node_or_element,
101
106
  severity: :error,
102
107
  )
103
108
  end
@@ -111,14 +116,14 @@ module SvgConform
111
116
  context.add_error(
112
117
  requirement_id: id,
113
118
  message: "External CSS import not allowed: #{url}",
114
- node: node,
119
+ node: node_or_element,
115
120
  severity: :error,
116
121
  )
117
122
  end
118
123
 
119
- def check_link_element(node, context)
120
- rel = get_attribute(node, "rel")
121
- href = get_attribute(node, "href")
124
+ def check_link_element(node_or_element, context)
125
+ rel = get_attribute(node_or_element, "rel")
126
+ href = get_attribute(node_or_element, "href")
122
127
 
123
128
  return unless rel&.downcase == "stylesheet" && href
124
129
 
@@ -127,13 +132,13 @@ module SvgConform
127
132
  context.add_error(
128
133
  requirement_id: id,
129
134
  message: "External CSS link not allowed: #{href}",
130
- node: node,
135
+ node: node_or_element,
131
136
  severity: :error,
132
137
  )
133
138
  end
134
139
 
135
- def check_style_attribute(node, context)
136
- style_value = get_attribute(node, "style")
140
+ def check_style_attribute(node_or_element, context)
141
+ style_value = get_attribute(node_or_element, "style")
137
142
  return unless style_value
138
143
 
139
144
  # Check for url() references in style attributes
@@ -145,70 +150,7 @@ module SvgConform
145
150
  context.add_error(
146
151
  requirement_id: id,
147
152
  message: "External URL reference in style attribute not allowed: #{url}",
148
- node: node,
149
- severity: :error,
150
- )
151
- end
152
-
153
- def check_style_element_sax(element, context)
154
- # Check for @import rules in style elements
155
- content = element.text_content
156
-
157
- if content =~ /@import\s+url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
158
- url = ::Regexp.last_match(1)
159
- unless allowed_url?(url)
160
- context.add_error(
161
- requirement_id: id,
162
- message: "External CSS import not allowed: #{url}",
163
- node: element,
164
- severity: :error,
165
- )
166
- end
167
- end
168
-
169
- return unless content =~ /@import\s+['"]([^'"]+)['"]/i
170
-
171
- url = ::Regexp.last_match(1)
172
- return if allowed_url?(url)
173
-
174
- context.add_error(
175
- requirement_id: id,
176
- message: "External CSS import not allowed: #{url}",
177
- node: element,
178
- severity: :error,
179
- )
180
- end
181
-
182
- def check_link_element_sax(element, context)
183
- rel = element.raw_attributes["rel"]
184
- href = element.raw_attributes["href"]
185
-
186
- return unless rel&.downcase == "stylesheet" && href
187
-
188
- return if allowed_url?(href)
189
-
190
- context.add_error(
191
- requirement_id: id,
192
- message: "External CSS link not allowed: #{href}",
193
- node: element,
194
- severity: :error,
195
- )
196
- end
197
-
198
- def check_style_attribute_sax(element, context)
199
- style_value = element.raw_attributes["style"]
200
- return unless style_value
201
-
202
- # Check for url() references in style attributes
203
- return unless style_value =~ /url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i
204
-
205
- url = ::Regexp.last_match(1)
206
- return if allowed_url?(url)
207
-
208
- context.add_error(
209
- requirement_id: id,
210
- message: "External URL reference in style attribute not allowed: #{url}",
211
- node: element,
153
+ node: node_or_element,
212
154
  severity: :error,
213
155
  )
214
156
  end
@@ -18,96 +18,41 @@ module SvgConform
18
18
  root = document.root
19
19
  return unless root&.name == "svg"
20
20
 
21
- viewbox = get_attribute(root, "viewBox")
22
-
23
- if viewbox.nil? || viewbox.empty?
24
- context.add_error(
25
- requirement: self,
26
- node: root,
27
- message: "SVG root element must have a viewBox attribute",
28
- data: { missing_attribute: "viewBox" },
29
- )
30
-
31
- # Add informational message about calculated viewBox if width/height are present
32
- width = get_attribute(root, "width")
33
- height = get_attribute(root, "height")
34
-
35
- if width && height && valid_number?(width) && valid_number?(height)
36
- calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
37
- context.add_error(
38
- requirement: self,
39
- node: root,
40
- message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
41
- data: {
42
- calculated_viewbox: calculated_viewbox,
43
- source_width: width,
44
- source_height: height,
45
- },
46
- )
47
- end
48
- return
49
- end
50
-
51
- # Validate viewBox format (should be "min-x min-y width height")
52
- # Also accept comma-separated with parentheses like "(0, 0, 100, 100)" (svgcheck is lenient)
53
- normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
54
- parts = normalized_viewbox.strip.split(/\s+/)
55
- unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
56
- context.add_error(
57
- requirement: self,
58
- node: root,
59
- message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
60
- data: {
61
- viewbox_value: viewbox,
62
- parsed_parts: parts,
63
- },
64
- )
65
- return
66
- end
67
-
68
- # Check that width and height are positive
69
- width = parts[2].to_f
70
- height = parts[3].to_f
71
-
72
- return unless width <= 0 || height <= 0
73
-
74
- context.add_error(
75
- requirement: self,
76
- node: root,
77
- message: "viewBox width and height must be positive values",
78
- data: {
79
- viewbox_value: viewbox,
80
- width: width,
81
- height: height,
82
- },
83
- )
21
+ validate_viewbox(root, context)
84
22
  end
85
23
 
86
24
  def validate_sax_element(element, context)
87
25
  # Only check the root SVG element
88
26
  return unless element.name == "svg" && element.parent.nil?
89
27
 
90
- viewbox = element.raw_attributes["viewBox"]
28
+ validate_viewbox(element, context)
29
+ end
30
+
31
+ private
32
+
33
+ # Shared validation logic for both DOM and SAX modes
34
+ def validate_viewbox(svg_root, context)
35
+ viewbox = get_attribute(svg_root, "viewBox")
91
36
 
92
37
  if viewbox.nil? || viewbox.empty?
93
38
  context.add_error(
94
39
  requirement_id: id,
95
40
  message: "SVG root element must have a viewBox attribute",
96
- node: element,
41
+ node: svg_root,
97
42
  severity: :error,
98
43
  data: { missing_attribute: "viewBox" },
99
44
  )
100
45
 
101
46
  # Add informational message about calculated viewBox if width/height are present
102
- width = element.raw_attributes["width"]
103
- height = element.raw_attributes["height"]
47
+ width = get_attribute(svg_root, "width")
48
+ height = get_attribute(svg_root, "height")
104
49
 
105
50
  if width && height && valid_number?(width) && valid_number?(height)
106
51
  calculated_viewbox = "0 0 #{width.to_f} #{height.to_f}"
107
52
  context.add_error(
108
53
  requirement_id: id,
109
54
  message: "Trying to put in the attribute with value '#{calculated_viewbox}'",
110
- node: element,
55
+ node: svg_root,
111
56
  severity: :error,
112
57
  data: {
113
58
  calculated_viewbox: calculated_viewbox,
@@ -120,13 +65,14 @@ module SvgConform
120
65
  end
121
66
 
122
67
  # Validate viewBox format (should be "min-x min-y width height")
68
+ # Also accept comma-separated with parentheses like "(0, 0, 100, 100)" (svgcheck is lenient)
123
69
  normalized_viewbox = viewbox.strip.gsub(/[(),]/, " ").squeeze(" ")
124
70
  parts = normalized_viewbox.strip.split(/\s+/)
125
71
  unless parts.length == 4 && parts.all? { |part| valid_number?(part) }
126
72
  context.add_error(
127
73
  requirement_id: id,
128
74
  message: "viewBox attribute must contain four numeric values (min-x min-y width height)",
129
- node: element,
75
+ node: svg_root,
130
76
  severity: :error,
131
77
  data: {
132
78
  viewbox_value: viewbox,
@@ -145,7 +91,7 @@ module SvgConform
145
91
  context.add_error(
146
92
  requirement_id: id,
147
93
  message: "viewBox width and height must be positive values",
148
- node: element,
94
+ node: svg_root,
149
95
  severity: :error,
150
96
  data: {
151
97
  viewbox_value: viewbox,
@@ -155,8 +101,6 @@ module SvgConform
155
101
  )
156
102
  end
157
103
 
158
- private
159
-
160
104
  def valid_number?(str)
161
105
  Float(str)
162
106
  true
@@ -3,6 +3,9 @@
3
3
  require "nokogiri"
4
4
  require_relative "element_proxy"
5
5
  require_relative "validation_context"
6
+ require_relative "validation/error_tracker"
7
+ require_relative "validation/structural_invalidity_tracker"
8
+ require_relative "validation/node_id_manager"
6
9
  require_relative "validation_result"
7
10
  require_relative "references"
8
11
 
@@ -139,14 +142,19 @@ module SvgConform
139
142
  context = ValidationContext.allocate
140
143
  context.instance_variable_set(:@document, nil)
141
144
  context.instance_variable_set(:@profile, @profile)
142
- context.instance_variable_set(:@errors, [])
143
- context.instance_variable_set(:@warnings, [])
144
- context.instance_variable_set(:@validity_errors, [])
145
- context.instance_variable_set(:@infos, [])
145
+ context.instance_variable_set(:@error_tracker, Validation::ErrorTracker.new)
146
+ context.instance_variable_set(:@fixes, [])
146
147
  context.instance_variable_set(:@data, {})
147
- context.instance_variable_set(:@structurally_invalid_node_ids, Set.new)
148
- context.instance_variable_set(:@node_id_cache, {})
149
- context.instance_variable_set(:@cache_populated, true) # Skip population for SAX
148
+ # Create NodeIdManager without a document (SAX mode)
149
+ node_id_manager = Validation::NodeIdManager.new(nil)
150
+ context.instance_variable_set(:@node_id_manager, node_id_manager)
151
+ # Create StructuralInvalidityTracker with a node ID generator
152
+ context.instance_variable_set(:@structural_invalidity_tracker,
153
+ Validation::StructuralInvalidityTracker.new(
154
+ node_id_generator: ->(node) {
155
+ node_id_manager.generate_node_id(node)
156
+ },
157
+ ))
150
158
  context.instance_variable_set(:@reference_manifest,
151
159
  References::ReferenceManifest.new(source_document: nil))
152
160
  context