LocConverter.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. class LocConverter {
  7. /**
  8. * Creates an instance of LocConverter.
  9. * @param {string} input input
  10. */
  11. constructor(input) {
  12. /** @type {string} */
  13. this._input = input;
  14. /** @type {number} */
  15. this.line = 1;
  16. /** @type {number} */
  17. this.column = 0;
  18. /** @type {number} */
  19. this.pos = 0;
  20. // Next `\n` at/after `pos` (`input.length` if none, -1 = unknown);
  21. // when >= pos there is no `\n` in [pos, _nextNewline).
  22. /** @type {number} */
  23. this._nextNewline = -1;
  24. }
  25. /**
  26. * Returns location converter.
  27. * @param {number} pos position
  28. * @returns {LocConverter} location converter
  29. */
  30. get(pos) {
  31. if (this.pos !== pos) {
  32. const input = this._input;
  33. if (this.pos < pos) {
  34. // Advance: O(1) on the same line via the cached next-newline offset,
  35. // otherwise hop newline-to-newline with native indexOf — each re-scan
  36. // is bounded by one line, amortized O(1)/byte for monotone callers.
  37. let next = this._nextNewline;
  38. if (next < this.pos) {
  39. next = input.indexOf("\n", this.pos);
  40. if (next === -1) next = input.length;
  41. }
  42. if (pos <= next) {
  43. this.column += pos - this.pos;
  44. } else {
  45. let line = this.line;
  46. let last = next;
  47. for (;;) {
  48. line++;
  49. next = input.indexOf("\n", last + 1);
  50. if (next === -1) next = input.length;
  51. // `next >= input.length` guards termination for out-of-range `pos`.
  52. if (pos <= next || next >= input.length) break;
  53. last = next;
  54. }
  55. this.line = line;
  56. this.column = pos - last - 1;
  57. }
  58. this._nextNewline = next;
  59. } else if (this.line === 1) {
  60. // Retreat on line 1: no `\n` precedes `pos`, the cache stays valid.
  61. this.column = pos;
  62. } else {
  63. // Retreat: count newlines crossed in (pos, this.pos), i.e.
  64. // exclude the newline at `this.pos` itself. By convention a
  65. // `\n` is the last column of its line, so when `this.pos`
  66. // sits on one we're already on the line containing it; only
  67. // newlines strictly **before** `this.pos` and at-or-after
  68. // `pos` represent crossed line boundaries. The smallest
  69. // crossed one is the next newline after the new position.
  70. let i = input.lastIndexOf("\n", this.pos - 1);
  71. while (i >= pos) {
  72. this._nextNewline = i;
  73. this.line--;
  74. i = i > 0 ? input.lastIndexOf("\n", i - 1) : -1;
  75. }
  76. this.column = i === -1 ? pos : pos - i - 1;
  77. }
  78. this.pos = pos;
  79. }
  80. return this;
  81. }
  82. }
  83. module.exports = LocConverter;