Skip to content

Commit

Permalink
feat: customize chapter number rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
huaichaow committed Feb 8, 2023
1 parent d58113b commit 8d2d060
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = tab
indent_style = space
indent_size = 2

[*.{ts,js}]
Expand Down
19 changes: 15 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { marked, Renderer, Slugger } from 'marked';
import Token = marked.Token;
import { Heading, HeadingWithChapterNumber } from './types';
import {
Heading,
HeadingWithChapterNumber,
MarkedTableOfContentsExtensionOptions,
} from './types';
import { renderTableOfContent } from './renderTableOfContents';
import { fixHeadingDepthFactory } from './fixHeadingDepthFactory';
import { numberingHeadingFactory } from './numberingHeadingFactory';

export default function markedTableOfContentsExtension() {
export default function markedTableOfContentsExtension(
options?: MarkedTableOfContentsExtensionOptions,
) {
const { renderChapterNumberHeading, renderChapterNumberTOC } = options || {};

let headings: Array<{ text: string; depth: number }> | null = null;
let fixHeadingDepth: ((heading: Heading) => void) | null = null;
let numberingHeading: ((heading: Heading) => void) | null = null;
Expand All @@ -26,11 +34,14 @@ export default function markedTableOfContentsExtension() {
fixHeadingDepth = fixHeadingDepthFactory();
}
if (!numberingHeading) {
numberingHeading = numberingHeadingFactory();
numberingHeading = numberingHeadingFactory(
renderChapterNumberTOC,
renderChapterNumberHeading,
);
}
fixHeadingDepth(token);
numberingHeading(token);
chapterNumbers.push((token as unknown as HeadingWithChapterNumber).chapterNumber);
chapterNumbers.push((token as unknown as HeadingWithChapterNumber).chapterNumberHeading);
headings.push(token);
}
};
Expand Down
81 changes: 54 additions & 27 deletions src/numberingHeadingFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,66 @@ import { createHeadings } from './testHelper';

let numberingHeading: (heading: Heading) => void;

const testDataSet = [
{
receivedDepths: [1, 2, 3],
chapterNumbers: ['1', '1.1', '1.1.1'],
},
{
receivedDepths: [1, 1, 1],
chapterNumbers: ['1', '2', '3'],
},
{
receivedDepths: [1, 2, 2],
chapterNumbers: ['1', '1.1', '1.2'],
},
{
receivedDepths: [1, 2, 3, 1, 2, 3],
chapterNumbers: ['1', '1.1', '1.1.1', '2', '2.1', '2.1.1'],
},
{
receivedDepths: [1, 2, 3, 2, 1],
chapterNumbers: ['1', '1.1', '1.1.1', '1.2', '2'],
},
];

describe('numberingHeadingFactory', () => {
beforeEach(() => {
numberingHeading = numberingHeadingFactory();
});

test.each([
{
receivedDepths: [1, 2, 3],
chapterNumbers: ['1', '1.1', '1.1.1'],
},
{
receivedDepths: [1, 1, 1],
chapterNumbers: ['1', '2', '3'],
},
{
receivedDepths: [1, 2, 2],
chapterNumbers: ['1', '1.1', '1.2'],
},
{
receivedDepths: [1, 2, 3, 1, 2, 3],
chapterNumbers: ['1', '1.1', '1.1.1', '2', '2.1', '2.1.1'],
},
{
receivedDepths: [1, 2, 3, 2, 1],
chapterNumbers: ['1', '1.1', '1.1.1', '1.2', '2'],
test.each(testDataSet)(
'should generate correct chapter numbers for $receivedDepths',
({ receivedDepths, chapterNumbers }) => {
const headings = createHeadings(receivedDepths);

headings.forEach((heading) => numberingHeading(heading));

chapterNumbers.forEach((expectedDepth, i) => {
expect((headings[i] as HeadingWithChapterNumber).chapterNumberTOC).toBe(expectedDepth);
expect((headings[i] as HeadingWithChapterNumber).chapterNumberHeading).toBe(expectedDepth);
});
},
])('should set correct chapter numbers for $receivedDepths', ({ receivedDepths, chapterNumbers }) => {
const headings = createHeadings(receivedDepths);
);

headings.forEach((heading) => numberingHeading(heading));
test.each(testDataSet)(
'should generate chapter number with render functions for $receivedDepths',
({ receivedDepths, chapterNumbers }) => {
const headings = createHeadings(receivedDepths);

chapterNumbers.forEach((expectedDepth, i) => {
expect((headings[i] as HeadingWithChapterNumber).chapterNumber).toBe(expectedDepth);
});
});
numberingHeading = numberingHeadingFactory(
(numbers: Array<number>) => numbers.join("-"),
(numbers: Array<number>) => numbers.join("|"),
);

headings.forEach((heading) => numberingHeading(heading));

chapterNumbers.forEach((expectedDepth, i) => {
expect((headings[i] as HeadingWithChapterNumber).chapterNumberTOC)
.toBe(expectedDepth.replace(/\./g, '-'));
expect((headings[i] as HeadingWithChapterNumber).chapterNumberHeading)
.toBe(expectedDepth.replace(/\./g, '|'));
});
},
);
});
17 changes: 14 additions & 3 deletions src/numberingHeadingFactory.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Heading, HeadingWithChapterNumber } from './types';
import { Heading, HeadingWithChapterNumber, RenderChapterNumberFn } from './types';

