@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.
@@ -1,5 +1,5 @@
1
1
  import { YamlEdit } from "../../YamlEdit.js";
2
- import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
2
+ import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
3
  import { Schema } from "effect";
4
4
 
5
5
  //#region src/internal/rules/document-start.ts
@@ -15,13 +15,17 @@ const TRIVIA = /* @__PURE__ */ new Set([
15
15
  "byte-order-mark",
16
16
  "directive"
17
17
  ]);
18
+ /** True when the stream carries `%YAML`/`%TAG` directives — which REQUIRE `---`. */
19
+ const hasDirectives = (ctx) => ctx.tokens.some((t) => t.kind === "directive");
20
+ /** The first non-trivia token: it decides whether the stream is headed by `---`. */
21
+ const headToken = (ctx) => ctx.tokens.find((t) => !TRIVIA.has(t.kind));
18
22
  /** The `---` marker at the head of the stream. */
19
23
  const documentStart = {
20
24
  id: "document-start",
21
25
  check: (ctx, options) => {
22
26
  const present = options?.present ?? true;
23
- const hasDirectives = ctx.tokens.some((t) => t.kind === "directive");
24
- const first = ctx.tokens.find((t) => !TRIVIA.has(t.kind));
27
+ const directives = hasDirectives(ctx);
28
+ const first = headToken(ctx);
25
29
  const headed = first?.kind === "document-start";
26
30
  if (present && !headed && ctx.text.trim() !== "") return [new YamlLintDiagnostic({
27
31
  rule: "document-start",
@@ -31,7 +35,7 @@ const documentStart = {
31
35
  length: 0,
32
36
  line: 0,
33
37
  character: 0,
34
- ...hasDirectives ? {} : { fix: YamlEdit.make({
38
+ ...directives ? {} : { fix: YamlEdit.make({
35
39
  offset: 0,
36
40
  length: 0,
37
41
  content: "---\n"
@@ -39,7 +43,7 @@ const documentStart = {
39
43
  })];
40
44
  if (!present && headed && first !== void 0) {
41
45
  const lineText = ctx.lines[first.line]?.text ?? "";
42
- const alone = !hasDirectives && lineText.trim() === "---";
46
+ const alone = !directives && lineText.trim() === "---";
43
47
  const terminator = ctx.text.startsWith("\r\n", first.offset + first.length) ? 2 : 1;
44
48
  return [new YamlLintDiagnostic({
45
49
  rule: "document-start",
@@ -57,6 +61,28 @@ const documentStart = {
57
61
  })];
58
62
  }
59
63
  return [];
64
+ },
65
+ infer: (ctx) => {
66
+ if (ctx.text.trim() === "") return [];
67
+ const first = headToken(ctx);
68
+ if (first === void 0) return [];
69
+ const headed = first.kind === "document-start";
70
+ if (!headed && hasDirectives(ctx)) return [];
71
+ return [StyleVote.make(headed ? {
72
+ dimension: "present",
73
+ value: true,
74
+ offset: first.offset,
75
+ length: first.length,
76
+ line: first.line,
77
+ character: first.character
78
+ } : {
79
+ dimension: "present",
80
+ value: false,
81
+ offset: 0,
82
+ length: 0,
83
+ line: 0,
84
+ character: 0
85
+ })];
60
86
  }
61
87
  };
62
88
 
@@ -1,5 +1,5 @@
1
1
  import { YamlEdit } from "../../YamlEdit.js";
2
- import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
2
+ import { StyleFloor, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
3
  import { insideScalarSpan, nonNegativeIntegerOption } from "./util.js";
4
4
  import { Schema } from "effect";
5
5
 
@@ -63,6 +63,26 @@ const emptyLines = {
63
63
  i = end;
64
64
  }
65
65
  return out;
66
+ },
67
+ infer: (ctx) => {
68
+ let longest = 0;
69
+ const lines = ctx.lines;
70
+ let i = 0;
71
+ while (i < lines.length) {
72
+ const line = lines[i];
73
+ if (line === void 0 || line.text !== "" || insideScalarSpan(ctx.tokens, line.offset)) {
74
+ i++;
75
+ continue;
76
+ }
77
+ let end = i;
78
+ while (end < lines.length && (lines[end]?.text ?? "x") === "") end++;
79
+ if (i !== 0 && end !== lines.length && end - i > longest) longest = end - i;
80
+ i = end;
81
+ }
82
+ return longest > 0 ? [StyleFloor.make({
83
+ dimension: "max",
84
+ value: longest
85
+ })] : [];
66
86
  }
67
87
  };
68
88
 
@@ -1,4 +1,4 @@
1
- import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
1
+ import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
2
2
  import { coveringToken, isScalarContinuationLine, nonNegativeIntegerOption } from "./util.js";
3
3
  import { Schema } from "effect";
4
4
 
@@ -30,6 +30,44 @@ const flowDepthAtLineStarts = (ctx) => {
30
30
  }
31
31
  return depths;
32
32
  };
33
+ /**
34
+ * The content lines block structure speaks about — shared by the check and
35
+ * the inference hook so the two read the same lines. Skips blanks, comment
36
+ * lines, directives, tab-indented lines (the parser's error), lines inside
37
+ * flow collections, scalar continuations and document markers.
38
+ */
39
+ const contentLines = (ctx) => {
40
+ const flowDepths = flowDepthAtLineStarts(ctx);
41
+ const content = [];
42
+ for (const line of ctx.lines) {
43
+ const indent = line.text.length - line.text.trimStart().length;
44
+ const firstChar = line.text[indent];
45
+ if (firstChar === void 0) continue;
46
+ if (firstChar === "#") continue;
47
+ if (firstChar === "%") continue;
48
+ if (line.text.slice(0, indent).includes(" ")) continue;
49
+ if ((flowDepths[line.number] ?? 0) > 0) continue;
50
+ if (isScalarContinuationLine(ctx.tokens, line.offset, line.offset + indent)) continue;
51
+ if (line.text.slice(indent).startsWith("---") || line.text.slice(indent).startsWith("...")) continue;
52
+ content.push({
53
+ line,
54
+ indent,
55
+ firstChar
56
+ });
57
+ }
58
+ return content;
59
+ };
60
+ /**
61
+ * True when consecutive content lines `prev` → `curr` are a mapping key
62
+ * followed by a sequence entry — the pair the `indentSequences` policy is
63
+ * about. Shared by the check and the inference hook.
64
+ */
65
+ const isKeyThenSeqEntry = (ctx, prev, curr, unit) => {
66
+ if (curr.firstChar !== "-") return false;
67
+ if (coveringToken(ctx.tokens, curr.line.offset + curr.indent)?.kind !== "block-seq-entry") return false;
68
+ if (!prev.line.text.replace(/ #.*$/, "").trimEnd().endsWith(":")) return false;
69
+ return curr.indent === prev.indent || curr.indent === prev.indent + (unit ?? curr.indent - prev.indent);
70
+ };
33
71
  /** Block-structure indent style. */
34
72
  const indentation = {
35
73
  id: "indentation",
@@ -38,24 +76,7 @@ const indentation = {
38
76
  const spacesOpt = opts.spaces ?? "consistent";
39
77
  const seqOpt = opts.indentSequences ?? "consistent";
40
78
  const out = [];
41
- const flowDepths = flowDepthAtLineStarts(ctx);
42
- const content = [];
43
- for (const line of ctx.lines) {
44
- const indent = line.text.length - line.text.trimStart().length;
45
- const firstChar = line.text[indent];
46
- if (firstChar === void 0) continue;
47
- if (firstChar === "#") continue;
48
- if (firstChar === "%") continue;
49
- if (line.text.slice(0, indent).includes(" ")) continue;
50
- if ((flowDepths[line.number] ?? 0) > 0) continue;
51
- if (isScalarContinuationLine(ctx.tokens, line.offset, line.offset + indent)) continue;
52
- if (line.text.slice(indent).startsWith("---") || line.text.slice(indent).startsWith("...")) continue;
53
- content.push({
54
- line,
55
- indent,
56
- firstChar
57
- });
58
- }
79
+ const content = contentLines(ctx);
59
80
  let unit = typeof spacesOpt === "number" ? spacesOpt : void 0;
60
81
  const stack = [0];
61
82
  for (const { line, indent } of content) {
@@ -79,10 +100,7 @@ const indentation = {
79
100
  for (let i = 1; i < content.length; i++) {
80
101
  const prev = content[i - 1];
81
102
  const curr = content[i];
82
- if (curr.firstChar !== "-") continue;
83
- if (coveringToken(ctx.tokens, curr.line.offset + curr.indent)?.kind !== "block-seq-entry") continue;
84
- if (!prev.line.text.replace(/ #.*$/, "").trimEnd().endsWith(":")) continue;
85
- if (curr.indent !== prev.indent && curr.indent !== prev.indent + (unit ?? curr.indent - prev.indent)) continue;
103
+ if (!isKeyThenSeqEntry(ctx, prev, curr, unit)) continue;
86
104
  const indented = curr.indent > prev.indent;
87
105
  if (seqIndented === void 0) seqIndented = indented;
88
106
  else if (indented !== seqIndented) out.push(new YamlLintDiagnostic({
@@ -96,6 +114,41 @@ const indentation = {
96
114
  }));
97
115
  }
98
116
  return out;
117
+ },
118
+ infer: (ctx) => {
119
+ const out = [];
120
+ const content = contentLines(ctx);
121
+ let unit;
122
+ const stack = [0];
123
+ for (const { line, indent } of content) {
124
+ const top = stack[stack.length - 1];
125
+ if (indent > top) {
126
+ out.push(StyleVote.make({
127
+ dimension: "spaces",
128
+ value: indent - top,
129
+ offset: line.offset,
130
+ length: indent,
131
+ line: line.number,
132
+ character: 0
133
+ }));
134
+ if (unit === void 0) unit = indent - top;
135
+ stack.push(indent);
136
+ } else if (indent < top) while (stack.length > 1 && stack[stack.length - 1] > indent) stack.pop();
137
+ }
138
+ for (let i = 1; i < content.length; i++) {
139
+ const prev = content[i - 1];
140
+ const curr = content[i];
141
+ if (!isKeyThenSeqEntry(ctx, prev, curr, unit)) continue;
142
+ out.push(StyleVote.make({
143
+ dimension: "indentSequences",
144
+ value: curr.indent > prev.indent,
145
+ offset: curr.line.offset,
146
+ length: curr.indent,
147
+ line: curr.line.number,
148
+ character: 0
149
+ }));
150
+ }
151
+ return out;
99
152
  }
100
153
  };
101
154
 
@@ -1,4 +1,4 @@
1
- import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
1
+ import { StyleFloor, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
2
2
  import { nonNegativeIntegerOption } from "./util.js";
3
3
  import { Schema } from "effect";
4
4
 
@@ -29,6 +29,14 @@ const lineLength = {
29
29
  character: max
30
30
  }));
31
31
  return out;
32
+ },
33
+ infer: (ctx) => {
34
+ let longest = 0;
35
+ for (const line of ctx.lines) if (line.text.length > longest) longest = line.text.length;
36
+ return longest > 0 ? [StyleFloor.make({
37
+ dimension: "max",
38
+ value: longest
39
+ })] : [];
32
40
  }
33
41
  };
34
42
 
@@ -1,5 +1,6 @@
1
1
  import { YamlEdit } from "../../YamlEdit.js";
2
- import { YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
2
+ import { requoteScalarText } from "../requote.js";
3
+ import { StyleVote, YamlLintDiagnostic, YamlLintSeverity } from "../../YamlLintRule.js";
3
4
  import { positionAt, walkScalars } from "./util.js";
4
5
  import { Schema } from "effect";
5
6
 
@@ -14,20 +15,19 @@ const quotedStringsOptions = Schema.Struct({
14
15
  quoteType: Schema.optionalKey(Schema.Literals(["single", "double"])),
15
16
  required: Schema.optionalKey(Schema.Boolean)
16
17
  });
17
- /** A value-preserving requote/wrap edit, or undefined when none is safe. */
18
+ /**
19
+ * A value-preserving requote/wrap edit, or undefined when none is safe.
20
+ * Delegates to the shared helper's CONSERVATIVE mode (#347) — the shipped
21
+ * fix behavior stays exactly as released; the escaping-capable mode belongs
22
+ * to the format path's opt-in `requoteScalars`, not the lint fix.
23
+ */
18
24
  const safeQuoteFix = (ctx, scalar, quote) => {
19
- if (scalar.tag !== void 0 || scalar.anchor !== void 0) return void 0;
20
- if (typeof scalar.value !== "string") return void 0;
21
- const raw = ctx.text.slice(scalar.offset, scalar.offset + scalar.length);
22
- if (raw.includes("\n")) return void 0;
23
- const inner = scalar.style === "plain" ? raw : raw.slice(1, -1);
24
- if (inner !== scalar.value) return void 0;
25
- if (inner.includes(quote)) return void 0;
26
- if (quote === "\"" && inner.includes("\\")) return void 0;
25
+ const content = requoteScalarText(ctx.text, scalar, quote, "conservative");
26
+ if (content === void 0) return void 0;
27
27
  return YamlEdit.make({
28
28
  offset: scalar.offset,
29
29
  length: scalar.length,
30
- content: `${quote}${inner}${quote}`
30
+ content
31
31
  });
32
32
  };
33
33
  /** Quote-style policy for string value scalars. */
@@ -74,6 +74,24 @@ const quotedStrings = {
74
74
  }
75
75
  });
76
76
  return out;
77
+ },
78
+ infer: (ctx) => {
79
+ const out = [];
80
+ walkScalars(ctx.document.contents, "root", (scalar, role) => {
81
+ if (role === "key") return;
82
+ if (typeof scalar.value !== "string") return;
83
+ if (scalar.style !== "single-quoted" && scalar.style !== "double-quoted") return;
84
+ const pos = positionAt(ctx.lines, scalar.offset);
85
+ out.push(StyleVote.make({
86
+ dimension: "quoteType",
87
+ value: scalar.style === "single-quoted" ? "single" : "double",
88
+ offset: scalar.offset,
89
+ length: scalar.length,
90
+ line: pos.line,
91
+ character: pos.character
92
+ }));
93
+ });
94
+ return out;
77
95
  }
78
96
  };
79
97
 
@@ -105,6 +105,23 @@ function wouldBeResolved(s) {
105
105
  if (FLOAT_RE.test(s)) return true;
106
106
  return false;
107
107
  }
108
+ const BOOL_11_RE = /^(?:y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$/;
109
+ const INT_11_RE = /^[-+]?(?:0b[0-1_]+|0o?[0-7_]+|[0-9][0-9_]*|0x[0-9a-fA-F_]+|[1-9][0-9_]*(?::[0-5]?[0-9])+)$/;
110
+ const FLOAT_11_RE = /^[-+]?(?:[0-9][0-9_]*(?:\.[0-9_]*(?:[eE][-+]?[0-9]+)?|[eE][-+]?[0-9]+)|\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*)$/;
111
+ const TIMESTAMP_11_RE = /^[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:(?:[Tt]|[ \t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\.[0-9]*)?(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::?[0-9][0-9])?))?)?$/;
112
+ /**
113
+ * Returns true if a string value would be resolved as a non-string type by a
114
+ * YAML 1.1 parser. The delta over {@link wouldBeResolved}: null spellings are
115
+ * identical in both editions (and already covered), so only the boolean,
116
+ * integer, float and timestamp families are tested here.
117
+ */
118
+ function wouldBeResolved11(s) {
119
+ if (BOOL_11_RE.test(s)) return true;
120
+ if (INT_11_RE.test(s)) return true;
121
+ if (FLOAT_11_RE.test(s)) return true;
122
+ if (TIMESTAMP_11_RE.test(s)) return true;
123
+ return false;
124
+ }
108
125
  /**
109
126
  * Returns true if a string requires quoting to be safely represented as a plain scalar.
110
127
  *
@@ -112,11 +129,16 @@ function wouldBeResolved(s) {
112
129
  * embedded newlines, leading indicator characters or whitespace, and inline
113
130
  * comment/mapping-value patterns (`: `, ` #`). This is the single gate that
114
131
  * decides whether a plain scalar is safe or must be wrapped in quotes.
132
+ *
133
+ * `quoteCompat` is strictly additive: `"yaml-1.1"` additionally quotes
134
+ * strings a YAML 1.1 parser would coerce ({@link wouldBeResolved11}) and can
135
+ * never un-quote anything the 1.2 rules require quoted.
115
136
  */
116
- function requiresQuoting(s, ignoreType = false) {
137
+ function requiresQuoting(s, ignoreType = false, quoteCompat) {
117
138
  if (s === "") return true;
118
139
  if (s.includes("\n")) return true;
119
140
  if (!ignoreType && wouldBeResolved(s)) return true;
141
+ if (!ignoreType && quoteCompat === "yaml-1.1" && wouldBeResolved11(s)) return true;
120
142
  const first = s[0];
121
143
  if (first === " " || first === " ") return true;
122
144
  if (first !== void 0 && INDICATOR_CHARS.has(first)) {
@@ -197,7 +219,7 @@ function renderSingleQuoted(s) {
197
219
  * Block literal and block folded styles are always accepted for single-line
198
220
  * strings even though the output is unusual.
199
221
  */
200
- function renderString(s, style, indent, ignoreType = false, canonical = false, explicitChomp, parentPosition, quoteStyle = "single", explicitIndent) {
222
+ function renderString(s, style, indent, ignoreType = false, canonical = false, explicitChomp, parentPosition, quoteStyle = "single", explicitIndent, quoteCompat) {
201
223
  const fidelity = !canonical;
202
224
  const fidelityIndent = fidelity ? explicitIndent : void 0;
203
225
  if (s.includes("\n")) {
@@ -230,7 +252,7 @@ function renderString(s, style, indent, ignoreType = false, canonical = false, e
230
252
  if (s === "" && (style === "block-literal" || style === "block-folded")) return renderDoubleQuoted(s, canonical);
231
253
  switch (style) {
232
254
  case "plain":
233
- if (requiresQuoting(s, ignoreType)) {
255
+ if (requiresQuoting(s, ignoreType, quoteCompat)) {
234
256
  if (s.includes(" ") || s.includes("\r")) return renderDoubleQuoted(s, canonical);
235
257
  for (let i = 0; i < s.length; i++) if (isControlChar(s.charCodeAt(i))) return renderDoubleQuoted(s, canonical);
236
258
  return quoteStyle === "double" ? renderDoubleQuoted(s, canonical) : renderSingleQuoted(s);
@@ -289,6 +311,7 @@ function createContext(options) {
289
311
  sortKeys: options?.sortKeys ?? false,
290
312
  indentSequences: options?.indentSequences ?? false,
291
313
  quoteStyle: options?.quoteStyle ?? "single",
314
+ quoteCompat: options?.quoteCompat,
292
315
  forceDefaultStyles: options?.forceDefaultStyles ?? false,
293
316
  seen: /* @__PURE__ */ new Set()
294
317
  };
@@ -308,7 +331,7 @@ function stringifyLines(value, ctx, depth, allowFold = true) {
308
331
  if (typeof value === "bigint") return [value.toString()];
309
332
  if (typeof value === "string") {
310
333
  const indentStr = " ".repeat(ctx.indent);
311
- const rendered = renderString(value, ctx.defaultScalarStyle, indentStr, false, ctx.forceDefaultStyles, void 0, void 0, ctx.quoteStyle);
334
+ const rendered = renderString(value, ctx.defaultScalarStyle, indentStr, false, ctx.forceDefaultStyles, void 0, void 0, ctx.quoteStyle, void 0, ctx.quoteCompat);
312
335
  return (allowFold && ctx.lineWidth > 0 ? foldRenderedScalar(rendered, indentStr, ctx.lineWidth) : rendered).split("\n");
313
336
  }
314
337
  if (Array.isArray(value)) {
@@ -373,7 +396,7 @@ function stringifyObjectLines(obj, ctx, depth) {
373
396
  const keys = Object.keys(obj);
374
397
  if (keys.length === 0) return ["{}"];
375
398
  if (ctx.sortKeys) keys.sort();
376
- const renderPlainKey = (k) => renderString(k, "plain", "", false, false, void 0, void 0, ctx.quoteStyle);
399
+ const renderPlainKey = (k) => renderString(k, "plain", "", false, false, void 0, void 0, ctx.quoteStyle, void 0, ctx.quoteCompat);
377
400
  if (ctx.defaultCollectionStyle === "flow") return [`{${keys.map((k) => {
378
401
  return `${renderPlainKey(k)}: ${stringifyLines(obj[k], ctx, depth + 1, false).join(" ")}`;
379
402
  }).join(", ")}}`];
@@ -584,7 +607,7 @@ function stringifyScalarNodeLines(node, ctx) {
584
607
  else lines = ["null"];
585
608
  } else if (typeof val === "boolean") lines = [val ? "true" : "false"];
586
609
  else if (typeof val === "number") lines = [node.raw !== void 0 ? node.raw : renderNumber(val)];
587
- else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition, ctx.quoteStyle, node.blockIndent).split("\n");
610
+ else if (typeof val === "string") lines = renderString(val, style, " ".repeat(ctx.indent), !!node.tag, ctx.forceDefaultStyles, node.chomp, ctx.parentPosition, ctx.quoteStyle, node.blockIndent, ctx.quoteCompat).split("\n");
588
611
  else lines = [renderDoubleQuoted(String(val))];
589
612
  if (node.tag) lines[0] = `${node.tag} ${lines[0]}`;
590
613
  if (node.anchor) lines[0] = `&${node.anchor} ${lines[0]}`;
@@ -1134,4 +1157,4 @@ function stringifyDocument(doc, options) {
1134
1157
  }
1135
1158
 
1136
1159
  //#endregion
1137
- export { StringifyDepthExceeded, StringifyFailure, stringifyDocument, stringifyValue, stripNodeComments };
1160
+ export { StringifyDepthExceeded, StringifyFailure, renderDoubleQuoted, renderSingleQuoted, stringifyDocument, stringifyValue, stripNodeComments };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/yaml",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "private": false,
5
5
  "description": "Zero-dependency YAML parsing, editing, formatting and linting as Effect schemas.",
6
6
  "keywords": [
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.12"
8
+ "packageVersion": "7.59.0"
9
9
  }
10
10
  ]
11
11
  }