Passed
Push — master ( d1a202...380a8f )
by Barry
01:01
created

helpers.js ➔ ... ➔ _findClosingCodeFence   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
nc 5
nop 2
dl 0
loc 27
rs 8.439
c 1
b 0
f 0
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
  let posn = start
54
  let x
55
  while (true) {
56
    x = findCode(txt, posn)
57
    if (x.start === -1 || x.commandString.indexOf('mdpInsert ') !== -1) {
58
      return x
59
    } else {
60
      posn = x.start + x.length
61
    }
62
  }
1 ignored issue
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
63
}
64
65
export function findMdpInsert (txt, start) {
66
  let s = _findMdpStartUnfenced(txt, start)
67
  if (s.start === -1) { return s }
68
  let s1 = JSON.parse(JSON.stringify(s)) // create copy
69
  let depth = 1
70
  let e
71
  let posn = s1.internalStart
72
  while (depth !== 0) {
73
    e = _findMdpEndUnfenced(txt, s, posn)
74
    if (e.start === -1) {
75
      // we have not found any more ends so we need to return a fail
76
      return e
77
    }
78
    s1 = _findMdpStartUnfenced(txt, posn)
79
    if (s1.start !== -1) {
80
      // we have found another start pattern
81
      if (s1.start < (e.internalStart + e.internalLength)) {
82
        depth++
83
        posn = s1.internalStart
84
      } else {
85
        depth--
86
        posn = e.start + e.length
87
      }
88
    } else {
89
      depth--
90
      posn = e.start + e.length
91
    }
92
    if (depth > 5) { return {start: -1} }
93
  }
94
  return e
1 ignored issue
show
Comprehensibility Bug introduced by
The variable e does not seem to be initialized in case the while loop on line 72 is not entered. Are you sure this can never be the case?
Loading history...
95
}
96
97
export function earlierOf (a, b) {
98
  // inspects the .start property of a and b and returns the one
99
  // with the lowest start position
100
  if (b.start !== -1 && (a.start === -1 || b.start < a.start)) {
101
    return b
102
  } else {
103
    return a
104
  }
105
}
106
107
function _findMdpStartUnfenced (txt, start) {
108
  let lookFrom = start
109
  let m, c
110
  while (true) {
111
    m = _findMdpStart(txt, lookFrom)
112
    if (m.start === -1) { return m }
113
    c = findCode(txt, lookFrom)
114
    if (c.start === -1 || m.start < c.start || m.start > (c.start + c.length)) {
115
      // the mdp start we've found is not within a code fence
116
      break
117
    }
118
    // the mdp start we've found is within a code fence so find the next one
119
    lookFrom = c.start + c.length
120
  }
121
  return m
1 ignored issue
show
Comprehensibility Bug introduced by
The variable m does not seem to be initialized in case the while loop on line 110 is not entered. Are you sure this can never be the case?
Loading history...
122
123
  function _findMdpStart (txt, start) {
124
    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
125
    regex.lastIndex = start
126
    let regexResult = regex.exec(txt)
127
    if (regexResult === null) { return {start: -1} }
128
    let r = {
129
      start: regexResult.index + regexResult[1].length,
130
      internalStart: regexResult.index + regexResult[0].length,
131
      commandString: regexResult[3].substring(1, regexResult[3].length - 1)
132
    }
133
    return r
134
  }
135
}
136
137
function _findMdpEndUnfenced (txt, opening, start) {
138
  let lookFrom = start
139
  let m, c
140
  while (true) {
141
    m = _findMdpEnd(txt, opening, lookFrom - 2)
142
    if (m.start === -1) { return m }
143
    c = findCode(txt, lookFrom)
144
    if (c.start === -1 || (m.internalStart + m.internalLength) < c.start || (m.internalStart + m.internalLength) > (c.start + c.length)) { break } // the mdp end we've found is not within a code fence
145
    // the mdp end we've found is within a code fence so find the next one
146
    lookFrom = c.start + c.length
147
  }
148
  return m
1 ignored issue
show
Comprehensibility Bug introduced by
The variable m does not seem to be initialized in case the while loop on line 140 is not entered. Are you sure this can never be the case?
Loading history...
149
150
  function _findMdpEnd (txt, opening, start) {
151
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
152
    let regex = /(\r\n|\n)([ ]{0,3}\[<[^\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
    r.internalLength = regexResult.index - r.internalStart
157
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
158
    return r
159
  }
160
}
161
162
function _findCodeSpan (txt, start) {
163
  // finds an inline Code Span in the format: 'some text ``echo myfile.txt`` more text'
164
  // look for start
165
  let lookFrom = start
166
  while (true) {
167
    let s = _findCodeSpanStart(txt, lookFrom)
168
    if (s.start === -1) { return s }
169
    // look for end
170
    let e = _findCodeSpanEnd(txt, s)
171
    if (e.start !== -1) { return e }
172
    lookFrom = s.internalStart
173
  }
174
175
  function _findCodeSpanStart (txt, start) {
176
    let regex = /(^|[^`])(`+)[^`]/g
177
    // 1st capture group is the first (or no) character prior to the identifying `'s
178
    // 2nd group is the ` characters (however many there are)
179
    regex.lastIndex = start
180
    let regexResult = regex.exec(txt)
181
    if (regexResult === null) { return {start: -1} }
182
    let r = {
183
      start: regexResult.index + regexResult[1].length,
184
      internalStart: regexResult.index + regexResult[1].length + regexResult[2].length,
185
      info: {
186
        codeFence: regexResult[2]
187
      }
188
    }
189
    return r
190
  }
191
192
  function _findCodeSpanEnd (txt, opening) {
193
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
194
    let regex = RegExp('([^`])(' + r.info.codeFence + ')($|[^`])', 'g')
195
    regex.lastIndex = r.internalStart
196
    let regexResult = regex.exec(txt)
197
    if (regexResult === null) { return {start: -1} }
198
    r.internalLength = regexResult.index + regexResult[1].length - r.internalStart
199
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
200
    r.commandString = ''
201
    return r
202
  }
1 ignored issue
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
203
}
204
205
function _findIndentedCode (txt, start) {
206
  let regex = /((?:^|\r\n|\n)[ ]{4,}[^\r\n\0]*){1,}/g
207
  regex.lastIndex = start
208
  let regexResult = regex.exec(txt)
209
  if (regexResult === null) {
210
    return {start: -1}
211
  } else {
212
    return {
213
      start: regexResult.index,
214
      length: regexResult[0].length,
215
      internalStart: regexResult.index,
216
      internalLength: regexResult[0].length,
217
      info: {indent: regexResult[2]},
218
      commandString: ''
219
    }
220
  }
221
}
222
223
function _findFencedCode (txt, start) {
224
  // if the internalLength is returned as -1 this means that text cannot simply be inserted at the internalStart
225
  // location. Instead an additional preceding new line must be inserted along with the new text
226
  // another way to look at this is that the internal text is 1 character short
227
  // a value of -2 indicates a CRLF needs to be inserted
228
  let a = _findOpeningCodeFence(txt, start)
229
  if (a.start === -1) { return a }
230
  return _findClosingCodeFence(txt, a)
231
232
  function _findOpeningCodeFence (txt, start) {
233
    // returns the location and type of the next opening code fence
234
    let regex = /(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})(([ ]{0,3})([`]{3,}|[~]{3,})([^\n\r\0`]*))($|\r\n|\n)/g
235
    /** The regex groups are:
236
      * 0: the full match including any preamble block markup
237
      * 1: the leading new line character(s)
238
      * 2: the preamble consisting of block characters or nothing
239
      * 3: the full codeFence line without preamble
240
      * 4: any leading blank spaces at the start of the codeFence line
241
      * 5: the ` or ~ characters identifying the codeFence
242
      * 6: anything else on the line following the codeFence
243
      * 7: the final new line character(s)
244
    **/
245
    regex.lastIndex = start
246
    let regexResult = regex.exec(txt)
247
    if (regexResult === null) {
248
      return {start: -1}
249
    }
250
    let r = { start: regexResult.index + regexResult[1].length,
251
      info: {
252
        blockQuote: regexResult[2],
253
        spacesCount: regexResult[4].length,
254
        codeFence: regexResult[5]
255
      },
256
      commandString: regexResult[6].trim(),
257
      internalStart: regexResult.index + regexResult[0].length
258
    }
259
    return r
260
  }
261
262
  function _findClosingCodeFence (txt, opening) {
263
    // updates the passed result structure with the location and type of the next closing code fence
264
    // to match the opening cofeFence passed in
265
    let regex
266
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
267
    regex = RegExp('(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})[ ]{0,3}[' + r.info.codeFence[0] + ']{' + r.info.codeFence.length + ',}[ ]*($|\r\n|\n)', 'g')
268
    regex.lastIndex = r.internalStart - 2
269
    let regexResult = regex.exec(txt)
270
    if (opening.info.blockQuote.length !== 0) {
271
      // we are in a block quote so the codeFence will end at the earlier of the found regex OR end of the block quote
272
      let b = _findEndOfBlock(txt, r.internalStart)
273
      if (b !== -1 && (regexResult === null || b < (regexResult.index + regexResult[1].length))) {
274
        // the block end dictates the code block end
275
        r.internalLength = b - r.internalStart
276
        r.length = b - r.start
277
        return r
278
      }
279
    }
280
    if (regexResult === null) {
281
      r.internalLength = txt.length - r.internalStart
282
      r.length = txt.length - r.start
283
    } else {
284
      r.internalLength = regexResult.index - r.internalStart
285
      r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
286
    }
287
    return r
288
  }
289
290
  function _findEndOfBlock (txt, start) {
291
    // finds the first line which is not marked as block
292
    let regex = /(\r\n|\n)(?!([ ]{0,3}> |>))[^>\r\n]*/g
293
    regex.lastIndex = start
294
    let regexResult = regex.exec(txt)
295
    if (regexResult === null) {
296
      return -1
297
    } else {
298
      return regexResult.index
299
    }
300
  }
301
}
302