Module: Moxml::Entity
- Defined in:
- lib/moxml/entity.rb,
lib/moxml/entity/restorer.rb,
lib/moxml/entity/reference.rb
Overview
Entity-reference round-trip pipeline.
XML libraries split into two camps: those that preserve named entity references natively (nokogiri) and those that expand or reject them at parse (ox, rexml, oga, libxml, leptris). Moxml bridges the camps with one pipeline, owned here and delegated to by Adapter::Base:
- Parse time — .preprocess_entities rewrites non-standard entity references to a two-character marker (see MARKER) so the reference survives native parsing verbatim. The five standard entities (& < > " ') are NOT converted.
- Read time — marker-bearing text is split into text and Entity::Reference nodes (adapters whose natives cannot hold references), or decoded in place (.decode_entities covers the five standard entities plus numeric character references).
- Serialize time — .restore_entities maps markers back to named references, including markers a native serializer already rendered as character references.
How references are STORED between parse and serialize is adapter-shaped (in-tree value objects for ox, attachment sequences for rexml/libxml, text markers for oga/leptris); the marker lifecycle and the Entity::Reference value type are the shared, single-sourced parts.
Defined Under Namespace
Constant Summary collapse
- MARKER =
Marker for adapters that resolve entities during parsing. U+FFFC (Object Replacement Character) + U+FEFF (BOM) is a two-character sentinel chosen because this exact sequence followed by a valid entity name pattern is vanishingly unlikely in real XML content.
"\u{FFFC}\u{FEFF}"- NAME_PATTERN =
"[a-zA-Z_][\\w.:-]*"- NAME_RE =
/&(#{NAME_PATTERN});/- MARKER_RE =
/\u{FFFC}\u{FEFF}(#{NAME_PATTERN});/- SERIALIZED_MARKER_RE =
/(#{NAME_PATTERN});/- STANDARD_ENTITIES =
%w[amp lt gt quot apos].freeze
Class Method Summary collapse
-
.decode_entities(text) ⇒ Object
Resolve numeric (&#NN; / &#xNN;) and the five standard named (& < > " ') XML entity references in one single pass.
- .preprocess_entities(xml) ⇒ Object
-
.preprocess_with_marker_flag(xml) ⇒ Object
Replace non-standard entity references with markers before parsing.
-
.restore_entities(text) ⇒ Object
Restore entity markers back to named entity references.
Class Method Details
.decode_entities(text) ⇒ Object
Resolve numeric (&#NN; / &#xNN;) and the five standard named (& < > " ') XML entity references in one single pass. The resulting characters are data; no further decoding is applied. Numeric refs producing invalid UTF-8 (NUL, surrogate halves, > U+10FFFF) are preserved verbatim to avoid silently emitting malformed bytes.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/moxml/entity.rb', line 109 def decode_entities(text) return text unless text.is_a?(String) && text.include?("&") text.gsub(DECODE_RE) do if (named = ::Regexp.last_match(1)) NAMED_DECODE_MAP[named] else code = ::Regexp.last_match(2) ? ::Regexp.last_match(2).to_i : ::Regexp.last_match(3).to_i(16) if code.zero? || code.between?(0xD800, 0xDFFF) || code > 0x10FFFF ::Regexp.last_match(0) else [code].pack("U") end end end end |
.preprocess_entities(xml) ⇒ Object
99 100 101 |
# File 'lib/moxml/entity.rb', line 99 def preprocess_entities(xml) preprocess_with_marker_flag(xml)[0] end |
.preprocess_with_marker_flag(xml) ⇒ Object
Replace non-standard entity references with markers before
parsing. Always returns a UTF-8 encoded string, and reports
whether the result can contain markers: the flag rides the
same & scan, so callers needing it (leptris parse) avoid a
second full-buffer multibyte probe.
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 |
# File 'lib/moxml/entity.rb', line 62 def preprocess_with_marker_flag(xml) return ["", false] if xml.nil? str = if xml.encoding == Encoding::BINARY # Binary strings are assumed to be UTF-8. If the bytes are # not valid UTF-8, fall back to encoding as UTF-8 with # replacement to avoid raising on gsub. dup = xml.dup.force_encoding("UTF-8") if dup.valid_encoding? dup else xml.dup.encode("UTF-8", "ASCII-8BIT", invalid: :replace, undef: :replace) end elsif xml.encoding == Encoding::UTF_8 xml else xml.encode("UTF-8") end # Fast path: no `&` means no entity references to mark — skip # the regex scan and string allocation entirely. The vast # majority of XML payloads contain no entity references. return [str, false] unless str.include?("&") marked = false processed = str.gsub(NAME_RE) do |match| name = ::Regexp.last_match(1) if STANDARD_ENTITIES.include?(name) match else marked = true "#{MARKER}#{name};" end end [processed, marked] end |
.restore_entities(text) ⇒ Object
Restore entity markers back to named entity references.
127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/moxml/entity.rb', line 127 def restore_entities(text) return text unless text.is_a?(String) # Force UTF-8 encoding since markers are UTF-8 characters str = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding("UTF-8") # Fast path: the vast majority of documents carry no entity # markers — two C-level scans beat two regex passes. return str unless str.include?(MARKER) || str.include?("") result = str.gsub(MARKER_RE, '&\1;') result.gsub(SERIALIZED_MARKER_RE, '&\1;') end |