1 | 'use strict';
|
---|
2 |
|
---|
3 | var Scalar = require('../nodes/Scalar.js');
|
---|
4 |
|
---|
5 | function resolveBlockScalar(scalar, strict, onError) {
|
---|
6 | const start = scalar.offset;
|
---|
7 | const header = parseBlockScalarHeader(scalar, strict, onError);
|
---|
8 | if (!header)
|
---|
9 | return { value: '', type: null, comment: '', range: [start, start, start] };
|
---|
10 | const type = header.mode === '>' ? Scalar.Scalar.BLOCK_FOLDED : Scalar.Scalar.BLOCK_LITERAL;
|
---|
11 | const lines = scalar.source ? splitLines(scalar.source) : [];
|
---|
12 | // determine the end of content & start of chomping
|
---|
13 | let chompStart = lines.length;
|
---|
14 | for (let i = lines.length - 1; i >= 0; --i) {
|
---|
15 | const content = lines[i][1];
|
---|
16 | if (content === '' || content === '\r')
|
---|
17 | chompStart = i;
|
---|
18 | else
|
---|
19 | break;
|
---|
20 | }
|
---|
21 | // shortcut for empty contents
|
---|
22 | if (chompStart === 0) {
|
---|
23 | const value = header.chomp === '+' && lines.length > 0
|
---|
24 | ? '\n'.repeat(Math.max(1, lines.length - 1))
|
---|
25 | : '';
|
---|
26 | let end = start + header.length;
|
---|
27 | if (scalar.source)
|
---|
28 | end += scalar.source.length;
|
---|
29 | return { value, type, comment: header.comment, range: [start, end, end] };
|
---|
30 | }
|
---|
31 | // find the indentation level to trim from start
|
---|
32 | let trimIndent = scalar.indent + header.indent;
|
---|
33 | let offset = scalar.offset + header.length;
|
---|
34 | let contentStart = 0;
|
---|
35 | for (let i = 0; i < chompStart; ++i) {
|
---|
36 | const [indent, content] = lines[i];
|
---|
37 | if (content === '' || content === '\r') {
|
---|
38 | if (header.indent === 0 && indent.length > trimIndent)
|
---|
39 | trimIndent = indent.length;
|
---|
40 | }
|
---|
41 | else {
|
---|
42 | if (indent.length < trimIndent) {
|
---|
43 | const message = 'Block scalars with more-indented leading empty lines must use an explicit indentation indicator';
|
---|
44 | onError(offset + indent.length, 'MISSING_CHAR', message);
|
---|
45 | }
|
---|
46 | if (header.indent === 0)
|
---|
47 | trimIndent = indent.length;
|
---|
48 | contentStart = i;
|
---|
49 | break;
|
---|
50 | }
|
---|
51 | offset += indent.length + content.length + 1;
|
---|
52 | }
|
---|
53 | // include trailing more-indented empty lines in content
|
---|
54 | for (let i = lines.length - 1; i >= chompStart; --i) {
|
---|
55 | if (lines[i][0].length > trimIndent)
|
---|
56 | chompStart = i + 1;
|
---|
57 | }
|
---|
58 | let value = '';
|
---|
59 | let sep = '';
|
---|
60 | let prevMoreIndented = false;
|
---|
61 | // leading whitespace is kept intact
|
---|
62 | for (let i = 0; i < contentStart; ++i)
|
---|
63 | value += lines[i][0].slice(trimIndent) + '\n';
|
---|
64 | for (let i = contentStart; i < chompStart; ++i) {
|
---|
65 | let [indent, content] = lines[i];
|
---|
66 | offset += indent.length + content.length + 1;
|
---|
67 | const crlf = content[content.length - 1] === '\r';
|
---|
68 | if (crlf)
|
---|
69 | content = content.slice(0, -1);
|
---|
70 | /* istanbul ignore if already caught in lexer */
|
---|
71 | if (content && indent.length < trimIndent) {
|
---|
72 | const src = header.indent
|
---|
73 | ? 'explicit indentation indicator'
|
---|
74 | : 'first line';
|
---|
75 | const message = `Block scalar lines must not be less indented than their ${src}`;
|
---|
76 | onError(offset - content.length - (crlf ? 2 : 1), 'BAD_INDENT', message);
|
---|
77 | indent = '';
|
---|
78 | }
|
---|
79 | if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
---|
80 | value += sep + indent.slice(trimIndent) + content;
|
---|
81 | sep = '\n';
|
---|
82 | }
|
---|
83 | else if (indent.length > trimIndent || content[0] === '\t') {
|
---|
84 | // more-indented content within a folded block
|
---|
85 | if (sep === ' ')
|
---|
86 | sep = '\n';
|
---|
87 | else if (!prevMoreIndented && sep === '\n')
|
---|
88 | sep = '\n\n';
|
---|
89 | value += sep + indent.slice(trimIndent) + content;
|
---|
90 | sep = '\n';
|
---|
91 | prevMoreIndented = true;
|
---|
92 | }
|
---|
93 | else if (content === '') {
|
---|
94 | // empty line
|
---|
95 | if (sep === '\n')
|
---|
96 | value += '\n';
|
---|
97 | else
|
---|
98 | sep = '\n';
|
---|
99 | }
|
---|
100 | else {
|
---|
101 | value += sep + content;
|
---|
102 | sep = ' ';
|
---|
103 | prevMoreIndented = false;
|
---|
104 | }
|
---|
105 | }
|
---|
106 | switch (header.chomp) {
|
---|
107 | case '-':
|
---|
108 | break;
|
---|
109 | case '+':
|
---|
110 | for (let i = chompStart; i < lines.length; ++i)
|
---|
111 | value += '\n' + lines[i][0].slice(trimIndent);
|
---|
112 | if (value[value.length - 1] !== '\n')
|
---|
113 | value += '\n';
|
---|
114 | break;
|
---|
115 | default:
|
---|
116 | value += '\n';
|
---|
117 | }
|
---|
118 | const end = start + header.length + scalar.source.length;
|
---|
119 | return { value, type, comment: header.comment, range: [start, end, end] };
|
---|
120 | }
|
---|
121 | function parseBlockScalarHeader({ offset, props }, strict, onError) {
|
---|
122 | /* istanbul ignore if should not happen */
|
---|
123 | if (props[0].type !== 'block-scalar-header') {
|
---|
124 | onError(props[0], 'IMPOSSIBLE', 'Block scalar header not found');
|
---|
125 | return null;
|
---|
126 | }
|
---|
127 | const { source } = props[0];
|
---|
128 | const mode = source[0];
|
---|
129 | let indent = 0;
|
---|
130 | let chomp = '';
|
---|
131 | let error = -1;
|
---|
132 | for (let i = 1; i < source.length; ++i) {
|
---|
133 | const ch = source[i];
|
---|
134 | if (!chomp && (ch === '-' || ch === '+'))
|
---|
135 | chomp = ch;
|
---|
136 | else {
|
---|
137 | const n = Number(ch);
|
---|
138 | if (!indent && n)
|
---|
139 | indent = n;
|
---|
140 | else if (error === -1)
|
---|
141 | error = offset + i;
|
---|
142 | }
|
---|
143 | }
|
---|
144 | if (error !== -1)
|
---|
145 | onError(error, 'UNEXPECTED_TOKEN', `Block scalar header includes extra characters: ${source}`);
|
---|
146 | let hasSpace = false;
|
---|
147 | let comment = '';
|
---|
148 | let length = source.length;
|
---|
149 | for (let i = 1; i < props.length; ++i) {
|
---|
150 | const token = props[i];
|
---|
151 | switch (token.type) {
|
---|
152 | case 'space':
|
---|
153 | hasSpace = true;
|
---|
154 | // fallthrough
|
---|
155 | case 'newline':
|
---|
156 | length += token.source.length;
|
---|
157 | break;
|
---|
158 | case 'comment':
|
---|
159 | if (strict && !hasSpace) {
|
---|
160 | const message = 'Comments must be separated from other tokens by white space characters';
|
---|
161 | onError(token, 'MISSING_CHAR', message);
|
---|
162 | }
|
---|
163 | length += token.source.length;
|
---|
164 | comment = token.source.substring(1);
|
---|
165 | break;
|
---|
166 | case 'error':
|
---|
167 | onError(token, 'UNEXPECTED_TOKEN', token.message);
|
---|
168 | length += token.source.length;
|
---|
169 | break;
|
---|
170 | /* istanbul ignore next should not happen */
|
---|
171 | default: {
|
---|
172 | const message = `Unexpected token in block scalar header: ${token.type}`;
|
---|
173 | onError(token, 'UNEXPECTED_TOKEN', message);
|
---|
174 | const ts = token.source;
|
---|
175 | if (ts && typeof ts === 'string')
|
---|
176 | length += ts.length;
|
---|
177 | }
|
---|
178 | }
|
---|
179 | }
|
---|
180 | return { mode, indent, chomp, comment, length };
|
---|
181 | }
|
---|
182 | /** @returns Array of lines split up as `[indent, content]` */
|
---|
183 | function splitLines(source) {
|
---|
184 | const split = source.split(/\n( *)/);
|
---|
185 | const first = split[0];
|
---|
186 | const m = first.match(/^( *)/);
|
---|
187 | const line0 = m?.[1]
|
---|
188 | ? [m[1], first.slice(m[1].length)]
|
---|
189 | : ['', first];
|
---|
190 | const lines = [line0];
|
---|
191 | for (let i = 1; i < split.length; i += 2)
|
---|
192 | lines.push([split[i], split[i + 1]]);
|
---|
193 | return lines;
|
---|
194 | }
|
---|
195 |
|
---|
196 | exports.resolveBlockScalar = resolveBlockScalar;
|
---|