w-ftp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +54 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +163 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +14 -0
  9. package/docs/WFtp.mjs.html +665 -0
  10. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  11. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  12. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  13. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  14. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  15. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  16. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  17. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  18. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  19. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  23. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  27. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  28. package/docs/global.html +439 -0
  29. package/docs/index.html +81 -0
  30. package/docs/jsftp.js.html +925 -0
  31. package/docs/scripts/collapse.js +20 -0
  32. package/docs/scripts/linenumber.js +25 -0
  33. package/docs/scripts/nav.js +12 -0
  34. package/docs/scripts/polyfill.js +4 -0
  35. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  36. package/docs/scripts/prettify/lang-css.js +2 -0
  37. package/docs/scripts/prettify/prettify.js +28 -0
  38. package/docs/scripts/search.js +83 -0
  39. package/docs/styles/jsdoc.css +765 -0
  40. package/docs/styles/prettify.css +79 -0
  41. package/g-download.mjs +84 -0
  42. package/g-upload.mjs +65 -0
  43. package/package.json +30 -0
  44. package/src/WFtp.mjs +596 -0
  45. package/src/jsftp.js +856 -0
  46. package/test/all.test.mjs +10 -0
  47. package/toolg/addVersion.mjs +4 -0
  48. package/toolg/cleanFolder.mjs +4 -0
  49. package/toolg/gDistRollup.mjs +27 -0
  50. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,665 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>WFtp.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
