@speclynx/apidom-parser-adapter-yaml-1-2 6.0.6 → 6.1.1

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,15 +1,16 @@
1
- import { tail, compose, pathOr, map, concat, transduce, pipe, trim, split, join, curry } from 'ramda';
2
- import { isInteger, trimStart, trimEnd, isUndefined, trimCharsStart, isEmptyString, repeatStr, concatRight } from 'ramda-adjunct';
3
- import { unraw } from 'unraw';
1
+ import { tail, pathOr, pipe, trim, curry, replace } from 'ramda';
2
+ import { isInteger, isEmptyString, repeatStr } from 'ramda-adjunct';
4
3
 
5
4
  /**
6
5
  * Helpers.
7
6
  */
8
7
 
9
- const blockStyleRegExp = /^(?<style>[|>])(?<chomping>[+-]?)(?<indentation>[0-9]*)\s/;
8
+ // indentation and chomping indicators may appear in either order (YAML 1.2 §8.1.1)
9
+ const blockStyleRegExp = /^(?<style>[|>])(?<indicators>[+-]?[0-9]?|[0-9][+-])(?:\s|$)/;
10
10
  const getIndentationIndicator = content => {
11
11
  const matches = content.match(blockStyleRegExp);
12
- const indicator = pathOr('', ['groups', 'indentation'], matches);
12
+ const indicators = pathOr('', ['groups', 'indicators'], matches);
13
+ const indicator = indicators.replace(/[+-]/, '');
13
14
  return isEmptyString(indicator) ? undefined : parseInt(indicator, 10);
14
15
  };
15
16
  const getIndentation = content => {
@@ -20,30 +21,72 @@ const getIndentation = content => {
20
21
  return repeatStr(' ', explicitIndentationIndicator);
21
22
  }
22
23
 
23
- // we assume indentation indicator from first line
24
- const firstLine = pathOr('', [1], content.split('\n'));
24
+ // we assume indentation indicator from first non-empty line (YAML 1.2 §8.1.1.1);
25
+ // with no non-empty line, the first line is used so that its spaces are still stripped
26
+ const lines = tail(content.split('\n'));
27
+ const firstLine = lines.find(line => !/^ *\r?$/.test(line)) ?? pathOr('', [0], lines);
25
28
  const implicitIndentationIndicator = pathOr(0, ['groups', 'indentation', 'length'], firstLine.match(/^(?<indentation>[ ]*)/));
26
29
  return repeatStr(' ', implicitIndentationIndicator);
27
30
  };
31
+
32
+ // splits a block scalar body into deindented content lines and trailing lines (YAML 1.2 §8.1.1.2):
33
+ // trailing lines are the whitespace-only lines after the last content line, minus any up to and
34
+ // including the last one indented deeper than the block, which are content; without any content
35
+ // line, all lines are trailing
36
+ const splitBlockScalar = (content, chomping) => {
37
+ const indentation = getIndentation(content).length;
38
+ const normalized = normalizeLineBreaks(content);
39
+ const body = normalized.includes('\n') ? normalized.slice(normalized.indexOf('\n') + 1) : '';
40
+ const lines = body === '' ? [] : body.split('\n');
41
+ const deindent = line => line.slice(Math.min(indentation, line.search(/[^ ]|$/)));
42
+
43
+ // without keep chomping, trailing whitespace-only lines not indented deeper than the first
44
+ // content line don't belong to the block scalar
45
+ const firstContent = lines.find(line => /[^ ]/.test(line));
46
+ if (chomping !== '+' && firstContent !== undefined) {
47
+ const contentIndentation = firstContent.search(/[^ ]/);
48
+ while (/^ *$/.test(lines.at(-1)) && lines.at(-1).length <= contentIndentation) lines.pop();
49
+ }
50
+ let chompStart = lines.length;
51
+ while (chompStart > 0 && /^ *$/.test(lines[chompStart - 1])) chompStart -= 1;
52
+ if (chompStart === 0) return {
53
+ lines: [],
54
+ trailing: lines.map(deindent)
55
+ };
56
+ for (let i = lines.length - 1; i >= chompStart; i -= 1) {
57
+ if (lines[i].length > indentation) {
58
+ chompStart = i + 1;
59
+ break;
60
+ }
61
+ }
62
+ return {
63
+ lines: lines.slice(0, chompStart).map(deindent),
64
+ trailing: lines.slice(chompStart).map(deindent)
65
+ };
66
+ };
28
67
  const getChompingIndicator = content => {
29
68
  const matches = content.match(blockStyleRegExp);
30
- const indicator = pathOr('', ['groups', 'chomping'], matches);
69
+ const indicators = pathOr('', ['groups', 'indicators'], matches);
70
+ const indicator = indicators.replace(/[0-9]/, '');
31
71
  return isEmptyString(indicator) ? undefined : indicator;
32
72
  };
