immosquare-yaml 0.1.28 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3b992a868778a9c56accd519704c486fe36d61628d99f5494f92f0400f1cde5
4
- data.tar.gz: ff1f1567cd7bd741a73f10ce5378375e0d464da5a33d7a4cf7e332d4c3c602a6
3
+ metadata.gz: 3a23201af8649660878ead589649938df652d4c3b4eccd8b5d2463643e7d0b79
4
+ data.tar.gz: 4ee0383267d4a9f5e718d0189395f043a67d663abb43409f5b1f35782e76768e
5
5
  SHA512:
6
- metadata.gz: 8bcef442f5fe2c707f674807421071a3d8c948569f753bedcb2b77002fbe9c8c076ce000a1b461fae25f3228cfed282c25cdd2d461ae0f02cb9e2664fdb3633a
7
- data.tar.gz: 98f6c75b116d1bb6d216fb9d5fab628b2c7a1b6894e0b1d7d8309c2b351ad10a3c06311c365b94b6389ed5659d83bf90d48941c1e774effc33940e67e43c70da
6
+ metadata.gz: c349896c32e18ce5fe66b8c00cc33f7e55828ddc48a4dd4e69977ea1cad46940c06186ac043574b514ed8d36357233f7442a4749c2215bca7e8c80878883a4c7
7
+ data.tar.gz: 2c0a872feb19356fca0895f00c1bc49eeba7bb6a40dab31e9710fd3a6b062d4e35b2cb0bee7b8953cc86bc0dcdab68bd77feb7eba9435b514d11a62ff030c5fe
@@ -1,3 +1,3 @@
1
1
  module ImmosquareYaml
2
- VERSION = "0.1.28".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -1,6 +1,4 @@
1
- require "English"
2
1
  require "psych"
3
- require "date"
4
2
  require "fileutils"
5
3
  require "immosquare-extensions"
6
4
  require_relative "immosquare-yaml/configuration"
@@ -8,19 +6,27 @@ require_relative "immosquare-yaml/shared_methods"
8
6
  require_relative "immosquare-yaml/railtie" if defined?(Rails)
9
7
 
10
8
  ##============================================================##
11
- ## Importing the 'English' library allows us to use more human-readable
12
- ## global variables, such as $INPUT_RECORD_SEPARATOR instead of $/,
13
- ## which enhances code clarity and makes it easier to understand
14
- ## the purpose of these variables in our code.
9
+ ## ImmosquareYaml post-processeur Psych dédié aux fichiers
10
+ ## de traduction (locales Rails).
11
+ ##
12
+ ## Trois responsabilités :
13
+ ## - parse(file) : YAML → Hash, en s'appuyant sur l'AST Psych
14
+ ## - dump(hash) : Hash → YAML formaté (quotes minimales,
15
+ ## blocs littéraux, emojis décodés)
16
+ ## - clean(file) : parse + tri par clé + dump → écrit
17
+ ##
18
+ ## La gem résout cinq problèmes que Psych seul ne traite pas :
19
+ ## 1. Norway problem (yes/no/on/off lus comme String)
20
+ ## 2. Tri déterministe par clé
21
+ ## 3. Préservation des blocs littéraux (|, |-)
22
+ ## 4. Quotes minimales pour la lisibilité
23
+ ## 5. Décodage des escapes \U0001F600 → emoji
15
24
  ##============================================================##
16
25
  module ImmosquareYaml
17
26
  extend SharedMethods
18
27
 
19
28
  class << self
20
29
 
21
- ##============================================================##
22
- ## Gem configuration
23
- ##============================================================##
24
30
  attr_writer :configuration
25
31
 
26
32
  def configuration
@@ -32,67 +38,27 @@ module ImmosquareYaml
32
38
  end
33
39
 
34
40
  ##============================================================##
35
- ## This method cleans a specified YAML file by processing it line by line.
36
- ## It executes a comprehensive cleaning routine, which involves parsing the
37
- ## YAML content to a hash, optionally sorting it, and then dumping it back
38
- ## to a YAML format.
39
- ##
40
- ## Params:
41
- ## +file_path+:: Path to the YAML file that needs to be cleaned.
42
- ## +options+:: A hash of options where :sort controls whether the output should be sorted (default is true).
43
- ##
44
- ## Returns:
45
- ## Boolean indicating the success (true) or failure (false) of the operation.
41
+ ## clean(file_path, sort: true, output: file_path)
42
+ ## Charge le fichier, le re-écrit propre et trié.
43
+ ## Retourne true / false selon le succès.
46
44
  ##============================================================##
47
45
  def clean(file_path, **options)
48
- ##============================================================##
49
- ## Default options
50
- ##============================================================##
51
46
  options = {
52
47
  :sort => true,
53
48
  :output => file_path
54
49
  }.merge(options)
55
50
 
56
51
  begin
57
- output_file_path = nil
58
52
  raise("File not found") if !File.exist?(file_path)
59
53
 
60
- ##============================================================##
61
- ## Setup variables
62
- ##============================================================##
63
- output_file_path = options[:output]
64
-
65
- ##============================================================##
66
- ## Backup original content for restoration after parsing if necessary
67
- ##============================================================##
68
- original_content = File.read(file_path) if output_file_path != file_path
69
-
70
- ##============================================================##
71
- ## The cleaning procedure is initialized with a comprehensive clean, transforming
72
- ## the YAML content to a hash to facilitate optional sorting, before
73
- ## rewriting it to the YAML file in its cleaned and optionally sorted state.
74
- ##============================================================##
75
- clean_yml(file_path)
76
- parsed_yml = parse(file_path)
77
- parsed_yml = parsed_yml.sort_by_key
78
- parsed_yml = dump(parsed_yml)
79
-
80
- ##============================================================##
81
- ## Restore original content if necessary
82
- ##============================================================##
83
- File.write(file_path, original_content) if output_file_path != file_path
54
+ parsed_yml = parse(file_path, :sort => options[:sort])
55
+ return false if parsed_yml == false
84
56
 
