1 | ;
|
2 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
3 | // See LICENSE in the project root for license information.
|
4 | Object.defineProperty(exports, "__esModule", { value: true });
|
5 | exports.IndentedWriter = void 0;
|
6 | const node_core_library_1 = require("@rushstack/node-core-library");
|
7 | /**
|
8 | * A utility for writing indented text.
|
9 | *
|
10 | * @remarks
|
11 | *
|
12 | * Note that the indentation is inserted at the last possible opportunity.
|
13 | * For example, this code...
|
14 | *
|
15 | * ```ts
|
16 | * writer.write('begin\n');
|
17 | * writer.increaseIndent();
|
18 | * writer.write('one\ntwo\n');
|
19 | * writer.decreaseIndent();
|
20 | * writer.increaseIndent();
|
21 | * writer.decreaseIndent();
|
22 | * writer.write('end');
|
23 | * ```
|
24 | *
|
25 | * ...would produce this output:
|
26 | *
|
27 | * ```
|
28 | * begin
|
29 | * one
|
30 | * two
|
31 | * end
|
32 | * ```
|
33 | */
|
34 | class IndentedWriter {
|
35 | constructor(builder) {
|
36 | /**
|
37 | * The text characters used to create one level of indentation.
|
38 | * Two spaces by default.
|
39 | */
|
40 | this.defaultIndentPrefix = ' ';
|
41 | this._builder = builder === undefined ? new node_core_library_1.StringBuilder() : builder;
|
42 | this._latestChunk = undefined;
|
43 | this._previousChunk = undefined;
|
44 | this._atStartOfLine = true;
|
45 | this._indentStack = [];
|
46 | this._indentText = '';
|
47 | this._beforeStack = [];
|
48 | this._isWritingBeforeStack = false;
|
49 | }
|
50 | /**
|
51 | * Retrieves the output that was built so far.
|
52 | */
|
53 | getText() {
|
54 | return this._builder.toString();
|
55 | }
|
56 | toString() {
|
57 | return this.getText();
|
58 | }
|
59 | /**
|
60 | * Increases the indentation. Normally the indentation is two spaces,
|
61 | * however an arbitrary prefix can optional be specified. (For example,
|
62 | * the prefix could be "// " to indent and comment simultaneously.)
|
63 | * Each call to IndentedWriter.increaseIndent() must be followed by a
|
64 | * corresponding call to IndentedWriter.decreaseIndent().
|
65 | */
|
66 | increaseIndent(indentPrefix) {
|
67 | this._indentStack.push(indentPrefix !== undefined ? indentPrefix : this.defaultIndentPrefix);
|
68 | this._updateIndentText();
|
69 | }
|
70 | /**
|
71 | * Decreases the indentation, reverting the effect of the corresponding call
|
72 | * to IndentedWriter.increaseIndent().
|
73 | */
|
74 | decreaseIndent() {
|
75 | this._indentStack.pop();
|
76 | this._updateIndentText();
|
77 | }
|
78 | /**
|
79 | * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur
|
80 | * in pairs.
|
81 | */
|
82 | indentScope(scope, indentPrefix) {
|
83 | this.increaseIndent(indentPrefix);
|
84 | scope();
|
85 | this.decreaseIndent();
|
86 | }
|
87 | /**
|
88 | * Adds a newline if the file pointer is not already at the start of the line (or start of the stream).
|
89 | */
|
90 | ensureNewLine() {
|
91 | const lastCharacter = this.peekLastCharacter();
|
92 | if (lastCharacter !== '\n' && lastCharacter !== '') {
|
93 | this._writeNewLine();
|
94 | }
|
95 | }
|
96 | /**
|
97 | * Adds up to two newlines to ensure that there is a blank line above the current line.
|
98 | */
|
99 | ensureSkippedLine() {
|
100 | if (this.peekLastCharacter() !== '\n') {
|
101 | this._writeNewLine();
|
102 | }
|
103 | const secondLastCharacter = this.peekSecondLastCharacter();
|
104 | if (secondLastCharacter !== '\n' && secondLastCharacter !== '') {
|
105 | this._writeNewLine();
|
106 | }
|
107 | }
|
108 | /**
|
109 | * Returns the last character that was written, or an empty string if no characters have been written yet.
|
110 | */
|
111 | peekLastCharacter() {
|
112 | if (this._latestChunk !== undefined) {
|
113 | return this._latestChunk.substr(-1, 1);
|
114 | }
|
115 | return '';
|
116 | }
|
117 | /**
|
118 | * Returns the second to last character that was written, or an empty string if less than one characters
|
119 | * have been written yet.
|
120 | */
|
121 | peekSecondLastCharacter() {
|
122 | if (this._latestChunk !== undefined) {
|
123 | if (this._latestChunk.length > 1) {
|
124 | return this._latestChunk.substr(-2, 1);
|
125 | }
|
126 | if (this._previousChunk !== undefined) {
|
127 | return this._previousChunk.substr(-1, 1);
|
128 | }
|
129 | }
|
130 | return '';
|
131 | }
|
132 | /**
|
133 | * Writes `before` and `after` messages if and only if `mayWrite` writes anything.
|
134 | *
|
135 | * If `mayWrite` writes "CONTENT", this method will write "<before>CONTENT<after>".
|
136 | * If `mayWrite` writes nothing, this method will write nothing.
|
137 | */
|
138 | writeTentative(before, after, mayWrite) {
|
139 | this._beforeStack.push(before);
|
140 | // If this function writes anything, then _all_ messages in the "before stack" will also be
|
141 | // written. This means that the stack will be empty (as when we write a message from the stack,
|
142 | // we remove it from the stack).
|
143 | mayWrite();
|
144 | // If the stack is not empty, it means that `mayWrite` didn't write anything. Pop the last-
|
145 | // added message from the stack, we'll never write it. Otherwise, if the stack is empty, then
|
146 | // write the "after" message.
|
147 | if (this._beforeStack.length > 0) {
|
148 | this._beforeStack.pop();
|
149 | }
|
150 | else {
|
151 | this.write(after);
|
152 | }
|
153 | }
|
154 | /**
|
155 | * Writes some text to the internal string buffer, applying indentation according
|
156 | * to the current indentation level. If the string contains multiple newlines,
|
157 | * each line will be indented separately.
|
158 | */
|
159 | write(message) {
|
160 | if (message.length === 0) {
|
161 | return;
|
162 | }
|
163 | if (!this._isWritingBeforeStack) {
|
164 | this._writeBeforeStack();
|
165 | }
|
166 | // If there are no newline characters, then append the string verbatim
|
167 | if (!/[\r\n]/.test(message)) {
|
168 | this._writeLinePart(message);
|
169 | return;
|
170 | }
|
171 | // Otherwise split the lines and write each one individually
|
172 | let first = true;
|
173 | for (const linePart of message.split('\n')) {
|
174 | if (!first) {
|
175 | this._writeNewLine();
|
176 | }
|
177 | else {
|
178 | first = false;
|
179 | }
|
180 | if (linePart) {
|
181 | this._writeLinePart(linePart.replace(/[\r]/g, ''));
|
182 | }
|
183 | }
|
184 | }
|
185 | /**
|
186 | * A shorthand for writing an optional message, followed by a newline.
|
187 | * Indentation is applied following the semantics of IndentedWriter.write().
|
188 | */
|
189 | writeLine(message = '') {
|
190 | if (message.length > 0) {
|
191 | this.write(message);
|
192 | }
|
193 | else if (!this._isWritingBeforeStack) {
|
194 | this._writeBeforeStack();
|
195 | }
|
196 | this._writeNewLine();
|
197 | }
|
198 | /**
|
199 | * Writes a string that does not contain any newline characters.
|
200 | */
|
201 | _writeLinePart(message) {
|
202 | if (message.length > 0) {
|
203 | if (this._atStartOfLine && this._indentText.length > 0) {
|
204 | this._write(this._indentText);
|
205 | }
|
206 | this._write(message);
|
207 | this._atStartOfLine = false;
|
208 | }
|
209 | }
|
210 | _writeNewLine() {
|
211 | if (this._atStartOfLine && this._indentText.length > 0) {
|
212 | this._write(this._indentText);
|
213 | }
|
214 | this._write('\n');
|
215 | this._atStartOfLine = true;
|
216 | }
|
217 | _write(s) {
|
218 | this._previousChunk = this._latestChunk;
|
219 | this._latestChunk = s;
|
220 | this._builder.append(s);
|
221 | }
|
222 | /**
|
223 | * Writes all messages in our before stack, processing them in FIFO order. This stack is
|
224 | * populated by the `writeTentative` method.
|
225 | */
|
226 | _writeBeforeStack() {
|
227 | this._isWritingBeforeStack = true;
|
228 | for (const message of this._beforeStack) {
|
229 | this.write(message);
|
230 | }
|
231 | this._isWritingBeforeStack = false;
|
232 | this._beforeStack = [];
|
233 | }
|
234 | _updateIndentText() {
|
235 | this._indentText = this._indentStack.join('');
|
236 | }
|
237 | }
|
238 | exports.IndentedWriter = IndentedWriter;
|
239 | //# sourceMappingURL=IndentedWriter.js.map |
\ | No newline at end of file |