@osdlc/yaml-splice 0.0.1-stage → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2f3fc7c: A malformed edit is now refused as the new `EditErrorCode` `InvalidEdit`, and an error that is not a refusal is passed through instead of being reported as `InvalidResult`.
8
+
9
+ - `InvalidEdit`: a `Set` with no `value` (or `value: undefined`), an unknown tag, or a `Set` value nested more than 124 levels deep (the JSON decoder's recursion limit). These were reported as `InvalidResult`, which says the edit would damage the document, when the edit itself was bad.
10
+ - **Breaking:** `editYaml` rethrows any error without a known code unchanged. Before, it wrapped it in `EditError('InvalidResult', …)`. A Rust panic in the WebAssembly module (`RuntimeError: unreachable`) now reaches the caller as a `RuntimeError`, so a crashed module no longer looks like a normal refusal. Code that catches only `EditError` now sees these errors propagate.
11
+
12
+ ## 0.1.4
13
+
14
+ ### Patch Changes
15
+
16
+ - fa792c7: The first release on npm. Versions 0.1.0 to 0.1.3 were used only during development and were never published. It edits top-level fields of a YAML mapping in place, on bytes: set, remove or rename a field, and every byte outside the edited span stays as it was, including comments, key order, quoting, a BOM and line endings. It runs the `yaml-splice` Rust crate through WebAssembly, so it has no runtime dependencies and no native code.
17
+
18
+ - `editYaml(source, edits)` applies a batch of `FieldEdit`s (`Set`, `Remove`, `Rename`) in order and returns an `EditOutcome` (`bytes` and `changed`). The batch is atomic: a refused edit throws an `EditError` with an `EditErrorCode` and, where known, an `EditSpan`, and nothing is returned.
19
+ - Keys match case-insensitively.
20
+
21
+ Fixed since the last development version:
22
+
23
+ - `Set` on a quoted value with a comment after it (`title: 'Draft' # note`) no longer deletes the comment.
24
+ - `Set` on a block scalar (`|` or `>`) no longer joins the next key onto the new value's line, and `Remove` on one no longer deletes the next line too.
25
+ - A comment indented less than a block scalar is kept: `Set` no longer joins it onto the new value's line, and `Remove` no longer deletes it.
26
+ - An empty block scalar (`a: |`, also with a tag or an anchor) can now be set and removed. Both were refused.
27
+ - When a new value is written as a block, the old value's trailing comment moves to the block's header line instead of becoming part of the value.
28
+ - A keep-chomped (`|+`) value keeps its trailing line breaks: setting `"kept\n\n"` no longer writes `"kept\n"`.
29
+ - `editYaml` checks each edit against what it asked for. It throws `EditError('InvalidResult', …)` when an edit would change the value or order of a key it does not address, or when the addressed key would not read back as set, removed or renamed. Before, it returned that document. A `.nan` value in another key no longer counts as a change.
30
+
31
+ All seven exports are stable. Node.js 22 or later and Bun; no browser or bundler build yet. Shipped as compiled JavaScript with type declarations. Tested on Linux and macOS. Licensed `MIT OR Apache-2.0`.
32
+
3
33
  ## History before the first public release
4
34
 
5
35
  These versions were developed in a private repository before the package moved here, and
package/README.md CHANGED
@@ -40,6 +40,8 @@ ESM only. Node.js 22 or later, or Bun.
40
40
  ## Quick start
41
41
 
42
42
  ```ts
43
+ import assert from 'node:assert/strict'
44
+
43
45
  import { editYaml } from '@osdlc/yaml-splice'
44
46
 
45
47
  const source = new TextEncoder().encode("title: 'Draft' # shown in the header\nstatus: draft\n")
@@ -49,8 +51,8 @@ const out = editYaml(source, [
49
51
  { Rename: { from: 'title', to: 'name' } },
50
52
  ])
51
53
 
52
- new TextDecoder().decode(out.bytes) // "name: 'Draft' # shown in the header\nstatus: done\n"
53
- out.changed // true
54
+ assert.equal(new TextDecoder().decode(out.bytes), "name: 'Draft' # shown in the header\nstatus: done\n")
55
+ assert.equal(out.changed, true)
54
56
  ```
55
57
 
56
58
  More in `examples/`: setting values, removing and renaming in one batch, and
@@ -70,10 +72,13 @@ handling refusals.
70
72
  - `EditError` is the refusal. Its `code` (an `EditErrorCode`) says why, and
71
73
  `name` is the same code. `span`, an `EditSpan`, is the byte range
72
74
  `[start, end)` in the input that the refusal is about, when there is one.
73
- - `EditErrorCode` is one of `InvalidUtf8`, `MalformedYaml`,
75
+ - `EditErrorCode` is one of `InvalidEdit`, `InvalidUtf8`, `MalformedYaml`,
74
76
  `UnsupportedDocument`, `RootNotMapping`, `AmbiguousKey`, `MissingKey`,
75
77
  `KeyConflict`, `UnrenderableValue` and `InvalidResult`. A minor release may
76
78
  add a code, so give a `switch` over it a `default` branch.
79
+ - Any other error `editYaml` throws is not a refusal and is passed through
80
+ as is. A `RuntimeError` from the WebAssembly module means it crashed; do
81
+ not keep calling it as if the edit had been refused.
77
82
  - `EditSpan` is `{ start, end }`, in bytes. `RootNotMapping`, `AmbiguousKey`
78
83
  and `KeyConflict` always carry one; `MalformedYaml` and
79
84
  `UnsupportedDocument` carry one when the parser knows the location.
@@ -94,6 +99,11 @@ Each export has TSDoc in the source. Your editor shows it.
94
99
  byte zero.
95
100
  - `NaN` and `±Infinity` anywhere in a `Set` value are refused as
96
101
  `UnrenderableValue` before any byte is written.
102
+ - An edit the editor cannot read is refused as `InvalidEdit`: a `Set` with
103
+ no `value` (or `value: undefined`), an unknown tag, or a `Set` value
104
+ nested more than 124 levels deep. The last is the JSON decoder's recursion
105
+ limit (serde_json's 128 levels, less the levels the edit list itself
106
+ takes).
97
107
  - Each edit is checked against what it asked for: the addressed key must read
98
108
  back as set, removed or renamed, and no other key may change value or
99
109
  order. If not, the batch is refused as `InvalidResult` instead of returning
@@ -138,6 +148,8 @@ The experimental exports:
138
148
  | An edit that changes no value returns the input bytes | `tests/index.test.ts`, "a semantic no-op …" |
139
149
  | Non-finite numbers are refused before any write | `tests/index.test.ts`, "a non-finite number in a Set value" |
140
150
  | `EditErrorCode` lists every refusal the crate has | `tests/index.test.ts`, "EditErrorCode" |
151
+ | A malformed or too-deeply nested edit is `InvalidEdit` | `tests/index.test.ts`, "an edit the editor cannot read" |
152
+ | An error that is not a refusal is passed through | `tests/index.test.ts`, "an error that is not a refusal" |
141
153
 
142
154
  ## Runtimes and platforms
143
155
 
package/dist/index.d.ts CHANGED
@@ -35,11 +35,12 @@ export interface EditOutcome {
35
35
  readonly changed: boolean;
36
36
  }
37
37
  /**
38
- * Why an edit was refused: one code per variant of the crate's `EditError`.
39
- * A minor release may add a code, so a `switch` over it needs a `default`
40
- * branch.
38
+ * Why an edit was refused: one code per variant of the crate's `EditError`,
39
+ * plus `InvalidEdit` for an edit the editor could not read: a missing `value`,
40
+ * an unknown tag, or a `Set` value nested more than 124 levels deep. A minor
41
+ * release may add a code, so a `switch` over it needs a `default` branch.
41
42
  */
42
- export type EditErrorCode = 'InvalidUtf8' | 'MalformedYaml' | 'UnsupportedDocument' | 'RootNotMapping' | 'AmbiguousKey' | 'MissingKey' | 'KeyConflict' | 'UnrenderableValue' | 'InvalidResult';
43
+ export type EditErrorCode = 'InvalidEdit' | 'InvalidUtf8' | 'MalformedYaml' | 'UnsupportedDocument' | 'RootNotMapping' | 'AmbiguousKey' | 'MissingKey' | 'KeyConflict' | 'UnrenderableValue' | 'InvalidResult';
43
44
  /**
44
45
  * A byte span `[start, end)` into the input. `RootNotMapping`, `AmbiguousKey`
45
46
  * and `KeyConflict` always carry one; `MalformedYaml` and `UnsupportedDocument`
@@ -50,7 +51,7 @@ export interface EditSpan {
50
51
  readonly end: number;
51
52
  }
52
53
  /**
53
- * A typed, structured refusal from the Rust core. `name`/`code` is the variant
54
+ * A typed refusal of an edit batch. `name`/`code` is the variant
54
55
  * and `span` carries the byte range where one is known. Thrown by
55
56
  * {@link editYaml} in place of the raw bindgen error.
56
57
  */
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { editYaml as editYamlWasm, } from './wasm/yaml_splice_wasm.js';
16
16
  /**
17
- * A typed, structured refusal from the Rust core. `name`/`code` is the variant
17
+ * A typed refusal of an edit batch. `name`/`code` is the variant
18
18
  * and `span` carries the byte range where one is known. Thrown by
19
19
  * {@link editYaml} in place of the raw bindgen error.
20
20
  */
@@ -34,17 +34,20 @@ export class EditError extends Error {
34
34
  this.span = span;
35
35
  }
36
36
  }