85
- ##============================================================##
86
- ## Write the cleaned YAML content to the specified output file
87
- ##============================================================##
88
- FileUtils.mkdir_p(File.dirname(output_file_path))
89
- File.write(output_file_path, parsed_yml)
57
+ output = dump(parsed_yml)
58
+ FileUtils.mkdir_p(File.dirname(options[:output]))
59
+ File.write(options[:output], output)
90
60
  true
91
61
  rescue StandardError => e
92
- ##============================================================##
93
- ## Restore original content if necessary
94
- ##============================================================##
95
- File.write(file_path, original_content) if output_file_path != file_path && !original_content.nil?
96
62
  puts(e.message)
97
63
  puts(e.backtrace)
98
64
  false
@@ -100,55 +66,39 @@ module ImmosquareYaml
100
66
  end
101
67
 
102
68
  ##============================================================##
103
- ## This method parses a specified YAML file, carrying out a preliminary
104
- ## cleaning operation to ensure a smooth parsing process. Following this,
105
- ## the cleaned file is transformed into a hash, which can optionally be sorted.
106
- ## It operates under the assumption that the file is properly structured.
69
+ ## parse(file_path, sort: true)
70
+ ## Lit un fichier YAML et retourne un Hash Ruby.
71
+ ## Hash trié par clé par défaut.
107
72
  ##
108
- ## Params:
109
- ## +file_path+:: Path to the YAML file that needs to be parsed.
110
- ## +options+:: A hash of options where :sort controls whether the output should be sorted (default is true).
111
- ##
112
- ## Returns:
113
- ## A hash representation of the YAML file or false if an error occurs.
73
+ ## Implémentation : on parcourt l'AST Psych plutôt que d'appeler
74
+ ## Psych.load. Cela permet de :
75
+ ## - distinguer un scalaire plain "yes" d'un bool true
76
+ ## - garder les valeurs problématiques (Norway) en String
77
+ ## - décoder nous-mêmes les escapes \U... pour les blocs
78
+ ## littéraux qui ne sont pas désescapés par Psych
114
79
  ##============================================================##
115
80
  def parse(file_path, **options)
116
81
  options = {:sort => true}.merge(options)
117
82
 
118
83
  begin
119
- original_content = nil
120
84
  raise("File not found") if !File.exist?(file_path)
121
85
 
122
86
  ##============================================================##
123
- ## Backup original content for restoration after parsing
87
+ ## Psych.parse_file retourne un Document. Si le fichier est
88
+ ## vide ou ne contient que des commentaires, root est nil.
124
89
  ##============================================================##
125
- original_content = File.read(file_path)
90
+ doc = Psych.parse_file(file_path)
91
+ return {} if !doc || doc.root.nil?
126
92
 
127
- ##============================================================##
128
- ## clean the file
129
- ##============================================================##
130
- clean_yml(file_path)
131
-
132
- ##============================================================##
133
- ## parse the file & sort if necessary
134
- ##============================================================##
135
- parsed_xml = parse_xml(file_path)
136
- parsed_xml = parsed_xml.sort_by_key if options[:sort]
137
-
138
- ##============================================================##
139
- ## Restore original content
140
- ##============================================================##
141
- File.write(file_path, original_content) if !original_content.nil?
93
+ result = node_to_value(doc.root, {})
142
94
 
143
95
  ##============================================================##
144
- ## Return the parsed YAML file
96
+ ## On accepte tous les types racine (Hash, Array, scalaire),
97
+ ## mais on ne trie que si la racine est un Hash.
145
98
  ##============================================================##
146
- parsed_xml
99
+ result = result.sort_by_key if options[:sort] && result.is_a?(Hash)
100
+ result
147
101
  rescue StandardError => e
148
- ##============================================================##
149
- ## Restore original content
150
- ##============================================================##
151
- File.write(file_path, original_content) if !original_content.nil?
152
102
  puts(e.message)
153
103
  puts(e.backtrace)
154
104
  false
@@ -156,26 +106,28 @@ module ImmosquareYaml
156
106
  end
157
107
 
158
108
  ##============================================================##
159
- ## This method performs a dump operation to obtain a well-structured
160
- ## YAML file from a hash input. It iterates through each key-value pair in the
161
- ## hash and constructs a series of lines representing the YAML file, with
162
- ## appropriate indentations and handling of various value types including
163
- ## strings with newline characters.
164
- ##
165
- ## Params:
166
- ## +hash+:: The input hash to be converted into a YAML representation.
167
- ## +lines+:: An array to hold the constructed lines (default is an empty array).
168
- ## +indent+:: The current indentation level (default is 0).
169
- ##
170
- ## Returns:
171
- ## A string representing the YAML representation of the input hash.
109
+ ## dump(hash) String YAML
110
+ ## Sérialise un Hash en YAML avec nos règles de formatage :
111
+ ## - clés "yes/no/on/..." re-quotées
112
+ ## - valeurs plain quand c'est sûr, sinon doublequotées
113
+ ## - chaînes multi-lignes en bloc littéral | ou |-
114
+ ## - arrays imbriqués délégués à Psych.dump puis indentés
172
115
  ##============================================================##
