relaton-bib 2.1.7 → 2.1.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d36c3ed1e4ae0042f96a06a6320dccbe089a87a691b77b8ad14e276217bd22
4
- data.tar.gz: d6db31f185b72668da0c289bc462a62d1cfd1b9dda542f37b9dee7257d5282b4
3
+ metadata.gz: 74bf6a3c3abe13cbadc903c2d2b811b125f654033e092ed33305834663404854
4
+ data.tar.gz: 396420681c9fe3567117b7e6a992c56a0cbfd3b219d36d650d8deb072a36435b
5
5
  SHA512:
6
- metadata.gz: 304fcb5b02e2c019d532164228acede4ed81c210eafededbbdd9446ac94fcd32d142be5be7689db09bc17b7aa3e6cfb333b892220ba36e64fcccc2bdda2df29c
7
- data.tar.gz: 390cf8b808ab85b099d573b4bea833d265a2326d81c00936e5cebe6ae79c9243991843d44a46d3d24e403137f1ec27b433acc532c51f99a0d5e91d0ed66d6744
6
+ metadata.gz: 9fdb28c540d26dd47ac56af80c292cae33f6cafa96672161d732f85e457b4a7336c2c5f11d5f5b852284433f73b08cfefe7d2af0c1e2d99e1557c05f985f46da
7
+ data.tar.gz: 1585a8df0f2a2ad420eb26ed1d11a6f36483f90f10510f3cb63f72145d0d401b4499016100475b37195f31033d4c600bd281d8d4f97b7f648732212f7a5378b2
@@ -42,15 +42,151 @@ module Relaton
42
42
 
43
43
  TAG_RX = %r{<[a-zA-Z/!?]}
44
44
 
45
+ # Captures a namespace prefix, on a tag or on an attribute: the
46
+ # "jats" of <jats:p> and </jats:italic>, and the "xlink" of
47
+ # xlink:href.
48
+ NS_PREFIX_RX = %r{(?:</?|\s)([A-Za-z_][\w.-]*):(?=[A-Za-z_])}
49
+
50
+ # Namespace that declares a prefix which the content leaves
51
+ # undeclared. The sanitiser removes it again before it serialises.
52
+ NS_PLACEHOLDER = "urn:x-relaton-undeclared:%s".freeze
53
+
54
+ # Element that carries the placeholder declarations. Its children
55
+ # are the sanitised content, so the element itself never reaches
56
+ # the output.
57
+ NS_WRAPPER = "relaton-sanitizer-root".freeze
58
+
59
+ # Reserved prefixes. XML declares both, so the content must not.
60
+ NS_RESERVED = %w[xml xmlns].freeze
61
+
62
+ # Serialise without the FORMAT option, so the sanitiser keeps the
63
+ # shape of element-only content instead of adding newlines and
64
+ # indent.
65
+ SAVE_OPTS = Nokogiri::XML::Node::SaveOptions::AS_XML
66
+
45
67
  def self.sanitize(content)
46
68
  return content unless sanitizable?(content)
47
69
 
70
+ node = parse(content)
71
+ return content if node.nil?
72
+
73
+ sanitize_children(node)
74
+ node.children.map do |c|
75
+ c.to_xml(encoding: "UTF-8", save_with: SAVE_OPTS)
76
+ end.join
77
+ end
78
+
79
+ #
80
+ # Parse the content into a node whose children are the content.
81
+ #
82
+ # @param [String] content The raw marked-up content.
83
+ #
84
+ # @return [Nokogiri::XML::Node, nil] The node, or nil when the
85
+ # content does not parse.
86
+ #
87
+ def self.parse(content)
48
88
  fragment = Nokogiri::XML::DocumentFragment.parse(content)
49
- return content if fragment.errors.any?
89
+ return fragment if fragment.errors.empty?
90
+
91
+ parse_with_prefixes(content)
92
+ end
93
+ private_class_method :parse
50
94
 
