Skip to content

Commit 10633d8

Browse files
feat: add html mode (#14)
1 parent 355142f commit 10633d8

File tree

7 files changed

+118
-8
lines changed

7 files changed

+118
-8
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/* eslint-disable no-magic-numbers */
2+
import {resolve} from 'path';
3+
import {readFileSync} from 'fs';
4+
import {transform} from '../src';
5+
6+
describe('transform', () => {
7+
it('run in html mode', () => {
8+
const content = readFileSync(resolve(__dirname, 'fixtures/readme-with-html.md'), 'utf8');
9+
const headers = transform(content, {mode: 'github.com', isHtml: true});
10+
11+
expect(headers.toc.split('\n')).toEqual(
12+
['**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*',
13+
'',
14+
'<p align="center">',
15+
'<a href="#installation">Installation</a>',
16+
'<span>|</span>',
17+
'<a href="#api">API</a>',
18+
'</p>',
19+
''],
20+
);
21+
});
22+
23+
it('run in html mode with custom settings', () => {
24+
const content = readFileSync(resolve(__dirname, 'fixtures/readme-with-html.md'), 'utf8');
25+
const headers = transform(content, {
26+
mode: 'github.com',
27+
isHtml: true,
28+
htmlTemplate: '<ul>${ITEMS}</ul>',
29+
itemTemplate: '<li><a href="${LINK}" target="_blank">${TEXT}</a></li>',
30+
separator: '',
31+
});
32+
33+
expect(headers.toc.split('\n')).toEqual(
34+
['**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*',
35+
'',
36+
'<ul>',
37+
'<li><a href="#installation" target="_blank">Installation</a></li>',
38+
'',
39+
'<li><a href="#api" target="_blank">API</a></li>',
40+
'</ul>',
41+
''],
42+
);
43+
});
44+
});

__tests__/utils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* eslint-disable no-magic-numbers */
2+
import {replaceVariables} from '../src/lib/utils';
3+
4+
describe('replaceVariables', () => {
5+
it('should replace variables', () => {
6+
expect(replaceVariables('', [])).toBe('');
7+
expect(replaceVariables('abc/${test1}/${test2}/${test1}/xyz', [
8+
{key: 'test1', replace: '1'},
9+
{key: 'test3', replace: '3'},
10+
])).toBe('abc/1/${test2}/1/xyz');
11+
});
12+
});

src/constant.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ export const CHECK_CLOSING_COMMENT = '<!-- END doctoc ';
66
export const DEFAULT_TITLE = '**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*';
77
export const MARKDOWN_EXTENSIONS = ['.md', '.markdown'];
88
export const IGNORED_DIRS = ['.', '..', '.git', 'node_modules'];
9+
export const DEFAULT_HTML_TEMPLATE = '<p align="center">${ITEMS}</p>';
10+
export const DEFAULT_ITEM_TEMPLATE = '<a href="${LINK}">${TEXT}</a>';
11+
export const DEFAULT_SEPARATOR = '<span>|</span>';

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@ export {
88
DEFAULT_TITLE,
99
MARKDOWN_EXTENSIONS,
1010
IGNORED_DIRS,
11+
DEFAULT_HTML_TEMPLATE,
12+
DEFAULT_ITEM_TEMPLATE,
13+
DEFAULT_SEPARATOR,
1114
} from './constant';

src/lib/transform.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import updateSection from 'update-section';
33
import * as md from '@textlint/markdown-to-ast';
44
import {TxtNode} from '@textlint/ast-node-types';
55
import {getHtmlHeaders} from './get-html-headers';
6+
import {replaceVariables} from './utils';
67
import {
78
OPENING_COMMENT,
89
CLOSING_COMMENT,
910
CHECK_OPENING_COMMENT,
1011
CHECK_CLOSING_COMMENT,
1112
DEFAULT_TITLE,
13+
DEFAULT_HTML_TEMPLATE,
14+
DEFAULT_ITEM_TEMPLATE,
15+
DEFAULT_SEPARATOR,
1216
} from '..';
1317
import {TransformOptions, Header, HeaderWithRepetition, HeaderWithAnchor, SectionInfo, TransformResult} from '../types';
1418

@@ -22,10 +26,11 @@ const getTargetComments = (checkComments: Array<string>, defaultComments: string
2226

2327
export const matchesStart = (checkOpeningComments?: Array<string>) => (line: string): boolean => getTargetComments(checkOpeningComments ?? [], CHECK_OPENING_COMMENT).some(comment => new RegExp(comment).test(line));
2428
export const matchesEnd = (checkClosingComments?: Array<string>) => (line: string): boolean => getTargetComments(checkClosingComments ?? [], CHECK_CLOSING_COMMENT).some(comment => new RegExp(comment).test(line));
25-
const addAnchor = (mode: string | undefined, moduleName: string | undefined, header: HeaderWithRepetition): HeaderWithAnchor => {
29+
const addAnchor = (mode: string, moduleName: string | undefined, header: HeaderWithRepetition): HeaderWithAnchor => {
2630
return {
2731
...header,
2832
anchor: anchor(header.name, mode, header.repetition, moduleName),
33+
hash: getUrlHash(header.name, mode, header.repetition, moduleName),
2934
};
3035
};
3136

@@ -115,6 +120,24 @@ const determineTitle = (title: string | undefined, isNotitle: boolean | undefine
115120
return wrapTitle(getTitle(title, lines, info), isFolding);
116121
};
117122

123+
const getHeaderContents = (headers: Array<HeaderWithAnchor>, indentation: string, lowestRank: number, entryPrefix: string): string => headers.map(header => getHeaderItem(header, indentation, lowestRank, entryPrefix)).join('\n');
124+
125+
const getHeaderItem = (header: HeaderWithAnchor, indentation: string, lowestRank: number, entryPrefix: string): string => {
126+
return `${indentation.repeat(header.rank - lowestRank)}${entryPrefix} ${header.anchor}`;
127+
};
128+
129+
const getHtmlHeaderContents = (headers: Array<HeaderWithAnchor>, htmlTemplate: string | undefined, itemTemplate: string | undefined, separator: string | undefined): string => replaceVariables(htmlTemplate ?? DEFAULT_HTML_TEMPLATE, [{
130+
key: 'ITEMS',
131+
replace: `\n${headers.map(header => getHeaderItemHtml(header, itemTemplate)).join(`\n${separator ?? DEFAULT_SEPARATOR}\n`)}\n`,
132+
}]);
133+
134+
const getHeaderItemHtml = (header: HeaderWithAnchor, itemTemplate: string | undefined): string => {
135+
return replaceVariables(itemTemplate ?? DEFAULT_ITEM_TEMPLATE, [
136+
{key: 'LINK', replace: `#${header.hash}`},
137+
{key: 'TEXT', replace: header.name},
138+
]);
139+
};
140+
118141
export const transform = (
119142
content: string,
120143
{
@@ -131,14 +154,18 @@ export const transform = (
131154
closingComment,
132155
checkOpeningComments,
133156
checkClosingComments,
157+
isHtml,
158+
htmlTemplate,
159+
itemTemplate,
160+
separator,
134161
}: TransformOptions = {},
135162
): TransformResult => {
136-
mode = mode || 'github.com';
137-
entryPrefix = entryPrefix || '-';
163+
const _mode = mode || 'github.com';
164+
const _entryPrefix = entryPrefix || '-';
138165

139166
// only limit *HTML* headings by default
140167
// eslint-disable-next-line no-magic-numbers
141-
const maxHeaderLevelHtml = maxHeaderLevel || 4;
168+
const maxHeaderLevelHtml = isHtml ? 1 : (maxHeaderLevel || 4);
142169
const lines = content.split('\n');
143170
const info: SectionInfo = updateSection.parse(lines, matchesStart(checkOpeningComments), matchesEnd(checkClosingComments));
144171

@@ -158,19 +185,19 @@ export const transform = (
158185
const headers = getMarkdownHeaders(linesToToc, maxHeaderLevel).concat(getHtmlHeaders(linesToToc, maxHeaderLevelHtml));
159186
headers.sort((header1, header2) => header1.line - header2.line);
160187

161-
const allHeaders = countHeaders(headers, mode, moduleName);
188+
const allHeaders = countHeaders(headers, _mode, moduleName);
162189
const lowestRank = Math.min(...allHeaders.map(header => header.rank));
163-
const linkedHeaders = allHeaders.map(header => addAnchor(mode, moduleName, header));
190+
const linkedHeaders = allHeaders.map(header => addAnchor(_mode, moduleName, header));
164191

165192
const inferredTitle = linkedHeaders.length ? determineTitle(title, isNotitle, isFolding, lines, info) : '';
166193
const titleSeparator = inferredTitle ? '\n\n' : '\n';
167194

168195
// 4 spaces required for proper indention on Bitbucket and GitLab
169-
const indentation = (mode === 'bitbucket.org' || mode === 'gitlab.com') ? ' ' : ' ';
196+
const indentation = (_mode === 'bitbucket.org' || _mode === 'gitlab.com') ? ' ' : ' ';
170197
const toc =
171198
inferredTitle +
172199
titleSeparator +
173-
linkedHeaders.map(header => indentation.repeat(header.rank - lowestRank) + entryPrefix + ' ' + header.anchor).join('\n') +
200+
(isHtml ? getHtmlHeaderContents(linkedHeaders, htmlTemplate, itemTemplate, separator) : getHeaderContents(linkedHeaders, indentation, lowestRank, _entryPrefix)) +
174201
'\n';
175202
const wrappedToc = (openingComment ?? OPENING_COMMENT) + '\n' + wrapToc(toc, inferredTitle, isFolding) + '\n' + (closingComment ?? CLOSING_COMMENT);
176203
if (currentToc === wrappedToc) {

src/lib/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2+
3+
export const getRegExp = (value: string): RegExp => new RegExp(escapeRegExp(value));
4+
5+
export const replaceAll = (string: string, key: string | RegExp, value: string): string => string.split(key).join(value);
6+
7+
export const replaceVariables = (string: string, variables: { key: string; replace: string }[]): string => {
8+
let replaced = string;
9+
for (const variable of variables) {
10+
if (getRegExp(`\${${variable.key}}`).test(replaced)) {
11+
replaced = replaceAll(replaced, `\${${variable.key}}`, variable.replace);
12+
}
13+
}
14+
15+
return replaced;
16+
};

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export type TransformOptions = Partial<{
1414
closingComment: string;
1515
checkOpeningComments: Array<string>;
1616
checkClosingComments: Array<string>;
17+
isHtml: boolean;
18+
htmlTemplate: string;
19+
itemTemplate: string;
20+
separator: string;
1721
}>
1822

1923
export type FileInfo = {
@@ -44,6 +48,7 @@ export type HeaderWithRepetition = Header & {
4448

4549
export type HeaderWithAnchor = HeaderWithRepetition & {
4650
anchor: string;
51+
hash: string;
4752
}
4853

4954
export type SectionInfo = {

0 commit comments

Comments
 (0)