173
- def dump(hash, lines = [], indent = 0)
116
+ def dump(hash)
117
+ render_hash(hash, [], 0)
118
+ end
119
+
120
+
121
+ private
122
+
123
+
124
+ ##============================================================##
125
+ ## Rendu récursif d'un Hash. Les paramètres lines et indent
126
+ ## sont des accumulateurs internes — exposés dans la signature
127
+ ## privée uniquement.
128
+ ##============================================================##
129
+ def render_hash(hash, lines, indent)
174
130
  hash.each do |key, value|
175
- ##============================================================##
176
- ## Preparing the key with the proper indentation before identifying
177
- ## the type of the value to handle it appropriately in the YAML representation.
178
- ##============================================================##
179
131
  line = "#{SPACE * indent}#{clean_key(key)}:"
180
132
 
181
133
  case value
@@ -184,10 +136,9 @@ module ImmosquareYaml
184
136
  when String
185
137
  if value.include?(NEWLINE) || value.include?('\n')
186
138
  ##============================================================##
187
- ## We display the line with the key
188
- ## then the indentation if necessary
189
- ## then - if necessary (the + is not displayed because it is
190
- ## the default behavior)
139
+ ## Bloc littéral. On ajoute "-" si la valeur ne se termine
140
+ ## pas par un newline (chomp). Indent indicator si la valeur
141
+ ## a des leading spaces sur ses lignes.
191
142
  ##============================================================##
192
143
  line += "#{SPACE}|"
193
144
  indent_level = value[/\A */].size
@@ -196,25 +147,21 @@ module ImmosquareYaml
196
147
  lines << line
197
148
 
198
149
  ##============================================================##
199
- ## Remove quotes surrounding the value if they are present.
200
- ## They are not necessary in this case after | or |-
150
+ ## Décode les escapes \U0001F600 dans les blocs littéraux
151
+ ## (Psych ne les désescape pas pour LITERAL/FOLDED).
201
152
  ##============================================================##