51
- sanitize_children(fragment)
52
- fragment.children.map { |c| c.to_xml(encoding: "UTF-8") }.join
95
+ #
96
+ # Parse content that uses undeclared namespace prefixes.
97
+ #
98
+ # An undeclared prefix is always a parse error, so without this the
99
+ # sanitiser gives up on exactly the third-party markup that needs
100
+ # sanitising most. Declare every prefix that the content uses on a
101
+ # wrapper element, parse, then remove the placeholder namespaces
102
+ # from the elements and from the attributes. See metanorma-pdfa#99.
103
+ #
104
+ # An undeclared prefix inside an OPAQUE <stem> goes as well. The
105
+ # sanitiser cannot keep it: an undeclared prefix in the output is
106
+ # the exact failure that this method removes. Only a namespace that
107
+ # the content declares itself survives verbatim.
108
+ #
109
+ # @param [String] content The raw marked-up content.
110
+ #
111
+ # @return [Nokogiri::XML::Element, nil] The wrapper element, or nil
112
+ # when the content uses no prefix or does not parse.
113
+ #
114
+ def self.parse_with_prefixes(content)
115
+ decl = placeholder_declarations(content) or return
116
+ name = wrapper_name(content)
117
+ doc = Nokogiri::XML "<#{name} #{decl}>#{content}</#{name}>"
118
+ return unless doc.errors.empty?
119
+
120
+ drop_placeholder_namespaces doc.root
121
+ end
122
+ private_class_method :parse_with_prefixes
123
+
124
+ #
125
+ # Declare every namespace prefix that the content uses.
126
+ #
127
+ # @param [String] content The raw marked-up content.
128
+ #
129
+ # @return [String, nil] The declarations, or nil when the content
130
+ # uses no prefix.
131
+ #
132
+ def self.placeholder_declarations(content)
133
+ prefixes = content.scan(NS_PREFIX_RX).flatten.uniq - NS_RESERVED
134
+ return if prefixes.empty?
135
+
136
+ prefixes.map do |pfx|
137
+ %(xmlns:#{pfx}="#{format NS_PLACEHOLDER, pfx}")
138
+ end.join(" ")
139
+ end
140
+ private_class_method :placeholder_declarations
141
+
142
+ #
143
+ # Name a wrapper element that the content does not close itself.
144
+ #
145
+ # Content that holds the literal end tag of the wrapper would close
146
+ # it early. The document then has more than one root, the parse
147
+ # fails, and the sanitiser gives up on content that it can handle.
148
+ #
149
+ # @param [String] content The raw marked-up content.
150
+ #
151
+ # @return [String] A name that the content does not contain.
152
+ #
153
+ def self.wrapper_name(content)
154
+ name = NS_WRAPPER
155
+ name += "-x" while content.include?(name)
156
+ name
157
+ end
158
+ private_class_method :wrapper_name
159
+
160
+ #
161
+ # Remove the placeholder namespaces, and only those.
162
+ #
163
+ # Nokogiri's remove_namespaces! would also strip a namespace that
164
+ # the content declares itself, such as the MathML xmlns inside an
165
+ # OPAQUE <stem>, which must survive verbatim. Match the wrapper's
166
+ # own declarations, so a namespace of the content never matches,
167
+ # whatever its URI.
168
+ #
169
+ # The declarations stay on the wrapper element. Only its children
170
+ # reach the output, so the declarations never leak. Do not
171
+ # serialise the root itself.
172
+ #
173
+ # @param [Nokogiri::XML::Element] root The wrapper element.
174
+ #
175
+ # @return [Nokogiri::XML::Element] The same element.
176
+ #
177
+ def self.drop_placeholder_namespaces(root)
178
+ placeholders = root.namespace_definitions
179
+ root.traverse do |node|
180
+ node.namespace = nil if placeholders.include?(node.namespace)
181
+ next unless node.element?
182
+
183
+ node.attribute_nodes.each do |attr|
184
+ attr.namespace = nil if placeholders.include?(attr.namespace)
185
+ end
186
+ end
187
+ root
53
188
  end
189
+ private_class_method :drop_placeholder_namespaces
54
190
 
55
191
  def self.sanitizable?(content)
56
192
  content.is_a?(::String) && !content.empty? && content.match?(TAG_RX)
@@ -1,5 +1,5 @@
1
1
  module Relaton
2
2
  module Bib
3
- VERSION = "2.1.7".freeze
3
+ VERSION = "2.1.9".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton-bib
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.7
4
+ version: 2.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-31 00:00:00.000000000 Z
11
+ date: 2026-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bibtex-ruby