Passed
Push — master ( 8d4289...fc2d57 )
by Barry
01:09
created

helpers.js ➔ _findCodeEndingAfter   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 11
rs 9.4285
1
/** findMdpInsert and findCode functions use a similar layout to return the location and contents
2
  *   .start          => points at the character in the string why the other item starts (ie. comment or code block)
3
  *   .length         => is the overall length of the comment or code block.
4
  *   .internalStart  => points at the character in the string where the internal payload starts
5
  *   .internalLength => is the length of the internal payload
6
  *   .commandString  => is the command string found within the particular item
7
  *   .info           => is a structure containing further info about what was found
8
  * if start is returned as -1 then nothing was found
9
  *
10
  * The internalStart/internaLength defines the internal content which will be replaced. This does not include
11
  * leading and lagging CRLF/LF. So the replacement text is not required to have either leading or lagging line
12
  * endings. However, if the internalLength is negative this means that leading CRLF or LF must be added by the insertion
13
  * routine. The reason for this is that it allows insertions between code fences or mdpInsert pairs which have zero lines
14
  * between them.
15
  *
16
**/
17
18
export function findCode (txt, start) {
19
  /**
20
  * finds the next code in the string provided starting at position start
21
  * returns an object containing start, length, internalStart, internalLength
22
  *
23
  * there are three types of code insertion - code span (inline), fenced code and indented code
24
  *
25
  * eg. span (1 or more backticks):
26
  * some text ``echo myfile.txt`` more text
27
  *
28
  * eg. indented (4 or more indent spaces):
29
  * some text
30
  *     function test() {
31
  *       console.log('test')
32
  *     }
33
  * more text
34
  *
35
  * eg. fenced (3 or more backticks on a row on their own)
36
  * some text
37
  * ``` js
38
  * function test() {
39
  *   console.log('test')
40
  * }
41
  * ```
42
  * more text
43
  *
44
  **/
45
  let x = _findFencedCode(txt, start)
46
  let y = _findIndentedCode(txt, start)
47
  let z = _findCodeSpan(txt, start)
48
49
  return earlierOf(x, earlierOf(y, z))
50
}
51
52
export function findMdpCode (txt, start) {
53
  // finds the next location of mdpInsert in a code span within txt
54
  let posn = start
55
  let x
56
  while (true) {
57
    x = findCode(txt, posn)
58
    if (x.start === -1 || x.commandString.indexOf('mdpInsert ') !== -1) {
59
      return x
60
    } else {
61
      posn = x.start + x.length
62
    }
63
  }
64
}
65
66
export function findMdpInsert (txt, start) {
67
  let s = _findMdpStartUnfenced(txt, start)
68
  if (s.start === -1) { return s }
69
  let s1 = JSON.parse(JSON.stringify(s)) // create copy
70
  let depth = 1
71
  let e
72
  let posn = s1.internalStart - 2
73
  while (depth !== 0) {
74
    e = _findMdpEndUnfenced(txt, s, posn)
75
    if (e.start === -1) {
76
      // we have not found any more ends so we need to return a fail
77
      return e
78
    }
79
    s1 = _findMdpStartUnfenced(txt, posn)
80
    if (s1.start !== -1) {
81
      // we have found another start pattern
82
      if (s1.start < (e.internalStart + e.internalLength)) {
83
        depth++
84
        posn = s1.internalStart - 2
85
      } else {
86
        depth--
87
        posn = e.start + e.length
88
      }
89
    } else {
90
      depth--
91
      posn = e.start + e.length
92
    }
93
    if (depth > 5) { return {start: -1} }
94
  }
95
  return e
96
}
97
98
export function earlierOf (a, b) {
99
  // inspects the .start property of a and b and returns the one
100
  // with the lowest start position
101
  if (b.start !== -1 && (a.start === -1 || b.start < a.start)) {
102
    return b
103
  } else {
104
    return a
105
  }
106
}
107
108
export function replaceLineEndings (txt, CRLF) {
109
  // replaces line endings within txt, with CRLF if CRLF is true, otherwise just LF
110
  if (CRLF === true) {
111
    // NB can't do the replacement of '\n' with '\r\n' using regex due to javascript limitations
112
    let p = 0 // current position in the string
113
    let x = 0 // location of '\n' in the string
114
    let t = '' // output string
115
    while (true) {
116
      x = txt.indexOf('\n', p)
117
      if (x === -1) {
118
        // we've not got any more '\n' in the string so complete and exit
119
        t = t + txt.substr(p)
120
        return t
121
      } else if (x === 0 || txt.substr(x - 1, 1) !== '\r') {
122
        t = t + txt.substring(p, x) + '\r\n'
123
        p = x + 1
124
      } else {
125
        t = t + txt.substring(p, x + 1)
126
        p = x + 1
127
      }
128
    }
129
  } else {
130
    return txt.replace(/(\r\n)/g, '\n')
131
  }
132
}
133
134
function _findMdpStartUnfenced (txt, start) {
135
  let lookFrom = start
136
  let m, c
137
  while (true) {
138
    m = _findMdpStart(txt, lookFrom)
139
    if (m.start === -1) { return m }
140
    // we need to find the first code which ends after the mdp start
141
    c = _findCodeEndingAfter(txt, lookFrom, m.start)
142
    if (c.start === -1 || m.start < c.start) {
143
      // the mdp start we've found is not within a code fence
144
      break
145
    }
146
    // the mdp start we've found is within a code fence so find the next one
147
    lookFrom = c.start + c.length
148
  }
149
  return m
150
151
  function _findMdpStart (txt, start) {
152
    let regex = /(\r\n|\n|^)([ ]{0,3}\[>[^\r\n\t\0[\]]*\]: # (\([^\r\n\t\0]*\)|"[^\r\n\t\0]*"|'[^\r\n\t\0]*'))(\r\n|\n)/g
153
    regex.lastIndex = start
154
    let regexResult = regex.exec(txt)
155
    if (regexResult === null) { return {start: -1} }
156
    let r = {
157
      start: regexResult.index + regexResult[1].length,
158
      internalStart: regexResult.index + regexResult[0].length,
159
      commandString: regexResult[3].substring(1, regexResult[3].length - 1)
160
    }
161
    return r
162
  }
163
}
164
165
function _findCodeEndingAfter (txt, start, endingAfter) {
166
  // returns the first code block which ends after the position specified
167
  // the search starts at start
168
  let pos = start
169
  let c
170
  do {
171
    c = findCode(txt, pos)
172
    pos = c.start + c.length
173
  } while (c.start !== -1 && pos <= endingAfter)
174
  return c
175
}
176
177
function _findMdpEndUnfenced (txt, opening, start) {
178
  let lookFrom = start
179
  let m, c
180
  while (true) {
181
    m = _findMdpEnd(txt, opening, lookFrom - 2)
182
    if (m.start === -1) { return m }
183
    // we need to find the first code which ends after the mdpEnd starts
184
    c = _findCodeEndingAfter(txt, lookFrom, (m.internalStart + m.internalLength))
185
    if (c.start === -1 || (m.internalStart + m.internalLength) < c.start) { break } // the mdp end we've found is not within a code fence
186
    // the mdp end we've found is within a code fence so find the next one
187
    lookFrom = c.start + c.length
188
  }
189
  return m
190
191
  function _findMdpEnd (txt, opening, start) {
192
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
193
    let regex = /(\r\n|\n)([ ]{0,3}\[<[^\r\n\t\0[\]]*\]: #)(\r\n|\n|$)/g
194
    regex.lastIndex = start
195
    let regexResult = regex.exec(txt)
196
    if (regexResult === null) { return {start: -1} }
197
    r.internalLength = regexResult.index - r.internalStart
198
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
199
    return r
200
  }
201
}
202
203
function _findCodeSpan (txt, start) {
204
  // finds an inline Code Span in the format: 'some text ``echo myfile.txt`` more text'
205
  // look for start
206
  let lookFrom = start
207
  while (true) {
208
    let s = _findCodeSpanStart(txt, lookFrom)
209
    if (s.start === -1) { return s }
210
    // look for end
211
    let e = _findCodeSpanEnd(txt, s)
212
    if (e.start !== -1) { return e }
213
    lookFrom = s.internalStart
214
  }
215
216
  function _findCodeSpanStart (txt, start) {
217
    let regex = /(^|[^`])(`+)[^`]/g
218
    // 1st capture group is the first (or no) character prior to the identifying `'s
219
    // 2nd group is the ` characters (however many there are)
220
    regex.lastIndex = start
221
    let regexResult = regex.exec(txt)
222
    if (regexResult === null) { return {start: -1} }
223
    let r = {
224
      start: regexResult.index + regexResult[1].length,
225
      internalStart: regexResult.index + regexResult[1].length + regexResult[2].length,
226
      info: {
227
        codeFence: regexResult[2]
228
      }
229
    }
230
    return r
231
  }
232
233
  function _findCodeSpanEnd (txt, opening) {
234
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
235
    let regex = RegExp('([^`])(' + r.info.codeFence + ')($|[^`])', 'g')
236
    regex.lastIndex = r.internalStart
237
    let regexResult = regex.exec(txt)
238
    if (regexResult === null) { return {start: -1} }
239
    r.internalLength = regexResult.index + regexResult[1].length - r.internalStart
240
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
241
    r.commandString = ''
242
    return r
243
  }
244
}
245
246
function _findIndentedCode (txt, start) {
247
  let regex = /((?:^|\r\n|\n)[ ]{4,}[^\r\n\0]*){1,}/g
248
  regex.lastIndex = start
249
  let regexResult = regex.exec(txt)
250
  if (regexResult === null) {
251
    return {start: -1}
252
  } else {
253
    return {
254
      start: regexResult.index,
255
      length: regexResult[0].length,
256
      internalStart: regexResult.index,
257
      internalLength: regexResult[0].length,
258
      info: {indent: regexResult[2]},
259
      commandString: ''
260
    }
261
  }
262
}
263
264
function _findFencedCode (txt, start) {
265
  // if the internalLength is returned as -1 this means that text cannot simply be inserted at the internalStart
266
  // location. Instead an additional preceding new line must be inserted along with the new text
267
  // another way to look at this is that the internal text is 1 character short
268
  // a value of -2 indicates a CRLF needs to be inserted
269
  let a = _findOpeningCodeFence(txt, start)
270
  if (a.start === -1) { return a }
271
  return _findClosingCodeFence(txt, a)
272
273
  function _findOpeningCodeFence (txt, start) {
274
    // returns the location and type of the next opening code fence
275
    let regex = /(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})(([ ]{0,3})([`]{3,}|[~]{3,})([^\n\r\0`]*))($|\r\n|\n)/g
276
    /** The regex groups are:
277
      * 0: the full match including any preamble block markup
278
      * 1: the leading new line character(s)
279
      * 2: the preamble consisting of block characters or nothing
280
      * 3: the full codeFence line without preamble
281
      * 4: any leading blank spaces at the start of the codeFence line
282
      * 5: the ` or ~ characters identifying the codeFence
283
      * 6: anything else on the line following the codeFence
284
      * 7: the final new line character(s)
285
    **/
286
    regex.lastIndex = start
287
    let regexResult = regex.exec(txt)
288
    if (regexResult === null) {
289
      return {start: -1}
290
    }
291
    let r = { start: regexResult.index + regexResult[1].length,
292
      info: {
293
        blockQuote: regexResult[2],
294
        spacesCount: regexResult[4].length,
295
        codeFence: regexResult[5]
296
      },
297
      commandString: regexResult[6].trim(),
298
      internalStart: regexResult.index + regexResult[0].length
299
    }
300
    return r
301
  }
302
303
  function _findClosingCodeFence (txt, opening) {
304
    // updates the passed result structure with the location and type of the next closing code fence
305
    // to match the opening cofeFence passed in
306
    let regex
307
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
308
    regex = RegExp('(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})[ ]{0,3}[' + r.info.codeFence[0] + ']{' + r.info.codeFence.length + ',}[ ]*($|\r\n|\n)', 'g')
309
    regex.lastIndex = r.internalStart - 2
310
    let regexResult = regex.exec(txt)
311
    if (opening.info.blockQuote.length !== 0) {
312
      // we are in a block quote so the codeFence will end at the earlier of the found regex OR end of the block quote
313
      let b = _findEndOfBlock(txt, r.internalStart)
314
      if (b !== -1 && (regexResult === null || b < (regexResult.index + regexResult[1].length))) {
315
        // the block end dictates the code block end
316
        r.internalLength = b - r.internalStart
317
        r.length = b - r.start
318
        return r
319
      }
320
    }
321
    if (regexResult === null) {
322
      r.internalLength = txt.length - r.internalStart
323
      r.length = txt.length - r.start
324
    } else {
325
      r.internalLength = regexResult.index - r.internalStart
326
      r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
327
    }
328
    return r
329
  }
330
331
  function _findEndOfBlock (txt, start) {
332
    // finds the first line which is not marked as block
333
    let regex = /(\r\n|\n)(?!([ ]{0,3}> |>))[^>\r\n]*/g
334
    regex.lastIndex = start
335
    let regexResult = regex.exec(txt)
336
    if (regexResult === null) {
337
      return -1
338
    } else {
339
      return regexResult.index
340
    }
341
  }
342
}
343