@effected/yaml 0.10.0 → 0.12.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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)
6
6
  [![TypeScript 7.0](https://img.shields.io/badge/TypeScript-7.0-3178c6.svg)](https://www.typescriptlang.org/)
7
7
 
8
- Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix, and decode straight into a validated domain schema.
8
+ Zero-dependency YAML 1.2 parsing, editing, formatting and linting expressed as Effect schemas and pure functions. Parse a single document or a multi-document stream into plain values or an offset-preserving AST, resolve anchors and aliases, read and preserve per-node comments, strip comments, compute byte-minimal edits, format, modify by path, walk a document as a `Stream` of AST events or positioned tokens, lint it against a rule catalog with surgical autofix — inferring the lint config from files you already have — and decode straight into a validated domain schema.
9
9
 
10
10
  > **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
11
11
  > development against a single pinned Effect v4 prerelease. Packages graduate to
@@ -156,6 +156,23 @@ console.log(Result.isSuccess(fixed) ? fixed.success : "fatal parse error — not
156
156
 
157
157
  Positions are zero-based throughout, so the first finding above sits on the second line of the source. Fixes route through `YamlEdit.applyAll` rather than a reformat, so an autofix cannot move a comment it was not asked to touch, and a rule whose repair would need reflowing the document ships without a fix at all. `YamlLintConfig.default` and `YamlLintConfig.relaxed` are the presets; compose your own by spreading a preset's `rules` into `YamlLintConfig.make`. Config validation is rule-aware for the built-in catalog — a misspelled option key on a built-in rule or an attempt to switch off the always-on `parse-validity` rule fails schema validation instead of being quietly ignored — while an unknown (custom) rule id passes through with its options treated as opaque, left for the custom rule itself to validate. Custom rules are plain array concatenation — `YamlLint.run(text, [...YamlLint.builtins, myRule], config)` — and configuring `myRule` in that config works precisely because of the pass-through.
158
158
 
159
+ A repo with no lint config yet can infer one from what its files already do. `YamlLint.inferLenient` observes a text's style — quote type, indentation width, document markers and the rest — and returns the config the dominant style implies, plus the residual: the diagnostics that config still produces on the same text.
160
+
161
+ ```ts
162
+ import { YamlLint } from "@effected/yaml";
163
+
164
+ const workflow = ["---", "name: ci", "on:", " push:", " branches: ['main']", "jobs:", " build:", " runs-on: ubuntu-latest", ""].join("\n");
165
+
166
+ const { config, residual } = YamlLint.inferLenient(workflow, YamlLint.builtins);
167
+ console.log(config.rules["quoted-strings"], config.rules["document-start"], config.rules.indentation);
168
+ // { quoteType: 'single' } { present: true } { spaces: 2 }
169
+
170
+ for (const d of residual) console.log(`${d.line}:${d.character} ${d.rule} ${d.message}`);
171
+ // 2:0 truthy Truthy value "on" is not in the allowed spellings
172
+ ```
173
+
174
+ `YamlLint.inferStrict` is the same step in `Result` form: it fails with a structured `YamlStyleConflictError` — naming every conflicting dimension, all observed spellings, counts and positions — when the observed style disagrees with itself, instead of picking a winner. Across many files, compose the primitives instead of the conveniences: `YamlLint.observe` each file, merge the `StyleEvidence` values with `StyleEvidence.combine` (evidence is a monoid, so the order does not matter), then resolve once with `YamlLint.resolveStrict` or `YamlLint.resolveLenient`. Dimensions never observed fall back to the base config rather than failing. Policy rules with no detectable style — `truthy`, `key-duplicates` — stay default-driven, as do `line-length` and `empty-lines`, whose observations are floor-only: the longest line proves the limit is at least that long, not what it should be, so the evidence is carried but never resolved into config. A custom rule participates by implementing the optional `infer` hook on `YamlRule`.
175
+
159
176
  ## Migrating from Prettier
160
177
 
161
178
  `YamlFormat.formatToString(text, undefined, options)` replaces a `prettier --write` over YAML files. The option mapping:
@@ -163,34 +180,53 @@ Positions are zero-based throughout, so the first finding above sits on the seco
163
180
  | Prettier | `YamlFormattingOptions` |
164
181
  | --- | --- |
165
182
  | `tabWidth` | `indent` |
166
- | `singleQuote` | `quoteStyle: "single" \| "double"` |
183
+ | `singleQuote` | `quoteStyle: "single" \| "double"` + `requoteScalars: true` |
167
184
  | indented block sequences (always on) | `indentSequences: true` |
168
185
  | `printWidth` / `proseWrap` | no analog — see below |
169
186
  | comments (best-effort) | `preserveComments: true` (the default) |
170
187
 
171
188
  Two defaults differ from Prettier's output and are worth setting explicitly. The stringify default is `quoteStyle: "single"` for byte-compatibility with the kit's legacy form, so if you ran Prettier with its own default (`singleQuote: false`), set `quoteStyle: "double"`. Likewise `indentSequences` defaults to `false` — the flat legacy form with the `-` markers at the key's column — while Prettier always indented block sequences one level, so an ex-Prettier consumer almost certainly wants `indentSequences: true`.
172
189
 
173
- One expectation to reset when carrying over `singleQuote`: on the format path, `quoteStyle` governs quotes the stringifier *introduces* — it never re-quotes scalars that already carry quotes in your source. Formatting `a: 'x'` with `quoteStyle: "double"` leaves `'x'` untouched; Prettier would have rewritten it to `"x"`. Existing quote styles are source, and the formatter preserves source. If it looks like the option "does nothing" on an already-quoted corpus, this is why — the same class of format-path inertness as `lineWidth` below.
190
+ The `requoteScalars: true` half of the `singleQuote` row is what buys full parity. By default, `quoteStyle` on the format path governs only quotes the stringifier *introduces* — a scalar already quoted in your source keeps its own quote style, so formatting `a: 'x'` with `quoteStyle: "double"` leaves `'x'` untouched, where Prettier would have rewritten it to `"x"`. With `requoteScalars: true` (default `false`), `quoteStyle` applies to already-quoted source scalars too. A re-quote happens only when it provably preserves the parsed value: single→double applies proper double-quote escaping, and double→single is skipped when the value carries characters single quotes cannot express. Plain scalars stay plain, block scalars stay block, and tagged, anchored and multi-line scalars are never touched. One interaction to watch: `quoteStyle` omitted defaults to `"single"`, so `requoteScalars: true` on its own converts `"x"` to `'x'` — an ex-Prettier consumer who ran with `singleQuote: false` wants `quoteStyle: "double"` alongside it.
174
191
 
175
192
  `printWidth` and `proseWrap` have no analog here: `lineWidth` folding is a value-path feature only (`Yaml.stringify` / `Yaml.stringifyResult`), and the option is deliberately inert on the format path by contract — `YamlFormat` never reflows scalar content (the `lineWidth` TSDoc on `YamlStringifyOptions` pins this). Comments, which Prettier handled best-effort, are preserved exactly by default.
176
193
 
177
194
  Multi-document streams — a Kubernetes manifest, or the two-document `pnpm-lock.yaml` pnpm 11 writes under `configDependencies` — format whole: every document is re-emitted in order with its own `---`/`...` framing and comments, so no document is ever dropped. Document detection is CST-level, so a `---` inside a block scalar or quoted string is content and formats normally. The formatter leaves input byte-identical rather than corrupting it in exactly two cases: a fatal parse error in any document, and a stream carrying `%YAML`/`%TAG` directives (which the emitter cannot re-emit faithfully). Validate a whole stream with `Yaml.parseAllResult`, which fails typed when any document carries a fatal diagnostic; `YamlFormat.modify` stays single-document, since a path names no particular document of a stream.
178
195
 
196
+ ## Emitting for a YAML 1.1 consumer
197
+
198
+ YAML 1.2's Core Schema resolves fewer plain scalars to non-strings than YAML 1.1 does. A plain `on` or `yes` round-trips as a string under this package's own parser but becomes a boolean the moment a YAML 1.1 consumer — js-yaml, PyYAML, libyaml, the `yaml` npm package's YAML 1.1 schema, most CI systems' native YAML support — reads it back: the "Norway problem". `quoteCompat: "yaml-1.1"` closes that gap on the way out, quoting every plain scalar a 1.1 resolver would re-type: the extended booleans (`y`/`yes`/`no`/`on`/`off` and case variants), 1.1 timestamps, sexagesimal numbers (`1:30`), underscore-grouped numbers (`1_000`) and the octal/hex/binary integer forms (`0777`, `0x_FF`, `0b1010_0111`). It is strictly additive — nothing the 1.2 rules already quote loses a quote, and no string outside that list gains one — and composes with `quoteStyle` for the quote character and `lineWidth` for folding:
199
+
200
+ ```ts
201
+ import { Yaml, YamlStringifyOptions } from "@effected/yaml";
202
+ import { Effect } from "effect";
203
+
204
+ const frontmatter = { node: "on", released: "2024-01-15", port: "yes" };
205
+ const options = YamlStringifyOptions.make({ lineWidth: 0, quoteCompat: "yaml-1.1", quoteStyle: "double" });
206
+
207
+ Effect.runPromise(Yaml.stringify(frontmatter, options)).then(console.log);
208
+ // node: "on"
209
+ // released: "2024-01-15"
210
+ // port: "yes"
211
+ ```
212
+
213
+ That combination — `lineWidth: 0` (no folding), `quoteCompat: "yaml-1.1"`, `quoteStyle: "double"` — is the shape to reach for when stringifying frontmatter destined for YAML-1.1 tooling: Markdown frontmatter parsers and most static-site generators still resolve on the older rules. The option applies on both the value path (`Yaml.stringify`, `Yaml.stringifyResult`) and the node path (`YamlDocument#stringify`, `YamlFormat`), and scalars carrying an explicit tag are exempt, exactly like the 1.2 type-conflict check it extends.
214
+
179
215
  ## Features
180
216
 
181
217
  - `Yaml.parse` / `Yaml.parseAll` — error-recovery parsing of a single document or a `---`-separated stream into plain values, resolving anchors and aliases and aggregating every diagnostic into one `YamlParseError`.
182
- - `Yaml.stringify` — serialize a plain value back to YAML, failing typed with `YamlStringifyError` on circular references or on excessively deep nesting; a block-mapping key too long to render on one line spills into explicit-key form (`? key` / `: value`) so strict parsers still accept the output.
218
+ - `Yaml.stringify` — serialize a plain value back to YAML, failing typed with `YamlStringifyError` on circular references or on excessively deep nesting; a block-mapping key too long to render on one line spills into explicit-key form (`? key` / `: value`) so strict parsers still accept the output; `quoteCompat: "yaml-1.1"` additionally quotes plain scalars a YAML 1.1 resolver would re-type.
183
219
  - `Yaml.parseResult` / `Yaml.parseAllResult` / `Yaml.stringifyResult` — the pure synchronous counterparts, returning a `Result` instead of an `Effect` for config-time callers that cannot await; each runs the same engine call as its `Effect` variant, so the two forms cannot diverge, and a fatal diagnostic, a duplicate key, an alias bomb or a circular reference still fails typed rather than throwing. `parseAllResult` doubles as a whole-stream validity check: it fails when any document in the stream is invalid.
184
220
  - `Yaml.stripComments` — quote-aware comment removal that keeps line numbers stable, or every byte offset stable when given a replacement character.
185
221
  - `Yaml.equals` / `Yaml.equalsValue` — semantic equality that ignores comments, whitespace, formatting and mapping key order while keeping sequence order significant.
186
222
  - `Yaml.schema` / `Yaml.fromString` / `Yaml.YamlFromString` / `Yaml.allFromString` — string→domain schema factories that decode a single document or a whole stream into a validated Effect `Schema` value.
187
223
  - `YamlNode` — an offset-preserving AST (`YamlScalar`, `YamlMap`, `YamlSeq`, `YamlPair`, `YamlAlias`) for locating and reading nodes by position or path; every node class except `YamlPair` carries its own `commentBefore`, `comment` and `spaceBefore`, and a pair is just its `key` and `value`.
188
224
  - `YamlDocument` — the full parsed AST plus the recovered `errors` and `warnings` arrays, so a partially valid document is still inspectable, and the header and trailing comment blocks as `commentBefore` / `comment`, the same two names the node classes use.
189
- - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
225
+ - `YamlEdit` / `YamlFormat` — compute byte-minimal edit arrays for formatting and path-based modification, so a change preserves every comment and byte you did not touch; `preserveComments: false` opts out, and opt-in `requoteScalars` applies `quoteStyle` to scalars already quoted in the source, semantics-preserving or skipped. `format` handles multi-document streams whole, re-emitting every document and its framing; `modify` is single-document by contract (a path names no particular document of a stream) and fails typed with `MultiDocumentStream` rather than guessing.
190
226
  - `YamlVisitor` — walk a parsed document as a `Stream` of a tagged-enum event union, with `Stream.take` early termination on large inputs and a `placement` on every comment event.
191
227
  - `YamlToken` / `YamlTokens` — the positioned token stream the lint rules read: `tokenize` is a synchronous `Result` that always succeeds, surfacing lexical problems as `error`-kind tokens, and `stream` derives an Effect `Stream` from the same walk.
192
- - `YamlLint` — `run`, `fix` and the `builtins` catalog: 14 rules covering parse validity, line length, trailing whitespace, blank lines, end-of-file newline, document markers, duplicate keys, quoting, truthy spellings, comment and colon and hyphen spacing, and indentation.
193
- - `YamlLintConfig` / `YamlLintRule` / `YamlLintDiagnostic` — the `default` and `relaxed` presets, a config schema validated against the rule catalog, and the model a custom rule implements.
228
+ - `YamlLint` — `run`, `fix` and the `builtins` catalog: 14 rules covering parse validity, line length, trailing whitespace, blank lines, end-of-file newline, document markers, duplicate keys, quoting, truthy spellings, comment and colon and hyphen spacing, and indentation. Plus config inference for adopting the linter on an existing corpus: `observe` produces mergeable `StyleEvidence`, `resolveStrict` fails typed with `YamlStyleConflictError` on conflicting spellings, `resolveLenient` picks the dominant style, and `inferStrict` / `inferLenient` do it in one step per text.
229
+ - `YamlLintConfig` / `YamlLintRule` / `YamlLintDiagnostic` — the `default` and `relaxed` presets, a config schema validated against the rule catalog, and the model a custom rule implements, including the optional `infer` hook that lets a custom rule participate in config inference.
194
230
  - `YamlParseError` / `YamlStringifyError` / `YamlModificationError` — tagged errors carrying structured, positional `YamlDiagnostic` arrays rather than opaque messages.
195
231
 
196
232
  ## License
package/Yaml.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AliasExpansionBudgetExceeded, CollectionStyle, QuoteStyle, ScalarStyle, nodeToJsValue } from "./YamlNode.js";
1
+ import { AliasExpansionBudgetExceeded, CollectionStyle, QuoteCompat, QuoteStyle, ScalarStyle, nodeToJsValue } from "./YamlNode.js";
2
2
  import { composeAllDocuments, composeFirstDocument } from "./internal/composer/document.js";
3
3
  import { isFatalCode } from "./internal/diagnostics.js";
4
4
  import { StringifyDepthExceeded, StringifyFailure, stringifyValue } from "./internal/stringifier.js";
@@ -35,8 +35,9 @@ var YamlParseOptions = class extends Schema.Class("YamlParseOptions")({
35
35
  * Options controlling stringify behavior. All fields are omissible; absent
36
36
  * fields resolve to `indent` `2`, `lineWidth` `0`, `defaultScalarStyle`
37
37
  * `"plain"`, `defaultCollectionStyle` `"block"`, `sortKeys` `false`,
38
- * `indentSequences` `false`, `quoteStyle` `"single"`, `finalNewline` `true`
39
- * and `forceDefaultStyles` `false`.
38
+ * `indentSequences` `false`, `quoteStyle` `"single"`, `quoteCompat` absent
39
+ * (no dialect-compat quoting), `finalNewline` `true` and `forceDefaultStyles`
40
+ * `false`.
40
41
  *
41
42
  * `lineWidth` controls column-based scalar folding. The default `0` (and any
42
43
  * value `<= 0`) never wraps, emitting byte-identical output to the historic
@@ -114,6 +115,33 @@ var YamlStringifyOptions = class extends Schema.Class("YamlStringifyOptions")({
114
115
  * `"double-quoted"` is unaffected.
115
116
  */
116
117
  quoteStyle: Schema.optionalKey(QuoteStyle),
118
+ /**
119
+ * Additionally quote plain scalars a foreign resolution dialect would
120
+ * coerce to a non-string. Absent (the default) adds no quoting beyond the
121
+ * YAML 1.2 Core Schema rules — byte-identical to the released output.
122
+ *
123
+ * `"yaml-1.1"` quotes every plain scalar a YAML 1.1 parser (js-yaml,
124
+ * PyYAML, libyaml, and the `yaml` npm package's YAML 1.1 schema, whose
125
+ * lenient resolvers set the outer bound) would implicitly resolve to a
126
+ * non-string: the extended
127
+ * boolean spellings (`y`/`yes`/`on`/`off` and case variants — the "Norway
128
+ * problem"), timestamps (`2024-01-15`, `2001-12-15T02:59:43.1Z`, the
129
+ * space-separated 1.1 forms), sexagesimal numbers (`1:30`,
130
+ * `190:20:30.15`), underscore-separated numbers (`1_000`) and the
131
+ * base-2/8/16 integer forms (`0b1010_0111`, `0777`, `0x_FF`). Use it when
132
+ * emitted output is parsed downstream by a YAML 1.1 consumer and blanket
133
+ * `defaultScalarStyle` quoting is too noisy.
134
+ *
135
+ * Strictly additive: it never un-quotes anything the 1.2 rules require
136
+ * quoted, strings no 1.1 parser coerces stay plain, and the quote
137
+ * character stays governed by `quoteStyle`. Scalars carrying an explicit
138
+ * tag are exempt, exactly like the 1.2 type-conflict check. Applies
139
+ * wherever the plain fallback renders — values, mapping keys, flow items —
140
+ * on both the value path ({@link Yaml.stringify},
141
+ * {@link Yaml.stringifyResult}) and the node path (`YamlDocument#stringify`
142
+ * and the `YamlFormat` helpers).
143
+ */
144
+ quoteCompat: Schema.optionalKey(QuoteCompat),
117
145
  finalNewline: Schema.optionalKey(Schema.Boolean),
118
146
  forceDefaultStyles: Schema.optionalKey(Schema.Boolean)
119
147
  }) {};
@@ -167,6 +195,7 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
167
195
  sortKeys: options.sortKeys,
168
196
  indentSequences: options.indentSequences,
169
197
  quoteStyle: options.quoteStyle,
198
+ quoteCompat: options.quoteCompat,
170
199
  finalNewline: options.finalNewline,
171
200
  forceDefaultStyles: options.forceDefaultStyles
172
201
  };
package/YamlDocument.js CHANGED
@@ -163,6 +163,8 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
163
163
  defaultCollectionStyle: options.defaultCollectionStyle,
164
164
  sortKeys: options.sortKeys,
165
165
  indentSequences: options.indentSequences,
166
+ quoteStyle: options.quoteStyle,
167
+ quoteCompat: options.quoteCompat,
166
168
  finalNewline: options.finalNewline,
167
169
  forceDefaultStyles: options.forceDefaultStyles
168
170
  };
package/YamlFormat.js CHANGED
@@ -6,16 +6,33 @@ import { YamlDiagnostic } from "./YamlDiagnostic.js";
6
6
  import { YamlStringifyOptions } from "./Yaml.js";
7
7
  import { YamlEdit, YamlRange } from "./YamlEdit.js";
8
8
  import { computeEdits } from "./internal/diff.js";
9
+ import { requoteScalarText } from "./internal/requote.js";
9
10
  import { Effect, Schema } from "effect";
10
11
 
11
12
  //#region src/YamlFormat.ts
12
13
  /**
13
14
  * Options controlling formatting behavior: every {@link YamlStringifyOptions}
14
- * field (derived, not hand-duplicated — including `indentSequences` and
15
- * `quoteStyle`) plus
16
- * `preserveComments` (default `true`) and `range` (restrict edits to a
15
+ * field (derived, not hand-duplicated — including `indentSequences`,
16
+ * `quoteStyle` and `quoteCompat`) plus
17
+ * `preserveComments` (default `true`), `range` (restrict edits to a
17
18
  * region; see the module-level remarks on the `range` parameter vs. this
18
- * field).
19
+ * field) and `requoteScalars` (default `false`).
20
+ *
21
+ * `requoteScalars` makes `quoteStyle` apply to scalars **already quoted in
22
+ * the source** on the format path — by default formatting preserves an
23
+ * existing scalar's own quote style, and `quoteStyle` governs only quotes the
24
+ * stringifier introduces. When enabled, a re-quote happens only when it
25
+ * provably preserves the parsed value: single→double applies proper
26
+ * double-quote escaping, double→single is skipped whenever the value carries
27
+ * characters single quotes cannot express (newlines, tabs, control and other
28
+ * non-printable characters — single-quoted style can escape nothing but
29
+ * `'`). Plain scalars stay plain, block scalars stay block, and scalars
30
+ * carrying a tag or anchor — or spanning multiple source lines — are left
31
+ * untouched. The option exists only where a source quote exists to re-quote:
32
+ * it is read by {@link YamlFormat.format} / {@link YamlFormat.formatToString}
33
+ * and is deliberately absent from `Yaml.stringify` (which serializes plain
34
+ * values) and {@link YamlFormat.modify} (which takes a bare
35
+ * {@link YamlStringifyOptions}).
19
36
  *
20
37
  * Construct with the validated `YamlFormattingOptions.make({ ... })` static —
21
38
  * the kit convention (never `new`). Call sites that take a
@@ -37,7 +54,8 @@ import { Effect, Schema } from "effect";
37
54
  var YamlFormattingOptions = class extends Schema.Class("YamlFormattingOptions")({
38
55
  ...YamlStringifyOptions.fields,
39
56
  preserveComments: Schema.optionalKey(Schema.Boolean),
40
- range: Schema.optionalKey(YamlRange)
57
+ range: Schema.optionalKey(YamlRange),
58
+ requoteScalars: Schema.optionalKey(Schema.Boolean)
41
59
  }) {};
42
60
  /**
43
61
  * Raised when `YamlFormat.modify` cannot navigate the requested path against
@@ -87,6 +105,7 @@ const toStringifyInput = (options) => options === void 0 ? {} : {
87
105
  sortKeys: options.sortKeys,
88
106
  indentSequences: options.indentSequences,
89
107
  quoteStyle: options.quoteStyle,
108
+ quoteCompat: options.quoteCompat,
90
109
  finalNewline: options.finalNewline,
91
110
  forceDefaultStyles: options.forceDefaultStyles
92
111
  };
@@ -104,6 +123,49 @@ function definedFields(fields) {
104
123
  for (const key of Object.keys(fields)) if (fields[key] !== void 0) out[key] = fields[key];
105
124
  return out;
106
125
  }
126
+ /**
127
+ * Walk a composed AST and flip the `style` of every scalar the shared
128
+ * escaping-mode helper deems re-quotable to the target quote's style. The
129
+ * stringifier then emits those scalars through the very renderers the helper
130
+ * used to prove the replacement value-preserving, so flip-and-stringify and
131
+ * the helper's replacement text cannot disagree. Everything the helper skips
132
+ * — plain scalars, block scalars, tagged/anchored nodes, multi-line source
133
+ * spans, impossible double→single escapes — keeps its node untouched, and
134
+ * the surrounding diff keeps the edit surgical per scalar span.
135
+ */
136
+ function requoteNode(node, text, quote) {
137
+ if (node instanceof YamlScalar) {
138
+ if (requoteScalarText(text, node, quote, "escaping") === void 0) return node;
139
+ return YamlScalar.make({
140
+ value: node.value,
141
+ style: quote === "\"" ? "double-quoted" : "single-quoted",
142
+ ...definedFields({
143
+ commentBefore: node.commentBefore,
144
+ comment: node.comment,
145
+ spaceBefore: node.spaceBefore,
146
+ raw: node.raw,
147
+ sourceMultiline: node.sourceMultiline
148
+ }),
149
+ offset: node.offset,
150
+ length: node.length
151
+ });
152
+ }
153
+ if (node instanceof YamlMap) return rebuildMap(node, node.items.map((pair) => YamlPair.make({
154
+ key: requoteNode(pair.key, text, quote),
155
+ value: pair.value === null ? null : requoteNode(pair.value, text, quote)
156
+ })));
157
+ if (node instanceof YamlSeq) return rebuildSeq(node, node.items.map((item) => requoteNode(item, text, quote)));
158
+ return node;
159
+ }
160
+ /** Apply the opt-in re-quoting pass to a composed document's contents. */
161
+ function requoteDocument(doc, text, options) {
162
+ if (options?.requoteScalars !== true || doc.contents === null) return doc;
163
+ const quote = (options.quoteStyle ?? "single") === "double" ? "\"" : "'";
164
+ return {
165
+ ...doc,
166
+ contents: requoteNode(doc.contents, text, quote)
167
+ };
168
+ }
107
169
  /** Rebuild a composed document for re-emission, stripping comments when asked. */
108
170
  function toOutputDocument(doc, preserveComments) {
109
171
  return {
@@ -132,12 +194,12 @@ function toOutputDocument(doc, preserveComments) {
132
194
  */
133
195
  function formatDocument(text, options) {
134
196
  const { documents, streamErrors } = composeAllDocuments(text, {});
135
- if (documents.length > 1) return formatStream(documents, streamErrors, options);
197
+ if (documents.length > 1) return formatStream(text, documents, streamErrors, options);
136
198
  if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
137
199
  const doc = documents[0] ?? EMPTY_DOCUMENT;
138
200
  if (doc.errors.some((e) => isFatalCode(e.code))) return void 0;
139
201
  if (doc.directives.length > 0) return void 0;
140
- return stringifyDocument(toOutputDocument(doc, options?.preserveComments ?? true), toStringifyInput(options));
202
+ return stringifyDocument(toOutputDocument(requoteDocument(doc, text, options), options?.preserveComments ?? true), toStringifyInput(options));
141
203
  }
142
204
  /**
143
205
  * Format a multi-document stream: every document is re-emitted in order with
@@ -158,7 +220,7 @@ function formatDocument(text, options) {
158
220
  * line while keeping the dependent shorthand tags produced unparseable
159
221
  * output (probe-verified downstream against an independent oracle).
160
222
  */
161
- function formatStream(documents, streamErrors, options) {
223
+ function formatStream(text, documents, streamErrors, options) {
162
224
  if (streamErrors.some((e) => isFatalCode(e.code))) return void 0;
163
225
  if (documents.some((d) => d.errors.some((e) => isFatalCode(e.code)))) return void 0;
164
226
  if (documents.some((d) => d.directives.length > 0)) return void 0;
@@ -172,7 +234,7 @@ function formatStream(documents, streamErrors, options) {
172
234
  ...base,
173
235
  finalNewline: true
174
236
  } : base;
175
- parts.push(stringifyDocument(toOutputDocument(doc, preserveComments), docOptions));
237
+ parts.push(stringifyDocument(toOutputDocument(requoteDocument(doc, text, options), preserveComments), docOptions));
176
238
  }
177
239
  return parts.join("");
178
240
  }
@@ -351,6 +413,12 @@ var YamlFormat = class YamlFormat {
351
413
  * `{ offset, length }` object as well as a {@link YamlRange} instance, so
352
414
  * callers do not need `YamlRange.make(...)` for the common case.
353
415
  *
416
+ * Formatting preserves an existing scalar's own quote style by default —
417
+ * `quoteStyle` governs only quotes the stringifier introduces. The opt-in
418
+ * `options.requoteScalars` makes `quoteStyle` apply to already-quoted
419
+ * source scalars too, re-quoting only where the parsed value is provably
420
+ * preserved; see {@link YamlFormattingOptions} for the exact skip rules.
421
+ *
354
422
  * A plain `<<` mapping key is preserved unquoted, keeping its merge-key
355
423
  * meaning (`tag:yaml.org,2002:merge`) — quoting it to `'<<'` would produce
356
424
  * an ordinary string key that merges nothing, changing what the document
package/YamlLint.js CHANGED
Binary file
package/YamlLintRule.js CHANGED
@@ -31,6 +31,55 @@ var YamlLintDiagnostic = class extends Schema.Class("YamlLintDiagnostic")({
31
31
  character: Schema.Number,
32
32
  fix: Schema.optionalKey(YamlEdit)
33
33
  }) {};
34
+ /**
35
+ * One categorical style observation (#345): a single occurrence of a style
36
+ * choice in the source, voting a `value` for an inference `dimension`.
37
+ *
38
+ * The `dimension` IS the rule's option key and the `value` IS that option's
39
+ * value (`"double"` for `quoteType`, `2` for `spaces`, `false` for
40
+ * `present`), so the inference resolvers can turn unanimous or dominant
41
+ * votes into a config entry with no per-rule knowledge — which is what lets
42
+ * custom rules participate in inference for free. The position points at the
43
+ * occurrence that voted, so a strict-resolution conflict can name where each
44
+ * spelling was seen.
45
+ *
46
+ * The `_tag` literal is the RUNTIME discriminator between the two
47
+ * observation kinds: class instances are structurally assignable, so a
48
+ * custom rule may yield a plain object shaped like a vote, and the evidence
49
+ * builder must still sort it into the right tally without `instanceof`.
50
+ * `.make` defaults it — construction sites never pass `_tag`.
51
+ *
52
+ * @public
53
+ */
54
+ var StyleVote = class extends Schema.TaggedClass()("StyleVote", {
55
+ dimension: Schema.String,
56
+ value: Schema.Union([
57
+ Schema.String,
58
+ Schema.Number,
59
+ Schema.Boolean
60
+ ]),
61
+ offset: Schema.Number,
62
+ length: Schema.Number,
63
+ line: Schema.Number,
64
+ character: Schema.Number
65
+ }) {};
66
+ /**
67
+ * One measured style floor (#345): a value the source PROVES is at least
68
+ * `value`, without proving what the configured limit should be — the longest
69
+ * observed line proves `line-length.max` is at least that long, not what it
70
+ * is. Floors are carried in the evidence for callers that want them and are
71
+ * never resolved into config options; fabricating a max from the largest
72
+ * value one happened to see would be lying with a straight face.
73
+ *
74
+ * The `_tag` literal discriminates a floor from a {@link StyleVote} at
75
+ * runtime (see there); `.make` defaults it.
76
+ *
77
+ * @public
78
+ */
79
+ var StyleFloor = class extends Schema.TaggedClass()("StyleFloor", {
80
+ dimension: Schema.String,
81
+ value: Schema.Number
82
+ }) {};
34
83
 
35
84
  //#endregion
36
- export { YamlLintDiagnostic, YamlLintSeverity };
85
+ export { StyleFloor, StyleVote, YamlLintDiagnostic, YamlLintSeverity };
package/YamlNode.js CHANGED
@@ -30,6 +30,21 @@ const CollectionStyle = Schema.Literals(["block", "flow"]);
30
30
  */
31
31
  const QuoteStyle = Schema.Literals(["single", "double"]);
32
32
  /**
33
+ * Foreign resolution dialects the stringifier's plain-scalar fallback can
34
+ * defend against: setting the `quoteCompat` field of `YamlStringifyOptions`
35
+ * to `"yaml-1.1"` additionally quotes every plain scalar a YAML 1.1 parser
36
+ * (js-yaml, PyYAML, libyaml, and the `yaml` npm package's YAML 1.1 schema,
37
+ * whose lenient resolvers set the outer bound) would implicitly resolve to a
38
+ * non-string —
39
+ * `yes`/`no`/`on`/`off` booleans, ISO 8601 and space-separated timestamps,
40
+ * sexagesimal `1:30`, underscored `1_000` and base-2/8/16 numbers. Like
41
+ * `QuoteStyle` it is a stringify-option vocabulary, never a property of a
42
+ * composed node.
43
+ *
44
+ * @public
45
+ */
46
+ const QuoteCompat = Schema.Literals(["yaml-1.1"]);
47
+ /**
33
48
  * Block-scalar chomping indicators (`-` strip, default clip, `+` keep).
34
49
  * Referenced by the {@link YamlScalar} `chomp` field schema.
35
50
  *
@@ -442,4 +457,4 @@ function nodeToValue(node, anchors, budget, counting = false) {
442
457
  }
443
458
 
444
459
  //#endregion
445
- export { AliasExpansionBudgetExceeded, CollectionStyle, QuoteStyle, ScalarChomp, ScalarStyle, YamlAlias, YamlMap, YamlNode, YamlPair, YamlScalar, YamlSeq, aliasExpansionLimit, nodeToJsValue };
460
+ export { AliasExpansionBudgetExceeded, CollectionStyle, QuoteCompat, QuoteStyle, ScalarChomp, ScalarStyle, YamlAlias, YamlMap, YamlNode, YamlPair, YamlScalar, YamlSeq, aliasExpansionLimit, nodeToJsValue };