Passed
Push — master ( e51e0c...68cdf2 )
by Barry
01:05
created

helpers.js ➔ replaceLineEndings   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 2
dl 0
loc 25
rs 8.439
c 0
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
  // 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
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
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
    }
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...
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
    c = findCode(txt, lookFrom)
141
    if (c.start === -1 || m.start < c.start || m.start > (c.start + c.length)) {
142
      // the mdp start we've found is not within a code fence
143
      break
144
    }
145
    // the mdp start we've found is within a code fence so find the next one
146
    lookFrom = c.start + c.length
147
  }
148
  return m
149
150
  function _findMdpStart (txt, start) {
151
    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
152
    regex.lastIndex = start
153
    let regexResult = regex.exec(txt)
154
    if (regexResult === null) { return {start: -1} }
155
    let r = {
156
      start: regexResult.index + regexResult[1].length,
157
      internalStart: regexResult.index + regexResult[0].length,
158
      commandString: regexResult[3].substring(1, regexResult[3].length - 1)
159
    }
160
    return r
161
  }
162
}
163
164
function _findMdpEndUnfenced (txt, opening, start) {
165
  let lookFrom = start
166
  let m, c
167
  while (true) {
168
    m = _findMdpEnd(txt, opening, lookFrom - 2)
169
    if (m.start === -1) { return m }
170
    c = findCode(txt, lookFrom)
171
    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
172
    // the mdp end we've found is within a code fence so find the next one
173
    lookFrom = c.start + c.length
174
  }
175
  return m
176
177
  function _findMdpEnd (txt, opening, start) {
178
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
179
    let regex = /(\r\n|\n)([ ]{0,3}\[<[^\r\n\t\0[\]]*\]: #)(\r\n|\n|$)/g
180
    regex.lastIndex = start
181
    let regexResult = regex.exec(txt)
182
    if (regexResult === null) { return {start: -1} }
183
    r.internalLength = regexResult.index - r.internalStart
184
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
185
    return r
186
  }
187
}
188
189
function _findCodeSpan (txt, start) {
190
  // finds an inline Code Span in the format: 'some text ``echo myfile.txt`` more text'
191
  // look for start
192
  let lookFrom = start
193
  while (true) {
194
    let s = _findCodeSpanStart(txt, lookFrom)
195
    if (s.start === -1) { return s }
196
    // look for end
197
    let e = _findCodeSpanEnd(txt, s)
198
    if (e.start !== -1) { return e }
199
    lookFrom = s.internalStart
200
  }
201
202
  function _findCodeSpanStart (txt, start) {
203
    let regex = /(^|[^`])(`+)[^`]/g
204
    // 1st capture group is the first (or no) character prior to the identifying `'s
205
    // 2nd group is the ` characters (however many there are)
206
    regex.lastIndex = start
207
    let regexResult = regex.exec(txt)
208
    if (regexResult === null) { return {start: -1} }
209
    let r = {
210
      start: regexResult.index + regexResult[1].length,
211
      internalStart: regexResult.index + regexResult[1].length + regexResult[2].length,
212
      info: {
213
        codeFence: regexResult[2]
214
      }
215
    }
216
    return r
217
  }
218
219
  function _findCodeSpanEnd (txt, opening) {
220
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
221
    let regex = RegExp('([^`])(' + r.info.codeFence + ')($|[^`])', 'g')
222
    regex.lastIndex = r.internalStart
223
    let regexResult = regex.exec(txt)
224
    if (regexResult === null) { return {start: -1} }
225
    r.internalLength = regexResult.index + regexResult[1].length - r.internalStart
226
    r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
227
    r.commandString = ''
228
    return r
229
  }
230
}
231
232
function _findIndentedCode (txt, start) {
233
  let regex = /((?:^|\r\n|\n)[ ]{4,}[^\r\n\0]*){1,}/g
234
  regex.lastIndex = start
235
  let regexResult = regex.exec(txt)
236
  if (regexResult === null) {
237
    return {start: -1}
238
  } else {
239
    return {
240
      start: regexResult.index,
241
      length: regexResult[0].length,
242
      internalStart: regexResult.index,
243
      internalLength: regexResult[0].length,
244
      info: {indent: regexResult[2]},
245
      commandString: ''
246
    }
247
  }
248
}
249
250
function _findFencedCode (txt, start) {
251
  // if the internalLength is returned as -1 this means that text cannot simply be inserted at the internalStart
252
  // location. Instead an additional preceding new line must be inserted along with the new text
253
  // another way to look at this is that the internal text is 1 character short
254
  // a value of -2 indicates a CRLF needs to be inserted
255
  let a = _findOpeningCodeFence(txt, start)
256
  if (a.start === -1) { return a }
257
  return _findClosingCodeFence(txt, a)
258
259
  function _findOpeningCodeFence (txt, start) {
260
    // returns the location and type of the next opening code fence
261
    let regex = /(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})(([ ]{0,3})([`]{3,}|[~]{3,})([^\n\r\0`]*))($|\r\n|\n)/g
262
    /** The regex groups are:
263
      * 0: the full match including any preamble block markup
264
      * 1: the leading new line character(s)
265
      * 2: the preamble consisting of block characters or nothing
266
      * 3: the full codeFence line without preamble
267
      * 4: any leading blank spaces at the start of the codeFence line
268
      * 5: the ` or ~ characters identifying the codeFence
269
      * 6: anything else on the line following the codeFence
270
      * 7: the final new line character(s)
271
    **/
272
    regex.lastIndex = start
273
    let regexResult = regex.exec(txt)
274
    if (regexResult === null) {
275
      return {start: -1}
276
    }
277
    let r = { start: regexResult.index + regexResult[1].length,
278
      info: {
279
        blockQuote: regexResult[2],
280
        spacesCount: regexResult[4].length,
281
        codeFence: regexResult[5]
282
      },
283
      commandString: regexResult[6].trim(),
284
      internalStart: regexResult.index + regexResult[0].length
285
    }
286
    return r
287
  }
288
289
  function _findClosingCodeFence (txt, opening) {
290
    // updates the passed result structure with the location and type of the next closing code fence
291
    // to match the opening cofeFence passed in
292
    let regex
293
    let r = JSON.parse(JSON.stringify(opening)) // create copy of opening structure passed in
294
    regex = RegExp('(^|\r\n|\n)([ ]{0,3}> |>|[ ]{0,0})[ ]{0,3}[' + r.info.codeFence[0] + ']{' + r.info.codeFence.length + ',}[ ]*($|\r\n|\n)', 'g')
295
    regex.lastIndex = r.internalStart - 2
296
    let regexResult = regex.exec(txt)
297
    if (opening.info.blockQuote.length !== 0) {
298
      // we are in a block quote so the codeFence will end at the earlier of the found regex OR end of the block quote
299
      let b = _findEndOfBlock(txt, r.internalStart)
300
      if (b !== -1 && (regexResult === null || b < (regexResult.index + regexResult[1].length))) {
301
        // the block end dictates the code block end
302
        r.internalLength = b - r.internalStart
303
        r.length = b - r.start
304
        return r
305
      }
306
    }
307
    if (regexResult === null) {
308
      r.internalLength = txt.length - r.internalStart
309
      r.length = txt.length - r.start
310
    } else {
311
      r.internalLength = regexResult.index - r.internalStart
312
      r.length = regexResult.index + regexResult[0].length - regexResult[3].length - r.start
313
    }
314
    return r
315
  }
316
317
  function _findEndOfBlock (txt, start) {
318
    // finds the first line which is not marked as block
319
    let regex = /(\r\n|\n)(?!([ ]{0,3}> |>))[^>\r\n]*/g
320
    regex.lastIndex = start
321
    let regexResult = regex.exec(txt)
322
    if (regexResult === null) {
323
      return -1
324
    } else {
325
      return regexResult.index
326
    }
327
  }
328
}
329