@markbattistella/docsify-autoheaders 5.0.0 → 5.1.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.
- package/CHANGELOG.md +11 -2
- package/README.md +55 -116
- package/dist/docsify-auto-headers.js +610 -576
- package/dist/docsify-auto-headers.min.js +1 -2
- package/package.json +10 -10
|
@@ -1,579 +1,613 @@
|
|
|
1
|
-
/*! docsify-auto-headers 5.
|
|
1
|
+
/*! docsify-auto-headers 5.1.0 | (c) Mark Battistella */
|
|
2
2
|
; (() => {
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default settings for the Docsify Auto Headers plugin.
|
|
8
|
+
* @type {Object}
|
|
9
|
+
* @property {string} separator - The separator for header numbering (e.g., '.', '-', ')').
|
|
10
|
+
* @property {boolean} sidebar - Boolean indicating if headers should be added to the sidebar.
|
|
11
|
+
* @property {number|Object} levels - Number of header levels to include (1 to 6) or an object with start and finish properties.
|
|
12
|
+
* @property {boolean} debug - Boolean to enable or disable debug messages.
|
|
13
|
+
*/
|
|
14
|
+
const docsifyAutoHeadersDefaults = {
|
|
15
|
+
|
|
16
|
+
// Separator for header numbering (e.g., '.', '-', ')')
|
|
17
|
+
separator: '.',
|
|
18
|
+
|
|
19
|
+
// Boolean indicating if headers should be added to the sidebar
|
|
20
|
+
sidebar: false,
|
|
21
|
+
|
|
22
|
+
// Number of header levels to include (1 to 6) or an object with start and finish properties
|
|
23
|
+
levels: 6,
|
|
24
|
+
|
|
25
|
+
// Boolean to enable or disable debug messages
|
|
26
|
+
debug: false
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Main function for the Docsify Auto Headers plugin.
|
|
31
|
+
* @param {Object} hook - Docsify hook object.
|
|
32
|
+
* @param {Object} vm - Docsify virtual machine object.
|
|
33
|
+
*/
|
|
34
|
+
const autoHeaders = (hook, vm) => {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Predefined error messages for invalid configurations.
|
|
38
|
+
* @type {Object}
|
|
39
|
+
* @property {string} invalidConfiguration - General error message for invalid configuration.
|
|
40
|
+
* @property {string} invalidConfigurationSidebar - Error message for invalid sidebar configuration.
|
|
41
|
+
* @property {string} invalidConfigurationLevels - Error message for invalid levels configuration.
|
|
42
|
+
* @property {string} nonNumericHeadingLevel - Error message when levels start or finish values are not numbers.
|
|
43
|
+
* @property {string} badNumericOrderHeadingLevel - Error message when start value is greater than finish value.
|
|
44
|
+
* @property {string} outOfRangeHeadingLevel - Error message when start or finish values are out of range (1-6).
|
|
45
|
+
* @property {string} moreThanSixElements - Error message when more than 6 elements are found in the signifier.
|
|
46
|
+
* @property {string} invalidParsedElements - Error message when elements in the signifier are not purely numeric or alphabetic.
|
|
47
|
+
* @property {string} signifierNotFound - Error message when the auto header signifier is missing in the markdown file.
|
|
48
|
+
*/
|
|
49
|
+
const errorMessage = {
|
|
50
|
+
invalidConfiguration:
|
|
51
|
+
'Configuration settings are not set correctly. Please review the autoHeaders parameters and documentation.',
|
|
52
|
+
invalidConfigurationSidebar:
|
|
53
|
+
'The sidebar setting for autoHeaders only accepts a boolean of true or false. Please check you\'ve entered this data correctly.',
|
|
54
|
+
invalidConfigurationLevels:
|
|
55
|
+
'The levels settings for autoHeaders only accepts a number from 1-6 or an object with the start and finish options. Please check you\'ve entered this data correctly.',
|
|
56
|
+
nonNumericHeadingLevel:
|
|
57
|
+
'The levels setting has been configured with a start and finish option. However, the values for one of these is not a number. Please check you\'ve entered this data correctly.',
|
|
58
|
+
badNumericOrderHeadingLevel:
|
|
59
|
+
'The levels setting has been configured with a start and finish option. However, the start value is greater than the finish. Please check you\'ve entered this data correctly.',
|
|
60
|
+
outOfRangeHeadingLevel:
|
|
61
|
+
'The levels setting has been configured with a start and finish option. However, the values for one of these is not from 1-6. Please check you\'ve entered this data correctly.',
|
|
62
|
+
moreThanSixElements:
|
|
63
|
+
'The elements found in the signifier have equated to more than 6 headings. Please check the configuration of your markdown that you have no more than 6 numbers',
|
|
64
|
+
invalidParsedElements:
|
|
65
|
+
'The elements found in the signifier are not numbers only or alphabet only. Please check the configuration of your markdown that all the items are numeric or alphabetic.',
|
|
66
|
+
signifierNotFound:
|
|
67
|
+
'The current markdown file is missing the @autoHeader: or <!-- autoHeader: --> signifier',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Boolean flag indicating whether the processing should continue.
|
|
72
|
+
* @type {boolean}
|
|
73
|
+
*/
|
|
74
|
+
let shouldContinue = true;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Logs an error message if a configuration is invalid.
|
|
78
|
+
* @param {boolean} shouldShow - Boolean indicating if the error message should be shown.
|
|
79
|
+
* @param {string} message - The error message to log.
|
|
80
|
+
* @returns {null} Always returns null after logging the error.
|
|
81
|
+
*/
|
|
82
|
+
const logErrorMessage = (shouldShow, message) => {
|
|
83
|
+
if (shouldShow) {
|
|
84
|
+
console.warn(`Docsify Auto Headers:\n>> ${message}`);
|
|
85
|
+
}
|
|
86
|
+
shouldContinue = false;
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sets the default options for the plugin by merging user-defined options with defaults.
|
|
92
|
+
* @param {Object} options - User-defined options to override the default settings.
|
|
93
|
+
* @param {string} options.separator - The separator for header numbering.
|
|
94
|
+
* @param {number|Object} options.levels - Number of header levels to include or an object with start and finish properties.
|
|
95
|
+
* @param {boolean} options.sidebar - Boolean indicating if headers should be added to the sidebar.
|
|
96
|
+
* @param {boolean} options.debug - Boolean to enable or disable debug messages.
|
|
97
|
+
* @returns {Object|null} The final options object or null if there is an invalid configuration.
|
|
98
|
+
*/
|
|
99
|
+
const setDefaultOptions = (options) => {
|
|
100
|
+
if (!options.separator || options.levels === undefined) {
|
|
101
|
+
return logErrorMessage(options.debug, errorMessage.invalidConfiguration);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Map user-friendly separator names to actual separator characters
|
|
105
|
+
const separatorMap = {
|
|
106
|
+
'decimal': '.',
|
|
107
|
+
'dot': '.',
|
|
108
|
+
'dash': '-',
|
|
109
|
+
'hyphen': '-',
|
|
110
|
+
'bracket': ')',
|
|
111
|
+
'parenthesis': ')'
|
|
112
|
+
};
|
|
113
|
+
// Determine the separator to use
|
|
114
|
+
const separator = separatorMap[options.separator] || options.separator;
|
|
115
|
+
|
|
116
|
+
// Set the levels, defaulting to 6 if not provided
|
|
117
|
+
const levels = options.levels || 6;
|
|
118
|
+
|
|
119
|
+
// Ensure sidebar and debug options are booleans
|
|
120
|
+
const sidebar = !!options.sidebar;
|
|
121
|
+
|
|
122
|
+
const debug = !!options.debug;
|
|
123
|
+
|
|
124
|
+
return { separator, levels, sidebar, debug };
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates and retrieves the sidebar setting.
|
|
129
|
+
* @param {boolean} input - The sidebar setting input.
|
|
130
|
+
* @param {Object} options - The current plugin options.
|
|
131
|
+
* @param {boolean} options.debug - Boolean to enable or disable debug messages.
|
|
132
|
+
* @returns {boolean|null} The validated sidebar setting or null if the input is invalid.
|
|
133
|
+
*/
|
|
134
|
+
const validateThenGetSidebar = (input, options) => {
|
|
135
|
+
if (typeof input !== 'boolean') {
|
|
136
|
+
return logErrorMessage(options.debug, errorMessage.invalidConfigurationSidebar);
|
|
137
|
+
}
|
|
138
|
+
return input;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Validates and retrieves the heading range setting.
|
|
143
|
+
* @param {number|Object} input - The levels setting input, which can be a number or an object with start and finish properties.
|
|
144
|
+
* @param {Object} options - The current plugin options.
|
|
145
|
+
* @param {boolean} options.debug - Boolean to enable or disable debug messages.
|
|
146
|
+
* @returns {Object|null} The validated heading range configuration or null if the input is invalid.
|
|
147
|
+
*/
|
|
148
|
+
const validateThenGetHeadingRange = (input, options) => {
|
|
149
|
+
if (typeof input !== 'number' && (typeof input !== 'object' || input === null)) {
|
|
150
|
+
return logErrorMessage(options.debug, errorMessage.invalidConfigurationLevels);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Helper function to check if a value is within a specified range
|
|
154
|
+
const isInRange = (value, min, max) => value >= min && value <= max;
|
|
155
|
+
let start, finish;
|
|
156
|
+
|
|
157
|
+
if (typeof input === 'number') {
|
|
158
|
+
start = 1;
|
|
159
|
+
finish = input;
|
|
160
|
+
} else if (typeof input === 'object') {
|
|
161
|
+
({ start, finish } = input);
|
|
162
|
+
if (typeof start !== 'number' || typeof finish !== 'number') {
|
|
163
|
+
return logErrorMessage(options.debug, errorMessage.nonNumericHeadingLevel);
|
|
164
|
+
}
|
|
165
|
+
if (start > finish) {
|
|
166
|
+
return logErrorMessage(options.debug, errorMessage.badNumericOrderHeadingLevel);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isInRange(start, 1, 6) || !isInRange(finish, 1, 6)) {
|
|
171
|
+
return logErrorMessage(options.debug, errorMessage.outOfRangeHeadingLevel);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const headings = {};
|
|
175
|
+
for (let i = 1; i <= 6; i++) {
|
|
176
|
+
headings[`h${i}`] = { inScope: isInRange(i, start, finish) };
|
|
177
|
+
}
|
|
178
|
+
return headings;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Converts a number to a header string based on the type (numeric or alphabetic).
|
|
183
|
+
* @param {number} num - The number to convert.
|
|
184
|
+
* @param {string} type - The type of conversion ("numeric", "alphabetic-lower", "alphabetic-upper").
|
|
185
|
+
* @returns {string} The converted header string.
|
|
186
|
+
*/
|
|
187
|
+
const numberToHeader = (num, type) => {
|
|
188
|
+
if (type === "numeric") {
|
|
189
|
+
return num + ""; // Convert number to string
|
|
190
|
+
} else {
|
|
191
|
+
num--; // Adjust the number for zero-based index
|
|
192
|
+
let result = '';
|
|
193
|
+
while (num >= 0) {
|
|
194
|
+
let remainder = num % 26;
|
|
195
|
+
result = String.fromCharCode(65 + remainder) + result;
|
|
196
|
+
num = Math.floor(num / 26) - 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Convert result to lowercase if type is "alphabetic-lower"
|
|
200
|
+
return (type === "alphabetic-lower") ? result.toLowerCase() : result;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Converts a header string to a number based on the type.
|
|
206
|
+
* @param {string} header - The header string to convert.
|
|
207
|
+
* @param {string} type - The type of conversion ("numeric", "alphabetic-lower", "alphabetic-upper").
|
|
208
|
+
* @returns {number} The converted number.
|
|
209
|
+
*/
|
|
210
|
+
const headerToNumber = (header, type) => {
|
|
211
|
+
if (type === "numeric") {
|
|
212
|
+
return parseInt(header, 10); // Convert header string to number
|
|
213
|
+
} else {
|
|
214
|
+
header = header.toUpperCase(); // Convert header to uppercase for alphabetic conversion
|
|
215
|
+
let result = 0;
|
|
216
|
+
for (let i = 0; i < header.length; i++) {
|
|
217
|
+
result *= 26;
|
|
218
|
+
result += header.charCodeAt(i) - 65 + 1;
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parses the starting values for headers from the signifier.
|
|
226
|
+
* @param {string} headerNumbers - The header numbers string.
|
|
227
|
+
* @param {Object} options - The current plugin options.
|
|
228
|
+
* @param {string} options.separator - The separator for header numbering.
|
|
229
|
+
* @param {boolean} options.debug - Boolean to enable or disable debug messages.
|
|
230
|
+
* @returns {Array<Object>|null} The parsed header values as an array of objects or null if the input is invalid.
|
|
231
|
+
*/
|
|
232
|
+
const parseHeadingStartingValues = (headerNumbers, options) => {
|
|
233
|
+
// Helper functions to check if a string is all numeric or alphabetic
|
|
234
|
+
const isAllNumeric = (str) => /^\d+$/.test(str);
|
|
235
|
+
const isAllAlphabeticLower = (str) => /^[a-z]+$/.test(str);
|
|
236
|
+
const isAllAlphabeticUpper = (str) => /^[A-Z]+$/.test(str);
|
|
237
|
+
|
|
238
|
+
// Split the header numbers string by the separator and trim each element
|
|
239
|
+
let elements = headerNumbers.split(options.separator).map(el => el.trim());
|
|
240
|
+
if (elements.length > 6) {
|
|
241
|
+
return logErrorMessage(options.debug, errorMessage.moreThanSixElements);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check if all elements are numeric or alphabetic
|
|
245
|
+
const isNumeric = elements.every(isAllNumeric);
|
|
246
|
+
const isAlphabeticLower = elements.every(isAllAlphabeticLower);
|
|
247
|
+
const isAlphabeticUpper = elements.every(isAllAlphabeticUpper);
|
|
248
|
+
if (!isNumeric && !isAlphabeticLower && !isAlphabeticUpper) {
|
|
249
|
+
return logErrorMessage(options.debug, errorMessage.invalidParsedElements);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Ensure there are at least 6 elements by filling with default values
|
|
253
|
+
while (elements.length < 6) {
|
|
254
|
+
elements.push(isNumeric ? '1' : (isAlphabeticLower ? 'a' : 'A'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Determine the type of header values
|
|
258
|
+
let type = isNumeric ? 'numeric' : (isAlphabeticLower ? 'alphabetic-lower' : 'alphabetic-upper');
|
|
259
|
+
|
|
260
|
+
// Convert the elements to header values
|
|
261
|
+
return elements.map(x => ({ counter: headerToNumber(x, type), type }));
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Detects the presence of an autoHeader signifier in a markdown document.
|
|
266
|
+
*
|
|
267
|
+
* This function checks in two places:
|
|
268
|
+
* 1. YAML front matter at the top of the document (`--- ... ---`).
|
|
269
|
+
* - If present, it searches for an `autoHeader:` key and uses its value.
|
|
270
|
+
* - The YAML block is stripped from the returned markdown.
|
|
271
|
+
* 2. The start of the markdown body.
|
|
272
|
+
* - Supports `@autoHeader:` or `<!-- autoHeader: ... -->` notations.
|
|
273
|
+
*
|
|
274
|
+
* If no signifier is found, the function returns `null`.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} markdown - The raw markdown content.
|
|
277
|
+
* @returns {Object|null} Returns an object if a signifier is found:
|
|
278
|
+
* @property {string} headingSignifier - The extracted starting header values.
|
|
279
|
+
* @property {string} markdown - The markdown content with the signifier or front matter removed.
|
|
280
|
+
* Otherwise returns `null` if no signifier is found.
|
|
281
|
+
*/
|
|
282
|
+
const detectAutoHeader = (markdown) => {
|
|
283
|
+
const yamlFrontMatterPattern = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
284
|
+
const autoHeaderPattern = /^\s*(?:@autoHeader:|<!--\s+autoHeader:)([\d.a-zA-Z\-:,~]*)(?:\s+-->)?/;
|
|
285
|
+
const yamlMatch = markdown.match(yamlFrontMatterPattern);
|
|
286
|
+
if (yamlMatch) {
|
|
287
|
+
const autoHeaderLine = yamlMatch[1].match(/^\s*autoHeader:\s*(.*)$/m);
|
|
288
|
+
if (autoHeaderLine) {
|
|
289
|
+
return {
|
|
290
|
+
headingSignifier: autoHeaderLine[1].trim(),
|
|
291
|
+
markdown: markdown.slice(yamlMatch[0].length),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Fallback: check start-of-doc markers
|
|
297
|
+
const match = markdown.match(autoHeaderPattern);
|
|
298
|
+
if (match) {
|
|
299
|
+
return {
|
|
300
|
+
headingSignifier: match[1],
|
|
301
|
+
markdown: markdown.substring(match[0].length),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Validates the presence of the auto header signifier in the markdown.
|
|
310
|
+
* @param {string} markdown - The markdown content.
|
|
311
|
+
* @param {Object} options - The current plugin options.
|
|
312
|
+
* @param {boolean} options.debug - Boolean to enable or disable debug messages.
|
|
313
|
+
* @returns {Object|null} Object with headingSignifier and markdown, or null if not found.
|
|
314
|
+
*/
|
|
315
|
+
const validateAutoHeaderSignifier = (markdown, options) => {
|
|
316
|
+
const result = detectAutoHeader(markdown);
|
|
317
|
+
if (!result) {
|
|
318
|
+
return logErrorMessage(options.debug, errorMessage.signifierNotFound);
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Creates count context objects for header levels.
|
|
325
|
+
* @param {Object} levels - The header levels configuration.
|
|
326
|
+
* @returns {Object} An object containing the current counts and scoped tag names.
|
|
327
|
+
*/
|
|
328
|
+
const createCountContextObjects = (levels) => {
|
|
329
|
+
const configEntries = Object.entries(levels);
|
|
330
|
+
|
|
331
|
+
// Create a map of current counts for each header level
|
|
332
|
+
const currentCounts = new Map(
|
|
333
|
+
configEntries.map(([key, { counter, type }]) => {
|
|
334
|
+
counter = parseInt(counter, 10); // Convert counter to an integer
|
|
335
|
+
counter = Number.isFinite(counter) ? counter - 1 : 0; // Decrement counter or set to 0 if not a number
|
|
336
|
+
return [key, { current: counter, type, skipDownstreamReset: true }];
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Create a set of tag names that are in scope
|
|
341
|
+
const scopedTagNames = new Set(
|
|
342
|
+
configEntries.filter(([_, { inScope }]) => inScope).map(([key]) => key)
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return { currentCounts, scopedTagNames };
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Applies the current count to the heading node.
|
|
350
|
+
* @param {HTMLElement} headingNode - The heading node to update.
|
|
351
|
+
* @param {Object} options - The current plugin options.
|
|
352
|
+
* @param {string} options.separator - The separator for header numbering.
|
|
353
|
+
*/
|
|
354
|
+
const applyCurrentCountThroughBoundContext = function (headingNode, options) {
|
|
355
|
+
const { currentCounts, scopedTagNames } = this;
|
|
356
|
+
const headingName = headingNode.tagName.toLowerCase();
|
|
357
|
+
const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
358
|
+
const counts = currentCounts.get(headingName);
|
|
359
|
+
|
|
360
|
+
counts.current += 1; // Increment the current count for the heading level
|
|
361
|
+
if (!counts.skipDownstreamReset) {
|
|
362
|
+
// Reset counts for heading levels below the current level
|
|
363
|
+
headingNameList.slice(headingNameList.indexOf(headingName) + 1).forEach(name => {
|
|
364
|
+
if (currentCounts.has(name)) {
|
|
365
|
+
const nextMinorCount = currentCounts.get(name);
|
|
366
|
+
nextMinorCount.current = 0;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
counts.skipDownstreamReset = false; // Reset the flag
|
|
371
|
+
|
|
372
|
+
if (scopedTagNames.has(headingName)) {
|
|
373
|
+
// Generate the counter value for the heading
|
|
374
|
+
const counterValue = headingNameList
|
|
375
|
+
.slice(0, headingNameList.indexOf(headingName) + 1)
|
|
376
|
+
.map(name => {
|
|
377
|
+
const { current, type } = currentCounts.get(name);
|
|
378
|
+
return numberToHeader(current, type);
|
|
379
|
+
})
|
|
380
|
+
.join(options.separator) + options.separator;
|
|
381
|
+
|
|
382
|
+
headingNode.innerHTML = `${counterValue} ${headingNode.innerHTML}`; // Update the heading's innerHTML
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Parses the markdown content and updates headers based on the settings.
|
|
388
|
+
* @param {string} markdown - The markdown content to parse.
|
|
389
|
+
* @param {Object} options - The current plugin options.
|
|
390
|
+
* @param {string} options.separator - The separator for header numbering.
|
|
391
|
+
* @param {Object} levels - The header levels configuration.
|
|
392
|
+
* @returns {string} The updated markdown content with applied header counts.
|
|
393
|
+
*/
|
|
394
|
+
const parseMarkdown = (markdown, options, levels) => {
|
|
395
|
+
const { currentCounts, scopedTagNames } = createCountContextObjects(levels);
|
|
396
|
+
const headingPattern = /^(#{1,6})\s+(.*)$/gm; // Regex pattern to match markdown headers
|
|
397
|
+
const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
398
|
+
|
|
399
|
+
let result = markdown.replace(headingPattern, (match, hashes, text) => {
|
|
400
|
+
const headingLevel = hashes.length;
|
|
401
|
+
const headingName = `h${headingLevel}`;
|
|
402
|
+
const counts = currentCounts.get(headingName);
|
|
403
|
+
|
|
404
|
+
counts.current += 1; // Increment the current count for the heading level
|
|
405
|
+
if (!counts.skipDownstreamReset) {
|
|
406
|
+
// Reset counts for heading levels below the current level
|
|
407
|
+
headingNameList.slice(headingLevel).forEach(name => {
|
|
408
|
+
if (currentCounts.has(name)) {
|
|
409
|
+
const nextMinorCount = currentCounts.get(name);
|
|
410
|
+
nextMinorCount.current = 0;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
counts.skipDownstreamReset = false; // Reset the flag
|
|
415
|
+
|
|
416
|
+
if (scopedTagNames.has(headingName)) {
|
|
417
|
+
// Generate the counter value for the heading
|
|
418
|
+
const counterValue = headingNameList
|
|
419
|
+
.slice(0, headingLevel)
|
|
420
|
+
.map(name => {
|
|
421
|
+
const { current, type } = currentCounts.get(name);
|
|
422
|
+
return numberToHeader(current, type);
|
|
423
|
+
})
|
|
424
|
+
.join(options.separator) + options.separator;
|
|
425
|
+
return `${hashes} ${counterValue} ${text}`; // Update the header in the markdown
|
|
426
|
+
} else {
|
|
427
|
+
return match; // Return the original match if the heading is not in scope
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
return result; // Return the updated markdown content
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Applies scoped heading counts to the input content.
|
|
435
|
+
* @param {Object} levels - The header levels configuration.
|
|
436
|
+
* @param {Object} options - The current plugin options.
|
|
437
|
+
* @param {string} options.separator - The separator for header numbering.
|
|
438
|
+
* @param {string} input - The input content (HTML or markdown).
|
|
439
|
+
* @param {string} type - The type of input content ('html' or 'markdown').
|
|
440
|
+
* @returns {string} The updated content with applied header counts.
|
|
441
|
+
*/
|
|
442
|
+
const applyScopedHeadingCounts = (levels, options, input, type) => {
|
|
443
|
+
const { currentCounts, scopedTagNames } = createCountContextObjects(levels);
|
|
444
|
+
|
|
445
|
+
if (type === 'html') {
|
|
446
|
+
// Parse the input HTML content
|
|
447
|
+
const parser = new DOMParser();
|
|
448
|
+
input = parser.parseFromString(input, 'text/html').body;
|
|
449
|
+
|
|
450
|
+
// Get all heading elements
|
|
451
|
+
const headingList = [...input.querySelectorAll('h1, h2, h3, h4, h5, h6')];
|
|
452
|
+
|
|
453
|
+
// Apply the current count to each heading element
|
|
454
|
+
headingList.forEach(
|
|
455
|
+
heading => applyCurrentCountThroughBoundContext.call(
|
|
456
|
+
{ currentCounts, scopedTagNames },
|
|
457
|
+
heading,
|
|
458
|
+
options
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return input.innerHTML; // Return the updated HTML content
|
|
463
|
+
} else if (type === 'markdown') {
|
|
464
|
+
// Parse and update the markdown content
|
|
465
|
+
return parseMarkdown(input, options, levels);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Set the default options by merging user-defined options with default settings
|
|
470
|
+
const defaultOptions = setDefaultOptions(docsifyAutoHeadersDefaults);
|
|
471
|
+
if (!defaultOptions) return;
|
|
472
|
+
|
|
473
|
+
// Initialize options with validated settings
|
|
474
|
+
/**
|
|
475
|
+
* The plugin options after validation and initialization.
|
|
476
|
+
* @type {Object}
|
|
477
|
+
* @property {string} separator - The separator for header numbering.
|
|
478
|
+
* @property {Object} levels - The validated header levels configuration.
|
|
479
|
+
* @property {boolean} sidebar - Boolean indicating if headers should be added to the sidebar.
|
|
480
|
+
* @property {boolean} debug - Boolean to enable or disable debug messages.
|
|
481
|
+
*/
|
|
482
|
+
let options = {
|
|
483
|
+
separator: defaultOptions.separator,
|
|
484
|
+
levels: validateThenGetHeadingRange(defaultOptions.levels, defaultOptions),
|
|
485
|
+
sidebar: validateThenGetSidebar(defaultOptions.sidebar, defaultOptions),
|
|
486
|
+
debug: defaultOptions.debug,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Docsify hook to process markdown before it is rendered.
|
|
491
|
+
* @param {Function} hook - Docsify hook function.
|
|
492
|
+
* @param {Object} options - The current plugin options.
|
|
493
|
+
*/
|
|
494
|
+
hook.beforeEach(markdown => {
|
|
495
|
+
shouldContinue = true;
|
|
496
|
+
if (!shouldContinue) return markdown;
|
|
497
|
+
|
|
498
|
+
// Validate the presence of the auto header signifier in the markdown
|
|
499
|
+
const result = validateAutoHeaderSignifier(markdown, options);
|
|
500
|
+
if (!result) {
|
|
501
|
+
return markdown;
|
|
502
|
+
}
|
|
503
|
+
let headingSignifier;
|
|
504
|
+
({ headingSignifier, markdown } = result); // Destructure the result
|
|
505
|
+
|
|
506
|
+
// Validate and retrieve the heading range setting
|
|
507
|
+
const headingRanges = validateThenGetHeadingRange(defaultOptions.levels, options);
|
|
508
|
+
if (!headingRanges) {
|
|
509
|
+
return markdown;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Parse the starting values for headers from the signifier
|
|
513
|
+
const startingHeadingValues = parseHeadingStartingValues(
|
|
514
|
+
headingSignifier,
|
|
515
|
+
options
|
|
516
|
+
);
|
|
517
|
+
if (!startingHeadingValues) {
|
|
518
|
+
return markdown;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Create the heading configuration
|
|
522
|
+
const headingConfiguration = {};
|
|
523
|
+
for (const [index, key] of Object.keys(headingRanges).entries()) {
|
|
524
|
+
headingConfiguration[key] = {
|
|
525
|
+
...headingRanges[key],
|
|
526
|
+
...startingHeadingValues[index],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Update the options with the new heading configuration
|
|
531
|
+
options.levels = headingConfiguration;
|
|
532
|
+
|
|
533
|
+
return markdown; // Return the updated markdown
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
// Check if the sidebar option is enabled
|
|
538
|
+
if (options.sidebar) {
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Docsify hook to process markdown before it is rendered if the sidebar option is enabled.
|
|
542
|
+
* @param {Function} hook - Docsify hook function.
|
|
543
|
+
* @param {Object} options - The current plugin options.
|
|
544
|
+
*/
|
|
545
|
+
hook.beforeEach((markdown, next) => {
|
|
546
|
+
let output;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
// Apply scoped heading counts to the markdown content
|
|
550
|
+
output = applyScopedHeadingCounts(
|
|
551
|
+
options.levels,
|
|
552
|
+
options,
|
|
553
|
+
markdown,
|
|
554
|
+
'markdown'
|
|
555
|
+
);
|
|
556
|
+
if (!shouldContinue) output = markdown;
|
|
557
|
+
|
|
558
|
+
} catch (error) {
|
|
559
|
+
output = markdown;
|
|
560
|
+
console.warn(error.message);
|
|
561
|
+
} finally {
|
|
562
|
+
next(output); // Pass the updated markdown to the next hook
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Docsify hook to process HTML after it is rendered if the sidebar option is disabled.
|
|
569
|
+
* @param {Function} hook - Docsify hook function.
|
|
570
|
+
* @param {Object} options - The current plugin options.
|
|
571
|
+
*/
|
|
572
|
+
hook.afterEach((html, next) => {
|
|
573
|
+
let output;
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
// Apply scoped heading counts to the HTML content
|
|
577
|
+
output = applyScopedHeadingCounts(
|
|
578
|
+
options.levels,
|
|
579
|
+
options,
|
|
580
|
+
html,
|
|
581
|
+
'html'
|
|
582
|
+
);
|
|
583
|
+
if (!shouldContinue) output = html;
|
|
584
|
+
|
|
585
|
+
} catch (error) {
|
|
586
|
+
output = html;
|
|
587
|
+
console.warn(error.message);
|
|
588
|
+
} finally {
|
|
589
|
+
next(output); // Pass the updated HTML to the next hook
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Initializes the plugin if running in a browser environment.
|
|
597
|
+
* This adds the autoHeaders plugin to the Docsify instance.
|
|
598
|
+
*/
|
|
599
|
+
if (window) {
|
|
600
|
+
window.$docsify = window.$docsify || {};
|
|
601
|
+
|
|
602
|
+
// Merge user-defined settings with default settings
|
|
603
|
+
window.$docsify.autoHeaders = Object.assign(
|
|
604
|
+
docsifyAutoHeadersDefaults,
|
|
605
|
+
window.$docsify.autoHeaders
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// Add the autoHeaders function to the list of Docsify plugins
|
|
609
|
+
window.$docsify.plugins = (
|
|
610
|
+
window.$docsify.plugins || []
|
|
611
|
+
).concat(autoHeaders);
|
|
612
|
+
}
|
|
579
613
|
})();
|