Class: YamlExporter::Exporter::BlockScalarTree

Inherits:
Psych::Visitors::YAMLTree
  • Object
show all
Defined in:
lib/yaml_exporter/exporter.rb

Overview

Renders any LiteralString value as a literal block scalar (|). Plain Strings and every other type fall through to Psych's default handling, so the output is byte-identical to YAML.dump whenever no LiteralString is present.

Astral-plane characters (codepoints >= U+10000 — emoji, CJK Ext B, …) are the one thing that defeats block style: libyaml's emitter treats any 4-byte UTF-8 character as non-printable, and a non-printable character can only be written escaped (\U0001F4A1), which exists only in double-quoted style. So a single emoji silently drops the whole value back to an inline quoted scalar. BMP characters (umlauts, accents, ✓, →) are unaffected.

To keep block style we swap each astral character out for a Private Use Area sentinel — which libyaml does consider printable — before emitting, then swap the real characters back into the finished document. The sentinels never survive in the output, and the round-trip is exact: the parser reads literal astral characters in block scalars without trouble.

Constant Summary collapse

ASTRAL =
/[\u{10000}-\u{10FFFF}]/
SENTINEL_OPEN =

U+E000/U+E001 are Private Use Area: printable to libyaml, and they never carry meaning in real text, so a <open>digits<close> token cannot collide with exported content.

"\u{E000}"
SENTINEL_CLOSE =
"\u{E001}"
SENTINEL =
/#{SENTINEL_OPEN}\d+#{SENTINEL_CLOSE}/.freeze

Instance Method Summary collapse

Instance Method Details

#accept(target) ⇒ Object



100
101
102
103
104
105
106
# File 'lib/yaml_exporter/exporter.rb', line 100

def accept(target)
  if target.is_a?(LiteralString)
    return @emitter.scalar(escape_astral(target.to_s), nil, nil, true, true, Psych::Nodes::Scalar::LITERAL)
  end

  super
end

#escape_astral(text) ⇒ Object

Replaces each astral character with a sentinel token, remembering the reverse mapping so #restore_astral can put the real characters back.



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/yaml_exporter/exporter.rb', line 110

def escape_astral(text)
  return text unless text.match?(ASTRAL)

  text.gsub(ASTRAL) do |char|
    forward[char] ||= begin
      token = "#{SENTINEL_OPEN}#{substitutions.size}#{SENTINEL_CLOSE}"
      substitutions[token] = char
      token
    end
  end
end

#restore_astral(yaml) ⇒ Object



122
123
124
125
126
# File 'lib/yaml_exporter/exporter.rb', line 122

def restore_astral(yaml)
  return yaml if substitutions.empty?

  yaml.gsub(SENTINEL) { |token| substitutions.fetch(token) }
end