yeptris 0.6.35.2 → 0.6.36.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: 91c0f75f27e70c5ac4da707314d3079f8a178ecd21a05cb451e60843209533d9
4
- data.tar.gz: d0cdab59581878eeee966cf49de48c8004ef038c287ee5771cc7e7d74a81a9ee
3
+ metadata.gz: 2ac5a7348d464838d4c6da16ab1a579423f30149936adeee3e2e46ea83cde163
4
+ data.tar.gz: 265108ae979d918dffb870acacbd42a86b5569c5df04f8642f0012c91d464c2d
5
5
  SHA512:
6
- metadata.gz: 27e54a598ac3347f20dcf6849db1029a350d23f398566ad467b5f2f2517ada8588533c0ebb438d5b50b8ddd9a5b0eedf5c5234065360ff30f1aa4f105375987a
7
- data.tar.gz: 9b94a1f7bc21912c30d99637f0048666d151f700e0be4cf449530cbc7bb456206322b56c0bdb3bf85b958aa0aeb740fc9538fbbc952e879efc503e777bb58eec
6
+ metadata.gz: 345cf71ecfc482b1b7471346affb2e79b857a6e467378902786677af08613bf03d5c6a8eb65f6c1ec5c4b6e4b0e624a58c326638b16eabe4b38bf56e566c369f
7
+ data.tar.gz: 617a17fbe98d6cb8d7a94bcc0e1b5af49da2243aaaca11af4c50b130ffb8d071a050d40d9ea862a8a97bf65f20c957df18b18652982e7aa5814296687276e818
@@ -202,6 +202,17 @@ class Yeptris::Document
202
202
  Yeptris::FFI::Owned.string(Yeptris::FFI.yeptris_serialize_json_ex(@c_ptr, len, 1), len)
203
203
  end
204
204
 
205
+ # JSON.pretty_generate's shape; nil indent rides the engine default
206
+ # (two spaces), any other String's bytes ride verbatim per depth
207
+ def serialize_json_pretty(indent = nil)
208
+ ensure_alive!
209
+ len = ::FFI::MemoryPointer.new(:size_t)
210
+ data = indent&.b
211
+ Yeptris::FFI::Owned.string(
212
+ Yeptris::FFI.yeptris_serialize_json_pretty(@c_ptr, len, data, data ? data.bytesize : 0), len
213
+ )
214
+ end
215
+
205
216
  def serialize_json
206
217
  ensure_alive!
207
218
  len = ::FFI::MemoryPointer.new(:size_t)
data/lib/yeptris/ffi.rb CHANGED
@@ -276,6 +276,14 @@ module Yeptris
276
276
  false
277
277
  end
278
278
 
279
+ # pretty JSON generation (JSON.pretty_generate's shape); the JSON
280
+ # surface feature-detects via the @missing convention below (the
281
+ # deferred stub records the miss at attach, so a begin/rescue
282
+ # never fires)
283
+ attach_function :yeptris_serialize_json_pretty,
284
+ %i[yeptris_document pointer pointer size_t], :pointer
285
+ JSON_PRETTY = !(@missing ||= []).include?(:yeptris_serialize_json_pretty)
286
+
279
287
  # CBOR (RFC 8949, TODO.cbor): absent on libraries before the
280
288
  # codec's release — the CBOR module feature-detects
281
289
  attach_function :yeptris_cbor_decode, %i[pointer size_t uint32 yeptris_status_out],
data/lib/yeptris/json.rb CHANGED
@@ -367,7 +367,7 @@ module Yeptris
367
367
  # and null stay plain), Symbols become their name.
368
368
  class DumpError < Error; end
369
369
 
370
- def dump(obj)
370
+ def dump(obj, pretty: false, indent: " ")
371
371
  parts = String.new(encoding: Encoding::BINARY,
372
372
  capacity: 12 * 16) # 12-byte entries, appended in place
373
373
  blob = String.new(encoding: Encoding::BINARY)
@@ -377,7 +377,17 @@ module Yeptris
377
377
  # FFI passes String args BY REFERENCE — no MemoryPointer copies
378
378
  rc = doc.build_entries(parts, parts.bytesize / 12, blob, blob.bytesize)
379
379
  raise DumpError, "document_build failed: #{rc}" unless rc == ::Yeptris::FFI::OK
