state_block.mjs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. // Parser state class
  2. import Token from '../token.mjs'
  3. import { isSpace } from '../common/utils.mjs'
  4. function StateBlock (src, md, env, tokens) {
  5. this.src = src
  6. // link to parser instance
  7. this.md = md
  8. this.env = env
  9. //
  10. // Internal state vartiables
  11. //
  12. this.tokens = tokens
  13. this.bMarks = [] // line begin offsets for fast jumps
  14. this.eMarks = [] // line end offsets for fast jumps
  15. this.tShift = [] // offsets of the first non-space characters (tabs not expanded)
  16. this.sCount = [] // indents for each line (tabs expanded)
  17. // An amount of virtual spaces (tabs expanded) between beginning
  18. // of each line (bMarks) and real beginning of that line.
  19. //
  20. // It exists only as a hack because blockquotes override bMarks
  21. // losing information in the process.
  22. //
  23. // It's used only when expanding tabs, you can think about it as
  24. // an initial tab length, e.g. bsCount=21 applied to string `\t123`
  25. // means first tab should be expanded to 4-21%4 === 3 spaces.
  26. //
  27. this.bsCount = []
  28. // block parser variables
  29. // required block content indent (for example, if we are
  30. // inside a list, it would be positioned after list marker)
  31. this.blkIndent = 0
  32. this.line = 0 // line index in src
  33. this.lineMax = 0 // lines count
  34. this.tight = false // loose/tight mode for lists
  35. this.ddIndent = -1 // indent of the current dd block (-1 if there isn't any)
  36. this.listIndent = -1 // indent of the current list block (-1 if there isn't any)
  37. // can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'
  38. // used in lists to determine if they interrupt a paragraph
  39. this.parentType = 'root'
  40. this.level = 0
  41. // Create caches
  42. // Generate markers.
  43. const s = this.src
  44. for (let start = 0, pos = 0, indent = 0, offset = 0, len = s.length, indent_found = false; pos < len; pos++) {
  45. const ch = s.charCodeAt(pos)
  46. if (!indent_found) {
  47. if (isSpace(ch)) {
  48. indent++
  49. if (ch === 0x09) {
  50. offset += 4 - offset % 4
  51. } else {
  52. offset++
  53. }
  54. continue
  55. } else {
  56. indent_found = true
  57. }
  58. }
  59. if (ch === 0x0A || pos === len - 1) {
  60. if (ch !== 0x0A) { pos++ }
  61. this.bMarks.push(start)
  62. this.eMarks.push(pos)
  63. this.tShift.push(indent)
  64. this.sCount.push(offset)
  65. this.bsCount.push(0)
  66. indent_found = false
  67. indent = 0
  68. offset = 0
  69. start = pos + 1
  70. }
  71. }
  72. // Push fake entry to simplify cache bounds checks
  73. this.bMarks.push(s.length)
  74. this.eMarks.push(s.length)
  75. this.tShift.push(0)
  76. this.sCount.push(0)
  77. this.bsCount.push(0)
  78. this.lineMax = this.bMarks.length - 1 // don't count last fake line
  79. }
  80. // Push new token to "stream".
  81. //
  82. StateBlock.prototype.push = function (type, tag, nesting) {
  83. const token = new Token(type, tag, nesting)
  84. token.block = true
  85. if (nesting < 0) this.level-- // closing tag
  86. token.level = this.level
  87. if (nesting > 0) this.level++ // opening tag
  88. this.tokens.push(token)
  89. return token
  90. }
  91. StateBlock.prototype.isEmpty = function isEmpty (line) {
  92. return this.bMarks[line] + this.tShift[line] >= this.eMarks[line]
  93. }
  94. StateBlock.prototype.skipEmptyLines = function skipEmptyLines (from) {
  95. for (let max = this.lineMax; from < max; from++) {
  96. if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) {
  97. break
  98. }
  99. }
  100. return from
  101. }
  102. // Skip spaces from given position.
  103. StateBlock.prototype.skipSpaces = function skipSpaces (pos) {
  104. for (let max = this.src.length; pos < max; pos++) {
  105. const ch = this.src.charCodeAt(pos)
  106. if (!isSpace(ch)) { break }
  107. }
  108. return pos
  109. }
  110. // Skip spaces from given position in reverse.
  111. StateBlock.prototype.skipSpacesBack = function skipSpacesBack (pos, min) {
  112. if (pos <= min) { return pos }
  113. while (pos > min) {
  114. if (!isSpace(this.src.charCodeAt(--pos))) { return pos + 1 }
  115. }
  116. return pos
  117. }
  118. // Skip char codes from given position
  119. StateBlock.prototype.skipChars = function skipChars (pos, code) {
  120. for (let max = this.src.length; pos < max; pos++) {
  121. if (this.src.charCodeAt(pos) !== code) { break }
  122. }
  123. return pos
  124. }
  125. // Skip char codes reverse from given position - 1
  126. StateBlock.prototype.skipCharsBack = function skipCharsBack (pos, code, min) {
  127. if (pos <= min) { return pos }
  128. while (pos > min) {
  129. if (code !== this.src.charCodeAt(--pos)) { return pos + 1 }
  130. }
  131. return pos
  132. }
  133. // cut lines range from source.
  134. StateBlock.prototype.getLines = function getLines (begin, end, indent, keepLastLF) {
  135. if (begin >= end) {
  136. return ''
  137. }
  138. const queue = new Array(end - begin)
  139. for (let i = 0, line = begin; line < end; line++, i++) {
  140. let lineIndent = 0
  141. const lineStart = this.bMarks[line]
  142. let first = lineStart
  143. let last
  144. if (line + 1 < end || keepLastLF) {
  145. // No need for bounds check because we have fake entry on tail.
  146. last = this.eMarks[line] + 1
  147. } else {
  148. last = this.eMarks[line]
  149. }
  150. while (first < last && lineIndent < indent) {
  151. const ch = this.src.charCodeAt(first)
  152. if (isSpace(ch)) {
  153. if (ch === 0x09) {
  154. lineIndent += 4 - (lineIndent + this.bsCount[line]) % 4
  155. } else {
  156. lineIndent++
  157. }
  158. } else if (first - lineStart < this.tShift[line]) {
  159. // patched tShift masked characters to look like spaces (blockquotes, list markers)
  160. lineIndent++
  161. } else {
  162. break
  163. }
  164. first++
  165. }
  166. if (lineIndent > indent) {
  167. // partially expanding tabs in code blocks, e.g '\t\tfoobar'
  168. // with indent=2 becomes ' \tfoobar'
  169. queue[i] = new Array(lineIndent - indent + 1).join(' ') + this.src.slice(first, last)
  170. } else {
  171. queue[i] = this.src.slice(first, last)
  172. }
  173. }
  174. return queue.join('')
  175. }
  176. // re-export Token class to use in block rules
  177. StateBlock.prototype.Token = Token
  178. export default StateBlock