@markbattistella/docsify-autoheaders 5.0.1 → 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.
@@ -1,579 +1,613 @@
1
- /*! docsify-auto-headers 5.0.1 | (c) Mark Battistella */
1
+ /*! docsify-auto-headers 5.1.0 | (c) Mark Battistella */
2
2
  ; (() => {
3
3
 
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
- * Validates the presence of the auto header signifier in the markdown.
266
- * @param {string} markdown - The markdown content.
267
- * @param {Object} options - The current plugin options.
268
- * @param {boolean} options.debug - Boolean to enable or disable debug messages.
269
- * @returns {Object|null} An object containing the heading signifier and the remaining markdown content, or null if the signifier is not found.
270
- */
271
- const validateAutoHeaderSignifier = (markdown, options) => {
272
- // Regular expression pattern to match the auto header signifier
273
- const autoHeaderPattern = /^\s*(?:@autoHeader:|<!--\s+autoHeader:)([\d.a-zA-Z\-:,~]*)(?:\s+-->)?/;
274
- const match = markdown.match(autoHeaderPattern);
275
-
276
- if (!match) {
277
- return logErrorMessage(options.debug, errorMessage.signifierNotFound);
278
- }
279
-
280
- // Remove the matched signifier from the markdown
281
- markdown = markdown.substring(match[0].length);
282
-
283
- return {
284
- headingSignifier: match[1], // The captured signifier
285
- markdown, // The remaining markdown content
286
- };
287
- };
288
-
289
- /**
290
- * Creates count context objects for header levels.
291
- * @param {Object} levels - The header levels configuration.
292
- * @returns {Object} An object containing the current counts and scoped tag names.
293
- */
294
- const createCountContextObjects = (levels) => {
295
- const configEntries = Object.entries(levels);
296
-
297
- // Create a map of current counts for each header level
298
- const currentCounts = new Map(
299
- configEntries.map(([key, { counter, type }]) => {
300
- counter = parseInt(counter, 10); // Convert counter to an integer
301
- counter = Number.isFinite(counter) ? counter - 1 : 0; // Decrement counter or set to 0 if not a number
302
- return [key, { current: counter, type, skipDownstreamReset: true }];
303
- })
304
- );
305
-
306
- // Create a set of tag names that are in scope
307
- const scopedTagNames = new Set(
308
- configEntries.filter(([_, { inScope }]) => inScope).map(([key]) => key)
309
- );
310
-
311
- return { currentCounts, scopedTagNames };
312
- };
313
-
314
- /**
315
- * Applies the current count to the heading node.
316
- * @param {HTMLElement} headingNode - The heading node to update.
317
- * @param {Object} options - The current plugin options.
318
- * @param {string} options.separator - The separator for header numbering.
319
- */
320
- const applyCurrentCountThroughBoundContext = function (headingNode, options) {
321
- const { currentCounts, scopedTagNames } = this;
322
- const headingName = headingNode.tagName.toLowerCase();
323
- const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
324
- const counts = currentCounts.get(headingName);
325
-
326
- counts.current += 1; // Increment the current count for the heading level
327
- if (!counts.skipDownstreamReset) {
328
- // Reset counts for heading levels below the current level
329
- headingNameList.slice(headingNameList.indexOf(headingName) + 1).forEach(name => {
330
- if (currentCounts.has(name)) {
331
- const nextMinorCount = currentCounts.get(name);
332
- nextMinorCount.current = 0;
333
- }
334
- });
335
- }
336
- counts.skipDownstreamReset = false; // Reset the flag
337
-
338
- if (scopedTagNames.has(headingName)) {
339
- // Generate the counter value for the heading
340
- const counterValue = headingNameList
341
- .slice(0, headingNameList.indexOf(headingName) + 1)
342
- .map(name => {
343
- const { current, type } = currentCounts.get(name);
344
- return numberToHeader(current, type);
345
- })
346
- .join(options.separator) + options.separator;
347
-
348
- headingNode.innerHTML = `${counterValue} ${headingNode.innerHTML}`; // Update the heading's innerHTML
349
- }
350
- };
351
-
352
- /**
353
- * Parses the markdown content and updates headers based on the settings.
354
- * @param {string} markdown - The markdown content to parse.
355
- * @param {Object} options - The current plugin options.
356
- * @param {string} options.separator - The separator for header numbering.
357
- * @param {Object} levels - The header levels configuration.
358
- * @returns {string} The updated markdown content with applied header counts.
359
- */
360
- const parseMarkdown = (markdown, options, levels) => {
361
- const { currentCounts, scopedTagNames } = createCountContextObjects(levels);
362
- const headingPattern = /^(#{1,6})\s+(.*)$/gm; // Regex pattern to match markdown headers
363
- const headingNameList = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
364
-
365
- let result = markdown.replace(headingPattern, (match, hashes, text) => {
366
- const headingLevel = hashes.length;
367
- const headingName = `h${headingLevel}`;
368
- const counts = currentCounts.get(headingName);
369
-
370
- counts.current += 1; // Increment the current count for the heading level
371
- if (!counts.skipDownstreamReset) {
372
- // Reset counts for heading levels below the current level
373
- headingNameList.slice(headingLevel).forEach(name => {
374
- if (currentCounts.has(name)) {
375
- const nextMinorCount = currentCounts.get(name);
376
- nextMinorCount.current = 0;
377
- }
378
- });
379
- }
380
- counts.skipDownstreamReset = false; // Reset the flag
381
-
382
- if (scopedTagNames.has(headingName)) {
383
- // Generate the counter value for the heading
384
- const counterValue = headingNameList
385
- .slice(0, headingLevel)
386
- .map(name => {
387
- const { current, type } = currentCounts.get(name);
388
- return numberToHeader(current, type);
389
- })
390
- .join(options.separator) + options.separator;
391
- return `${hashes} ${counterValue} ${text}`; // Update the header in the markdown
392
- } else {
393
- return match; // Return the original match if the heading is not in scope
394
- }
395
- });
396
- return result; // Return the updated markdown content
397
- };
398
-
399
- /**
400
- * Applies scoped heading counts to the input content.
401
- * @param {Object} levels - The header levels configuration.
402
- * @param {Object} options - The current plugin options.
403
- * @param {string} options.separator - The separator for header numbering.
404
- * @param {string} input - The input content (HTML or markdown).
405
- * @param {string} type - The type of input content ('html' or 'markdown').
406
- * @returns {string} The updated content with applied header counts.
407
- */
408
- const applyScopedHeadingCounts = (levels, options, input, type) => {
409
- const { currentCounts, scopedTagNames } = createCountContextObjects(levels);
410
-
411
- if (type === 'html') {
412
- // Parse the input HTML content
413
- const parser = new DOMParser();
414
- input = parser.parseFromString(input, 'text/html').body;
415
-
416
- // Get all heading elements
417
- const headingList = [...input.querySelectorAll('h1, h2, h3, h4, h5, h6')];
418
-
419
- // Apply the current count to each heading element
420
- headingList.forEach(
421
- heading => applyCurrentCountThroughBoundContext.call(
422
- { currentCounts, scopedTagNames },
423
- heading,
424
- options
425
- )
426
- );
427
-
428
- return input.innerHTML; // Return the updated HTML content
429
- } else if (type === 'markdown') {
430
- // Parse and update the markdown content
431
- return parseMarkdown(input, options, levels);
432
- }
433
- };
434
-
435
- // Set the default options by merging user-defined options with default settings
436
- const defaultOptions = setDefaultOptions(docsifyAutoHeadersDefaults);
437
- if (!defaultOptions) return;
438
-
439
- // Initialize options with validated settings
440
- /**
441
- * The plugin options after validation and initialization.
442
- * @type {Object}
443
- * @property {string} separator - The separator for header numbering.
444
- * @property {Object} levels - The validated header levels configuration.
445
- * @property {boolean} sidebar - Boolean indicating if headers should be added to the sidebar.
446
- * @property {boolean} debug - Boolean to enable or disable debug messages.
447
- */
448
- let options = {
449
- separator: defaultOptions.separator,
450
- levels: validateThenGetHeadingRange(defaultOptions.levels, defaultOptions),
451
- sidebar: validateThenGetSidebar(defaultOptions.sidebar, defaultOptions),
452
- debug: defaultOptions.debug,
453
- };
454
-
455
- /**
456
- * Docsify hook to process markdown before it is rendered.
457
- * @param {Function} hook - Docsify hook function.
458
- * @param {Object} options - The current plugin options.
459
- */
460
- hook.beforeEach(markdown => {
461
- shouldContinue = true;
462
- if (!shouldContinue) return markdown;
463
-
464
- // Validate the presence of the auto header signifier in the markdown
465
- const result = validateAutoHeaderSignifier(markdown, options);
466
- if (!result) {
467
- return markdown;
468
- }
469
- let headingSignifier;
470
- ({ headingSignifier, markdown } = result); // Destructure the result
471
-
472
- // Validate and retrieve the heading range setting
473
- const headingRanges = validateThenGetHeadingRange(defaultOptions.levels, options);
474
- if (!headingRanges) {
475
- return markdown;
476
- }
477
-
478
- // Parse the starting values for headers from the signifier
479
- const startingHeadingValues = parseHeadingStartingValues(
480
- headingSignifier,
481
- options
482
- );
483
- if (!startingHeadingValues) {
484
- return markdown;
485
- }
486
-
487
- // Create the heading configuration
488
- const headingConfiguration = {};
489
- for (const [index, key] of Object.keys(headingRanges).entries()) {
490
- headingConfiguration[key] = {
491
- ...headingRanges[key],
492
- ...startingHeadingValues[index],
493
- };
494
- }
495
-
496
- // Update the options with the new heading configuration
497
- options.levels = headingConfiguration;
498
-
499
- return markdown; // Return the updated markdown
500
- });
501
-
502
-
503
- // Check if the sidebar option is enabled
504
- if (options.sidebar) {
505
-
506
- /**
507
- * Docsify hook to process markdown before it is rendered if the sidebar option is enabled.
508
- * @param {Function} hook - Docsify hook function.
509
- * @param {Object} options - The current plugin options.
510
- */
511
- hook.beforeEach((markdown, next) => {
512
- let output;
513
-
514
- try {
515
- // Apply scoped heading counts to the markdown content
516
- output = applyScopedHeadingCounts(
517
- options.levels,
518
- options,
519
- markdown,
520
- 'markdown'
521
- );
522
- if (!shouldContinue) output = markdown;
523
-
524
- } catch (error) {
525
- output = markdown;
526
- console.warn(error.message);
527
- } finally {
528
- next(output); // Pass the updated markdown to the next hook
529
- }
530
- });
531
- } else {
532
-
533
- /**
534
- * Docsify hook to process HTML after it is rendered if the sidebar option is disabled.
535
- * @param {Function} hook - Docsify hook function.
536
- * @param {Object} options - The current plugin options.
537
- */
538
- hook.afterEach((html, next) => {
539
- let output;
540
-
541
- try {
542
- // Apply scoped heading counts to the HTML content
543
- output = applyScopedHeadingCounts(
544
- options.levels,
545
- options,
546
- html,
547
- 'html'
548
- );
549
- if (!shouldContinue) output = html;
550
-
551
- } catch (error) {
552
- output = html;
553
- console.warn(error.message);
554
- } finally {
555
- next(output); // Pass the updated HTML to the next hook
556
- }
557
- });
558
- }
559
- };
560
-
561
- /**
562
- * Initializes the plugin if running in a browser environment.
563
- * This adds the autoHeaders plugin to the Docsify instance.
564
- */
565
- if (window) {
566
- window.$docsify = window.$docsify || {};
567
-
568
- // Merge user-defined settings with default settings
569
- window.$docsify.autoHeaders = Object.assign(
570
- docsifyAutoHeadersDefaults,
571
- window.$docsify.autoHeaders
572
- );
573
-
574
- // Add the autoHeaders function to the list of Docsify plugins
575
- window.$docsify.plugins = (
576
- window.$docsify.plugins || []
577
- ).concat(autoHeaders);
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
  })();