mod.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  1. import { escapeForWithinString, getStringFromStrOrFunc } from "./utils/string_utils.js";
  2. /** @internal */
  3. var CommentChar;
  4. (function (CommentChar) {
  5. CommentChar[CommentChar["Line"] = 0] = "Line";
  6. CommentChar[CommentChar["Star"] = 1] = "Star";
  7. })(CommentChar || (CommentChar = {}));
  8. // Using the char codes is a performance improvement (about 5.5% faster when writing because it eliminates additional string allocations).
  9. const CHARS = {
  10. BACK_SLASH: "\\".charCodeAt(0),
  11. FORWARD_SLASH: "/".charCodeAt(0),
  12. NEW_LINE: "\n".charCodeAt(0),
  13. CARRIAGE_RETURN: "\r".charCodeAt(0),
  14. ASTERISK: "*".charCodeAt(0),
  15. DOUBLE_QUOTE: "\"".charCodeAt(0),
  16. SINGLE_QUOTE: "'".charCodeAt(0),
  17. BACK_TICK: "`".charCodeAt(0),
  18. OPEN_BRACE: "{".charCodeAt(0),
  19. CLOSE_BRACE: "}".charCodeAt(0),
  20. DOLLAR_SIGN: "$".charCodeAt(0),
  21. SPACE: " ".charCodeAt(0),
  22. TAB: "\t".charCodeAt(0),
  23. };
  24. const isCharToHandle = new Set([
  25. CHARS.BACK_SLASH,
  26. CHARS.FORWARD_SLASH,
  27. CHARS.NEW_LINE,
  28. CHARS.CARRIAGE_RETURN,
  29. CHARS.ASTERISK,
  30. CHARS.DOUBLE_QUOTE,
  31. CHARS.SINGLE_QUOTE,
  32. CHARS.BACK_TICK,
  33. CHARS.OPEN_BRACE,
  34. CHARS.CLOSE_BRACE,
  35. ]);
  36. /**
  37. * Code writer that assists with formatting and visualizing blocks of JavaScript or TypeScript code.
  38. */
  39. class CodeBlockWriter {
  40. /**
  41. * Constructor.
  42. * @param opts - Options for the writer.
  43. */
  44. constructor(opts = {}) {
  45. /** @internal */
  46. Object.defineProperty(this, "_indentationText", {
  47. enumerable: true,
  48. configurable: true,
  49. writable: true,
  50. value: void 0
  51. });
  52. /** @internal */
  53. Object.defineProperty(this, "_newLine", {
  54. enumerable: true,
  55. configurable: true,
  56. writable: true,
  57. value: void 0
  58. });
  59. /** @internal */
  60. Object.defineProperty(this, "_useTabs", {
  61. enumerable: true,
  62. configurable: true,
  63. writable: true,
  64. value: void 0
  65. });
  66. /** @internal */
  67. Object.defineProperty(this, "_quoteChar", {
  68. enumerable: true,
  69. configurable: true,
  70. writable: true,
  71. value: void 0
  72. });
  73. /** @internal */
  74. Object.defineProperty(this, "_indentNumberOfSpaces", {
  75. enumerable: true,
  76. configurable: true,
  77. writable: true,
  78. value: void 0
  79. });
  80. /** @internal */
  81. Object.defineProperty(this, "_currentIndentation", {
  82. enumerable: true,
  83. configurable: true,
  84. writable: true,
  85. value: 0
  86. });
  87. /** @internal */
  88. Object.defineProperty(this, "_queuedIndentation", {
  89. enumerable: true,
  90. configurable: true,
  91. writable: true,
  92. value: void 0
  93. });
  94. /** @internal */
  95. Object.defineProperty(this, "_queuedOnlyIfNotBlock", {
  96. enumerable: true,
  97. configurable: true,
  98. writable: true,
  99. value: void 0
  100. });
  101. /** @internal */
  102. Object.defineProperty(this, "_length", {
  103. enumerable: true,
  104. configurable: true,
  105. writable: true,
  106. value: 0
  107. });
  108. /** @internal */
  109. Object.defineProperty(this, "_newLineOnNextWrite", {
  110. enumerable: true,
  111. configurable: true,
  112. writable: true,
  113. value: false
  114. });
  115. /** @internal */
  116. Object.defineProperty(this, "_currentCommentChar", {
  117. enumerable: true,
  118. configurable: true,
  119. writable: true,
  120. value: undefined
  121. });
  122. /** @internal */
  123. Object.defineProperty(this, "_stringCharStack", {
  124. enumerable: true,
  125. configurable: true,
  126. writable: true,
  127. value: []
  128. });
  129. /** @internal */
  130. Object.defineProperty(this, "_isInRegEx", {
  131. enumerable: true,
  132. configurable: true,
  133. writable: true,
  134. value: false
  135. });
  136. /** @internal */
  137. Object.defineProperty(this, "_isOnFirstLineOfBlock", {
  138. enumerable: true,
  139. configurable: true,
  140. writable: true,
  141. value: true
  142. });
  143. // An array of strings is used rather than a single string because it was
  144. // found to be ~11x faster when printing a 10K line file (~11s to ~1s).
  145. /** @internal */
  146. Object.defineProperty(this, "_texts", {
  147. enumerable: true,
  148. configurable: true,
  149. writable: true,
  150. value: []
  151. });
  152. this._newLine = opts.newLine || "\n";
  153. this._useTabs = opts.useTabs || false;
  154. this._indentNumberOfSpaces = opts.indentNumberOfSpaces || 4;
  155. this._indentationText = getIndentationText(this._useTabs, this._indentNumberOfSpaces);
  156. this._quoteChar = opts.useSingleQuote ? "'" : `"`;
  157. }
  158. /**
  159. * Gets the options.
  160. */
  161. getOptions() {
  162. return {
  163. indentNumberOfSpaces: this._indentNumberOfSpaces,
  164. newLine: this._newLine,
  165. useTabs: this._useTabs,
  166. useSingleQuote: this._quoteChar === "'",
  167. };
  168. }
  169. queueIndentationLevel(countOrText) {
  170. this._queuedIndentation = this._getIndentationLevelFromArg(countOrText);
  171. this._queuedOnlyIfNotBlock = undefined;
  172. return this;
  173. }
  174. /**
  175. * Writes the text within the provided action with hanging indentation.
  176. * @param action - Action to perform with hanging indentation.
  177. */
  178. hangingIndent(action) {
  179. return this._withResetIndentation(() => this.queueIndentationLevel(this.getIndentationLevel() + 1), action);
  180. }
  181. /**
  182. * Writes the text within the provided action with hanging indentation unless writing a block.
  183. * @param action - Action to perform with hanging indentation unless a block is written.
  184. */
  185. hangingIndentUnlessBlock(action) {
  186. return this._withResetIndentation(() => {
  187. this.queueIndentationLevel(this.getIndentationLevel() + 1);
  188. this._queuedOnlyIfNotBlock = true;
  189. }, action);
  190. }
  191. setIndentationLevel(countOrText) {
  192. this._currentIndentation = this._getIndentationLevelFromArg(countOrText);
  193. return this;
  194. }
  195. withIndentationLevel(countOrText, action) {
  196. return this._withResetIndentation(() => this.setIndentationLevel(countOrText), action);
  197. }
  198. /** @internal */
  199. _withResetIndentation(setStateAction, writeAction) {
  200. const previousState = this._getIndentationState();
  201. setStateAction();
  202. try {
  203. writeAction();
  204. }
  205. finally {
  206. this._setIndentationState(previousState);
  207. }
  208. return this;
  209. }
  210. /**
  211. * Gets the current indentation level.
  212. */
  213. getIndentationLevel() {
  214. return this._currentIndentation;
  215. }
  216. /**
  217. * Writes a block using braces.
  218. * @param block - Write using the writer within this block.
  219. */
  220. block(block) {
  221. this._newLineIfNewLineOnNextWrite();
  222. if (this.getLength() > 0 && !this.isLastNewLine()) {
  223. this.spaceIfLastNot();
  224. }
  225. this.inlineBlock(block);
  226. this._newLineOnNextWrite = true;
  227. return this;
  228. }
  229. /**
  230. * Writes an inline block with braces.
  231. * @param block - Write using the writer within this block.
  232. */
  233. inlineBlock(block) {
  234. this._newLineIfNewLineOnNextWrite();
  235. this.write("{");
  236. this._indentBlockInternal(block);
  237. this.newLineIfLastNot().write("}");
  238. return this;
  239. }
  240. indent(timesOrBlock = 1) {
  241. if (typeof timesOrBlock === "number") {
  242. this._newLineIfNewLineOnNextWrite();
  243. return this.write(this._indentationText.repeat(timesOrBlock));
  244. }
  245. else {
  246. this._indentBlockInternal(timesOrBlock);
  247. if (!this.isLastNewLine()) {
  248. this._newLineOnNextWrite = true;
  249. }
  250. return this;
  251. }
  252. }
  253. /** @internal */
  254. _indentBlockInternal(block) {
  255. if (this.getLastChar() != null) {
  256. this.newLineIfLastNot();
  257. }
  258. this._currentIndentation++;
  259. this._isOnFirstLineOfBlock = true;
  260. if (block != null) {
  261. block();
  262. }
  263. this._isOnFirstLineOfBlock = false;
  264. this._currentIndentation = Math.max(0, this._currentIndentation - 1);
  265. }
  266. conditionalWriteLine(condition, strOrFunc) {
  267. if (condition) {
  268. this.writeLine(getStringFromStrOrFunc(strOrFunc));
  269. }
  270. return this;
  271. }
  272. /**
  273. * Writes a line of text.
  274. * @param text - String to write.
  275. */
  276. writeLine(text) {
  277. this._newLineIfNewLineOnNextWrite();
  278. if (this.getLastChar() != null) {
  279. this.newLineIfLastNot();
  280. }
  281. this._writeIndentingNewLines(text);
  282. this.newLine();
  283. return this;
  284. }
  285. /**
  286. * Writes a newline if the last line was not a newline.
  287. */
  288. newLineIfLastNot() {
  289. this._newLineIfNewLineOnNextWrite();
  290. if (!this.isLastNewLine()) {
  291. this.newLine();
  292. }
  293. return this;
  294. }
  295. /**
  296. * Writes a blank line if the last written text was not a blank line.
  297. */
  298. blankLineIfLastNot() {
  299. if (!this.isLastBlankLine()) {
  300. this.blankLine();
  301. }
  302. return this;
  303. }
  304. /**
  305. * Writes a blank line if the condition is true.
  306. * @param condition - Condition to evaluate.
  307. */
  308. conditionalBlankLine(condition) {
  309. if (condition) {
  310. this.blankLine();
  311. }
  312. return this;
  313. }
  314. /**
  315. * Writes a blank line.
  316. */
  317. blankLine() {
  318. return this.newLineIfLastNot().newLine();
  319. }
  320. /**
  321. * Writes a newline if the condition is true.
  322. * @param condition - Condition to evaluate.
  323. */
  324. conditionalNewLine(condition) {
  325. if (condition) {
  326. this.newLine();
  327. }
  328. return this;
  329. }
  330. /**
  331. * Writes a newline.
  332. */
  333. newLine() {
  334. this._newLineOnNextWrite = false;
  335. this._baseWriteNewline();
  336. return this;
  337. }
  338. quote(text) {
  339. this._newLineIfNewLineOnNextWrite();
  340. this._writeIndentingNewLines(text == null ? this._quoteChar : this._quoteChar + escapeForWithinString(text, this._quoteChar) + this._quoteChar);
  341. return this;
  342. }
  343. /**
  344. * Writes a space if the last character was not a space.
  345. */
  346. spaceIfLastNot() {
  347. this._newLineIfNewLineOnNextWrite();
  348. if (!this.isLastSpace()) {
  349. this._writeIndentingNewLines(" ");
  350. }
  351. return this;
  352. }
  353. /**
  354. * Writes a space.
  355. * @param times - Number of times to write a space.
  356. */
  357. space(times = 1) {
  358. this._newLineIfNewLineOnNextWrite();
  359. this._writeIndentingNewLines(" ".repeat(times));
  360. return this;
  361. }
  362. /**
  363. * Writes a tab if the last character was not a tab.
  364. */
  365. tabIfLastNot() {
  366. this._newLineIfNewLineOnNextWrite();
  367. if (!this.isLastTab()) {
  368. this._writeIndentingNewLines("\t");
  369. }
  370. return this;
  371. }
  372. /**
  373. * Writes a tab.
  374. * @param times - Number of times to write a tab.
  375. */
  376. tab(times = 1) {
  377. this._newLineIfNewLineOnNextWrite();
  378. this._writeIndentingNewLines("\t".repeat(times));
  379. return this;
  380. }
  381. conditionalWrite(condition, textOrFunc) {
  382. if (condition) {
  383. this.write(getStringFromStrOrFunc(textOrFunc));
  384. }
  385. return this;
  386. }
  387. /**
  388. * Writes the provided text.
  389. * @param text - Text to write.
  390. */
  391. write(text) {
  392. this._newLineIfNewLineOnNextWrite();
  393. this._writeIndentingNewLines(text);
  394. return this;
  395. }
  396. /**
  397. * Writes text to exit a comment if in a comment.
  398. */
  399. closeComment() {
  400. const commentChar = this._currentCommentChar;
  401. switch (commentChar) {
  402. case CommentChar.Line:
  403. this.newLine();
  404. break;
  405. case CommentChar.Star:
  406. if (!this.isLastNewLine()) {
  407. this.spaceIfLastNot();
  408. }
  409. this.write("*/");
  410. break;
  411. default: {
  412. const _assertUndefined = commentChar;
  413. break;
  414. }
  415. }
  416. return this;
  417. }
  418. /**
  419. * Inserts text at the provided position.
  420. *
  421. * This method is "unsafe" because it won't update the state of the writer unless
  422. * inserting at the end position. It is biased towards being fast at inserting closer
  423. * to the start or end, but slower to insert in the middle. Only use this if
  424. * absolutely necessary.
  425. * @param pos - Position to insert at.
  426. * @param text - Text to insert.
  427. */
  428. unsafeInsert(pos, text) {
  429. const textLength = this._length;
  430. const texts = this._texts;
  431. verifyInput();
  432. if (pos === textLength) {
  433. return this.write(text);
  434. }
  435. updateInternalArray();
  436. this._length += text.length;
  437. return this;
  438. function verifyInput() {
  439. if (pos < 0) {
  440. throw new Error(`Provided position of '${pos}' was less than zero.`);
  441. }
  442. if (pos > textLength) {
  443. throw new Error(`Provided position of '${pos}' was greater than the text length of '${textLength}'.`);
  444. }
  445. }
  446. function updateInternalArray() {
  447. const { index, localIndex } = getArrayIndexAndLocalIndex();
  448. if (localIndex === 0) {
  449. texts.splice(index, 0, text);
  450. }
  451. else if (localIndex === texts[index].length) {
  452. texts.splice(index + 1, 0, text);
  453. }
  454. else {
  455. const textItem = texts[index];
  456. const startText = textItem.substring(0, localIndex);
  457. const endText = textItem.substring(localIndex);
  458. texts.splice(index, 1, startText, text, endText);
  459. }
  460. }
  461. function getArrayIndexAndLocalIndex() {
  462. if (pos < textLength / 2) {
  463. // start searching from the front
  464. let endPos = 0;
  465. for (let i = 0; i < texts.length; i++) {
  466. const textItem = texts[i];
  467. const startPos = endPos;
  468. endPos += textItem.length;
  469. if (endPos >= pos) {
  470. return { index: i, localIndex: pos - startPos };
  471. }
  472. }
  473. }
  474. else {
  475. // start searching from the back
  476. let startPos = textLength;
  477. for (let i = texts.length - 1; i >= 0; i--) {
  478. const textItem = texts[i];
  479. startPos -= textItem.length;
  480. if (startPos <= pos) {
  481. return { index: i, localIndex: pos - startPos };
  482. }
  483. }
  484. }
  485. throw new Error("Unhandled situation inserting. This should never happen.");
  486. }
  487. }
  488. /**
  489. * Gets the length of the string in the writer.
  490. */
  491. getLength() {
  492. return this._length;
  493. }
  494. /**
  495. * Gets if the writer is currently in a comment.
  496. */
  497. isInComment() {
  498. return this._currentCommentChar !== undefined;
  499. }
  500. /**
  501. * Gets if the writer is currently at the start of the first line of the text, block, or indentation block.
  502. */
  503. isAtStartOfFirstLineOfBlock() {
  504. return this.isOnFirstLineOfBlock() && (this.isLastNewLine() || this.getLastChar() == null);
  505. }
  506. /**
  507. * Gets if the writer is currently on the first line of the text, block, or indentation block.
  508. */
  509. isOnFirstLineOfBlock() {
  510. return this._isOnFirstLineOfBlock;
  511. }
  512. /**
  513. * Gets if the writer is currently in a string.
  514. */
  515. isInString() {
  516. return this._stringCharStack.length > 0 && this._stringCharStack[this._stringCharStack.length - 1] !== CHARS.OPEN_BRACE;
  517. }
  518. /**
  519. * Gets if the last chars written were for a newline.
  520. */
  521. isLastNewLine() {
  522. const lastChar = this.getLastChar();
  523. return lastChar === "\n" || lastChar === "\r";
  524. }
  525. /**
  526. * Gets if the last chars written were for a blank line.
  527. */
  528. isLastBlankLine() {
  529. let foundCount = 0;
  530. // todo: consider extracting out iterating over past characters, but don't use
  531. // an iterator because it will be slow.
  532. for (let i = this._texts.length - 1; i >= 0; i--) {
  533. const currentText = this._texts[i];
  534. for (let j = currentText.length - 1; j >= 0; j--) {
  535. const currentChar = currentText.charCodeAt(j);
  536. if (currentChar === CHARS.NEW_LINE) {
  537. foundCount++;
  538. if (foundCount === 2) {
  539. return true;
  540. }
  541. }
  542. else if (currentChar !== CHARS.CARRIAGE_RETURN) {
  543. return false;
  544. }
  545. }
  546. }
  547. return false;
  548. }
  549. /**
  550. * Gets if the last char written was a space.
  551. */
  552. isLastSpace() {
  553. return this.getLastChar() === " ";
  554. }
  555. /**
  556. * Gets if the last char written was a tab.
  557. */
  558. isLastTab() {
  559. return this.getLastChar() === "\t";
  560. }
  561. /**
  562. * Gets the last char written.
  563. */
  564. getLastChar() {
  565. const charCode = this._getLastCharCodeWithOffset(0);
  566. return charCode == null ? undefined : String.fromCharCode(charCode);
  567. }
  568. /**
  569. * Gets if the writer ends with the provided text.
  570. * @param text - Text to check if the writer ends with the provided text.
  571. */
  572. endsWith(text) {
  573. const length = this._length;
  574. return this.iterateLastCharCodes((charCode, index) => {
  575. const offset = length - index;
  576. const textIndex = text.length - offset;
  577. if (text.charCodeAt(textIndex) !== charCode) {
  578. return false;
  579. }
  580. return textIndex === 0 ? true : undefined;
  581. }) || false;
  582. }
  583. /**
  584. * Iterates over the writer characters in reverse order. The iteration stops when a non-null or
  585. * undefined value is returned from the action. The returned value is then returned by the method.
  586. *
  587. * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()`
  588. * will combine the internal array into a string.
  589. */
  590. iterateLastChars(action) {
  591. return this.iterateLastCharCodes((charCode, index) => action(String.fromCharCode(charCode), index));
  592. }
  593. /**
  594. * Iterates over the writer character char codes in reverse order. The iteration stops when a non-null or
  595. * undefined value is returned from the action. The returned value is then returned by the method.
  596. *
  597. * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()`
  598. * will combine the internal array into a string. Additionally, this is slightly more efficient that
  599. * `iterateLastChars` as this won't allocate a string per character.
  600. */
  601. iterateLastCharCodes(action) {
  602. let index = this._length;
  603. for (let i = this._texts.length - 1; i >= 0; i--) {
  604. const currentText = this._texts[i];
  605. for (let j = currentText.length - 1; j >= 0; j--) {
  606. index--;
  607. const result = action(currentText.charCodeAt(j), index);
  608. if (result != null) {
  609. return result;
  610. }
  611. }
  612. }
  613. return undefined;
  614. }
  615. /**
  616. * Gets the writer's text.
  617. */
  618. toString() {
  619. if (this._texts.length > 1) {
  620. const text = this._texts.join("");
  621. this._texts.length = 0;
  622. this._texts.push(text);
  623. }
  624. return this._texts[0] || "";
  625. }
  626. /** @internal */
  627. _writeIndentingNewLines(text) {
  628. text = text || "";
  629. if (text.length === 0) {
  630. writeIndividual(this, "");
  631. return;
  632. }
  633. const items = text.split(CodeBlockWriter._newLineRegEx);
  634. items.forEach((s, i) => {
  635. if (i > 0) {
  636. this._baseWriteNewline();
  637. }
  638. if (s.length === 0) {
  639. return;
  640. }
  641. writeIndividual(this, s);
  642. });
  643. function writeIndividual(writer, s) {
  644. if (!writer.isInString()) {
  645. const isAtStartOfLine = writer.isLastNewLine() || writer.getLastChar() == null;
  646. if (isAtStartOfLine) {
  647. writer._writeIndentation();
  648. }
  649. }
  650. writer._updateInternalState(s);
  651. writer._internalWrite(s);
  652. }
  653. }
  654. /** @internal */
  655. _baseWriteNewline() {
  656. if (this._currentCommentChar === CommentChar.Line) {
  657. this._currentCommentChar = undefined;
  658. }
  659. const lastStringCharOnStack = this._stringCharStack[this._stringCharStack.length - 1];
  660. if ((lastStringCharOnStack === CHARS.DOUBLE_QUOTE || lastStringCharOnStack === CHARS.SINGLE_QUOTE) && this._getLastCharCodeWithOffset(0) !== CHARS.BACK_SLASH) {
  661. this._stringCharStack.pop();
  662. }
  663. this._internalWrite(this._newLine);
  664. this._isOnFirstLineOfBlock = false;
  665. this._dequeueQueuedIndentation();
  666. }
  667. /** @internal */
  668. _dequeueQueuedIndentation() {
  669. if (this._queuedIndentation == null) {
  670. return;
  671. }
  672. if (this._queuedOnlyIfNotBlock && wasLastBlock(this)) {
  673. this._queuedIndentation = undefined;
  674. this._queuedOnlyIfNotBlock = undefined;
  675. }
  676. else {
  677. this._currentIndentation = this._queuedIndentation;
  678. this._queuedIndentation = undefined;
  679. }
  680. function wasLastBlock(writer) {
  681. let foundNewLine = false;
  682. return writer.iterateLastCharCodes(charCode => {
  683. switch (charCode) {
  684. case CHARS.NEW_LINE:
  685. if (foundNewLine) {
  686. return false;
  687. }
  688. else {
  689. foundNewLine = true;
  690. }
  691. break;
  692. case CHARS.CARRIAGE_RETURN:
  693. return undefined;
  694. case CHARS.OPEN_BRACE:
  695. return true;
  696. default:
  697. return false;
  698. }
  699. });
  700. }
  701. }
  702. /** @internal */
  703. _updateInternalState(str) {
  704. for (let i = 0; i < str.length; i++) {
  705. const currentChar = str.charCodeAt(i);
  706. // This is a performance optimization to short circuit all the checks below. If the current char
  707. // is not in this set then it won't change any internal state so no need to continue and do
  708. // so many other checks (this made it 3x faster in one scenario I tested).
  709. if (!isCharToHandle.has(currentChar)) {
  710. continue;
  711. }
  712. const pastChar = i === 0 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 1);
  713. const pastPastChar = i === 0 ? this._getLastCharCodeWithOffset(1) : i === 1 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 2);
  714. // handle regex
  715. if (this._isInRegEx) {
  716. if (pastChar === CHARS.FORWARD_SLASH && pastPastChar !== CHARS.BACK_SLASH || pastChar === CHARS.NEW_LINE) {
  717. this._isInRegEx = false;
  718. }
  719. else {
  720. continue;
  721. }
  722. }
  723. else if (!this.isInString() && !this.isInComment() && isRegExStart(currentChar, pastChar, pastPastChar)) {
  724. this._isInRegEx = true;
  725. continue;
  726. }
  727. // handle comments
  728. if (!this.isInString()) {
  729. if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.FORWARD_SLASH) {
  730. this._currentCommentChar = CommentChar.Line;
  731. }
  732. else if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.ASTERISK) {
  733. this._currentCommentChar = CommentChar.Star;
  734. }
  735. else if (this._currentCommentChar === CommentChar.Star && pastChar === CHARS.ASTERISK && currentChar === CHARS.FORWARD_SLASH) {
  736. this._currentCommentChar = undefined;
  737. }
  738. }
  739. if (this.isInComment()) {
  740. continue;
  741. }
  742. // handle strings
  743. const lastStringCharOnStack = this._stringCharStack.length === 0 ? undefined : this._stringCharStack[this._stringCharStack.length - 1];
  744. if (pastChar !== CHARS.BACK_SLASH && (currentChar === CHARS.DOUBLE_QUOTE || currentChar === CHARS.SINGLE_QUOTE || currentChar === CHARS.BACK_TICK)) {
  745. if (lastStringCharOnStack === currentChar) {
  746. this._stringCharStack.pop();
  747. }
  748. else if (lastStringCharOnStack === CHARS.OPEN_BRACE || lastStringCharOnStack === undefined) {
  749. this._stringCharStack.push(currentChar);
  750. }
  751. }
  752. else if (pastPastChar !== CHARS.BACK_SLASH && pastChar === CHARS.DOLLAR_SIGN && currentChar === CHARS.OPEN_BRACE && lastStringCharOnStack === CHARS.BACK_TICK) {
  753. this._stringCharStack.push(currentChar);
  754. }
  755. else if (currentChar === CHARS.CLOSE_BRACE && lastStringCharOnStack === CHARS.OPEN_BRACE) {
  756. this._stringCharStack.pop();
  757. }
  758. }
  759. }
  760. /** @internal - This is private, but exposed for testing. */
  761. _getLastCharCodeWithOffset(offset) {
  762. if (offset >= this._length || offset < 0) {
  763. return undefined;
  764. }
  765. for (let i = this._texts.length - 1; i >= 0; i--) {
  766. const currentText = this._texts[i];
  767. if (offset >= currentText.length) {
  768. offset -= currentText.length;
  769. }
  770. else {
  771. return currentText.charCodeAt(currentText.length - 1 - offset);
  772. }
  773. }
  774. return undefined;
  775. }
  776. /** @internal */
  777. _writeIndentation() {
  778. const flooredIndentation = Math.floor(this._currentIndentation);
  779. this._internalWrite(this._indentationText.repeat(flooredIndentation));
  780. const overflow = this._currentIndentation - flooredIndentation;
  781. if (this._useTabs) {
  782. if (overflow > 0.5) {
  783. this._internalWrite(this._indentationText);
  784. }
  785. }
  786. else {
  787. const portion = Math.round(this._indentationText.length * overflow);
  788. // build up the string first, then append it for performance reasons
  789. let text = "";
  790. for (let i = 0; i < portion; i++) {
  791. text += this._indentationText[i];
  792. }
  793. this._internalWrite(text);
  794. }
  795. }
  796. /** @internal */
  797. _newLineIfNewLineOnNextWrite() {
  798. if (!this._newLineOnNextWrite) {
  799. return;
  800. }
  801. this._newLineOnNextWrite = false;
  802. this.newLine();
  803. }
  804. /** @internal */
  805. _internalWrite(text) {
  806. if (text.length === 0) {
  807. return;
  808. }
  809. this._texts.push(text);
  810. this._length += text.length;
  811. }
  812. /** @internal */
  813. _getIndentationLevelFromArg(countOrText) {
  814. if (typeof countOrText === "number") {
  815. if (countOrText < 0) {
  816. throw new Error("Passed in indentation level should be greater than or equal to 0.");
  817. }
  818. return countOrText;
  819. }
  820. else if (typeof countOrText === "string") {
  821. if (!CodeBlockWriter._spacesOrTabsRegEx.test(countOrText)) {
  822. throw new Error("Provided string must be empty or only contain spaces or tabs.");
  823. }
  824. const { spacesCount, tabsCount } = getSpacesAndTabsCount(countOrText);
  825. return tabsCount + spacesCount / this._indentNumberOfSpaces;
  826. }
  827. else {
  828. throw new Error("Argument provided must be a string or number.");
  829. }
  830. }
  831. /** @internal */
  832. _setIndentationState(state) {
  833. this._currentIndentation = state.current;
  834. this._queuedIndentation = state.queued;
  835. this._queuedOnlyIfNotBlock = state.queuedOnlyIfNotBlock;
  836. }
  837. /** @internal */
  838. _getIndentationState() {
  839. return {
  840. current: this._currentIndentation,
  841. queued: this._queuedIndentation,
  842. queuedOnlyIfNotBlock: this._queuedOnlyIfNotBlock,
  843. };
  844. }
  845. }
  846. /** @internal */
  847. Object.defineProperty(CodeBlockWriter, "_newLineRegEx", {
  848. enumerable: true,
  849. configurable: true,
  850. writable: true,
  851. value: /\r?\n/
  852. });
  853. /** @internal */
  854. Object.defineProperty(CodeBlockWriter, "_spacesOrTabsRegEx", {
  855. enumerable: true,
  856. configurable: true,
  857. writable: true,
  858. value: /^[ \t]*$/
  859. });
  860. export default CodeBlockWriter;
  861. function isRegExStart(currentChar, pastChar, pastPastChar) {
  862. return pastChar === CHARS.FORWARD_SLASH
  863. && currentChar !== CHARS.FORWARD_SLASH
  864. && currentChar !== CHARS.ASTERISK
  865. && pastPastChar !== CHARS.ASTERISK
  866. && pastPastChar !== CHARS.FORWARD_SLASH;
  867. }
  868. function getIndentationText(useTabs, numberSpaces) {
  869. if (useTabs) {
  870. return "\t";
  871. }
  872. return Array(numberSpaces + 1).join(" ");
  873. }
  874. function getSpacesAndTabsCount(str) {
  875. let spacesCount = 0;
  876. let tabsCount = 0;
  877. for (let i = 0; i < str.length; i++) {
  878. const charCode = str.charCodeAt(i);
  879. if (charCode === CHARS.SPACE) {
  880. spacesCount++;
  881. }
  882. else if (charCode === CHARS.TAB) {
  883. tabsCount++;
  884. }
  885. }
  886. return { spacesCount, tabsCount };
  887. }