37
- const ERROR_CODES = new Set([
38
- 'InvalidUtf8',
39
- 'MalformedYaml',
40
- 'UnsupportedDocument',
41
- 'RootNotMapping',
42
- 'AmbiguousKey',
43
- 'MissingKey',
44
- 'KeyConflict',
45
- 'UnrenderableValue',
46
- 'InvalidResult',
47
- ]);
37
+ // Every code, so the type check fails when a code is missing here.
38
+ const ERROR_CODE_TABLE = {
39
+ InvalidEdit: true,
40
+ InvalidUtf8: true,
41
+ MalformedYaml: true,
42
+ UnsupportedDocument: true,
43
+ RootNotMapping: true,
44
+ AmbiguousKey: true,
45
+ MissingKey: true,
46
+ KeyConflict: true,
47
+ UnrenderableValue: true,
48
+ InvalidResult: true,
49
+ };
50
+ const ERROR_CODES = new Set(Object.keys(ERROR_CODE_TABLE));
48
51
  /**
49
52
  * Edit `source` (raw UTF-8 YAML bytes) with an ordered batch of `edits`,
50
53
  * returning new bytes plus a `changed` flag, or throwing a typed
@@ -67,7 +70,7 @@ export function editYaml(source, edits) {
67
70
  result = editYamlWasm(source, editsJson);
68
71
  }
69
72
  catch (raw) {
70
- throw toEditError(raw);
73
+ throw isRefusal(raw) ? toEditError(raw) : raw;
71
74
  }
72
75
  // Read both fields into a plain object, THEN free the handle. The glue's
73
76
  // `bytes` getter returns an owned copy (`.slice()`), so it survives `free()`.
@@ -106,16 +109,25 @@ function assertFiniteValue(value, key) {
106
109
  assertFiniteValue(item, key);
107
110
  }
108
111
  }
109
- /** Map a thrown bindgen `Error` (`.name` = variant, `.span`) to `EditError`. */
112
+ /**
113
+ * The `.name` the wasm gives a crate `EditError` variant it has no name for
114
+ * (the crate's enum is `#[non_exhaustive]`). Still a refusal: reported as
115
+ * `InvalidResult`, as before 0.2.0.
116
+ */
117
+ const UNNAMED_REFUSAL = 'EditError';
118
+ /**
119
+ * Whether a thrown value is one of the wasm side's refusals: an `Error` whose
120
+ * `name` is a known code, or {@link UNNAMED_REFUSAL}. Anything else, such as a
121
+ * Rust panic (`RuntimeError: unreachable`), is rethrown as is: it means the
122
+ * module is broken, not that the edit was refused.
123
+ */
124
+ function isRefusal(raw) {
125
+ return raw instanceof Error && (ERROR_CODES.has(raw.name) || raw.name === UNNAMED_REFUSAL);
126
+ }
127
+ /** Map a bindgen refusal (`.name` = code, `.span`) to `EditError`. */
110
128
  function toEditError(raw) {
111
- if (raw instanceof Error) {
112
- const code = ERROR_CODES.has(raw.name)
113
- ? raw.name
114
- : 'InvalidResult';
115
- const span = readSpan(raw.span);
116
- return new EditError(code, raw.message, span);
117
- }
118
- return new EditError('InvalidResult', String(raw));
129
+ const code = ERROR_CODES.has(raw.name) ? raw.name : 'InvalidResult';
130
+ return new EditError(code, raw.message, readSpan(raw.span));
119
131
  }
120
132
  function readSpan(span) {
121
133
  if (span !== null &&
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osdlc/yaml-splice",
3
- "version": "0.0.1-stage",
3
+ "version": "0.2.0",
4
4
  "description": "Edit top-level fields of a YAML mapping in place, on bytes: set, remove or rename a field, and every byte outside the edited span stays as it was, including comments, key order, quoting, a BOM and line endings. The yaml-splice Rust crate, built to WebAssembly. Node.js and Bun.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "sksizer",