From f00bf65a66d16d34118907ef8e5e11a3b3dee376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=94=B0=20=E6=86=B2=E5=A4=AA=E9=83=8E?= Date: Sun, 5 May 2024 21:11:08 +0900 Subject: [PATCH 1/3] feat(helper/toc): specify maximum number of items to output --- lib/plugins/helper/toc.ts | 26 ++++++- test/scripts/helpers/toc.ts | 136 ++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/lib/plugins/helper/toc.ts b/lib/plugins/helper/toc.ts index 02a4b1ad7d..791397f117 100644 --- a/lib/plugins/helper/toc.ts +++ b/lib/plugins/helper/toc.ts @@ -3,6 +3,7 @@ import { tocObj, escapeHTML, encodeURL } from 'hexo-util'; interface Options { min_depth?: number; max_depth?: number; + max_items?: number; class?: string; class_item?: string; class_link?: string; @@ -17,6 +18,7 @@ function tocHelper(str: string, options: Options = {}) { options = Object.assign({ min_depth: 1, max_depth: 6, + max_items: Infinity, class: 'toc', class_item: '', class_link: '', @@ -27,7 +29,7 @@ function tocHelper(str: string, options: Options = {}) { list_number: true }, options); - const data = tocObj(str, { min_depth: options.min_depth, max_depth: options.max_depth }); + const data = getAndTruncateTocObj(str, { min_depth: options.min_depth, max_depth: options.max_depth }, options.max_items); if (!data.length) return ''; @@ -102,4 +104,26 @@ function tocHelper(str: string, options: Options = {}) { return result; } +function getAndTruncateTocObj(str: string, options: {min_depth: number, max_depth: number}, max_items: number) { + let data = tocObj(str, { min_depth: options.min_depth, max_depth: options.max_depth }); + + if (data.length === 0) { + return data; + } + if (max_items < 1) { + return data; + } + + const min = Math.min(...data.map(item => item.level)); + const max = Math.max(...data.map(item => item.level)); + + for (let currentLevel = max; data.length > max_items && currentLevel > min; currentLevel--) { + data = data.filter(item => item.level < currentLevel); + } + + data = data.slice(0, max_items); + + return data; +} + export = tocHelper; diff --git a/test/scripts/helpers/toc.ts b/test/scripts/helpers/toc.ts index 75b479b2ec..2070f1b6c9 100644 --- a/test/scripts/helpers/toc.ts +++ b/test/scripts/helpers/toc.ts @@ -552,4 +552,140 @@ describe('toc', () => { toc(html, { class: 'foo', class_child: 'bar' }).should.eql(expected); }); + + it('max_items - result contains only h1 items', () => { + const className = 'toc'; + const expected = [ + '
    ', + '
  1. ', + '', + '1. ', // list_number enabled + 'Title 1', + '', + // '
      ', + // + // '
    ', + '
  2. ', + '
  3. ', + '', + '2. ', // list_number enabled + 'Title 2', + '', + // '
      ', + // + // '
    ', + '
  4. ', + '
  5. ', + '', + '3. ', // list_number enabled + 'Title should escape &, <, ', and "', + '', + '
  6. ', + '
  7. ', + '', + '4. ', // list_number enabled + 'Chapter 1 should be printed to toc', + '', + '
  8. ', + '
' + ].join(''); + + toc(html, { max_items: 4}).should.eql(expected); // The number of `h1` is 4 + toc(html, { max_items: 7}).should.eql(expected); // Maximum number 7 cannot display up to `h2` + }); + + it('max_items - result contains h1 and h2 items', () => { + const className = 'toc'; + const expected = [ + '
    ', + '
  1. ', + '', + '1. ', // list_number enabled + 'Title 1', + '', + '
      ', + '
    1. ', + '', + '1.1. ', // list_number enabled + 'Title 1.1', + '', + // '
        ', + // + // '
      ', + '
    2. ', + '
    3. ', + '', + '1.2. ', // list_number enabled + 'Title 1.2', + '', + '
    4. ', + '
    5. ', + '', + '1.3. ', // list_number enabled + 'Title 1.3', + '', + // '
        ', + // + // '
      ', + '
    6. ', + '
    ', + '
  2. ', + '
  3. ', + '', + '2. ', // list_number enabled + 'Title 2', + '', + '
      ', + '
    1. ', + '', + '2.1. ', // list_number enabled + 'Title 2.1', + '', + '
    2. ', + '
    ', + '
  4. ', + '
  5. ', + '', + '3. ', // list_number enabled + 'Title should escape &, <, ', and "', + '', + '
  6. ', + '
  7. ', + '', + '4. ', // list_number enabled + 'Chapter 1 should be printed to toc', + '', + '
  8. ', + '
' + ].join(''); + + toc(html, { max_items: 8}).should.eql(expected); // Maximum number 8 can display up to `h2` + toc(html, { max_items: 9}).should.eql(expected); // Maximum number 10 is required to display up to `h3` + }); + + it('max_items - result of h1 was truncated', () => { + const className = 'toc'; + const expected = [ + '
    ', + '
  1. ', + '', + '1. ', // list_number enabled + 'Title 1', + '', + // '
      ', + // + // '
    ', + '
  2. ', + '
  3. ', + '', + '2. ', // list_number enabled + 'Title 2', + '', + '
  4. ', + // + '
' + ].join(''); + + toc(html, { max_items: 2}).should.eql(expected); // `h1` is truncated from the end + }); }); From c17db644464bd9d4d6c7431698db0bca9dd2455d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=94=B0=20=E6=86=B2=E5=A4=AA=E9=83=8E?= Date: Sun, 26 May 2024 12:39:25 +0900 Subject: [PATCH 2/3] perf: skip evaluation if `max_items` is not specified --- lib/plugins/helper/toc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/helper/toc.ts b/lib/plugins/helper/toc.ts index 791397f117..94fc8401c1 100644 --- a/lib/plugins/helper/toc.ts +++ b/lib/plugins/helper/toc.ts @@ -110,7 +110,7 @@ function getAndTruncateTocObj(str: string, options: {min_depth: number, max_dept if (data.length === 0) { return data; } - if (max_items < 1) { + if (max_items < 1 || max_items === Infinity) { return data; } From f8d04a6f3e3cba4bb2ec034c7771373b18b34bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=94=B0=20=E6=86=B2=E5=A4=AA=E9=83=8E?= Date: Sun, 26 May 2024 12:42:38 +0900 Subject: [PATCH 3/3] perf: streamline getting min and max heading level --- lib/plugins/helper/toc.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/plugins/helper/toc.ts b/lib/plugins/helper/toc.ts index 94fc8401c1..f933e177e8 100644 --- a/lib/plugins/helper/toc.ts +++ b/lib/plugins/helper/toc.ts @@ -114,8 +114,9 @@ function getAndTruncateTocObj(str: string, options: {min_depth: number, max_dept return data; } - const min = Math.min(...data.map(item => item.level)); - const max = Math.max(...data.map(item => item.level)); + const levels = data.map(item => item.level); + const min = Math.min(...levels); + const max = Math.max(...levels); for (let currentLevel = max; data.length > max_items && currentLevel > min; currentLevel--) { data = data.filter(item => item.level < currentLevel);