380
- out = ::Yeptris::FFI::JSON_EX ? doc.serialize_json_compact : doc.serialize_json
380
+ out =
381
+ if pretty
382
+ unless ::Yeptris::FFI::JSON_PRETTY
383
+ raise DumpError, "pretty JSON needs a libyeptris with yeptris_serialize_json_pretty"
384
+ end
385
+ doc.serialize_json_pretty(indent)
386
+ elsif ::Yeptris::FFI::JSON_EX
387
+ doc.serialize_json_compact
388
+ else
389
+ doc.serialize_json
390
+ end
381
391
  # JSON.generate emits no trailing newline (the document writer does)
382
392
  out.end_with?("\n") ? out[0, out.bytesize - 1] : out
383
393
  ensure
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.6.35.2".freeze
6
+ VERSION = "0.6.36.0".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.35
6
+ VERSION 0.6.36
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -38,6 +38,16 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument doc, size_t* len);
38
38
  * compact == 0 is byte-identical to yeptris_serialize_json. */
39
39
  YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument doc, size_t* len, int compact);
40
40
 
41
+ /* The pretty variant: JSON.pretty_generate's shape — one entry per
42
+ * line, ": " after keys, ",\n" + indent between entries, empty
43
+ * collections inline. indent/indent_len select the indent unit
44
+ * (NULL/0 = two spaces; the bytes are written verbatim per depth).
45
+ * Memory: returns a malloc'd NUL-terminated buffer (caller frees)
46
+ * with *len (may be NULL) set, or NULL on a NULL document /
47
+ * allocation failure / zero documents. */
48
+ YEPTRIS_API char* yeptris_serialize_json_pretty(YeptrisDocument doc, size_t* len,
49
+ const char* indent, size_t indent_len);
50
+
41
51
  #ifdef __cplusplus
42
52
  }
43
53
  #endif
@@ -308,6 +308,14 @@ YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument handle, size_t* len,
308
308
  return out;
309
309
  }
310
310
 
