UNPKG

7.89 kBJavaScriptView Raw
1"use strict";
2// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3// See LICENSE in the project root for license information.
4Object.defineProperty(exports, "__esModule", { value: true });
5exports.IndentedWriter = void 0;
6const 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 */
34class 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}
238exports.IndentedWriter = IndentedWriter;
239//# sourceMappingURL=IndentedWriter.js.map
\No newline at end of file