33
- const chomp = (indicator, content) => {
34
- // clip (single newline at end)
35
- if (isUndefined(indicator)) {
36
- return `${trimEnd(content)}\n`;
73
+ const chomp = (indicator, value, {
74
+ lines,
75
+ trailing
76
+ }) => {
77
+ // a block without content is empty, unless keep chomping preserves its line breaks
78
+ if (lines.length === 0) {
79
+ return indicator === '+' && trailing.length > 0 ? '\n'.repeat(Math.max(1, trailing.length - 1)) : '';
37
80
  }
38
81
  // strip (no newline at end)
39
- if (indicator === '-') {
40
- return trimEnd(content);
41
- }
42
- // keep (all newlines from end)
82
+ if (indicator === '-') return value;
83
+ // keep (trailing lines and a newline at end)
43
84
  if (indicator === '+') {
44
- return content;
85
+ const kept = value + trailing.map(line => `\n${line}`).join('');
86
+ return kept.endsWith('\n') ? kept : `${kept}\n`;
45
87
  }
46
- return content;
88
+ // clip (single newline at end)
89
+ return `${value}\n`;
47
90
  };
48
91
 
49
92
  /**
@@ -53,56 +96,108 @@ const chomp = (indicator, content) => {
53
96
  // @ts-ignore
54
97
  const normalizeLineBreaks = val => val.replace(/\r\n/g, '\n');
55
98
 
56
- // prevent escaped line breaks from being converted to a space
57
- const preventLineBreakCollapseToSpace = val => val.replace(/\\\n\s*/g, '');
58
-
59
- // collapse line breaks into spaces
60
- const collapseLineBreakToSpace = val => {
61
- /**
62
- * Safari doesn't support negative lookbehind, thus we use mimicking technique:
63
- *
64
- * - https://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
65
- *
66
- * Ideally we want to use following replace, but that's not currently possible:
67
- *
68
- * .replace(/[^\n]\n([^\n]+)/g, (match: string, p1: string) => ` ${p1.trimLeft()}`)
69
- */
70
- return val.replace(/(\n)?\n([^\n]+)/g, (match, p1, p2) => p1 ? match : ` ${p2.trimStart()}`).replace(/[\n]{2}/g, '\n');
99
+ // folds the line breaks of a flow scalar (YAML 1.2 §6.5, §7.3):
100
+ // white space around a line break is dropped, a single line break becomes a space and every
101
+ // further one a line break; with escapes (double-quoted style), escape sequences are left for
102
+ // unescaping and an escaped line break is dropped together with the next line's indentation
103
+ const foldFlowLines = curry((escapes, text) => text.replace(
104
+ // a white space run not followed by a line break is consumed whole, which keeps the search
105
+ // linear instead of rescanning the run from every position in it
106
+ escapes ? /\\(?:\n[ \t]*|[^])|[ \t]*\n(?:[ \t]*\n)*[ \t]*|[ \t]+/g : /[ \t]*\n(?:[ \t]*\n)*[ \t]*|[ \t]+/g, match => {
107
+ if (match.startsWith('\\')) return match[1] === '\n' ? '' : match;
108
+ if (!match.includes('\n')) return match;
109
+ const lineBreaks = match.split('\n').length - 1;
110
+ return lineBreaks === 1 ? ' ' : '\n'.repeat(lineBreaks - 1);
111
+ }));
112
+
113
+ // folds the line break between two lines into a space; empty lines between them stay as line
114
+ // breaks, and a line break next to a more-indented line isn't folded (YAML 1.2 §6.5)
115
+ const foldLines = text => {
116
+ const parts = text.split(/\n(\n*)/); // lines alternating with the empty lines between them
117
+ let folded = parts[0];
118
+ for (let i = 1; i < parts.length; i += 2) {
119
+ const moreIndented = /^[ \t]/.test(parts[i - 1]) || /^[ \t]/.test(parts[i + 1]);
120
+ folded += (moreIndented ? `\n${parts[i]}` : parts[i] || ' ') + parts[i + 1];
121
+ }
122
+ return folded;
123
+ };
124
+
125
+ // escaped characters of YAML 1.2 §5.7, productions [42] to [58]
126
+ const escapedCharacters = {
127
+ '0': '\u0000',
128
+ // null
129
+ a: '\u0007',
130
+ // bell
131
+ b: '\u0008',
132
+ // backspace
133
+ t: '\u0009',
134
+ // horizontal tab
135
+ '\t': '\u0009',
136
+ // horizontal tab
137
+ n: '\u000a',
138
+ // line feed
139
+ v: '\u000b',
140
+ // vertical tab
141
+ f: '\u000c',
142
+ // form feed
143
+ r: '\u000d',
144
+ // carriage return
145
+ e: '\u001b',
146
+ // escape
147
+ ' ': '\u0020',
148
+ // space
149
+ '"': '\u0022',
150
+ // double quote
151
+ '/': '\u002f',
152
+ // slash
153
+ '\\': '\u005c',
154
+ // backslash
155
+ N: '\u0085',
156
+ // next line
157
+ _: '\u00a0',
158
+ // non-breaking space
159
+ L: '\u2028',
160
+ // line separator
161
+ P: '\u2029' // paragraph separator
71
162
  };
163
+
164
+ // unescapes the escape sequences of a double-quoted scalar (YAML 1.2 §5.7); an escape sequence
165
+ // outside of the spec is kept as-is
166
+ const unescapeDoubleQuoted = text => text.replace(/\\(?:x([0-9a-fA-F]{2})|u([0-9a-fA-F]{4})|U([0-9a-fA-F]{8})|([^]))/g, (match, x, u, U, char) => {
167
+ const hex = x ?? u ?? U;
168
+ if (hex === undefined) return escapedCharacters[char] ?? match;
169
+ const codePoint = parseInt(hex, 16);
170
+ return codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : match;
171
+ });
72
172
  const removeQuotes = curry((quoteType, val) => val.replace(new RegExp(`^${quoteType}`), '').replace(new RegExp(`${quoteType}$`), ''));
73
173
 
74
174
  /**
75
175
  * Formats Flow Scalar Plain style.
76
176
  * https://yaml.org/spec/1.2/spec.html#id2788859
77
177
  */
78
- export const formatFlowPlain = pipe(normalizeLineBreaks, trim, collapseLineBreakToSpace, split('\n'), map(trimStart), join('\n'));
178
+ export const formatFlowPlain = pipe(normalizeLineBreaks, trim, foldFlowLines(false));
79
179
 
80
180
  /**
81
181
  * Formats Flow Scalar Single-Quoted style.
82
182
  * https://yaml.org/spec/1.2/spec.html#id2788097
83
183
  */
84
184
 
85
- export const formatFlowSingleQuoted = pipe(normalizeLineBreaks, trim, collapseLineBreakToSpace, split('\n'), map(trimStart), join('\n'), removeQuotes("'"));
185
+ export const formatFlowSingleQuoted = pipe(normalizeLineBreaks, trim, foldFlowLines(false), removeQuotes("'"), replace(/''/g, "'"));
86
186
 
87
187
  /**
88
188
  * Formats Flow Scalar Double-Quoted style.
89
189
  * https://yaml.org/spec/1.2/spec.html#id2787109
90
190
  */
91
- export const formatFlowDoubleQuoted = pipe(normalizeLineBreaks, trim, preventLineBreakCollapseToSpace, collapseLineBreakToSpace, unraw, split('\n'), map(trimStart), join('\n'), removeQuotes('"'));
191
+ export const formatFlowDoubleQuoted = pipe(normalizeLineBreaks, trim, foldFlowLines(true), unescapeDoubleQuoted, removeQuotes('"'));
92
192
 
93
193
  /**
94
194
  * Formats Block Scalar Literal style.
95
195
  * https://yaml.org/spec/1.2/spec.html#id2795688
96
196
  */
97
197
  export const formatBlockLiteral = content => {
98
- const indentation = getIndentation(content);
99
- const chompingIndicator = getChompingIndicator(content);
100
- const normalized = normalizeLineBreaks(content);
101
- const lines = tail(normalized.split('\n')); // first line only contains indicators
102
- const transducer = compose(map(trimCharsStart(indentation)), map(concatRight('\n')));
103
- // @ts-ignore
104
- const deindented = transduce(transducer, concat, '', lines);
105
- return chomp(chompingIndicator, deindented);
198
+ const chomping = getChompingIndicator(content);
199
+ const split = splitBlockScalar(content, chomping);
200
+ return chomp(chomping, split.lines.join('\n'), split);
106
201
  };
107
202
 
108
203
  /**
@@ -110,13 +205,11 @@ export const formatBlockLiteral = content => {
110
205
  * https://yaml.org/spec/1.2/spec.html#id2796251
111
206
  */
112
207
  export const formatBlockFolded = content => {
113
- const indentation = getIndentation(content);
114
- const chompingIndicator = getChompingIndicator(content);
115
- const normalized = normalizeLineBreaks(content);
116
- const lines = tail(normalized.split('\n')); // first line only contains indicators
117
- const transducer = compose(map(trimCharsStart(indentation)), map(concatRight('\n')));
118
- // @ts-ignore
119
- const deindented = transduce(transducer, concat, '', lines);
120
- const collapsed = collapseLineBreakToSpace(deindented);
121
- return chomp(chompingIndicator, collapsed);
208
+ const chomping = getChompingIndicator(content);
209
+ const split = splitBlockScalar(content, chomping);
210
+ // leading line breaks are preserved as-is; folding applies only between content lines
211
+ const text = split.lines.join('\n');
212
+ const leadingLineBreaks = text.match(/^\n*/)[0];
213
+ const folded = leadingLineBreaks + foldLines(text.slice(leadingLineBreaks.length));
214
+ return chomp(chomping, folded, split);
122
215
  };