18
+ </head>
19
+ <body>
20
+
21
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
22
+ <label for="nav-trigger" class="navicon-button x">
23
+ <div class="navicon"></div>
24
+ </label>
25
+
26
+ <label for="nav-trigger" class="overlay"></label>
27
+
28
+ <nav >
29
+
30
+ <h2><a href="index.html">Home</a></h2><h3>Global</h3><ul><li><a href="global.html#WFtp">WFtp</a></li></ul>
31
+ </nav>
32
+
33
+ <div id="main">
34
+
35
+ <h1 class="page-title">WFtp.mjs</h1>
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ <section>
44
+ <article>
45
+ <pre class="prettyprint source linenums"><code>import path from 'path'
46
+ import fs from 'fs'
47
+ import get from 'lodash/get'
48
+ import size from 'lodash/size'
49
+ import filter from 'lodash/filter'
50
+ import find from 'lodash/find'
51
+ import map from 'lodash/map'
52
+ import genPm from 'wsemi/src/genPm.mjs'
53
+ import cint from 'wsemi/src/cint.mjs'
54
+ import cstr from 'wsemi/src/cstr.mjs'
55
+ import ispint from 'wsemi/src/ispint.mjs'
56
+ import isestr from 'wsemi/src/isestr.mjs'
57
+ import iseobj from 'wsemi/src/iseobj.mjs'
58
+ import isfun from 'wsemi/src/isfun.mjs'
59
+ import isnum from 'wsemi/src/isnum.mjs'
60
+ import strleft from 'wsemi/src/strleft.mjs'
61
+ import pmSeries from 'wsemi/src/pmSeries.mjs'
62
+ import getFileName from 'wsemi/src/getFileName.mjs'
63
+ import fsTreeFolder from 'wsemi/src/fsTreeFolder.mjs'
64
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
65
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
66
+ import fsCreateFolder from 'wsemi/src/fsCreateFolder.mjs'
67
+ // import Jsftp from 'jsftp'
68
+ import Jsftp from './jsftp.js'
69
+ console.log('Jsftp', Jsftp)
70
+
71
+
72
+ /**
73
+ * 指定FTP伺服器資料夾內檔案並自動同步至本機,只能用於Windows作業系統
74
+ *
75
+ * @param {String} fp 輸入檔案路徑字串
76
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
77
+ * @param {String} [opt.ver='4.8'] 輸入調用windows程序之Net Framework版本字串,可有'4.5'、'4.6'、'4.7.2'與'4.8',預設'4.8'
78
+ * @returns {Promise} 回傳Promise,resolve回傳成功訊息,reject回傳錯誤訊息
79
+ * @example
80
+ * import WFtp from './src/WFtp.mjs'
81
+ *
82
+ * async function test() {
83
+ *
84
+ * //fp
85
+ * let fp = './g.mseed'
86
+ *
87
+ * //WFtp
88
+ * let r = await WFtp(fp)
89
+ * console.log(r)
90
+ * // [
91
+ * // {
92
+ * // name: 'SE.RST01.00.HNE.D.2020-09-17T013552.000000.csv',
93
+ * // path: null,
94
+ * // data: { heads: [Object], records: [Array] }
95
+ * // },
96
+ * // {
97
+ * // name: 'SE.RST01.00.HNN.D.2020-09-17T013552.000000.csv',
98
+ * // path: null,
99
+ * // data: { heads: [Object], records: [Array] }
100
+ * // },
101
+ * // {
102
+ * // name: 'SE.RST01.00.HNZ.D.2020-09-17T013552.000000.csv',
103
+ * // path: null,
104
+ * // data: { heads: [Object], records: [Array] }
105
+ * // }
106
+ * // ]
107
+ *
108
+ * }
109
+ * test()
110
+ * .catch((err) => {
111
+ * console.log('catch', err)
112
+ * })
113
+ *
114
+ */
115
+ function WFtp() {
116
+
117
+ function WFtpCore() {
118
+ let Ftp = null
119
+
120
+
121
+ async function ftpConn(opt = {}) {
122
+
123
+ //pm
124
+ let pm = genPm()
125
+
126
+ //hostname
127
+ let hostname = get(opt, 'hostname')
128
+ if (!isestr(hostname)) {
129
+ pm.reject(`hostname[${hostname}] is not a effective string`)
130
+ return pm
131
+ }
132
+
133
+ //port
134
+ let port = get(opt, 'port')
135
+ if (!ispint(port)) {
136
+ pm.reject(`port[${port}] is not a positive integer`)
137
+ return pm
138
+ }
139
+ port = cint(port)
140
+
141
+ //username
142
+ let username = get(opt, 'username')
143
+ if (!isestr(username)) {
144
+ pm.reject(`username[${username}] is not a effective string`)
145
+ return pm
146
+ }
147
+
148
+ //password
149
+ let password = get(opt, 'password')
150
+ if (!isestr(password)) {
151
+ pm.reject(`password[${password}] is not a effective string`)
152
+ return pm
153
+ }
154
+
155
+ try {
156
+ Ftp = new Jsftp({
157
+ host: hostname,
158
+ port, // defaults to 21
159
+ user: username, // defaults to "anonymous"
160
+ pass: password, // defaults to "@anonymous"
161
+ })
162
+ pm.resolve('ok')
163
+ }
164
+ catch (err) {
165
+ pm.reject(err)
166
+ }
167
+
168
+ return pm
169
+ }
170
+
171
+
172
+ async function ftpLs(remoteFd = '.') {
173
+
174
+ //pm
175
+ let pm = genPm()
176
+
177
+ //check
178
+ if (Ftp === null) {
179
+ pm.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
180
+ return pm
181
+ }
182
+
183
+ Ftp.ls(remoteFd, (err, res) => {
184
+ if (err) {
185
+ pm.reject(err)
186
+ }
187
+ else {
188
+ pm.resolve(res)
189
+ }
190
+ })
191
+
192
+ return pm
193
+ }
194
+
195
+
196
+ async function ftpQuit() {
197
+
198
+ //pm
199
+ let pm = genPm()
200
+
201
+ //check
202
+ if (Ftp === null) {
203
+ pm.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
204
+ return pm
205
+ }
206
+
207
+ Ftp.raw('quit', (err, res) => {
208
+ if (err) {
209
+ pm.reject(err)
210
+ }
211
+ else {
212
+ pm.resolve(res)
213
+ }
214
+ })
215
+
216
+ return pm
217
+ }
218
+
219
+
220
+ async function ftpStateFile(fpRemote) {
221
+
222
+ //ftpLs
223
+ let file = null
224
+ await ftpLs(fpRemote)
225
+ .then((res) => {
226
+ file = res
227
+ })
228
+ .catch(() => {
229
+ file = null
230
+ })
231
+
232
+ //get
233
+ file = get(file, 0, null)
234
+ // console.log('file', file)
235
+
236
+ return file
237
+ }
238
+
239
+
240
+ async function ftpCheckFile(fpRemote) {
241
+
242
+ //ftpStateFile
243
+ let file = await ftpStateFile(fpRemote)
244
+
245
+ //b
246
+ let b = iseobj(file)
247
+
248
+ return b
249
+ }
250
+
251
+
252
+ async function ftpDownload(fpRemote, fpLocal, cbProcess) {
253
+
254
+ //pm
255
+ let pm = genPm()
256
+
257
+ //check
258
+ if (Ftp === null) {
259
+ pm.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
260
+ return pm
261
+ }
262
+
263
+ //fpRemote
264
+ if (!isestr(fpRemote)) {
265
+ pm.reject(`fpRemote[${fpRemote}] is not a effective string`)
266
+ return pm
267
+ }
268
+
269
+ //fpLocal
270
+ if (!isestr(fpLocal)) {
271
+ pm.reject(`fpLocal[${fpLocal}] is not a effective string`)
272
+ return pm
273
+ }
274
+
275
+ //fdLocal
276
+ let fdLocal = path.basename(path.dirname(fpLocal))
277
+
278
+ //check fdLocal
279
+ if (!fsIsFolder(fdLocal)) {
280
+ fsCreateFolder(fdLocal)
281
+ }
282
+
283
+ //ftpStateFile
284
+ let file = await ftpStateFile(fpRemote)
285
+ // console.log('file', file)
286
+
287
+ //fileSize
288
+ let fileSize = get(file, 'size')
289
+
290
+ //check
291
+ if (!isnum(fileSize)) {
292
+ pm.reject(`can not get the size of file[${fpRemote}]`)
293
+ return pm
294
+ }
295
+ fileSize = cint(fileSize)
296
+ // console.log('fileSize', fileSize)
297
+
298
+ Ftp.get(fpRemote, (err, socket) => {
299
+
300
+ //check
301
+ if (err) {
302
+ pm.reject(err)
303
+ }
304
+
305
+ //check
306
+ if (fsIsFile(fpLocal)) {
307
+ fs.unlinkSync(fpLocal)
308
+ }
309
+
310
+ //createWriteStream
311
+ let writer = fs.createWriteStream(fpLocal)
312
+
313
+ //on data
314
+ let dss = 0
315
+ socket.on('data', (d) => {
316
+ // console.log('socket.on data', d)
317
+
318
+ //cbProcess
319
+ if (isfun(cbProcess)) {
320
+ let ds = size(d)
321
+ dss += ds
322
+ let r = dss / fileSize * 100
323
+ // console.log(`receving... ${dig((r), 1)}%`)
324
+ cbProcess({
325
+ progress: r,
326
+ name: getFileName(fpLocal),
327
+ fpRemote,
328
+ fpLocal,
329
+ })
330
+ }
331
+
332
+ //write
333
+ writer.write(d)
334
+
335
+ })
336
+
337
+ //on close
338
+ socket.on('close', (err) => {
339
+ if (err) {
340
+ pm.reject(err)
341
+ }
342
+ else {
343
+ pm.resolve('ok')
344
+ }
345
+ })
346
+
347
+ //resume
348
+ socket.resume()
349
+
350
+ })
351
+
352
+ return pm
353
+ }
354
+
355
+
356
+ async function ftpUpload(fpLocal, fpRemote, cbProcess) {
357
+
358
+ //pm
359
+ let pm = genPm()
360
+
361
+ //check
362
+ if (Ftp === null) {
363
+ pm.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
364
+ return pm
365
+ }
366
+
367
+ //fpRemote
368
+ if (!isestr(fpRemote)) {
369
+ pm.reject(`fpRemote[${fpRemote}] is not a effective string`)
370
+ return pm
371
+ }
372
+
373
+ //fpLocal
374
+ if (!isestr(fpLocal)) {
375
+ pm.reject(`fpLocal[${fpLocal}] is not a effective string`)
376
+ return pm
377
+ }
378
+
379
+ //check
380
+ if (!fsIsFile(fpLocal)) {
381
+ pm.reject(`fpLocal[${fpLocal}] is not a file`)
382
+ return pm
383
+ }
384
+
385
+ //statSync
386
+ let stat = fs.statSync(fpLocal)
387
+
388
+ //fileSize
389
+ let fileSize = get(stat, 'size')
390
+
391
+ //check
392
+ if (!isnum(fileSize)) {
393
+ pm.reject(`can not get the size of file[${fpLocal}]`)
394
+ return pm
395
+ }
396
+ fileSize = cint(fileSize)
397
+ // console.log('fileSize', fileSize)
398
+
399
+ //bufferLocal
400
+ let bufferLocal = fs.readFileSync(fpLocal)
401
+
402
+ // //createReadStream
403
+ // let reader = fs.createReadStream(fpLocal)
404
+
405
+ let dss = 0
406
+ Ftp.put(bufferLocal, fpRemote, (err, buffer) => {
407
+ // console.log('Ftp.put', err, buffer)
408
+
409
+ //check
410
+ if (err) {
411
+ pm.reject(err)
412
+ return
413
+ }
414
+
415
+ //check
416
+ if (size(buffer) === 0) {
417
+ pm.resolve('ok')
418
+ return
419
+ }
420
+
421
+ //cbProcess
422
+ if (isfun(cbProcess)) {
423
+ let ds = size(buffer)
424
+ dss += ds
425
+ let r = dss / fileSize * 100
426
+ // console.log(`receving... ${dig((r), 1)}%`)
427
+ cbProcess({
428
+ progress: r,
429
+ name: getFileName(fpLocal),
430
+ fpRemote,
431
+ fpLocal,
432
+ })
433
+ }
434
+
435
+ })
436
+
437
+ return pm
438
+ }
439
+
440
+
441
+ async function ftpSyncToLocal(fdRemote, fdLocal, cbProcess) {
442
+
443
+ //check
444
+ if (Ftp === null) {
445
+ return Promise.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
446
+ }
447
+
448
+ //ftpLs
449
+ let fsRemote = await ftpLs(fdRemote)
450
+ // fsRemote = map(fsRemote, (v) => {
451
+ // let d = dayjs(v.time)
452
+ // v.stat = d.format('YYYY-MM-DDTHH:mm:ss')
453
+ // return v
454
+ // })
455
+ // console.log('fsRemote', fsRemote)
456
+
457
+ //check fdLocal
458
+ if (!fsIsFolder(fdLocal)) {
459
+ fsCreateFolder(fdLocal)
460
+ }
461
+
462
+ //fsTreeFolder
463
+ let fsLocal = fsTreeFolder(fdLocal)
464
+ fsLocal = filter(fsLocal, (v) => {
465
+ return !v.isFolder
466
+ })
467
+ fsLocal = map(fsLocal, (v) => {
468
+ let stat = fs.statSync(v.path)
469
+ v.size = cstr(get(stat, 'size', ''))
470
+ return v
471
+ })
472
+ // console.log('fsLocal', fsLocal)
473
+
474
+ //pmSeries
475
+ let n = 0
476
+ let ss = []
477
+ await pmSeries(fsRemote, async (fileRemote) => {
478
+
479
+ //fileLocal
480
+ let fileLocal = find(fsLocal, { name: fileRemote.name })
481
+ // console.log('fileLocal', fileLocal)
482
+
483
+ //fpRemote
484
+ // let fpRemote = path.resolve(fdRemote, v.name)
485
+ let fpRemote = fdRemote
486
+ if (!strleft(fpRemote, 1) !== '/') {
487
+ fpRemote += '/'
488
+ }
489
+ fpRemote += get(fileRemote, 'name', '')
490
+ // console.log('fpRemote', fpRemote)
491
+
492
+ //fpLocal
493
+ let fpLocal = path.resolve(fdLocal, get(fileRemote, 'name', ''))
494
+ // console.log('fpLocal', fpLocal)
495
+
496
+ //cb
497
+ let cb = (msg) => {
498
+ if (isfun(cbProcess)) {
499
+ cbProcess(msg)
500
+ }
501
+ }
502
+
503
+ //ftpDownload
504
+ if (!iseobj(fileLocal)) {
505
+ //本地檔案不存在
506
+ await ftpDownload(fpRemote, fpLocal, cb)
507
+ n++
508
+ ss.push({
509
+ name: fileRemote.name,
510
+ reason: 'no local file',
511
+ })
512
+ }
513
+ else if (fileRemote.size !== fileLocal.size) {
514
+ //本地檔案大小與遠端檔案大小不同
515
+ await ftpDownload(fpRemote, fpLocal, cb)
516
+ n++
517
+ ss.push({
518
+ name: fileRemote.name,
519
+ reason: `size[${fileLocal.size}] of local file != size[${fileRemote.size}] of remote file`,
520
+ })
521
+ }
522
+
523
+ })
524
+
525
+ return {
526
+ num: n,
527
+ files: ss,
528
+ }
529
+ }
530
+
531
+
532
+ async function ftpSyncToRemote(fdLocal, fdRemote, cbProcess) {
533
+
534
+ //check
535
+ if (Ftp === null) {
536
+ return Promise.reject(`FTP has not been initialized. please use 'conn' to construct FTP`)
537
+ }
538
+
539
+ //ftpLs
540
+ let fsRemote = await ftpLs(fdRemote)
541
+ // fsRemote = map(fsRemote, (v) => {
542
+ // let d = dayjs(v.time)
543
+ // v.stat = d.format('YYYY-MM-DDTHH:mm:ss')
544
+ // return v
545
+ // })
546
+ // console.log('fsRemote', fsRemote)
547
+
548
+ //check fdLocal
549
+ if (!fsIsFolder(fdLocal)) {
550
+ return Promise.reject(`fdLocal[${fdLocal}] is not a folder`)
551
+ }
552
+
553
+ //fsTreeFolder
554
+ let fsLocal = fsTreeFolder(fdLocal)
555
+ fsLocal = filter(fsLocal, (v) => {
556
+ return !v.isFolder
557
+ })
558
+ fsLocal = map(fsLocal, (v) => {
559
+ let stat = fs.statSync(v.path)
560
+ v.size = cstr(get(stat, 'size', ''))
561
+ return v
562
+ })
563
+ // console.log('fsLocal', fsLocal)
564
+
565
+ //pmSeries
566
+ let n = 0
567
+ let ss = []
568
+ await pmSeries(fsLocal, async (fileLocal) => {
569
+
570
+ //fileRemote
571
+ let fileRemote = find(fsRemote, { name: fileLocal.name })
572
+ // console.log('fileRemote', fileRemote)
573
+
574
+ //fpRemote
575
+ // let fpRemote = path.resolve(fdRemote, v.name)
576
+ let fpRemote = fdRemote
577
+ if (!strleft(fpRemote, 1) !== '/') {
578
+ fpRemote += '/'
579
+ }
580
+ fpRemote += get(fileLocal, 'name', '')
581
+ // console.log('fpRemote', fpRemote)
582
+
583
+ //fpLocal
584
+ let fpLocal = path.resolve(fdLocal, get(fileLocal, 'name', ''))
585
+ // console.log('fpLocal', fpLocal)
586
+
587
+ //cb
588
+ let cb = (msg) => {
589
+ if (isfun(cbProcess)) {
590
+ cbProcess(msg)
591
+ }
592
+ }
593
+
594
+ //ftpUpload
595
+ if (!iseobj(fileRemote)) {
596
+ //遠端檔案不存在
597
+ await ftpUpload(fpLocal, fpRemote, cb)
598
+ n++
599
+ ss.push({
600
+ name: fileLocal.name,
601
+ reason: 'no Remote file',
602
+ })
603
+ }
604
+ else if (fileLocal.size !== fileRemote.size) {
605
+ //遠端檔案大小與本機檔案大小不同
606
+ await ftpUpload(fpLocal, fpRemote, cb)
607
+ n++
608
+ ss.push({
609
+ name: fileLocal.name,
610
+ reason: `size[${fileRemote.size}] of remote file != size[${fileLocal.size}] of local file`,
611
+ })
612
+ }
613
+
614
+ })
615
+
616
+ return {
617
+ num: n,
618
+ files: ss,
619
+ }
620
+ }
621
+
622
+
623
+ return {
624
+ conn: ftpConn,
625
+ ls: ftpLs,
626
+ stateFile: ftpStateFile,
627
+ checkFile: ftpCheckFile,
628
+ download: ftpDownload,
629
+ syncToLocal: ftpSyncToLocal,
630
+ syncToRemote: ftpSyncToRemote,
631
+ upload: ftpUpload,
632
+ quit: ftpQuit,
633
+ }
634
+ }
635
+
636
+ return new WFtpCore()
637
+ }
638
+
639
+
640
+ export default WFtp
641
+ </code></pre>
642
+ </article>
643
+ </section>
644
+
645
+
646
+
647
+
648
+
649
+
650
+ </div>
651
+
652
+ <br class="clear">
653
+
654
+ <footer>
655
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.6.10</a> on Sat Jul 23 2022 13:03:06 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
656
+ </footer>
657
+
658
+ <script>prettyPrint();</script>
659
+ <script src="scripts/polyfill.js"></script>
660
+ <script src="scripts/linenumber.js"></script>
661
+
662
+
663
+
664
+ </body>
665
+ </html>