@osdlc/yaml-splice 0.0.0-stage → 0.1.4
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 +57 -0
- package/CONTRIBUTING.md +90 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +153 -2
- package/SECURITY.md +23 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +129 -0
- package/dist/wasm/package.json +3 -0
- package/dist/wasm/yaml_splice_wasm.d.ts +31 -0
- package/dist/wasm/yaml_splice_wasm.js +229 -0
- package/dist/wasm/yaml_splice_wasm_bg.wasm +0 -0
- package/dist/wasm/yaml_splice_wasm_bg.wasm.d.ts +15 -0
- package/package.json +66 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
- Keys match case-insensitively.
|
|
11
|
+
|
|
12
|
+
Fixed since the last development version:
|
|
13
|
+
|
|
14
|
+
- `Set` on a quoted value with a comment after it (`title: 'Draft' # note`) no longer deletes the comment.
|
|
15
|
+
- `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.
|
|
16
|
+
- 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.
|
|
17
|
+
- An empty block scalar (`a: |`, also with a tag or an anchor) can now be set and removed. Both were refused.
|
|
18
|
+
- 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.
|
|
19
|
+
- A keep-chomped (`|+`) value keeps its trailing line breaks: setting `"kept\n\n"` no longer writes `"kept\n"`.
|
|
20
|
+
- `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.
|
|
21
|
+
|
|
22
|
+
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`.
|
|
23
|
+
|
|
24
|
+
## History before the first public release
|
|
25
|
+
|
|
26
|
+
These versions were developed in a private repository before the package moved here, and
|
|
27
|
+
were never published. They are rebuilt from the git history.
|
|
28
|
+
|
|
29
|
+
### [0.1.3] - 2026-09-07 (unpublished)
|
|
30
|
+
|
|
31
|
+
#### Fixed
|
|
32
|
+
|
|
33
|
+
- `editYaml` refuses `NaN`, `Infinity` and `-Infinity` anywhere in a `Set` value, nested values
|
|
34
|
+
included, with `EditError('UnrenderableValue', …)` before any byte is written. Before, they
|
|
35
|
+
were written as YAML `null`.
|
|
36
|
+
|
|
37
|
+
### [0.1.2] - 2026-09-06 (unpublished)
|
|
38
|
+
|
|
39
|
+
#### Fixed
|
|
40
|
+
|
|
41
|
+
- The built entry (`dist/`) loads under Node. It imported the WebAssembly glue from a path the
|
|
42
|
+
build did not create.
|
|
43
|
+
|
|
44
|
+
### [0.1.1] - 2026-07-21 (unpublished)
|
|
45
|
+
|
|
46
|
+
#### Fixed
|
|
47
|
+
|
|
48
|
+
- `EditError.span` is a truly optional property, so it type-checks under a consumer's
|
|
49
|
+
`exactOptionalPropertyTypes`.
|
|
50
|
+
|
|
51
|
+
### [0.1.0] - 2026-07-21 (unpublished)
|
|
52
|
+
|
|
53
|
+
#### Added
|
|
54
|
+
|
|
55
|
+
- `editYaml(source, edits)`: set, remove or rename top-level fields of a YAML mapping without
|
|
56
|
+
reserializing it, with the types `FieldEdit`, `EditValue`, `EditOutcome`, `EditError`,
|
|
57
|
+
`EditErrorCode` and `EditSpan`.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in `@osdlc/yaml-splice`.
|
|
4
|
+
|
|
5
|
+
## Issues
|
|
6
|
+
|
|
7
|
+
Report bugs and ask for features in the
|
|
8
|
+
[issue tracker](https://github.com/osdlc/osdlc/issues). For a bug, include the
|
|
9
|
+
library version, your runtime and its version, the YAML input (escape a BOM or
|
|
10
|
+
`\r` so it survives copying), and the edits you passed. Report security
|
|
11
|
+
problems privately. See [SECURITY.md](./SECURITY.md).
|
|
12
|
+
|
|
13
|
+
## Pull requests
|
|
14
|
+
|
|
15
|
+
This package lives in [osdlc/osdlc](https://github.com/osdlc/osdlc) under
|
|
16
|
+
`packages/ts/yaml-splice`. Open pull requests there. They are reviewed and
|
|
17
|
+
merged there.
|
|
18
|
+
|
|
19
|
+
## Where a change goes
|
|
20
|
+
|
|
21
|
+
The editing itself is the `yaml-splice` Rust crate, in `packages/rust/yaml-splice`.
|
|
22
|
+
`packages/rust/yaml-splice-wasm` exposes it to JavaScript, and this package's
|
|
23
|
+
`index.ts` is the typed wrapper over that. A change to how YAML is edited goes
|
|
24
|
+
into the crate, with a fixture in its conformance corpus
|
|
25
|
+
(`packages/rust/yaml-splice/tests/fixtures/conformance.json`), which both the
|
|
26
|
+
crate's tests and this package's tests run.
|
|
27
|
+
|
|
28
|
+
## Running the tests
|
|
29
|
+
|
|
30
|
+
The repository is a pnpm workspace. Install from the repository root, then
|
|
31
|
+
work in the package directory. The unit tests run under Bun, so install
|
|
32
|
+
[Bun](https://bun.sh) as well as Node 22 or later. No Rust toolchain is needed:
|
|
33
|
+
the built WebAssembly is committed in `wasm/`.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm install
|
|
37
|
+
cd packages/ts/yaml-splice
|
|
38
|
+
pnpm test
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`pnpm test` runs the Bun suite on the source, then builds the package and
|
|
42
|
+
runs `tests/corpus-node.mjs`, which runs the conformance corpus on the built
|
|
43
|
+
package under Node.
|
|
44
|
+
|
|
45
|
+
`pnpm run smoke` runs `tests/smoke.mjs`, a short check of the built package
|
|
46
|
+
under Node. It is not published.
|
|
47
|
+
`pnpm run coverage` reports test coverage.
|
|
48
|
+
|
|
49
|
+
Other checks:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pnpm run typecheck
|
|
53
|
+
pnpm run lint
|
|
54
|
+
pnpm run format:check
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Rebuilding the WebAssembly
|
|
58
|
+
|
|
59
|
+
`wasm/` is built from the crates by `packages/rust/yaml-splice-wasm/build.sh`
|
|
60
|
+
(`pnpm run build:wasm`). Rebuild it whenever either crate changes, and commit
|
|
61
|
+
the result. The script needs:
|
|
62
|
+
|
|
63
|
+
- the Rust toolchain pinned in `packages/rust/rust-toolchain.toml`, with the
|
|
64
|
+
`wasm32-unknown-unknown` target (`rustup target add wasm32-unknown-unknown`);
|
|
65
|
+
- `wasm-bindgen` 0.2.126 (`cargo install wasm-bindgen-cli --version 0.2.126`);
|
|
66
|
+
- `wasm-opt` from binaryen `version_131`.
|
|
67
|
+
|
|
68
|
+
The `wasm` job in `.github/workflows/rust-ci.yml` rebuilds `wasm/` on every
|
|
69
|
+
pull request that touches the crates or `wasm/`, and fails when the result
|
|
70
|
+
differs from what is committed. When it fails, it uploads the rebuilt files
|
|
71
|
+
as the `yaml-splice-wasm` artifact.
|
|
72
|
+
|
|
73
|
+
A change that changes the WebAssembly changes what this package does, so it
|
|
74
|
+
needs a changeset for `@osdlc/yaml-splice` (see below), even when no file in
|
|
75
|
+
this directory other than `wasm/` changed.
|
|
76
|
+
|
|
77
|
+
## Code style
|
|
78
|
+
|
|
79
|
+
- TypeScript, ES modules, and explicit `.ts` import paths.
|
|
80
|
+
- Prettier formats the code and oxlint checks it. Run `pnpm run format` and
|
|
81
|
+
`pnpm run lint` before you commit.
|
|
82
|
+
- Add a test for each behaviour change or bug fix.
|
|
83
|
+
- A new export gets TSDoc and a use in `examples/`, which imports the package
|
|
84
|
+
by name. A test fails if an export has no example.
|
|
85
|
+
- An experimental export carries `@experimental` and is listed in the
|
|
86
|
+
README's Stability section. A test checks that the two agree.
|
|
87
|
+
- For a change users can see, add a changeset: run `pnpm changeset` at the
|
|
88
|
+
repository root. Start a breaking change's summary with `**Breaking:**`.
|
|
89
|
+
Do not edit `CHANGELOG.md` or `version` by hand; the release writes them.
|
|
90
|
+
See [packages/README.md](https://github.com/osdlc/osdlc/blob/main/packages/README.md).
|
package/LICENSE-APACHE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright 2026 sksizer
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/LICENSE-MIT
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sksizer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,154 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @osdlc/yaml-splice
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Edit top-level fields of a YAML mapping in place, on bytes. Set, remove or
|
|
4
|
+
rename a field, and every byte outside the edited span stays as it was:
|
|
5
|
+
comments, key order, quoting, a byte-order mark and line endings. It runs the
|
|
6
|
+
`yaml-splice` Rust crate as WebAssembly, so it has no runtime dependencies and
|
|
7
|
+
no native code.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
A program that changes one value in a YAML file usually parses the file and
|
|
12
|
+
writes it out again. The values come back right, but the text does not:
|
|
13
|
+
comments move or vanish, quotes change, keys are reordered, and CRLF becomes
|
|
14
|
+
LF. In version control the whole file shows as changed, and the person who
|
|
15
|
+
wrote it loses their layout.
|
|
16
|
+
|
|
17
|
+
This package changes only the bytes of the field you edit. Everything else is
|
|
18
|
+
copied from the input. If an edit cannot be made that way, it refuses with a
|
|
19
|
+
typed error instead of guessing.
|
|
20
|
+
|
|
21
|
+
It is for programs that write YAML that people also edit by hand: Markdown
|
|
22
|
+
frontmatter, config files, and tools that sync metadata into notes.
|
|
23
|
+
|
|
24
|
+
When not to use it:
|
|
25
|
+
|
|
26
|
+
- To read values. Use a YAML parser, such as `yaml`.
|
|
27
|
+
- To edit nested values. Only top-level keys of a mapping are addressed; a
|
|
28
|
+
`Set` replaces a key's whole value.
|
|
29
|
+
- In a browser or through a bundler such as Vite. This release loads under
|
|
30
|
+
Node.js and Bun only (see Runtimes and platforms).
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install @osdlc/yaml-splice
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
ESM only. Node.js 22 or later, or Bun.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { editYaml } from '@osdlc/yaml-splice'
|
|
44
|
+
|
|
45
|
+
const source = new TextEncoder().encode("title: 'Draft' # shown in the header\nstatus: draft\n")
|
|
46
|
+
|
|
47
|
+
const out = editYaml(source, [
|
|
48
|
+
{ Set: { key: 'status', value: 'done' } },
|
|
49
|
+
{ Rename: { from: 'title', to: 'name' } },
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
new TextDecoder().decode(out.bytes) // "name: 'Draft' # shown in the header\nstatus: done\n"
|
|
53
|
+
out.changed // true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
More in `examples/`: setting values, removing and renaming in one batch, and
|
|
57
|
+
handling refusals.
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
- `editYaml(source, edits)` takes the document as UTF-8 bytes (`Uint8Array`)
|
|
62
|
+
and a list of `FieldEdit`s, and returns an `EditOutcome`: the new `bytes` and
|
|
63
|
+
`changed`. It throws an `EditError` when it refuses.
|
|
64
|
+
- `FieldEdit` is one edit: `{ Set: { key, value } }`,
|
|
65
|
+
`{ Remove: { key } }` or `{ Rename: { from, to } }`.
|
|
66
|
+
- `EditValue` is the value a `Set` writes: any JSON-shaped value. Maps and
|
|
67
|
+
lists are written as block YAML under the key.
|
|
68
|
+
- `EditOutcome` holds `bytes` and `changed`. When no value changed, `bytes`
|
|
69
|
+
equals the input and `changed` is `false`, so a caller can skip the write.
|
|
70
|
+
- `EditError` is the refusal. Its `code` (an `EditErrorCode`) says why, and
|
|
71
|
+
`name` is the same code. `span`, an `EditSpan`, is the byte range
|
|
72
|
+
`[start, end)` in the input that the refusal is about, when there is one.
|
|
73
|
+
- `EditErrorCode` is one of `InvalidUtf8`, `MalformedYaml`,
|
|
74
|
+
`UnsupportedDocument`, `RootNotMapping`, `AmbiguousKey`, `MissingKey`,
|
|
75
|
+
`KeyConflict`, `UnrenderableValue` and `InvalidResult`. A minor release may
|
|
76
|
+
add a code, so give a `switch` over it a `default` branch.
|
|
77
|
+
- `EditSpan` is `{ start, end }`, in bytes. `RootNotMapping`, `AmbiguousKey`
|
|
78
|
+
and `KeyConflict` always carry one; `MalformedYaml` and
|
|
79
|
+
`UnsupportedDocument` carry one when the parser knows the location.
|
|
80
|
+
|
|
81
|
+
Each export has TSDoc in the source. Your editor shows it.
|
|
82
|
+
|
|
83
|
+
## Behaviour
|
|
84
|
+
|
|
85
|
+
- The edits apply in order, and each one sees the result of the ones before
|
|
86
|
+
it. The batch is atomic: if any edit is refused, nothing is returned.
|
|
87
|
+
- Keys match case-insensitively: `Title` and `title` are the same key. Both
|
|
88
|
+
sides are lowercased and nothing else is normalized. Two existing keys that
|
|
89
|
+
differ only in case make an edit of either one `AmbiguousKey`.
|
|
90
|
+
- `Set` on a missing key appends it after the last top-level entry. `Remove`
|
|
91
|
+
of a missing key changes nothing. `Rename` of a missing key is
|
|
92
|
+
`MissingKey`; a rename onto an existing key is `KeyConflict`.
|
|
93
|
+
- New lines use the document's first line ending (LF or CRLF). A BOM stays at
|
|
94
|
+
byte zero.
|
|
95
|
+
- `NaN` and `±Infinity` anywhere in a `Set` value are refused as
|
|
96
|
+
`UnrenderableValue` before any byte is written.
|
|
97
|
+
- Each edit is checked against what it asked for: the addressed key must read
|
|
98
|
+
back as set, removed or renamed, and no other key may change value or
|
|
99
|
+
order. If not, the batch is refused as `InvalidResult` instead of returning
|
|
100
|
+
a damaged document.
|
|
101
|
+
|
|
102
|
+
## How it compares
|
|
103
|
+
|
|
104
|
+
- [`yaml`](https://www.npmjs.com/package/yaml) keeps comments through its Document
|
|
105
|
+
API, but writes the whole document out again, so quoting, spacing and
|
|
106
|
+
line endings can change. Use it to read and to build YAML; use this package
|
|
107
|
+
to change a value in a file a person edits.
|
|
108
|
+
- [`yaml-diff-patch`](https://www.npmjs.com/package/yaml-diff-patch) and
|
|
109
|
+
[`yawn-yaml`](https://www.npmjs.com/package/yawn-yaml) keep comments and most
|
|
110
|
+
formatting, but do not promise that the bytes outside the edit are
|
|
111
|
+
unchanged, and have no typed refusals.
|
|
112
|
+
- [`@osdlc/frontmatter-lossless`](https://www.npmjs.com/package/@osdlc/frontmatter-lossless)
|
|
113
|
+
splits a Markdown note into its frontmatter and body. The two compose:
|
|
114
|
+
split the note, edit the YAML bytes here, and put the note back together.
|
|
115
|
+
|
|
116
|
+
The Rust crate [`yaml-splice`](https://crates.io/crates/yaml-splice) is the
|
|
117
|
+
implementation. This package runs it as WebAssembly, and its tests run the
|
|
118
|
+
crate's conformance corpus.
|
|
119
|
+
|
|
120
|
+
## Stability
|
|
121
|
+
|
|
122
|
+
The package is 0.x, so any minor release may break the API. Within that, each
|
|
123
|
+
export is stable or experimental. A breaking change to a stable export is
|
|
124
|
+
listed in the CHANGELOG under "Breaking". Any release may change or remove an
|
|
125
|
+
experimental export.
|
|
126
|
+
|
|
127
|
+
The experimental exports:
|
|
128
|
+
|
|
129
|
+
- none
|
|
130
|
+
|
|
131
|
+
## Tested claims
|
|
132
|
+
|
|
133
|
+
| Claim | Test |
|
|
134
|
+
|---|---|
|
|
135
|
+
| Comments, key order and quoting outside the edit are kept | corpus fixture `a_changed_value_keeps_comments_key_order_and_quoting` |
|
|
136
|
+
| A BOM is kept | corpus fixture `a_bom_is_preserved_and_outside_every_span` |
|
|
137
|
+
| CRLF and mixed line endings are kept | corpus fixtures `crlf_line_endings_survive_outside_the_edited_span`, `mixed_line_endings_are_not_normalized` |
|
|
138
|
+
| An edit that changes no value returns the input bytes | `tests/index.test.ts`, "a semantic no-op …" |
|
|
139
|
+
| Non-finite numbers are refused before any write | `tests/index.test.ts`, "a non-finite number in a Set value" |
|
|
140
|
+
| `EditErrorCode` lists every refusal the crate has | `tests/index.test.ts`, "EditErrorCode" |
|
|
141
|
+
|
|
142
|
+
## Runtimes and platforms
|
|
143
|
+
|
|
144
|
+
Node.js 22 or later, and Bun. The WebAssembly glue is built for Node.js: it
|
|
145
|
+
reads the `.wasm` file with `fs` from beside itself. So the package does not
|
|
146
|
+
load in a browser, in Deno without Node compatibility, or through a bundler
|
|
147
|
+
such as Vite. The built package is tested on Node.js 22 and 24, and the test
|
|
148
|
+
suite also runs under Bun. It has no native code and no platform-specific
|
|
149
|
+
code, so it runs on any operating system Node.js runs on. Its tests run on
|
|
150
|
+
Linux and macOS.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
`MIT OR Apache-2.0`, at your option.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report vulnerabilities privately. Do not open a public issue.
|
|
6
|
+
|
|
7
|
+
Use GitHub private vulnerability reporting on the repository: open the
|
|
8
|
+
[Security tab](https://github.com/osdlc/osdlc/security) and choose
|
|
9
|
+
"Report a vulnerability".
|
|
10
|
+
|
|
11
|
+
Include the version, a short description, and steps to reproduce. A minimal
|
|
12
|
+
code sample helps.
|
|
13
|
+
|
|
14
|
+
## Supported versions
|
|
15
|
+
|
|
16
|
+
Only the latest release of the newest major version gets security fixes.
|
|
17
|
+
Upgrade to it.
|
|
18
|
+
|
|
19
|
+
## What to expect
|
|
20
|
+
|
|
21
|
+
This is a small project. We aim to reply within a week. We will tell you
|
|
22
|
+
whether we accept the report and keep you updated until a fix is released.
|
|
23
|
+
We will credit you in the release notes if you want that.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSON-shaped value a `Set` edit may write: the same shape a JavaScript value
|
|
3
|
+
* can take, matching the crate's `From<&serde_json::Value>` bridge. Object key
|
|
4
|
+
* order for NESTED maps is not load-bearing (see the crate's ordering caveat);
|
|
5
|
+
* the TOP-LEVEL key being edited is addressed by `key`, never rebuilt.
|
|
6
|
+
*/
|
|
7
|
+
export type EditValue = null | boolean | number | string | readonly EditValue[] | {
|
|
8
|
+
readonly [key: string]: EditValue;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One top-level field edit, externally tagged to match the crate's `FieldEdit`
|
|
12
|
+
* enum: exactly one of `Set`, `Remove` or `Rename`. A `Remove` of a key that is
|
|
13
|
+
* not there changes nothing.
|
|
14
|
+
*/
|
|
15
|
+
export type FieldEdit = {
|
|
16
|
+
readonly Set: {
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly value: EditValue;
|
|
19
|
+
};
|
|
20
|
+
} | {
|
|
21
|
+
readonly Remove: {
|
|
22
|
+
readonly key: string;
|
|
23
|
+
};
|
|
24
|
+
} | {
|
|
25
|
+
readonly Rename: {
|
|
26
|
+
readonly from: string;
|
|
27
|
+
readonly to: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/** The result of an edit batch. */
|
|
31
|
+
export interface EditOutcome {
|
|
32
|
+
/** The complete output bytes. On a semantic no-op, byte-identical to input. */
|
|
33
|
+
readonly bytes: Uint8Array;
|
|
34
|
+
/** Whether any edit changed a byte. `false` means `bytes` equals the input. */
|
|
35
|
+
readonly changed: boolean;
|
|
36
|
+
}
|
|
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.
|
|
41
|
+
*/
|
|
42
|
+
export type EditErrorCode = 'InvalidUtf8' | 'MalformedYaml' | 'UnsupportedDocument' | 'RootNotMapping' | 'AmbiguousKey' | 'MissingKey' | 'KeyConflict' | 'UnrenderableValue' | 'InvalidResult';
|
|
43
|
+
/**
|
|
44
|
+
* A byte span `[start, end)` into the input. `RootNotMapping`, `AmbiguousKey`
|
|
45
|
+
* and `KeyConflict` always carry one; `MalformedYaml` and `UnsupportedDocument`
|
|
46
|
+
* carry one when the parser knows the location.
|
|
47
|
+
*/
|
|
48
|
+
export interface EditSpan {
|
|
49
|
+
readonly start: number;
|
|
50
|
+
readonly end: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A typed, structured refusal from the Rust core. `name`/`code` is the variant
|
|
54
|
+
* and `span` carries the byte range where one is known. Thrown by
|
|
55
|
+
* {@link editYaml} in place of the raw bindgen error.
|
|
56
|
+
*/
|
|
57
|
+
export declare class EditError extends Error {
|
|
58
|
+
/** The refusal variant, mirrored onto `name` so `err.name` is the code. */
|
|
59
|
+
readonly code: EditErrorCode;
|
|
60
|
+
/** Where the refusal happened, when the variant carries a location. */
|
|
61
|
+
readonly span?: EditSpan;
|
|
62
|
+
constructor(code: EditErrorCode, message: string, span?: EditSpan);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Edit `source` (raw UTF-8 YAML bytes) with an ordered batch of `edits`,
|
|
66
|
+
* returning new bytes plus a `changed` flag, or throwing a typed
|
|
67
|
+
* {@link EditError}. Byte-identical to `yaml_splice::edit_yaml`.
|
|
68
|
+
*
|
|
69
|
+
* The edits apply in order, and each sees the result of the ones before it.
|
|
70
|
+
* The batch is atomic: if any edit is refused, nothing is returned and no
|
|
71
|
+
* partial edit is observable. A non-finite number (`NaN`, `±Infinity`) anywhere
|
|
72
|
+
* in a `Set` value is refused as `UnrenderableValue`.
|
|
73
|
+
*
|
|
74
|
+
* Keys match case-insensitively: `Title` and `title` are the same key. Both
|
|
75
|
+
* sides are lowercased and nothing else is normalized. Two existing keys that
|
|
76
|
+
* differ only in case make an edit of either one `AmbiguousKey`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function editYaml(source: Uint8Array, edits: readonly FieldEdit[]): EditOutcome;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit top-level fields of a YAML mapping in place, on bytes: set, remove or
|
|
3
|
+
* rename a field, and every byte outside the edited span stays as it was.
|
|
4
|
+
*
|
|
5
|
+
* The editor is the `yaml-splice` Rust crate, built to WebAssembly. This file
|
|
6
|
+
* is a typed wrapper over the generated glue in `./wasm`, and it is the only
|
|
7
|
+
* thing callers import. It passes the edits across as JSON, copies the result
|
|
8
|
+
* into a plain `EditOutcome`, and frees the wasm-side handle, so no handle
|
|
9
|
+
* that points into wasm memory reaches a caller. A refused edit is thrown as
|
|
10
|
+
* an {@link EditError}.
|
|
11
|
+
*
|
|
12
|
+
* The behaviour is the crate's, byte for byte: an edit that changes no value
|
|
13
|
+
* returns the input bytes, and every byte outside an edited span is kept.
|
|
14
|
+
*/
|
|
15
|
+
import { editYaml as editYamlWasm, } from './wasm/yaml_splice_wasm.js';
|
|
16
|
+
/**
|
|
17
|
+
* A typed, structured refusal from the Rust core. `name`/`code` is the variant
|
|
18
|
+
* and `span` carries the byte range where one is known. Thrown by
|
|
19
|
+
* {@link editYaml} in place of the raw bindgen error.
|
|
20
|
+
*/
|
|
21
|
+
export class EditError extends Error {
|
|
22
|
+
/** The refusal variant, mirrored onto `name` so `err.name` is the code. */
|
|
23
|
+
code;
|
|
24
|
+
/** Where the refusal happened, when the variant carries a location. */
|
|
25
|
+
span;
|
|
26
|
+
constructor(code, message, span) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = code;
|
|
29
|
+
this.code = code;
|
|
30
|
+
// Assign only when present so `span` stays a truly-optional property under a
|
|
31
|
+
// consumer's `exactOptionalPropertyTypes` (an absent property still reads as
|
|
32
|
+
// `undefined`; assigning `undefined` outright would violate that flag).
|
|
33
|
+
if (span !== undefined)
|
|
34
|
+
this.span = span;
|
|
35
|
+
}
|
|
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
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Edit `source` (raw UTF-8 YAML bytes) with an ordered batch of `edits`,
|
|
50
|
+
* returning new bytes plus a `changed` flag, or throwing a typed
|
|
51
|
+
* {@link EditError}. Byte-identical to `yaml_splice::edit_yaml`.
|
|
52
|
+
*
|
|
53
|
+
* The edits apply in order, and each sees the result of the ones before it.
|
|
54
|
+
* The batch is atomic: if any edit is refused, nothing is returned and no
|
|
55
|
+
* partial edit is observable. A non-finite number (`NaN`, `±Infinity`) anywhere
|
|
56
|
+
* in a `Set` value is refused as `UnrenderableValue`.
|
|
57
|
+
*
|
|
58
|
+
* Keys match case-insensitively: `Title` and `title` are the same key. Both
|
|
59
|
+
* sides are lowercased and nothing else is normalized. Two existing keys that
|
|
60
|
+
* differ only in case make an edit of either one `AmbiguousKey`.
|
|
61
|
+
*/
|
|
62
|
+
export function editYaml(source, edits) {
|
|
63
|
+
assertFiniteNumbers(edits);
|
|
64
|
+
const editsJson = JSON.stringify(edits);
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = editYamlWasm(source, editsJson);
|
|
68
|
+
}
|
|
69
|
+
catch (raw) {
|
|
70
|
+
throw toEditError(raw);
|
|
71
|
+
}
|
|
72
|
+
// Read both fields into a plain object, THEN free the handle. The glue's
|
|
73
|
+
// `bytes` getter returns an owned copy (`.slice()`), so it survives `free()`.
|
|
74
|
+
try {
|
|
75
|
+
return { bytes: result.bytes, changed: result.changed };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
result.free();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Refuse non-finite numbers before the JSON boundary. `JSON.stringify` renders
|
|
83
|
+
* `NaN` and `±Infinity` as `null`, so without this guard a `Set` would silently
|
|
84
|
+
* write `null` over the field instead of refusing the value.
|
|
85
|
+
*/
|
|
86
|
+
function assertFiniteNumbers(edits) {
|
|
87
|
+
for (const edit of edits) {
|
|
88
|
+
if ('Set' in edit)
|
|
89
|
+
assertFiniteValue(edit.Set.value, edit.Set.key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function assertFiniteValue(value, key) {
|
|
93
|
+
if (typeof value === 'number') {
|
|
94
|
+
if (!Number.isFinite(value)) {
|
|
95
|
+
throw new EditError('UnrenderableValue', `key '${key}': ${value} is not a finite number and has no YAML rendering`);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
for (const item of value)
|
|
101
|
+
assertFiniteValue(item, key);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (value !== null && typeof value === 'object') {
|
|
105
|
+
for (const item of Object.values(value))
|
|
106
|
+
assertFiniteValue(item, key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Map a thrown bindgen `Error` (`.name` = variant, `.span`) to `EditError`. */
|
|
110
|
+
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));
|
|
119
|
+
}
|
|
120
|
+
function readSpan(span) {
|
|
121
|
+
if (span !== null &&
|
|
122
|
+
typeof span === 'object' &&
|
|
123
|
+
typeof span.start === 'number' &&
|
|
124
|
+
typeof span.end === 'number') {
|
|
125
|
+
const { start, end } = span;
|
|
126
|
+
return { start, end };
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The successful outcome of an edit batch. A wasm-bindgen class: the caller
|
|
6
|
+
* reads `.bytes` / `.changed` and should `.free()` it when done (it owns a
|
|
7
|
+
* slot in wasm linear memory). The TS façade reads both into a plain object
|
|
8
|
+
* and frees this handle, so callers never see the class.
|
|
9
|
+
*/
|
|
10
|
+
export class EditResult {
|
|
11
|
+
private constructor();
|
|
12
|
+
free(): void;
|
|
13
|
+
[Symbol.dispose](): void;
|
|
14
|
+
/**
|
|
15
|
+
* The complete output bytes. On a semantic no-op these are byte-identical
|
|
16
|
+
* to the input.
|
|
17
|
+
*/
|
|
18
|
+
readonly bytes: Uint8Array;
|
|
19
|
+
/**
|
|
20
|
+
* Whether any edit changed a byte.
|
|
21
|
+
*/
|
|
22
|
+
readonly changed: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Edit `source` (raw UTF-8 YAML bytes) with `edits_json`, an array of
|
|
27
|
+
* externally-tagged field edits, e.g.
|
|
28
|
+
* `[{"Set":{"key":"status","value":"done"}}]`,
|
|
29
|
+
* `[{"Remove":{"key":"draft"}}]`, `[{"Rename":{"from":"a","to":"b"}}]`.
|
|
30
|
+
*/
|
|
31
|
+
export function editYaml(source: Uint8Array, edits_json: string): EditResult;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/* @ts-self-types="./yaml_splice_wasm.d.ts" */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The successful outcome of an edit batch. A wasm-bindgen class: the caller
|
|
5
|
+
* reads `.bytes` / `.changed` and should `.free()` it when done (it owns a
|
|
6
|
+
* slot in wasm linear memory). The TS façade reads both into a plain object
|
|
7
|
+
* and frees this handle, so callers never see the class.
|
|
8
|
+
*/
|
|
9
|
+
class EditResult {
|
|
10
|
+
static __wrap(ptr) {
|
|
11
|
+
const obj = Object.create(EditResult.prototype);
|
|
12
|
+
obj.__wbg_ptr = ptr;
|
|
13
|
+
EditResultFinalization.register(obj, obj.__wbg_ptr, obj);
|
|
14
|
+
return obj;
|
|
15
|
+
}
|
|
16
|
+
__destroy_into_raw() {
|
|
17
|
+
const ptr = this.__wbg_ptr;
|
|
18
|
+
this.__wbg_ptr = 0;
|
|
19
|
+
EditResultFinalization.unregister(this);
|
|
20
|
+
return ptr;
|
|
21
|
+
}
|
|
22
|
+
free() {
|
|
23
|
+
const ptr = this.__destroy_into_raw();
|
|
24
|
+
wasm.__wbg_editresult_free(ptr, 0);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The complete output bytes. On a semantic no-op these are byte-identical
|
|
28
|
+
* to the input.
|
|
29
|
+
* @returns {Uint8Array}
|
|
30
|
+
*/
|
|
31
|
+
get bytes() {
|
|
32
|
+
const ret = wasm.editresult_bytes(this.__wbg_ptr);
|
|
33
|
+
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
|
34
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
|
35
|
+
return v1;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether any edit changed a byte.
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
get changed() {
|
|
42
|
+
const ret = wasm.editresult_changed(this.__wbg_ptr);
|
|
43
|
+
return ret !== 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (Symbol.dispose) EditResult.prototype[Symbol.dispose] = EditResult.prototype.free;
|
|
47
|
+
exports.EditResult = EditResult;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Edit `source` (raw UTF-8 YAML bytes) with `edits_json`, an array of
|
|
51
|
+
* externally-tagged field edits, e.g.
|
|
52
|
+
* `[{"Set":{"key":"status","value":"done"}}]`,
|
|
53
|
+
* `[{"Remove":{"key":"draft"}}]`, `[{"Rename":{"from":"a","to":"b"}}]`.
|
|
54
|
+
* @param {Uint8Array} source
|
|
55
|
+
* @param {string} edits_json
|
|
56
|
+
* @returns {EditResult}
|
|
57
|
+
*/
|
|
58
|
+
function editYaml(source, edits_json) {
|
|
59
|
+
const ptr0 = passArray8ToWasm0(source, wasm.__wbindgen_malloc);
|
|
60
|
+
const len0 = WASM_VECTOR_LEN;
|
|
61
|
+
const ptr1 = passStringToWasm0(edits_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
62
|
+
const len1 = WASM_VECTOR_LEN;
|
|
63
|
+
const ret = wasm.editYaml(ptr0, len0, ptr1, len1);
|
|
64
|
+
if (ret[2]) {
|
|
65
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
66
|
+
}
|
|
67
|
+
return EditResult.__wrap(ret[0]);
|
|
68
|
+
}
|
|
69
|
+
exports.editYaml = editYaml;
|
|
70
|
+
function __wbg_get_imports() {
|
|
71
|
+
const import0 = {
|
|
72
|
+
__proto__: null,
|
|
73
|
+
__wbg___wbindgen_throw_344f42d3211c4765: function(arg0, arg1) {
|
|
74
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
75
|
+
},
|
|
76
|
+
__wbg_new_b667d279fd5aa943: function(arg0, arg1) {
|
|
77
|
+
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
|
78
|
+
return ret;
|
|
79
|
+
},
|
|
80
|
+
__wbg_new_da52cf8fe3429cb2: function() {
|
|
81
|
+
const ret = new Object();
|
|
82
|
+
return ret;
|
|
83
|
+
},
|
|
84
|
+
__wbg_set_8535240470bf2500: function() { return handleError(function (arg0, arg1, arg2) {
|
|
85
|
+
const ret = Reflect.set(arg0, arg1, arg2);
|
|
86
|
+
return ret;
|
|
87
|
+
}, arguments); },
|
|
88
|
+
__wbg_set_name_3bbc583faefa4193: function(arg0, arg1, arg2) {
|
|
89
|
+
arg0.name = getStringFromWasm0(arg1, arg2);
|
|
90
|
+
},
|
|
91
|
+
__wbindgen_cast_0000000000000001: function(arg0) {
|
|
92
|
+
// Cast intrinsic for `F64 -> Externref`.
|
|
93
|
+
const ret = arg0;
|
|
94
|
+
return ret;
|
|
95
|
+
},
|
|
96
|
+
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
97
|
+
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
98
|
+
const ret = getStringFromWasm0(arg0, arg1);
|
|
99
|
+
return ret;
|
|
100
|
+
},
|
|
101
|
+
__wbindgen_init_externref_table: function() {
|
|
102
|
+
const table = wasm.__wbindgen_externrefs;
|
|
103
|
+
const offset = table.grow(4);
|
|
104
|
+
table.set(0, undefined);
|
|
105
|
+
table.set(offset + 0, undefined);
|
|
106
|
+
table.set(offset + 1, null);
|
|
107
|
+
table.set(offset + 2, true);
|
|
108
|
+
table.set(offset + 3, false);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
__proto__: null,
|
|
113
|
+
"./yaml_splice_wasm_bg.js": import0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const EditResultFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
118
|
+
? { register: () => {}, unregister: () => {} }
|
|
119
|
+
: new FinalizationRegistry(ptr => wasm.__wbg_editresult_free(ptr, 1));
|
|
120
|
+
|
|
121
|
+
function addToExternrefTable0(obj) {
|
|
122
|
+
const idx = wasm.__externref_table_alloc();
|
|
123
|
+
wasm.__wbindgen_externrefs.set(idx, obj);
|
|
124
|
+
return idx;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function getArrayU8FromWasm0(ptr, len) {
|
|
128
|
+
ptr = ptr >>> 0;
|
|
129
|
+
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getStringFromWasm0(ptr, len) {
|
|
133
|
+
return decodeText(ptr >>> 0, len);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let cachedUint8ArrayMemory0 = null;
|
|
137
|
+
function getUint8ArrayMemory0() {
|
|
138
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
139
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
140
|
+
}
|
|
141
|
+
return cachedUint8ArrayMemory0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleError(f, args) {
|
|
145
|
+
try {
|
|
146
|
+
return f.apply(this, args);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
const idx = addToExternrefTable0(e);
|
|
149
|
+
wasm.__wbindgen_exn_store(idx);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
154
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
155
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
156
|
+
WASM_VECTOR_LEN = arg.length;
|
|
157
|
+
return ptr;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
161
|
+
if (realloc === undefined) {
|
|
162
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
163
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
164
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
165
|
+
WASM_VECTOR_LEN = buf.length;
|
|
166
|
+
return ptr;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let len = arg.length;
|
|
170
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
171
|
+
|
|
172
|
+
const mem = getUint8ArrayMemory0();
|
|
173
|
+
|
|
174
|
+
let offset = 0;
|
|
175
|
+
|
|
176
|
+
for (; offset < len; offset++) {
|
|
177
|
+
const code = arg.charCodeAt(offset);
|
|
178
|
+
if (code > 0x7F) break;
|
|
179
|
+
mem[ptr + offset] = code;
|
|
180
|
+
}
|
|
181
|
+
if (offset !== len) {
|
|
182
|
+
if (offset !== 0) {
|
|
183
|
+
arg = arg.slice(offset);
|
|
184
|
+
}
|
|
185
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
186
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
187
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
188
|
+
|
|
189
|
+
offset += ret.written;
|
|
190
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
WASM_VECTOR_LEN = offset;
|
|
194
|
+
return ptr;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function takeFromExternrefTable0(idx) {
|
|
198
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
199
|
+
wasm.__externref_table_dealloc(idx);
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
204
|
+
cachedTextDecoder.decode();
|
|
205
|
+
function decodeText(ptr, len) {
|
|
206
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cachedTextEncoder = new TextEncoder();
|
|
210
|
+
|
|
211
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
212
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
213
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
214
|
+
view.set(buf);
|
|
215
|
+
return {
|
|
216
|
+
read: arg.length,
|
|
217
|
+
written: buf.length
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let WASM_VECTOR_LEN = 0;
|
|
223
|
+
|
|
224
|
+
const wasmPath = `${__dirname}/yaml_splice_wasm_bg.wasm`;
|
|
225
|
+
const wasmBytes = require('fs').readFileSync(wasmPath);
|
|
226
|
+
const wasmModule = new WebAssembly.Module(wasmBytes);
|
|
227
|
+
let wasmInstance = new WebAssembly.Instance(wasmModule, __wbg_get_imports());
|
|
228
|
+
let wasm = wasmInstance.exports;
|
|
229
|
+
wasm.__wbindgen_start();
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_editresult_free: (a: number, b: number) => void;
|
|
5
|
+
export const editYaml: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
6
|
+
export const editresult_bytes: (a: number) => [number, number];
|
|
7
|
+
export const editresult_changed: (a: number) => number;
|
|
8
|
+
export const __wbindgen_exn_store: (a: number) => void;
|
|
9
|
+
export const __externref_table_alloc: () => number;
|
|
10
|
+
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
11
|
+
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
12
|
+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
13
|
+
export const __externref_table_dealloc: (a: number) => void;
|
|
14
|
+
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
15
|
+
export const __wbindgen_start: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/yaml-splice",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.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
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"author": "sksizer",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/osdlc/osdlc.git",
|
|
10
|
+
"directory": "packages/ts/yaml-splice"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/osdlc/osdlc/tree/main/packages/ts/yaml-splice#readme",
|
|
13
|
+
"bugs": "https://github.com/osdlc/osdlc/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"yaml",
|
|
16
|
+
"frontmatter",
|
|
17
|
+
"edit",
|
|
18
|
+
"lossless",
|
|
19
|
+
"comments",
|
|
20
|
+
"byte-identical",
|
|
21
|
+
"wasm",
|
|
22
|
+
"webassembly"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/",
|
|
40
|
+
"README.md",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"LICENSE-MIT",
|
|
43
|
+
"LICENSE-APACHE",
|
|
44
|
+
"SECURITY.md",
|
|
45
|
+
"CONTRIBUTING.md"
|
|
46
|
+
],
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/bun": "^1.1.14",
|
|
49
|
+
"@types/node": "^26.1.0",
|
|
50
|
+
"oxlint": "^1.65.0",
|
|
51
|
+
"prettier": "3.9.6",
|
|
52
|
+
"typescript": "~6.0.3",
|
|
53
|
+
"@osdlc/prettier-config": "^0.2.3",
|
|
54
|
+
"@osdlc/tsconfig": "^0.3.3"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs');fs.rmSync('dist/wasm',{recursive:true,force:true});fs.cpSync('wasm','dist/wasm',{recursive:true})\"",
|
|
58
|
+
"build:wasm": "bash ../../rust/yaml-splice-wasm/build.sh",
|
|
59
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
60
|
+
"test": "bun test && npm run test:node",
|
|
61
|
+
"test:node": "pnpm run build && node --test tests/corpus-node.mjs",
|
|
62
|
+
"coverage": "bun test --coverage",
|
|
63
|
+
"smoke": "node tests/smoke.mjs",
|
|
64
|
+
"format": "prettier --write .",
|
|
65
|
+
"format:check": "prettier --check .",
|
|
66
|
+
"lint": "oxlint .",
|
|
67
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.build.json --noEmit"
|
|
68
|
+
}
|
|
6
69
|
}
|