Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions __tests__/transform-html-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable no-magic-numbers */
import {resolve} from 'path';
import {readFileSync} from 'fs';
import {transform} from '../src';

describe('transform', () => {
it('run in html mode', () => {
const content = readFileSync(resolve(__dirname, 'fixtures/readme-with-html.md'), 'utf8');
const headers = transform(content, {mode: 'github.com', isHtml: true});

expect(headers.toc.split('\n')).toEqual(
['**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*',
'',
'<p align="center">',
'<a href="#installation">Installation</a>',
'<span>|</span>',
'<a href="#api">API</a>',
'</p>',
''],
);
});

it('run in html mode with custom settings', () => {
const content = readFileSync(resolve(__dirname, 'fixtures/readme-with-html.md'), 'utf8');
const headers = transform(content, {
mode: 'github.com',
isHtml: true,
htmlTemplate: '<ul>${ITEMS}</ul>',
itemTemplate: '<li><a href="${LINK}" target="_blank">${TEXT}</a></li>',
separator: '',
});

expect(headers.toc.split('\n')).toEqual(
['**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*',
'',
'<ul>',
'<li><a href="#installation" target="_blank">Installation</a></li>',
'',
'<li><a href="#api" target="_blank">API</a></li>',
'</ul>',
''],
);
});
});
12 changes: 12 additions & 0 deletions __tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable no-magic-numbers */
import {replaceVariables} from '../src/lib/utils';

describe('replaceVariables', () => {
it('should replace variables', () => {
expect(replaceVariables('', [])).toBe('');
expect(replaceVariables('abc/${test1}/${test2}/${test1}/xyz', [
{key: 'test1', replace: '1'},
{key: 'test3', replace: '3'},
])).toBe('abc/1/${test2}/1/xyz');
});
});
3 changes: 3 additions & 0 deletions src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export const CHECK_CLOSING_COMMENT = '<!-- END doctoc ';
export const DEFAULT_TITLE = '**Table of Contents** *generated with [DocToc](https://github.com/technote-space/doctoc)*';
export const MARKDOWN_EXTENSIONS = ['.md', '.markdown'];
export const IGNORED_DIRS = ['.', '..', '.git', 'node_modules'];
export const DEFAULT_HTML_TEMPLATE = '<p align="center">${ITEMS}</p>';
export const DEFAULT_ITEM_TEMPLATE = '<a href="${LINK}">${TEXT}</a>';
export const DEFAULT_SEPARATOR = '<span>|</span>';
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ export {
DEFAULT_TITLE,
MARKDOWN_EXTENSIONS,
IGNORED_DIRS,
DEFAULT_HTML_TEMPLATE,
DEFAULT_ITEM_TEMPLATE,
DEFAULT_SEPARATOR,
} from './constant';
43 changes: 35 additions & 8 deletions src/lib/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import updateSection from 'update-section';
import * as md from '@textlint/markdown-to-ast';
import {TxtNode} from '@textlint/ast-node-types';
import {getHtmlHeaders} from './get-html-headers';
import {replaceVariables} from './utils';
import {
OPENING_COMMENT,
CLOSING_COMMENT,
CHECK_OPENING_COMMENT,
CHECK_CLOSING_COMMENT,
DEFAULT_TITLE,
DEFAULT_HTML_TEMPLATE,
DEFAULT_ITEM_TEMPLATE,
DEFAULT_SEPARATOR,
} from '..';
import {TransformOptions, Header, HeaderWithRepetition, HeaderWithAnchor, SectionInfo, TransformResult} from '../types';

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