202
- value = value[1..-2] while (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
203
-
153
+ value = decode_unicode_escapes(value)
204
154
 
205
- ##============================================================##
206
- ## We parse on the 2 types of line breaks
207
- ##============================================================##
208
155
  value.split(/\\n|\n/).each do |subline|
209
156
  lines << "#{SPACE * (indent + INDENT_SIZE)}#{subline}"
210
157
  end
211
158
  else
212
- line += "#{SPACE}#{value}"
159
+ line += "#{SPACE}#{format_scalar_value(value)}"
213
160
  lines << line
214
161
  end
215
162
  when Hash
216
163
  lines << line
217
- dump(value, lines, indent + INDENT_SIZE)
164
+ render_hash(value, lines, indent + INDENT_SIZE)
218
165
  when Array
219
166
  formated_value = Psych.dump(value)
220
167
  if formated_value == "--- []\n"
@@ -226,568 +173,179 @@ module ImmosquareYaml
226
173
  lines << line
227
174
  lines << formated_value
228
175
  end
176
+ else
177
+ ##============================================================##
178
+ ## Numbers, booleans, dates : laissés tels quels.
179
+ ##============================================================##
180
+ line += "#{SPACE}#{value}"
181
+ lines << line
229
182
  end
230
183
  end
231
184
 
232
- ##============================================================##
233
- ## Finalizing the construction by adding a newline at the end and
234
- ## removing whitespace from empty lines.
235
- ##============================================================##
236
185
  lines += [NOTHING]
237
186
  lines = lines.map {|l| l.strip.empty? ? NOTHING : l }
238
187
  lines.join("\n")
239
188
  end
240
189
 
241
-
242
- private
243
-
244
-
245
-
246
190
  ##============================================================##
247
- ## Deeply cleans the specified YAML file
191
+ ## Walker AST : transforme un Psych::Nodes::* en valeur Ruby.
192
+ ## Le hash anchors mémorise les ancres rencontrées pour
193
+ ## résoudre les aliases.
248
194
  ##============================================================##
249
- def clean_yml(file_path)
250
- lines = []
251
- inblock_indent = nil
252
- weirdblock_indent = nil
253
- inblock = false
254
- weirdblock = false
255
- line_index = 1
256
-
257
- ##============================================================##
258
- ## First, we normalize the file by ensuring it always ends with an empty line
259
- ## This also allows us to get the total number of lines in the file,
260
- ## helping us to determine when we are processing the last line
261
- ##============================================================##
262
- line_count = File.normalize_last_line(file_path)
263
-
264
-
265
- File.foreach(file_path) do |current_line|
266
- last_line = line_index == line_count
267
-
268
- ##============================================================##
269
- ## Cleaning the current line by removing multiple spaces occurring after a non-space character
270
- ##============================================================##
271
- current_line = current_line.to_s.gsub(/(?<=\S)\s+/, SPACE)
272
-
273
- ##============================================================##
274
- ## Trimming potential whitespace characters from the end of the line
275
- ##============================================================##
276
- current_line = current_line.rstrip
277
-
278
- ##============================================================##
279
- ## Detecting blank lines to specially handle the last line within a block;
280
- ## if we are inside a block or it's the last line, we avoid skipping
281
- ##============================================================##
282
- blank_line = current_line.gsub(NEWLINE, NOTHING).empty?
283
- next if !(last_line || inblock || !blank_line)
284
-
285
- ##============================================================##
286
- ## Identifying the indentation level of the current line
287
- ##============================================================##
288
- last_inblock = inblock
289
- indent_level = current_line[/\A */].size
290
- need_to_clean_prev_inblock = inblock == true && ((!blank_line && indent_level <= inblock_indent) || last_line)
291
- need_to_clen_prev_weirdblock = weirdblock == true && (indent_level <= weirdblock_indent || last_line)
292
-
293
- ##============================================================##
294
- ## Handling the exit from a block:
295
- ## if we are exiting a block, we clean the entire block
296
- ##============================================================##
297
- if need_to_clean_prev_inblock
298
- inblock = false
299
- ##============================================================##
300
- ## Extracting the entire block by tracing back lines until we find a lesser indentation
301
- ## Subsequently determining the type of block we are in and clean accordingly
302
- ##============================================================##
303
- i = -1
304
- block_indent = lines[i][/\A */].size
305
- block_lines = [lines[i].lstrip]
306
- while lines[i][/\A */].size == lines[i - 1][/\A */].size
307
- block_lines << lines[i - 1].lstrip
308
- i -= 1
309
- end
310
-
311
- ##============================================================##
312
- ## Handling different types of blocks (literal blocks "|",
313
- ## folded blocks ">", etc.)
314
- ## and applying the respective formatting strategies based on
315
- ## block type and additional indent specified
316
- ##
317
- ## | => Literal blocks: It keeps line breaks as
318
- ## that they are given in the text block.
319
- ## Final new line: A new line is added to the
320
- ## end of text.
321
- ## |- => Literal blocks: It keeps line breaks as
322
- ## that they are given in the text block.
323
- ## New final line: The final line break is deleted,
324
- ## unlike the option |
325
- ## > Folded blocks: It replaces each new line with a space,
326
- ## transforming the block of text into a single line.
327
- ## However, it preserves newlines that follow an empty line.
328
- ## Final new line: A new line is added at the end of the text.
329
- ## We can also have |4- or |4+ to say with indentation 4
330
- ##============================================================##
331
- block_lines = block_lines.reverse
332
- block_type = lines[i - 1].split(": ").last
333
- indent_suppl = block_type.scan(/\d+/).first.to_i
334
- indent_suppl = indent_suppl > 0 ? indent_suppl - INDENT_SIZE : 0
335
- case block_type[0]
336
- when ">"
337
- lines[i - 1] = lines[i - 1].gsub(">", "|")
338
- lines[i] = "#{SPACE * (block_indent + indent_suppl)}#{clean_value(block_lines.join(SPACE))}"
339
- ((i + 1)..-1).to_a.size.times { lines.pop }
340
- else
341
- split = clean_value(block_lines.join(NEWLINE), false).split(NEWLINE)
342
- (i..-1).each do |ii|
343
- lines[ii] = "#{SPACE * (block_indent + indent_suppl)}#{split.shift}"
344
- end
345
- end
346
- end
347
-
348
- ##============================================================##
349
- ## Handling 'weirdblocks': cases where multi-line values are enclosed in quotes,
350
- ## which should actually be single-line values
351
- ## key: "
352
- ## line1
353
- ## line2
354
- ## line3"
355
- ## key: '
356
- ## line1
357
- ## line2
358
- ## line3'
359
- ##============================================================##
360
- if need_to_clen_prev_weirdblock
361
- weirdblock = false
362
- key, value = lines[-1].split(":", 2)
363
- lines[-1] = "#{key}: #{clean_value(value)}"
364
- end
365
-
366
- ##============================================================##
367
- ## Handling keys without values: if the previous line ends with a colon (:) and is not
368
- ## followed by a value, we assign 'null' as the value
369
- ##============================================================##
370
- if inblock == false && weirdblock == false && lines[-1] && lines[-1].end_with?(":") && last_inblock == false
371
- prev_indent = lines[-1][/\A */].size
372
- lines[-1] += " null" if prev_indent >= indent_level
373
- end
374
-
375
- ##============================================================##
376
- ## Splitting the current line into key and value parts for further processing
377
- ## You have to split on ":" and not on ": " because we don't have a space when it's
378
- ## just a key.. but we have a newline
379
- ## fr: => ["fr", "\n"]
380
- ##============================================================##
381
- split = inblock || weirdblock ? [current_line] : current_line.strip.split(":", 2)
382
- key = inblock || weirdblock ? nil : split[0].to_s.strip
383
-
384
- ##============================================================##
385
- ## Line processing based on various conditions such as being inside a block,
386
- ## starting with a comment symbol (#), or being a part of a 'weirdblock'
387
- ## Each case has its specific line cleaning strategy
388
- ## If the line is commented out, we keep and we remove newlines
389
- ##============================================================##
390
- if current_line.lstrip.start_with?("#")
391
- lines << current_line.gsub(NEWLINE, NOTHING)
392
- ##============================================================##
393
- ## If is in a block (multiline > | or |-), we clean
394
- ## the line because it can start with spaces tabs etc.
395
- ## and put it with the block indenter
396
- ##============================================================##
397
- elsif inblock == true
398
- current_line = current_line.gsub(NEWLINE, NOTHING).strip
399
- lines << "#{SPACE * (inblock_indent + INDENT_SIZE)}#{current_line}"
400
- ##============================================================##
401
- ## if the line ends with a multi-line character and we have a key.
402
- ## we start a block
403
- ## The regex works as follows:
404
- ## \S+ : All non-space characters at the start of the line.
405
- ## : : Matches the string ": " literally (space included).
406
- ## [>|] : Matches a single character that is either ">" or "|".
407
- ## (\d*) : Capture group that matches zero or more digits (0-9).
408
- ## [-+]? : Matches zero or a character that is either "-" or "+".
409
- ## $ : Matches the end of the line/string.
410
- ##============================================================##
411
- elsif current_line.rstrip.match?(/\S+: [>|](\d*)[-+]?$/)
412
- lines << current_line.gsub(NEWLINE, NOTHING)
413
- inblock_indent = indent_level
414
- inblock = true
415
- ##============================================================##
416
- ## We are in the scenario of a multiline block
417
- ## but without > | or |- at the end of the line
418
- ## which should actually be inline.
419
- ## mykey:
420
- ## line1
421
- ## line2
422
- ## line3
423
- ## my key: line1 line2 line3
424
- ##============================================================##
425
- elsif split.size < 2
426
- if current_line.lstrip.start_with?("-")
427
- lines << current_line
428
- else
429
- lines[-1] = (lines[-1] + " #{current_line.lstrip}").gsub(NEWLINE, NOTHING)
430
- end
431
- ##============================================================##
432
- ## Otherwise we are in the case of a classic line
433
- ## key: value
434
- ## or
435
- ## key: without value
436
- ## - key: value (list)
437
- ## - key: without value (list)
438
- ##============================================================##
439
- else
440
- key = clean_key(key)
441
- spaces = (SPACE * indent_level).to_s
442
- current_line = "#{spaces}#{key}:"
443
-
444
- if !split[1].empty?
445
- value = split[1].to_s.strip
446
-
447
- ##============================================================##
448
- ## We are in a multiline block which should be an inline
449
- ## if the value starts with a " and the number of " is odd
450
- ##============================================================##
451
- if (value.start_with?(DOUBLE_QUOTE) && value.count(DOUBLE_QUOTE).odd?) || (value.start_with?(SIMPLE_QUOTE) && value.count(SIMPLE_QUOTE).odd?)
452
- weirdblock = true
453
- weirdblock_indent = indent_level
454
- else
455
- value = clean_value(split[1])
456
- end
457
- current_line += " #{value}"
458
- end
459
-
195
+ def node_to_value(node, anchors)
196
+ case node
197
+ when Psych::Nodes::Scalar
198
+ value = scalar_to_ruby(node)
199
+ anchors[node.anchor] = value if node.anchor
200
+ value
201
+ when Psych::Nodes::Mapping
202
+ h = {}
203
+ node.children.each_slice(2) do |key_node, val_node|
460
204
  ##============================================================##
461
- ## Merging the cleaned key and value to form the cleaned row
205
+ ## Toujours convertir les clés en String. Cela évite les
206
+ ## hashs aux types mixtes (Integer/String) qui cassent le tri
207
+ ## et déstabilisent les fichiers de traduction.
462
208
  ##============================================================##
463
- lines << current_line
209
+ key = node_to_value(key_node, anchors).to_s
210
+ h[key] = node_to_value(val_node, anchors)
464
211
  end
465
-
466
- ##============================================================##
467
- ## We increment the line number
468
- ##============================================================##
469
- line_index += 1
212
+ anchors[node.anchor] = h if node.anchor
213
+ h
214
+ when Psych::Nodes::Sequence
215
+ arr = node.children.map {|c| node_to_value(c, anchors) }
216
+ anchors[node.anchor] = arr if node.anchor
217
+ arr
218
+ when Psych::Nodes::Alias
219
+ raise("Unknown YAML alias: *#{node.anchor}") if !anchors.key?(node.anchor)
220
+
221
+ anchors[node.anchor]
222
+ else
223
+ raise("Unsupported YAML node type: #{node.class}")
470
224
  end
471
-
472
- ##============================================================##
473
- ## We finish the file with a newline and we delete
474
- ## spaces on "empty" lines + double spaces
475
- ## with the same technique as above
476
- ##============================================================##
477
- lines += [NOTHING]
478
- lines = lines.map {|l| (l.strip.empty? ? NOTHING : l).to_s.gsub(/(?<=\S)\s+/, SPACE) }
479
- File.write(file_path, lines.join(NEWLINE))
480
225
  end
481
226
 
482
227
  ##============================================================##
483
- ## clean_key Function
484
- ## Purpose: Clean up and standardize YAML keys
485
- ## Strategy:
486
- ## 1. Forcefully convert the key to a string to handle gsub operations, especially if it's an integer.
487
- ## 2. Remove quotes if they are present.
488
- ## 3. Check if the key is an integer.
489
- ## 4. Re-add quotes if the key is a reserved word or an integer.
490
- ##
491
- ## This allows us to fetch the string without the surrounding quotes.
228
+ ## Convertit un Psych::Nodes::Scalar en valeur Ruby.
229
+ ## Règles :
230
+ ## - quoted (single/double) → toujours String
231
+ ## - plain vide ou null/~ nil
232
+ ## - plain "yes/no/on/off/true/false" String (Norway problem)
233
+ ## - plain entier Integer
234
+ ## - plain flottant Float
235
+ ## - sinon → String
236
+ ## - LITERAL/FOLDED : String, mais on décode \U... à l'usage
237
+ ## dans le dump (pas ici, pour ne pas perdre l'info brute)
492
238
  ##============================================================##
493
- def clean_key(key)
494
- ##============================================================##
495
- ## Convert key to string to avoid issues with gsub operations
496
- ##============================================================##
497
- key = key.to_s
239
+ def scalar_to_ruby(node)
240
+ raw = node.value
241
+ style = node.style
498
242
 
499
- ##============================================================##
500
- ## Remove surrounding quotes from the key
501
- ##============================================================##
502
- key = key[1..-2] if (key.start_with?(DOUBLE_QUOTE) && key.end_with?(DOUBLE_QUOTE)) || (key.start_with?(SIMPLE_QUOTE) && key.end_with?(SIMPLE_QUOTE))
243
+ return raw if [Psych::Nodes::Scalar::SINGLE_QUOTED, Psych::Nodes::Scalar::DOUBLE_QUOTED].include?(style)
244
+ return raw if [Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED].include?(style)
503
245
 
504
246
  ##============================================================##
505
- ## Check if the key is an integer
247
+ ## Style PLAIN : on type prudemment.
506
248
  ##============================================================##
507
- is_int = key =~ /\A[-+]?\d+\z/
249
+ return nil if raw == NOTHING || ["~", "null", "Null", "NULL"].include?(raw)
250
+ return raw if RESERVED_KEYS.include?(raw)
251
+ return raw.to_i if raw.match?(/\A-?\d+\z/)
252
+ return raw.to_f if raw.match?(/\A-?\d+\.\d+\z/)
508
253
 
509
- ##============================================================##
510
- ## Re-add quotes if the key is in the list of reserved keys or is an integer
511
- ##============================================================##
512
- key = "\"#{key}\"" if RESERVED_KEYS.include?(key) || is_int
513
- key
254
+ raw
514
255
  end
515
256
 
516
257
  ##============================================================##
517
- ## " [apple, orange, 'banana']" => [apple, orange, 'banana']
258
+ ## Décode les séquences \U0001F600 en emoji UTF-8.
259
+ ## Appelé sur les valeurs string au moment du dump (pas au
260
+ ## parse, pour préserver l'idempotence si l'utilisateur a vraiment
261
+ ## la séquence littérale dans son YAML).
518
262
  ##============================================================##
519
- def string_in_array(string)
520
- begin
521
- string_striped = string.strip
522
- string_striped.match(/^\[.*\]$/) ? string_striped[1..-2].split(/,\s?/) : string
523
- rescue StandardError
524
- string
525
- end
263
+ def decode_unicode_escapes(value)
264
+ value.gsub(/\\U([0-9A-Fa-f]{8})/) { [::Regexp.last_match(1).to_i(16)].pack("U*") }
526
265
  end
527
266
 
528
267
  ##============================================================##
529
- ## clean_value Function
530
- ## Purpose: Sanitize and standardize YAML values
531
- ## In YAML "inblock" scenarios, there's no need to add quotes
532
- ## around values as it's inherently handled.
268
+ ## clean_key : prépare une clé pour le dump.
269
+ ## - retire les quotes englobantes éventuelles
270
+ ## - re-quote si la clé est un mot réservé YAML 1.1 ou un entier
533
271
  ##============================================================##
534
- def clean_value(values, with_quotes_verif = true)
535
- ##============================================================##
536
- ## Convert key to array if not
537
- ## fruits: [apple, orange, 'banana']
538
- ## demo: "demo"
539
- ##============================================================##
540
- is_array = string_in_array(values)
541
- values = is_array.instance_of?(String) ? [values] : is_array
542
-
543
-
544
- values = values.map do |value|
545
- ##============================================================##
546
- ## Convert value to string to prevent issues in subsequent operations
547
- ##============================================================##
548
- value = value.to_s
549
-
550
- ##============================================================##
551
- ## Remove newline characters at the end of the value if present.
552
- ## This should be done prior to strip operation to handle scenarios
553
- ## where the value ends with a space followed by a newline.
554
- ##============================================================##
555
- value = value[0..-2] if value.end_with?(NEWLINE)
556
-
557
-
558
- ##============================================================##
559
- ## Clean up the value:
560
- ## - Remove tabs, carriage returns, form feeds, and vertical tabs.
561
- ## \t: corresponds to a tab
562
- ## \r: corresponds to a carriage return
563
- ## \f: corresponds to a form feed
564
- ## \v: corresponds to a vertical tab
565
- ## We keep the \n
566
- ##============================================================##
567
- value = value.gsub(/[\t\r\f\v]+/, NOTHING)
568
-
569
- ##============================================================##
570
- ## Replace multiple spaces with a single space.
571
- ##============================================================##
572
- value = value.gsub(/ {2,}/, SPACE)
573
-
574
- ##============================================================##
575
- ## Trim leading and trailing spaces.
576
- ##============================================================##
577
- value = value.strip
578
-
579
- ##============================================================##
580
- ## Replace special quotes with standard single quotes.
581
- ##============================================================##
582
- value = value.gsub(WEIRD_QUOTES_REGEX, SIMPLE_QUOTE)
583
-
584
- ##============================================================##
585
- ## Remove all quotes surrounding the value if they are present.
586
- ## They will be re-added later if necessary.
587
- ## """"value"""" => value
588
- ##============================================================##
589
- value = value[1..-2] while (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
590
-
591
- ##============================================================##
592
- ## Convert emoji representations such as \U0001F600 to their respective emojis.
593
- ##============================================================##
594
- value = value.gsub(/\\U([0-9A-Fa-f]{8})/) { [::Regexp.last_match(1).to_i(16)].pack("U*") }
595
-
596
- ##============================================================##
597
- ## Handling cases where the value must be surrounded by quotes
598
- ## if:
599
- ## management of "" and " ". Not possible to have more spaces
600
- ## because we have already removed the double spaces
601
- ## else
602
- ## value.include?(": ") => key: text with: here
603
- ## value.include?(" #") => key: text with # here
604
- ## value.include?(NEWLINE) => key: Line 1\nLine 2\nLine 3
605
- ## value.include?('\n') => key: Line 1"\n"Line 2"\n"Line 3
606
- ## value.start_with?(*YML_SPECIAL_CHARS) => key: @text
607
- ## value.end_with?(":") => key: text:
608
- ## RESERVED_KEYS.include?(value) => key: YES
609
- ## value.start_with?(SPACE) => key: 'text'
610
- ## value.end_with?(SPACE) => key: text '
611
- ##============================================================##
612
- if value.empty?
613
- value = "\"#{value}\""
614
- elsif with_quotes_verif == true
615
- value = "\"#{value}\"" if value.include?(": ") ||
616
- value.include?(" #") ||
617
- value.include?(NEWLINE) ||
618
- value.include?('\n') ||
619
- value.start_with?(*YML_SPECIAL_CHARS) ||
620
- value.end_with?(":") ||
621
- (is_array ? false : RESERVED_KEYS.include?(value)) ||
622
- value.start_with?(SPACE) ||
623
- value.end_with?(SPACE)
624
- end
625
-
626
- ##============================================================##
627
- ## Final clean to prevent
628
- ## "yes": YES
629
- ## "no": NO
630
- ##============================================================##
631
- value = "\"#{value}\"" if RESERVED_KEYS.include?(value)
632
-
633
- ##============================================================##
634
- ## Return the cleaned value
635
- ##============================================================##
636
- value
637
- end
638
- is_array.instance_of?(String) ? values.first : "[#{values.join(", ")}]"
639
- end
640
-
641
- ##============================================================##
642
- ## Normalize indentation for array values without intent
643
- ## for the first level.
644
- ##============================================================##
645
- def normalize_indentation(lines)
646
- initial_indentation = lines.first.match(/^(\s*)/)[1].length
647
- lines.map do |line|
648
- line[initial_indentation..(line.end_with?(NEWLINE) ? -2 : -1)]
649
- end
272
+ def clean_key(key)
273
+ key = strip_wrapping_quotes(key.to_s)
274
+ is_int = key.match?(/\A[-+]?\d+\z/)
275
+ key = "\"#{key}\"" if RESERVED_KEYS.include?(key) || is_int
276
+ key
650
277
  end
651
278
 
652
279
  ##============================================================##
653
- ## parse_xml Function
654
- ## Purpose: Parse an XML file into a nested hash representation.
280
+ ## format_scalar_value : prépare une valeur String pour le dump.
281
+ ## Décode les escapes Unicode et décide si on doit quoter.
655
282
  ##
656
- ## This method reads through the XML file line by line and creates a
657
- ## nested hash representation based on the structure and content of the XML.
283
+ ## On quote si la valeur contient des caractères qui auraient
284
+ ## un sens YAML particulier en plain (": ", " #", début par un
285
+ ## caractère spécial, fin par ":", mot réservé, espace en bord).
658
286
  ##============================================================##
659
- def parse_xml(file_path)
660
- nested_hash = {}
661
- inblock = nil
662
- inlist = nil
663
- inlist_data = nil
664
- last_keys = []
665
-
287
+ def format_scalar_value(value)
288
+ value = value.to_s
289
+ value = decode_unicode_escapes(value)
290
+ value = value.gsub(WEIRD_QUOTES_REGEX, SIMPLE_QUOTE)
666
291
 
667
292
  ##============================================================##
668
- ## We go over each line of the file to create a hash.
669
- ## We put the multiline blocks in an array to recover
670
- ## all the values and the formatting type then we will pass
671
- ## on each of these arrays subsequently to transform them
672
- ## in the corresponding string
293
+ ## On enlève les guillemets parasites éventuels (cas de fichiers
294
+ ## historiques produits par l'ancienne version).
673
295
  ##============================================================##
674
- File.foreach(file_path) do |line|
675
- ##============================================================##
676
- ## Determine the indentation level of the line.
677
- ##============================================================##
678
- indent_level = line[/\A */].size
679
-
680
- ##============================================================##
681
- ## Check for blank lines (which can be present within multi-line blocks)
682
- ##============================================================##
683
- blank_line = line.gsub(NEWLINE, NOTHING).empty?
684
-
685
- ##============================================================##
686
- ## Split the line into key and value.
687
- ##============================================================##
688
- split = line.strip.split(":", 2)
689
- key = split[0].to_s.strip
690
- inblock = nil if !inblock.nil? && !blank_line && indent_level <= inblock
691
-
692
-
693
- ##============================================================##
694
- ## inlist Enter
695
- ##============================================================##
696
- if inlist.nil? && !blank_line && line.strip.start_with?("-") && inblock.nil?
697
- inlist = indent_level
698
- inlist_data = []
699
- end
700
-
701
- ##============================================================##
702
- ## inlist Exit
703
- ## We use Pscyh to parse the yaml of the list content
704
- ##============================================================##
705
- if !inlist.nil? && !blank_line && indent_level < inlist
706
- yaml = normalize_indentation(inlist_data).join(NEWLINE)
707
- current_key = last_keys.last
708
- parent_keys = last_keys[0..-2]
709
- result = parent_keys.reduce(nested_hash) {|hash, k| hash[k] }
710
- result[current_key] = Psych.safe_load(yaml, :permitted_classes => [Date])
711
- inlist = nil
712
- inlist_data = []
713
- end
714
-
715
- ##============================================================##
716
- ## Set the key level based on indentation
717
- ##============================================================##
718
- last_keys = last_keys[0, (blank_line ? inblock + INDENT_SIZE : indent_level) / INDENT_SIZE]
296
+ value = strip_wrapping_quotes(value)
719
297
 
298
+ ##============================================================##
299
+ ## Note : un " au milieu d'une string plain est légal en YAML.
300
+ ## On ne quote que si le " est en début (déjà couvert par
301
+ ## start_with?(*YML_SPECIAL_CHARS)). Quoter dès qu'un " apparaît
302
+ ## n'importe où dans la valeur produirait des diffs inutiles.
303
+ ##============================================================##
304
+ need_quotes = value.empty? ||
305
+ value.include?(": ") ||
306
+ value.include?(" #") ||
307
+ value.start_with?(*YML_SPECIAL_CHARS) ||
308
+ value.end_with?(":") ||
309
+ RESERVED_KEYS.include?(value) ||
310
+ value.start_with?(SPACE) ||
311
+ value.end_with?(SPACE)
720
312
 
721
- ##============================================================##
722
- ## If inside a multi-line block, append the line to the current key's value
723
- ##============================================================##
724
- if !inblock.nil?
725
- current_key = last_keys.last
726
- parent_keys = last_keys[0..-2]
727
- result = parent_keys.reduce(nested_hash) {|hash, k| hash[k] }
728
- result[current_key][1] << line.strip
729
- ##============================================================##
730
- ## Handle list declarations.
731
- ##============================================================##
732
- elsif !inlist.nil?
733
- inlist_data << line
734
- ##============================================================##
735
- ## Handle multi-line key declarations.
736
- ## We no longer have the >
737
- ## because it is transformed in the clean_xml into |
738
- ##============================================================##
739
- elsif line.gsub("#{key}:", NOTHING).strip.start_with?("|")
740
- inblock = indent_level
741
- block_type = line.gsub("#{key}:", NOTHING).strip
742
- result = last_keys.reduce(nested_hash) {|hash, k| hash[k] }
743
- result[key] = ["#{CUSTOM_SEPARATOR}#{block_type}#{CUSTOM_SEPARATOR}", []]
744
- last_keys << key
745
- ##============================================================##
746
- ## Handle regular key-value pair declarations
747
- ##============================================================##
748
- else
749
- value = split[1].to_s.strip
750
- result = last_keys.reduce(nested_hash) {|hash, k| hash[k] }
751
- if value.empty?
752
- result[key] = {}
753
- last_keys << key
754
- else
755
- result[key] = value.strip == "null" ? nil : string_in_array(value)
756
- end
757
- end
758
- end
759
-
313
+ return value if !need_quotes
760
314
 
761
315
  ##============================================================##
762
- ## We go over each value then we process if it is a has
763
- ## | with final newline
764
- ## |4 with newline and indentation of 4
765
- ## |- without newline
766
- ## |4- without newline and indentation of 4
316
+ ## Choix du style de quoting :
317
+ ## - single-quoted par défaut (plus léger, pas d'escapes)
318
+ ## - double-quoted seulement si la valeur contient une
319
+ ## apostrophe ou un caractère qui nécessite un escape
320
+ ## (\, tab). Cela minimise les diffs git sur les fichiers
321
+ ## existants et améliore la lisibilité (HTML notamment).
767
322
  ##============================================================##
768
- deep_transform_values(nested_hash) do |value|
769
- if value.is_a?(Array) && !value[0].nil? && value[0].instance_of?(String) && value[0].start_with?(CUSTOM_SEPARATOR) && value[0].end_with?(CUSTOM_SEPARATOR)
770
- style_type = value[0].gsub(CUSTOM_SEPARATOR, NOTHING)
771
- indent_supp = style_type.scan(/\d+/).first.to_i
772
- indent_supp = [indent_supp - INDENT_SIZE, 0].max
773
- value[1] = value[1].map {|l| "#{SPACE * indent_supp}#{l}" }
774
- text = value[1].join(NEWLINE)
775
- modifier = style_type[-1]
776
- case modifier
777
- when "+"
778
- text << NEWLINE unless text.end_with?(NEWLINE)
779
- when "-"
780
- text.chomp!
781
- else
782
- text << NEWLINE unless text.end_with?(NEWLINE)
783
- end
784
- text
785
- else
786
- value
787
- end
323
+ if value.include?(SIMPLE_QUOTE) || value.include?("\\") || value.include?("\t")
324
+ yaml_double_quote(value)
325
+ else
326
+ "#{SIMPLE_QUOTE}#{value}#{SIMPLE_QUOTE}"
788
327
  end
789
328
  end
790
329
 
330
+ ##============================================================##
331
+ ## Échappe une string pour la sérialiser en YAML double-quoted.
332
+ ## On gère \, ", \t et \n. Les newlines réels n'arrivent pas
333
+ ## ici car ils sont rendus en bloc littéral plus haut.
334
+ ##============================================================##
335
+ def yaml_double_quote(value)
336
+ escaped = value.gsub("\\", "\\\\\\\\").gsub("\"", '\\"').gsub("\t", '\\t')
337
+ "\"#{escaped}\""
338
+ end
339
+
340
+ ##============================================================##
341
+ ## Retire récursivement les paires de guillemets englobants.
342
+ ## Sert pour les fichiers historiques produits par v0.1.28
343
+ ## qui pouvaient contenir des valeurs avec quotes incluses.
344
+ ##============================================================##
345
+ def strip_wrapping_quotes(value)
346
+ value = value[1..-2] while (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
347
+ value
348
+ end
791
349
 
792
350
  end
793
351
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: immosquare-yaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.28
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - immosquare
@@ -62,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
62
  - !ruby/object:Gem::Version
63
63
  version: '0'
64
64
  requirements: []
65
- rubygems_version: 3.7.1
65
+ rubygems_version: 4.0.11
66
66
  specification_version: 4
67
67
  summary: A YAML parser optimized for translation files.
68
68
  test_files: []