UNPKG

7.75 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 /**
42 * Whether to indent blank lines
43 */
44 this.indentBlankLines = false;
45 /**
46 * Trims leading spaces from the input text before applying the indent.
47 *
48 * @remarks
49 * Consider the following example:
50 *
51 * ```ts
52 * indentedWriter.increaseIndent(' '); // four spaces
53 * indentedWriter.write(' a\n b c\n');
54 * indentedWriter.decreaseIndent();
55 * ```
56 *
57 * Normally the output would be indented by 6 spaces: 4 from `increaseIndent()`, plus the 2 spaces
58 * from `write()`:
59 * ```
60 * a
61 * b c
62 * ```
63 *
64 * Setting `trimLeadingSpaces=true` will trim the leading spaces, so that the lines are indented
65 * by 4 spaces only:
66 * ```
67 * a
68 * b c
69 * ```
70 */
71 this.trimLeadingSpaces = false;
72 this._builder = builder === undefined ? new node_core_library_1.StringBuilder() : builder;
73 this._latestChunk = undefined;
74 this._previousChunk = undefined;
75 this._atStartOfLine = true;
76 this._previousLineIsBlank = true;
77 this._currentLineIsBlank = true;
78 this._indentStack = [];
79 this._indentText = '';
80 }
81 /**
82 * Retrieves the output that was built so far.
83 */
84 getText() {
85 return this._builder.toString();
86 }
87 toString() {
88 return this.getText();
89 }
90 /**
91 * Increases the indentation. Normally the indentation is two spaces,
92 * however an arbitrary prefix can optional be specified. (For example,
93 * the prefix could be "// " to indent and comment simultaneously.)
94 * Each call to IndentedWriter.increaseIndent() must be followed by a
95 * corresponding call to IndentedWriter.decreaseIndent().
96 */
97 increaseIndent(indentPrefix) {
98 this._indentStack.push(indentPrefix !== undefined ? indentPrefix : this.defaultIndentPrefix);
99 this._updateIndentText();
100 }
101 /**
102 * Decreases the indentation, reverting the effect of the corresponding call
103 * to IndentedWriter.increaseIndent().
104 */
105 decreaseIndent() {
106 this._indentStack.pop();
107 this._updateIndentText();
108 }
109 /**
110 * A shorthand for ensuring that increaseIndent()/decreaseIndent() occur
111 * in pairs.
112 */
113 indentScope(scope, indentPrefix) {
114 this.increaseIndent(indentPrefix);
115 scope();
116 this.decreaseIndent();
117 }
118 /**
119 * Adds a newline if the file pointer is not already at the start of the line (or start of the stream).
120 */
121 ensureNewLine() {
122 const lastCharacter = this.peekLastCharacter();
123 if (lastCharacter !== '\n' && lastCharacter !== '') {
124 this._writeNewLine();
125 }
126 }
127 /**
128 * Adds up to two newlines to ensure that there is a blank line above the current position.
129 * The start of the stream is considered to be a blank line, so `ensureSkippedLine()` has no effect
130 * unless some text has been written.
131 */
132 ensureSkippedLine() {
133 this.ensureNewLine();
134 if (!this._previousLineIsBlank) {
135 this._writeNewLine();
136 }
137 }
138 /**
139 * Returns the last character that was written, or an empty string if no characters have been written yet.
140 */
141 peekLastCharacter() {
142 if (this._latestChunk !== undefined) {
143 return this._latestChunk.substr(-1, 1);
144 }
145 return '';
146 }
147 /**
148 * Returns the second to last character that was written, or an empty string if less than one characters
149 * have been written yet.
150 */
151 peekSecondLastCharacter() {
152 if (this._latestChunk !== undefined) {
153 if (this._latestChunk.length > 1) {
154 return this._latestChunk.substr(-2, 1);
155 }
156 if (this._previousChunk !== undefined) {
157 return this._previousChunk.substr(-1, 1);
158 }
159 }
160 return '';
161 }
162 /**
163 * Writes some text to the internal string buffer, applying indentation according
164 * to the current indentation level. If the string contains multiple newlines,
165 * each line will be indented separately.
166 */
167 write(message) {
168 if (message.length === 0) {
169 return;
170 }
171 // If there are no newline characters, then append the string verbatim
172 if (!/[\r\n]/.test(message)) {
173 this._writeLinePart(message);
174 return;
175 }
176 // Otherwise split the lines and write each one individually
177 let first = true;
178 for (const linePart of message.split('\n')) {
179 if (!first) {
180 this._writeNewLine();
181 }
182 else {
183 first = false;
184 }
185 if (linePart) {
186 this._writeLinePart(linePart.replace(/[\r]/g, ''));
187 }
188 }
189 }
190 /**
191 * A shorthand for writing an optional message, followed by a newline.
192 * Indentation is applied following the semantics of IndentedWriter.write().
193 */
194 writeLine(message = '') {
195 if (message.length > 0) {
196 this.write(message);
197 }
198 this._writeNewLine();
199 }
200 /**
201 * Writes a string that does not contain any newline characters.
202 */
203 _writeLinePart(message) {
204 let trimmedMessage = message;
205 if (this.trimLeadingSpaces && this._atStartOfLine) {
206 trimmedMessage = message.replace(/^ +/, '');
207 }
208 if (trimmedMessage.length > 0) {
209 if (this._atStartOfLine && this._indentText.length > 0) {
210 this._write(this._indentText);
211 }
212 this._write(trimmedMessage);
213 if (this._currentLineIsBlank) {
214 if (/\S/.test(trimmedMessage)) {
215 this._currentLineIsBlank = false;
216 }
217 }
218 this._atStartOfLine = false;
219 }
220 }
221 _writeNewLine() {
222 if (this.indentBlankLines) {
223 if (this._atStartOfLine && this._indentText.length > 0) {
224 this._write(this._indentText);
225 }
226 }
227 this._previousLineIsBlank = this._currentLineIsBlank;
228 this._write('\n');
229 this._currentLineIsBlank = true;
230 this._atStartOfLine = true;
231 }
232 _write(s) {
233 this._previousChunk = this._latestChunk;
234 this._latestChunk = s;
235 this._builder.append(s);
236 }
237 _updateIndentText() {
238 this._indentText = this._indentStack.join('');
239 }
240}
241exports.IndentedWriter = IndentedWriter;
242//# sourceMappingURL=IndentedWriter.js.map
\No newline at end of file