export const matchesStart = (checkOpeningComments?: Array<string>) => (line: string): boolean => getTargetComments(checkOpeningComments ?? [], CHECK_OPENING_COMMENT).some(comment => new RegExp(comment).test(line));
export const matchesEnd = (checkClosingComments?: Array<string>) => (line: string): boolean => getTargetComments(checkClosingComments ?? [], CHECK_CLOSING_COMMENT).some(comment => new RegExp(comment).test(line));
const addAnchor = (mode: string | undefined, moduleName: string | undefined, header: HeaderWithRepetition): HeaderWithAnchor => {
const addAnchor = (mode: string, moduleName: string | undefined, header: HeaderWithRepetition): HeaderWithAnchor => {
return {
...header,
anchor: anchor(header.name, mode, header.repetition, moduleName),
hash: getUrlHash(header.name, mode, header.repetition, moduleName),
};
};

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

const getHeaderContents = (headers: Array<HeaderWithAnchor>, indentation: string, lowestRank: number, entryPrefix: string): string => headers.map(header => getHeaderItem(header, indentation, lowestRank, entryPrefix)).join('\n');

const getHeaderItem = (header: HeaderWithAnchor, indentation: string, lowestRank: number, entryPrefix: string): string => {
return `${indentation.repeat(header.rank - lowestRank)}${entryPrefix} ${header.anchor}`;
};

const getHtmlHeaderContents = (headers: Array<HeaderWithAnchor>, htmlTemplate: string | undefined, itemTemplate: string | undefined, separator: string | undefined): string => replaceVariables(htmlTemplate ?? DEFAULT_HTML_TEMPLATE, [{
key: 'ITEMS',
replace: `\n${headers.map(header => getHeaderItemHtml(header, itemTemplate)).join(`\n${separator ?? DEFAULT_SEPARATOR}\n`)}\n`,
}]);

const getHeaderItemHtml = (header: HeaderWithAnchor, itemTemplate: string | undefined): string => {
return replaceVariables(itemTemplate ?? DEFAULT_ITEM_TEMPLATE, [
{key: 'LINK', replace: `#${header.hash}`},
{key: 'TEXT', replace: header.name},
]);
};

export const transform = (
content: string,
{
Expand All @@ -131,14 +154,18 @@ export const transform = (
closingComment,
checkOpeningComments,
checkClosingComments,
isHtml,
htmlTemplate,
itemTemplate,
separator,
}: TransformOptions = {},
): TransformResult => {
mode = mode || 'github.com';
entryPrefix = entryPrefix || '-';
const _mode = mode || 'github.com';
const _entryPrefix = entryPrefix || '-';

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

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

const allHeaders = countHeaders(headers, mode, moduleName);
const allHeaders = countHeaders(headers, _mode, moduleName);
const lowestRank = Math.min(...allHeaders.map(header => header.rank));
const linkedHeaders = allHeaders.map(header => addAnchor(mode, moduleName, header));
const linkedHeaders = allHeaders.map(header => addAnchor(_mode, moduleName, header));

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

// 4 spaces required for proper indention on Bitbucket and GitLab
const indentation = (mode === 'bitbucket.org' || mode === 'gitlab.com') ? ' ' : ' ';
const indentation = (_mode === 'bitbucket.org' || _mode === 'gitlab.com') ? ' ' : ' ';
const toc =
inferredTitle +
titleSeparator +
linkedHeaders.map(header => indentation.repeat(header.rank - lowestRank) + entryPrefix + ' ' + header.anchor).join('\n') +
(isHtml ? getHtmlHeaderContents(linkedHeaders, htmlTemplate, itemTemplate, separator) : getHeaderContents(linkedHeaders, indentation, lowestRank, _entryPrefix)) +
'\n';
const wrappedToc = (openingComment ?? OPENING_COMMENT) + '\n' + wrapToc(toc, inferredTitle, isFolding) + '\n' + (closingComment ?? CLOSING_COMMENT);
if (currentToc === wrappedToc) {
Expand Down
16 changes: 16 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

export const getRegExp = (value: string): RegExp => new RegExp(escapeRegExp(value));

export const replaceAll = (string: string, key: string | RegExp, value: string): string => string.split(key).join(value);

export const replaceVariables = (string: string, variables: { key: string; replace: string }[]): string => {
let replaced = string;
for (const variable of variables) {
if (getRegExp(`\${${variable.key}}`).test(replaced)) {
replaced = replaceAll(replaced, `\${${variable.key}}`, variable.replace);
}
}

return replaced;
};
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export type TransformOptions = Partial<{
closingComment: string;
checkOpeningComments: Array<string>;
checkClosingComments: Array<string>;
isHtml: boolean;
htmlTemplate: string;
itemTemplate: string;
separator: string;
}>

export type FileInfo = {
Expand Down Expand Up @@ -44,6 +48,7 @@ export type HeaderWithRepetition = Header & {

export type HeaderWithAnchor = HeaderWithRepetition & {
anchor: string;
hash: string;
}

export type SectionInfo = {
Expand Down