/**
* It assumes that the depth of tokens feed into the returned `numberingHeading` function
* has been fixed. Otherwise, the behavior of the function is not predictable, and
* it makes no sense to numbering these headings.
*/

export function numberingHeadingFactory() {
export function numberingHeadingFactory(
renderChapterNumberTOC?: RenderChapterNumberFn,
renderChapterNumberHeading?: RenderChapterNumberFn,
) {
const chapterNumbers: Array<number> = [];
let prevDepth = 0;

Expand Down Expand Up @@ -42,6 +45,14 @@ export function numberingHeadingFactory() {
prevDepth = depth;

// append chapter number to token
(heading as unknown as HeadingWithChapterNumber).chapterNumber = getChapterNumber();
(heading as unknown as HeadingWithChapterNumber).chapterNumberTOC =
renderChapterNumberTOC
? renderChapterNumberTOC([...chapterNumbers])
: getChapterNumber();

(heading as unknown as HeadingWithChapterNumber).chapterNumberHeading =
renderChapterNumberHeading
? renderChapterNumberHeading([...chapterNumbers])
: getChapterNumber();
};
}
2 changes: 1 addition & 1 deletion src/renderTableOfContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function renderTreeStructureHeadings(headings: Headings): string {
const tokens: Array<string> = [];

function addTocItem(heading: Heading) {
const chapterNumber = (heading as HeadingWithChapterNumber).chapterNumber;
const chapterNumber = (heading as HeadingWithChapterNumber).chapterNumberTOC;
tokens.push(`<li>${chapterNumber} ${heading.text}</li>`);
}

Expand Down
38 changes: 38 additions & 0 deletions src/test/toc-with-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* todo: marked does not support reset defaults, so need to create a separate
* test module to test the extension with different options.
*/

import { marked } from 'marked';
import markedToc from '..';

marked.use(markedToc({
renderChapterNumberTOC: (numbers) => numbers.join("--"),
renderChapterNumberHeading: (numbers) => numbers.join("++"),
}));

function removeLeadingSpaces(html: string) {
return html
.replace(/\n +/g, '\n')
.replace(/^\s+/, '');
}

describe('marked-toc-extension with options', () => {
test('should render custom chapter numbers', () => {
const md = removeLeadingSpaces(`
# a
## b
### c
[TOC]
`);

const expectedHtml = removeLeadingSpaces(`
<h1 id="a">1 a</h1>
<h2 id="b">1++1 b</h2>
<h3 id="c">1++1++1 c</h3>
<ul><li>1 a</li><ul><li>1--1 b</li><ul><li>1--1--1 c</li></ul></ul></ul>
`);

expect(marked.parse(md)).toEqual(expectedHtml);
});
});
8 changes: 4 additions & 4 deletions src/test/toc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,20 @@ describe('marked-toc-extension', () => {
});

test('should auto fix heading depth', () => {
const md = removeLeadingSpaces(`
const md = removeLeadingSpaces(`
[TOC]
## l2
### l3
# l1
`);

const expectedHtml = removeLeadingSpaces(`
const expectedHtml = removeLeadingSpaces(`
<ul><li>1 l2</li><ul><li>1.1 l3</li></ul><li>2 l1</li></ul>
<h1 id="l2">1 l2</h1>
<h2 id="l3">1.1 l3</h2>
<h1 id="l1">2 l1</h1>
`);

expect(marked.parse(md)).toEqual(expectedHtml);
});
expect(marked.parse(md)).toEqual(expectedHtml);
});
});
14 changes: 11 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
export type Heading = {
text: string;
depth: number;
text: string;
depth: number;
}

export type HeadingWithChapterNumber = Heading & {
chapterNumber: string; // dot separated string, e.g., `1.2.3`
chapterNumberTOC: string;
chapterNumberHeading: string;
}

export type Headings = Array<Heading>;

export type RenderChapterNumberFn = (numbers: Array<number>) => string;

export type MarkedTableOfContentsExtensionOptions = {
renderChapterNumberTOC?: RenderChapterNumberFn;
renderChapterNumberHeading?: RenderChapterNumberFn;
};

0 comments on commit 8d2d060

Please sign in to comment.