yaml 2.8.1 → 2.8.3

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.
@@ -59,7 +59,7 @@ function composeCollection(CN, ctx, token, props, onError) {
59
59
  let tag = ctx.schema.tags.find(t => t.tag === tagName && t.collection === expType);
60
60
  if (!tag) {
61
61
  const kt = ctx.schema.knownTags[tagName];
62
- if (kt && kt.collection === expType) {
62
+ if (kt?.collection === expType) {
63
63
  ctx.schema.tags.push(Object.assign({}, kt, { default: false }));
64
64
  tag = kt;
65
65
  }
@@ -28,19 +28,26 @@ function composeNode(ctx, token, props, onError) {
28
28
  case 'block-map':
29
29
  case 'block-seq':
30
30
  case 'flow-collection':
31
- node = composeCollection(CN, ctx, token, props, onError);
32
- if (anchor)
33
- node.anchor = anchor.source.substring(1);
31
+ try {
32
+ node = composeCollection(CN, ctx, token, props, onError);
33
+ if (anchor)
34
+ node.anchor = anchor.source.substring(1);
35
+ }
36
+ catch (error) {
37
+ // Almost certainly here due to a stack overflow
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ onError(token, 'RESOURCE_EXHAUSTION', message);
40
+ }
34
41
  break;
35
42
  default: {
36
43
  const message = token.type === 'error'
37
44
  ? token.message
38
45
  : `Unsupported token (type: ${token.type})`;
39
46
  onError(token, 'UNEXPECTED_TOKEN', message);
40
- node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
41
47
  isSrcToken = false;
42
48
  }
43
49
  }
50
+ node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
44
51
  if (anchor && node.anchor === '')
45
52
  onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
46
53
  if (atKey &&
@@ -22,7 +22,7 @@ function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, ta
22
22
  });
23
23
  if (!props.found) {
24
24
  if (props.anchor || props.tag || value) {
25
- if (value && value.type === 'block-seq')
25
+ if (value?.type === 'block-seq')
26
26
  onError(props.end, 'BAD_INDENT', 'All sequence items must start at the same column');
27
27
  else
28
28
  onError(offset, 'MISSING_CHAR', 'Sequence item without - indicator');
@@ -133,7 +133,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
133
133
  }
134
134
  }
135
135
  else if (value) {
136
- if ('source' in value && value.source && value.source[0] === ':')
136
+ if ('source' in value && value.source?.[0] === ':')
137
137
  onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`);
138
138
  else
139
139
  onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`);
@@ -177,7 +177,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
177
177
  const expectedEnd = isMap ? '}' : ']';
178
178
  const [ce, ...ee] = fc.end;
179
179
  let cePos = offset;
180
- if (ce && ce.source === expectedEnd)
180
+ if (ce?.source === expectedEnd)
181
181
  cePos = ce.offset + ce.source.length;
182
182
  else {
183
183
  const name = fcName[0].toUpperCase() + fcName.substring(1);
@@ -46,7 +46,7 @@ const prettifyError = (src, lc) => (error) => {
46
46
  if (/[^ ]/.test(lineStr)) {
47
47
  let count = 1;
48
48
  const end = error.linePos[1];
49
- if (end && end.line === line && end.col > col) {
49
+ if (end?.line === line && end.col > col) {
50
50
  count = Math.max(1, Math.min(end.col - col, 80 - ci));
51
51
  }
52
52
  const pointer = ' '.repeat(ci) + '^'.repeat(count);
@@ -59,7 +59,7 @@ class Alias extends NodeBase {
59
59
  data = anchors.get(source);
60
60
  }
61
61
  /* istanbul ignore if */
62
- if (!data || data.res === undefined) {
62
+ if (data?.res === undefined) {
63
63
  const msg = 'This should not happen: Alias anchor was not resolved?';
64
64
  throw new ReferenceError(msg);
65
65
  }
@@ -226,7 +226,7 @@ class Parser {
226
226
  }
227
227
  *step() {
228
228
  const top = this.peek(1);
229
- if (this.type === 'doc-end' && (!top || top.type !== 'doc-end')) {
229
+ if (this.type === 'doc-end' && top?.type !== 'doc-end') {
230
230
  while (this.stack.length > 0)
231
231
  yield* this.pop();
232
232
  this.stack.push({
@@ -758,7 +758,7 @@ class Parser {
758
758
  do {
759
759
  yield* this.pop();
760
760
  top = this.peek(1);
761
- } while (top && top.type === 'flow-collection');
761
+ } while (top?.type === 'flow-collection');
762
762
  }
763
763
  else if (fc.end.length === 0) {
764
764
  switch (this.type) {
@@ -20,6 +20,7 @@ function createStringifyContext(doc, options) {
20
20
  nullStr: 'null',
21
21
  simpleKeys: false,
22
22
  singleQuote: null,
23
+ trailingComma: false,
23
24
  trueStr: 'true',
24
25
  verifyAliasOrder: true
25
26
  }, doc.schema.toStringOptions, options);
@@ -102,12 +102,22 @@ function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
102
102
  if (comment)
103
103
  reqNewline = true;
104
104
  let str = stringify(item, itemCtx, () => (comment = null));
105
- if (i < items.length - 1)
105
+ reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
106
+ if (i < items.length - 1) {
106
107
  str += ',';
108
+ }
109
+ else if (ctx.options.trailingComma) {
110
+ if (ctx.options.lineWidth > 0) {
111
+ reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
112
+ (str.length + 2) >
113
+ ctx.options.lineWidth);
114
+ }
115
+ if (reqNewline) {
116
+ str += ',';
117
+ }
118
+ }
107
119
  if (comment)
108
120
  str += lineComment(str, itemIndent, commentString(comment));
109
- if (!reqNewline && (lines.length > linesAtValue || str.includes('\n')))
110
- reqNewline = true;
111
121
  lines.push(str);
112
122
  linesAtValue = lines.length;
113
123
  }
@@ -4,7 +4,7 @@ function stringifyNumber({ format, minFractionDigits, tag, value }) {
4
4
  const num = typeof value === 'number' ? value : Number(value);
5
5
  if (!isFinite(num))
6
6
  return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf';
7
- let n = JSON.stringify(value);
7
+ let n = Object.is(value, -0) ? '-0' : JSON.stringify(value);
8
8
  if (!format &&
9
9
  minFractionDigits &&
10
10
  (!tag || tag === 'tag:yaml.org,2002:float') &&
@@ -101,7 +101,7 @@ function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
101
101
  ws += `\n${indentComment(cs, ctx.indent)}`;
102
102
  }
103
103
  if (valueStr === '' && !ctx.inFlow) {
104
- if (ws === '\n')
104
+ if (ws === '\n' && valueComment)
105
105
  ws = '\n\n';
106
106
  }
107
107
  else {
@@ -61,7 +61,7 @@ function composeCollection(CN, ctx, token, props, onError) {
61
61
  let tag = ctx.schema.tags.find(t => t.tag === tagName && t.collection === expType);
62
62
  if (!tag) {
63
63
  const kt = ctx.schema.knownTags[tagName];
64
- if (kt && kt.collection === expType) {
64
+ if (kt?.collection === expType) {
65
65
  ctx.schema.tags.push(Object.assign({}, kt, { default: false }));
66
66
  tag = kt;
67
67
  }
@@ -30,19 +30,26 @@ function composeNode(ctx, token, props, onError) {
30
30
  case 'block-map':
31
31
  case 'block-seq':
32
32
  case 'flow-collection':
33
- node = composeCollection.composeCollection(CN, ctx, token, props, onError);
34
- if (anchor)
35
- node.anchor = anchor.source.substring(1);
33
+ try {
34
+ node = composeCollection.composeCollection(CN, ctx, token, props, onError);
35
+ if (anchor)
36
+ node.anchor = anchor.source.substring(1);
37
+ }
38
+ catch (error) {
39
+ // Almost certainly here due to a stack overflow
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ onError(token, 'RESOURCE_EXHAUSTION', message);
42
+ }
36
43
  break;
37
44
  default: {
38
45
  const message = token.type === 'error'
39
46
  ? token.message
40
47
  : `Unsupported token (type: ${token.type})`;
41
48
  onError(token, 'UNEXPECTED_TOKEN', message);
42
- node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError);
43
49
  isSrcToken = false;
44
50
  }
45
51
  }
52
+ node ?? (node = composeEmptyNode(ctx, token.offset, undefined, null, props, onError));
46
53
  if (anchor && node.anchor === '')
47
54
  onError(anchor, 'BAD_ALIAS', 'Anchor cannot be an empty string');
48
55
  if (atKey &&
@@ -24,7 +24,7 @@ function resolveBlockSeq({ composeNode, composeEmptyNode }, ctx, bs, onError, ta
24
24
  });
25
25
  if (!props.found) {
26
26
  if (props.anchor || props.tag || value) {
27
- if (value && value.type === 'block-seq')
27
+ if (value?.type === 'block-seq')
28
28
  onError(props.end, 'BAD_INDENT', 'All sequence items must start at the same column');
29
29
  else
30
30
  onError(offset, 'MISSING_CHAR', 'Sequence item without - indicator');
@@ -135,7 +135,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
135
135
  }
136
136
  }
137
137
  else if (value) {
138
- if ('source' in value && value.source && value.source[0] === ':')
138
+ if ('source' in value && value.source?.[0] === ':')
139
139
  onError(value, 'MISSING_CHAR', `Missing space after : in ${fcName}`);
140
140
  else
141
141
  onError(valueProps.start, 'MISSING_CHAR', `Missing , or : between ${fcName} items`);
@@ -179,7 +179,7 @@ function resolveFlowCollection({ composeNode, composeEmptyNode }, ctx, fc, onErr
179
179
  const expectedEnd = isMap ? '}' : ']';
180
180
  const [ce, ...ee] = fc.end;
181
181
  let cePos = offset;
182
- if (ce && ce.source === expectedEnd)
182
+ if (ce?.source === expectedEnd)
183
183
  cePos = ce.offset + ce.source.length;
184
184
  else {
185
185
  const name = fcName[0].toUpperCase() + fcName.substring(1);
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { LineCounter } from './parse/line-counter';
2
- export type ErrorCode = 'ALIAS_PROPS' | 'BAD_ALIAS' | 'BAD_DIRECTIVE' | 'BAD_DQ_ESCAPE' | 'BAD_INDENT' | 'BAD_PROP_ORDER' | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' | 'DUPLICATE_KEY' | 'IMPOSSIBLE' | 'KEY_OVER_1024_CHARS' | 'MISSING_CHAR' | 'MULTILINE_IMPLICIT_KEY' | 'MULTIPLE_ANCHORS' | 'MULTIPLE_DOCS' | 'MULTIPLE_TAGS' | 'NON_STRING_KEY' | 'TAB_AS_INDENT' | 'TAG_RESOLVE_FAILED' | 'UNEXPECTED_TOKEN' | 'BAD_COLLECTION_TYPE';
2
+ export type ErrorCode = 'ALIAS_PROPS' | 'BAD_ALIAS' | 'BAD_DIRECTIVE' | 'BAD_DQ_ESCAPE' | 'BAD_INDENT' | 'BAD_PROP_ORDER' | 'BAD_SCALAR_START' | 'BLOCK_AS_IMPLICIT_KEY' | 'BLOCK_IN_FLOW' | 'DUPLICATE_KEY' | 'IMPOSSIBLE' | 'KEY_OVER_1024_CHARS' | 'MISSING_CHAR' | 'MULTILINE_IMPLICIT_KEY' | 'MULTIPLE_ANCHORS' | 'MULTIPLE_DOCS' | 'MULTIPLE_TAGS' | 'NON_STRING_KEY' | 'RESOURCE_EXHAUSTION' | 'TAB_AS_INDENT' | 'TAG_RESOLVE_FAILED' | 'UNEXPECTED_TOKEN' | 'BAD_COLLECTION_TYPE';
3
3
  export type LinePos = {
4
4
  line: number;
5
5
  col: number;
package/dist/errors.js CHANGED
@@ -48,7 +48,7 @@ const prettifyError = (src, lc) => (error) => {
48
48
  if (/[^ ]/.test(lineStr)) {
49
49
  let count = 1;
50
50
  const end = error.linePos[1];
51
- if (end && end.line === line && end.col > col) {
51
+ if (end?.line === line && end.col > col) {
52
52
  count = Math.max(1, Math.min(end.col - col, 80 - ci));
53
53
  }
54
54
  const pointer = ' '.repeat(ci) + '^'.repeat(count);
@@ -61,7 +61,7 @@ class Alias extends Node.NodeBase {
61
61
  data = anchors.get(source);
62
62
  }
63
63
  /* istanbul ignore if */
64
- if (!data || data.res === undefined) {
64
+ if (data?.res === undefined) {
65
65
  const msg = 'This should not happen: Alias anchor was not resolved?';
66
66
  throw new ReferenceError(msg);
67
67
  }
package/dist/options.d.ts CHANGED
@@ -326,6 +326,12 @@ export type ToStringOptions = {
326
326
  * Default: `null`
327
327
  */
328
328
  singleQuote?: boolean | null;
329
+ /**
330
+ * Add a trailing comma after the last entry in a flow map or flow sequence that's split across multiple lines.
331
+ *
332
+ * Default: `'false'`
333
+ */
334
+ trailingComma?: boolean;
329
335
  /**
330
336
  * String representation for `true`.
331
337
  * With the core schema, use `'true'`, `'True'`, or `'TRUE'`.
@@ -231,7 +231,7 @@ class Parser {
231
231
  }
232
232
  *step() {
233
233
  const top = this.peek(1);
234
- if (this.type === 'doc-end' && (!top || top.type !== 'doc-end')) {
234
+ if (this.type === 'doc-end' && top?.type !== 'doc-end') {
235
235
  while (this.stack.length > 0)
236
236
  yield* this.pop();
237
237
  this.stack.push({
@@ -763,7 +763,7 @@ class Parser {
763
763
  do {
764
764
  yield* this.pop();
765
765
  top = this.peek(1);
766
- } while (top && top.type === 'flow-collection');
766
+ } while (top?.type === 'flow-collection');
767
767
  }
768
768
  else if (fc.end.length === 0) {
769
769
  switch (this.type) {
@@ -22,6 +22,7 @@ function createStringifyContext(doc, options) {
22
22
  nullStr: 'null',
23
23
  simpleKeys: false,
24
24
  singleQuote: null,
25
+ trailingComma: false,
25
26
  trueStr: 'true',
26
27
  verifyAliasOrder: true
27
28
  }, doc.schema.toStringOptions, options);
@@ -104,12 +104,22 @@ function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) {
104
104
  if (comment)
105
105
  reqNewline = true;
106
106
  let str = stringify.stringify(item, itemCtx, () => (comment = null));
107
- if (i < items.length - 1)
107
+ reqNewline || (reqNewline = lines.length > linesAtValue || str.includes('\n'));
108
+ if (i < items.length - 1) {
108
109
  str += ',';
110
+ }
111
+ else if (ctx.options.trailingComma) {
112
+ if (ctx.options.lineWidth > 0) {
113
+ reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) +
114
+ (str.length + 2) >
115
+ ctx.options.lineWidth);
116
+ }
117
+ if (reqNewline) {
118
+ str += ',';
119
+ }
120
+ }
109
121
  if (comment)
110
122
  str += stringifyComment.lineComment(str, itemIndent, commentString(comment));
111
- if (!reqNewline && (lines.length > linesAtValue || str.includes('\n')))
112
- reqNewline = true;
113
123
  lines.push(str);
114
124
  linesAtValue = lines.length;
115
125
  }
@@ -6,7 +6,7 @@ function stringifyNumber({ format, minFractionDigits, tag, value }) {
6
6
  const num = typeof value === 'number' ? value : Number(value);
7
7
  if (!isFinite(num))
8
8
  return isNaN(num) ? '.nan' : num < 0 ? '-.inf' : '.inf';
9
- let n = JSON.stringify(value);
9
+ let n = Object.is(value, -0) ? '-0' : JSON.stringify(value);
10
10
  if (!format &&
11
11
  minFractionDigits &&
12
12
  (!tag || tag === 'tag:yaml.org,2002:float') &&
@@ -103,7 +103,7 @@ function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
103
103
  ws += `\n${stringifyComment.indentComment(cs, ctx.indent)}`;
104
104
  }
105
105
  if (valueStr === '' && !ctx.inFlow) {
106
- if (ws === '\n')
106
+ if (ws === '\n' && valueComment)
107
107
  ws = '\n\n';
108
108
  }
109
109
  else {
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "yaml",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "license": "ISC",
5
5
  "author": "Eemeli Aro <eemeli@gmail.com>",
6
+ "funding": "https://github.com/sponsors/eemeli",
6
7
  "repository": "github:eemeli/yaml",
7
8
  "description": "JavaScript parser and stringifier for YAML",
8
9
  "keywords": [
@@ -73,14 +74,13 @@
73
74
  "@babel/preset-env": "^7.12.11",
74
75
  "@eslint/js": "^9.9.1",
75
76
  "@rollup/plugin-babel": "^6.0.3",
76
- "@rollup/plugin-replace": "^5.0.2",
77
+ "@rollup/plugin-replace": "^6.0.3",
77
78
  "@rollup/plugin-typescript": "^12.1.1",
78
79
  "@types/jest": "^29.2.4",
79
80
  "@types/node": "^20.11.20",
80
81
  "babel-jest": "^29.0.1",
81
- "cross-env": "^7.0.3",
82
82
  "eslint": "^9.9.1",
83
- "eslint-config-prettier": "^9.0.0",
83
+ "eslint-config-prettier": "^10.1.8",
84
84
  "fast-check": "^2.12.0",
85
85
  "jest": "^29.0.1",
86
86
  "jest-resolve": "^29.7.0",