311
+ YEPTRIS_API char* yeptris_serialize_json_pretty(YeptrisDocument handle, size_t* len,
312
+ const char* indent, size_t indent_len) {
313
+ if (handle == NULL) {
314
+ return NULL;
315
+ }
316
+ return yep_serialize_json_pretty_ex((const yeptris_document*)handle, len, indent, indent_len);
317
+ }
318
+
311
319
  char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len) {
312
320
  if (doc == NULL) {
313
321
  return NULL;
@@ -364,9 +372,17 @@ char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len) {
364
372
  }
365
373
 
366
374
  char* yep_serialize_json_pretty(const yeptris_document* doc, size_t* len) {
375
+ return yep_serialize_json_pretty_ex(doc, len, NULL, 2);
376
+ }
377
+
378
+ char* yep_serialize_json_pretty_ex(const yeptris_document* doc, size_t* len, const char* indent,
379
+ size_t indent_len) {
367
380
  if (doc == NULL) {
368
381
  return NULL;
369
382
  }
383
+ if (yep_doc_dom((yeptris_document*)doc) == NULL) { /* #342 lazy */
384
+ return NULL;
385
+ }
370
386
  yep_emitter em;
371
387
  em.w.sc_dec = NULL;
372
388
  em.w.sc_i = 0;
@@ -386,6 +402,8 @@ char* yep_serialize_json_pretty(const yeptris_document* doc, size_t* len) {
386
402
  em.w.json_compact = 0;
387
403
  em.w.json_pretty = 1;
388
404
  em.w.pretty_depth = 0;
405
+ em.w.json_indent = indent;
406
+ em.w.json_indent_len = indent == NULL ? 2 : (int)indent_len;
389
407
  em.w.best_width = 0;
390
408
  em.w.col = 0;
391
409
  em.w.sv_input = doc->dom->input_base;
@@ -139,6 +139,18 @@ static void wr_indent(yep_writer* w, int depth) {
139
139
  }
140
140
  }
141
141
 
142
+ /* json_pretty's per-depth indent: the default two spaces per level,
143
+ * or depth copies of the caller's unit (json.h pretty entry). */
144
+ static void wr_pretty_indent(yep_writer* w, int depth) {
145
+ if (w->json_indent == NULL) {
146
+ wr_indent(w, depth * 2);
147
+ return;
148
+ }
149
+ for (int i = 0; i < depth; i++) {
150
+ wr_put(w, w->json_indent, (uint32_t)w->json_indent_len);
151
+ }
152
+ }
153
+
142
154
  static void emit_dq(yep_writer* w, const char* p, uint32_t n) {
143
155
  wr_byte(w, '"');
144
156
  uint32_t run = 0;
@@ -899,9 +911,9 @@ static void emit_flow(yep_emitter* em, uint32_t id) {
899
911
  int flow_indent = w->col;
900
912
  if (w->json_pretty && n->count > 0) {
901
913
  w->pretty_depth++;
902
- flow_indent = w->pretty_depth * 2; /* 2 spaces per level */
903
914
  wr_byte(w, '\n');
904
- wr_indent(w, flow_indent);
915
+ wr_pretty_indent(w, w->pretty_depth);
916
+ flow_indent = w->col;
905
917
  }
906
918
  uint32_t child = n->first_child;
907
919
  uint32_t idx = 0;
@@ -913,7 +925,7 @@ static void emit_flow(yep_emitter* em, uint32_t id) {
913
925
  * re-indent to the opening bracket's column */
914
926
  if (w->json_pretty) {
915
927
  wr_put(w, ",\n", 2);
916
- wr_indent(w, w->pretty_depth * 2);
928
+ wr_pretty_indent(w, w->pretty_depth);
917
929
  } else if (w->json_compact) {
918
930
  wr_byte(w, ',');
919
931
  } else if (w->best_width > 0 && w->col > w->best_width) {
@@ -964,7 +976,7 @@ static void emit_flow(yep_emitter* em, uint32_t id) {
964
976
  }
965
977
  if (w->json_pretty && idx > 0) {
966
978
  wr_byte(w, '\n');
967
- wr_indent(w, (w->pretty_depth - 1) * 2);
979
+ wr_pretty_indent(w, w->pretty_depth - 1);
968
980
  w->pretty_depth--;
969
981
  }
970
982
  wr_byte(w, map ? '}' : ']');
@@ -35,6 +35,8 @@ typedef struct yep_writer {
35
35
  int json_compact; /* json-c to_json_string: no padding spaces */
36
36
  int json_pretty; /* json-c PRETTY: 2-space indent, per-entry lines */
37
37
  int pretty_depth; /* json_pretty nesting level (indent = 2*depth) */
38
+ const char* json_indent; /* json_pretty indent unit; NULL = 2 spaces */
39
+ int json_indent_len; /* json_pretty unit byte length */
38
40
  int best_width; /* 13B: 0 = default 80; flow lines wrap after a
39
41
  * value past this width */
40
42
  int col; /* current output column (wr_* maintains) */
@@ -79,5 +81,7 @@ size_t yep_emit_run(yep_emitter* em, int dry);
79
81
  * nesting level, one entry per line. */
80
82
  char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len);
81
83
  char* yep_serialize_json_pretty(const yeptris_document* doc, size_t* len);
84
+ char* yep_serialize_json_pretty_ex(const yeptris_document* doc, size_t* len, const char* indent,
85
+ size_t indent_len);
82
86
 
83
87
  #endif /* YEP_EMIT_WRITER_H */
@@ -815,7 +815,17 @@ static int strict_dup_scan(const yep_value_ctx* c) {
815
815
  }
816
816
  continue;
817
817
  }
818
- yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0, "duplicate key in object");
818
+ /* name the key like the tape walk's message (json 3 names it
819
+ * too): the arena bytes are the text for the str-like kinds,
820
+ * which is every JSON key */
821
+ if (k.kind == YEP_V_STR || k.kind == YEP_V_TIMESTAMP || k.kind == YEP_V_ALIAS ||
822
+ k.kind == YEP_V_ANCHOR) {
823
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
824
+ "duplicate key \"%.*s\" in JSON object", (int)k.len,
825
+ k.len ? c->arena + k.off : "");
826
+ } else {
827
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0, "duplicate key in object");
828
+ }
819
829
  rc = 1;
820
830
  }
821
831
  for (size_t j = 0; j < depth; j++) {
@@ -555,9 +555,15 @@ static int e_flush_q_value(yep_engine* e) {
555
555
  * this pop the stack keeps a stale entry and the outer pair's null
556
556
  * flushes inside the wrong map (the nightly's run 38387614146). */
557
557
  static int e_pair_value_done(yep_engine* e, yep_ctx ctx, uint16_t key_col) {
558
+ /* the pair's own level, BEFORE its value parse: the value may open
559
+ * deeper frames ("? a:\n ? b<EOF>") and a post-parse depth read
560
+ * then matches the DEEPER pair's pending slot, popping the wrong
561
+ * entry and stranding the outer null (the nightly's round 24,
562
+ * crash-bcc0f52c) */
563
+ int pre_depth = e->depth;
558
564
  int rc = e_parse_value(e, YEP_CTX_AFTER_COLON, key_col);
559
565
  if (rc >= 0 && ctx == YEP_CTX_AFTER_Q && e->qmd_n > 0 &&
560
- e->depth == e->qmd_stack[e->qmd_n - 1]) {
566
+ pre_depth == e->qmd_stack[e->qmd_n - 1]) {
561
567
  e->qmd_n--; /* the inline value resolved the innermost '?' pending */
562
568
  }
563
569
  return rc;
@@ -1173,6 +1179,14 @@ static yep_view e_resolve_tag_raw(yep_engine* e, yep_view tag) {
1173
1179
  yep_view v = {tag.p + 2, tag.len - 3};
1174
1180
  return v;
1175
1181
  }
1182
+ /* the BARE '!' is the non-specific tag and is exempt from %TAG
1183
+ * directives (spec 7.3.3): it stays '!' here, and the plain-scalar
1184
+ * drop happens at emit_now (the nightly's round 22, crash-d6be3347:
1185
+ * '%TAG ! ...' + '- ! tag:clarkevans.com,2002:' resolved the
1186
+ * non-specific tag through the directive where libyaml drops it) */
1187
+ if (tag.len == 1 && tag.p[0] == '!') {
1188
+ return tag;
1189
+ }
1176
1190
  /* longest matching handle wins */
1177
1191
  int best = -1;
1178
1192
  uint32_t best_len = 0;
@@ -3628,6 +3642,15 @@ static int e_node(yep_engine* e, yep_ctx ctx, uint16_t floor_col) {
3628
3642
  while (e->pos < e->len && e->p[e->pos] != ':') {
3629
3643
  e->pos++;
3630
3644
  }
3645
+ if (e->pos >= e->len) {
3646
+ /* EOF without the ':': the explicit-key pair's value never
3647
+ * came ("? b<EOF>") — the pending stays armed and the
3648
+ * stream-end flush completes it at the deepest open map
3649
+ * (the nightly's round 24, crash-bcc0f52c); popping here
3650
+ * resolved a pair that has no value and stranded the OUTER
3651
+ * pending's null in the wrong map */
3652
+ return 0;
3653
+ }
3631
3654
  e->pos++; /* past ':' */
3632
3655
  return e_pair_value_done(e, ctx, key_col);
3633
3656
  }
@@ -4106,55 +4129,82 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4106
4129
  return t;
4107
4130
  }
4108
4131
  if (c->quote != 0) {
4109
- /* NEL folds inside quotes too; LS/PS stay raw content */
4110
- if (nel) {
4111
- t.n = 2;
4112
- t.fold_off = 0;
4113
- t.fold_len = 2;
4114
- return t;
4115
- }
4116
- if (lps) {
4117
- t.n = 3;
4118
- return t;
4119
- }
4120
- if (c->quote == '\'') {
4121
- if (ch == '\'') {
4122
- if (i + 1 < len && p[i + 1] == '\'') {
4123
- t.n = 2; /* '' escaped quote */
4124
- return t;
4125
- }
4126
- c->quote = 0;
4127
- }
4128
- return t;
4129
- }
4130
- if (ch == '\\') {
4131
- /* the escape consumes the next byte — but a multibyte
4132
- * break under the escape still folds (an escaped break is
4133
- * a fold continuation, and the blind pass folded it) */
4134
- if (i + 1 >= len) {
4132
+ /* A col-0 --- / ... marker RESCUES the quote: the parser ends
4133
+ * the document at the marker (the post-cut reset says "the
4134
+ * tail began at a --- line in a clean state"), so bytes after
4135
+ * it are NOT quoted content. Whole-buffer runs must classify
4136
+ * them the same way or the routes diverge: round 25 of the
4137
+ * nightly (crash-27a77050) fed ">\n: '\n---\na<U+2029>" and
4138
+ * the whole route kept the PS raw inside the never-closed
4139
+ * quote while the chunked route (fresh probe after the cut)
4140
+ * folded it — a plain scalar "a " vs "a". */
4141
+ int rescue = (c->at_break && i + 2 < len && (ch == '-' || ch == '.') && p[i + 1] == ch &&
4142
+ p[i + 2] == ch &&
4143
+ (i + 3 >= len || p[i + 3] == ' ' || p[i + 3] == '\t' || p[i + 3] == '\n' ||
4144
+ p[i + 3] == '\r'));
4145
+ if (rescue) {
4146
+ c->quote = 0;
4147
+ } else {
4148
+ /* NEL folds inside quotes too; LS/PS stay raw content — but
4149
+ * every break still arms the line-start state, or a col-0
4150
+ * marker after a quoted newline is invisible (at_break was
4151
+ * last armed OUTSIDE the quote) */
4152
+ if (nel) {
4153
+ t.n = 2;
4154
+ t.fold_off = 0;
4155
+ t.fold_len = 2;
4156
+ c->at_break = 1;
4135
4157
  return t;
4136
4158
  }
4137
- unsigned char nx = (unsigned char)p[i + 1];
4138
- if (nx == 0xC2 && i + 2 < len && (unsigned char)p[i + 2] == 0x85) {
4159
+ if (lps) {
4139
4160
  t.n = 3;
4140
- t.fold_off = 1;
4141
- t.fold_len = 2;
4161
+ c->at_break = 1;
4142
4162
  return t;
4143
4163
  }
4144
- if (nx == 0xE2 && i + 3 < len && (unsigned char)p[i + 2] == 0x80 &&
4145
- ((unsigned char)p[i + 3] & 0xFE) == 0xA8) {
4146
- t.n = 4;
4147
- t.fold_off = 1;
4148
- t.fold_len = 3;
4164
+ if (ch == '\n' || (ch == '\r' && (i + 1 >= len || p[i + 1] != '\n'))) {
4165
+ c->at_break = 1;
4149
4166
  return t;
4150
4167
  }
4151
- t.n = 2;
4168
+ c->at_break = 0;
4169
+ if (c->quote == '\'') {
4170
+ if (ch == '\'') {
4171
+ if (i + 1 < len && p[i + 1] == '\'') {
4172
+ t.n = 2; /* '' escaped quote */
4173
+ return t;
4174
+ }
4175
+ c->quote = 0;
4176
+ }
4177
+ return t;
4178
+ }
4179
+ if (ch == '\\') {
4180
+ /* the escape consumes the next byte — but a multibyte
4181
+ * break under the escape still folds (an escaped break is
4182
+ * a fold continuation, and the blind pass folded it) */
4183
+ if (i + 1 >= len) {
4184
+ return t;
4185
+ }
4186
+ unsigned char nx = (unsigned char)p[i + 1];
4187
+ if (nx == 0xC2 && i + 2 < len && (unsigned char)p[i + 2] == 0x85) {
4188
+ t.n = 3;
4189
+ t.fold_off = 1;
4190
+ t.fold_len = 2;
4191
+ return t;
4192
+ }
4193
+ if (nx == 0xE2 && i + 3 < len && (unsigned char)p[i + 2] == 0x80 &&
4194
+ ((unsigned char)p[i + 3] & 0xFE) == 0xA8) {
4195
+ t.n = 4;
4196
+ t.fold_off = 1;
4197
+ t.fold_len = 3;
4198
+ return t;
4199
+ }
4200
+ t.n = 2;
4201
+ return t;
4202
+ }
4203
+ if (ch == '"') {
4204
+ c->quote = 0;
4205
+ }
4152
4206
  return t;
4153
4207
  }
4154
- if (ch == '"') {
4155
- c->quote = 0;
4156
- }
4157
- return t;
4158
4208
  }
4159
4209
  if (c->in_verbatim) {
4160
4210
  if (ch == '>') {
@@ -5450,10 +5500,14 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
5450
5500
  }
5451
5501
  } else {
5452
5502
  e->q_key_pending = 0;
5453
- e->q_value_pending = 0;
5454
5503
  if (e->qmd_n > 0) {
5455
5504
  e->qmd_n--; /* the ':' line supplied the innermost value */
5456
5505
  }
5506
+ /* the flag governs the WHOLE stack: deeper pendings
5507
+ * keep it up so their nulls still flush (the nightly's
5508
+ * round 24, crash-bcc0f52c — the hard clear stranded
5509
+ * the outer pair's null) */
5510
+ e->q_value_pending = e->qmd_n > 0;
5457
5511
  }
5458
5512
  e->pos++;
5459
5513
  e->cv_line = e->line; /* the value parses on THIS line's colon */
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.35.2
4
+ version: 0.6.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-21 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi