feat(new tool): Markdown TOC Generator

Fix #573
This commit is contained in:
sharevb 2024-05-18 15:20:38 +02:00 committed by ShareVB
parent e876d03608
commit e32d38c057
9 changed files with 715 additions and 10 deletions

4
components.d.ts vendored
View file

@ -119,6 +119,7 @@ declare module '@vue/runtime-core' {
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default'] LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default'] MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default'] MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
MarkdownTocGenerator: typeof import('./src/tools/markdown-toc-generator/markdown-toc-generator.vue')['default']
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default'] MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default'] MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default'] MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
@ -129,15 +130,18 @@ declare module '@vue/runtime-core' {
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCode: typeof import('naive-ui')['NCode'] NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider'] NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi'] NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid'] NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInputNumber: typeof import('naive-ui')['NInputNumber'] NInputNumber: typeof import('naive-ui')['NInputNumber']
NLabel: typeof import('naive-ui')['NLabel'] NLabel: typeof import('naive-ui')['NLabel']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']

19
pnpm-lock.yaml generated
View file

@ -3351,7 +3351,7 @@ packages:
dependencies: dependencies:
'@unhead/dom': 0.5.1 '@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1 '@unhead/schema': 0.5.1
'@vueuse/shared': 10.7.2(vue@3.3.4) '@vueuse/shared': 10.11.0(vue@3.3.4)
unhead: 0.5.1 unhead: 0.5.1
vue: 3.3.4 vue: 3.3.4
transitivePeerDependencies: transitivePeerDependencies:
@ -3984,19 +3984,19 @@ packages:
- vue - vue
dev: false dev: false
/@vueuse/shared@10.3.0(vue@3.3.4): /@vueuse/shared@10.11.0(vue@3.3.4):
resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
dependencies: dependencies:
vue-demi: 0.14.5(vue@3.3.4) vue-demi: 0.14.8(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
dev: false dev: false
/@vueuse/shared@10.7.2(vue@3.3.4): /@vueuse/shared@10.3.0(vue@3.3.4):
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
dependencies: dependencies:
vue-demi: 0.14.6(vue@3.3.4) vue-demi: 0.14.5(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -9151,8 +9151,8 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-demi@0.14.6(vue@3.3.4): /vue-demi@0.14.8(vue@3.3.4):
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
@ -9442,6 +9442,7 @@ packages:
/workbox-google-analytics@7.0.0: /workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
dependencies: dependencies:
workbox-background-sync: 7.0.0 workbox-background-sync: 7.0.0
workbox-core: 7.0.0 workbox-core: 7.0.0

View file

@ -1,7 +1,8 @@
import { useRouteQuery } from '@vueuse/router'; import { useRouteQuery } from '@vueuse/router';
import { computed } from 'vue'; import { computed } from 'vue';
import { useStorage } from '@vueuse/core';
export { useQueryParam }; export { useQueryParam, useQueryParamOrStorage };
const transformers = { const transformers = {
number: { number: {
@ -16,6 +17,12 @@ const transformers = {
fromQuery: (value: string) => value.toLowerCase() === 'true', fromQuery: (value: string) => value.toLowerCase() === 'true',
toQuery: (value: boolean) => (value ? 'true' : 'false'), toQuery: (value: boolean) => (value ? 'true' : 'false'),
}, },
object: {
fromQuery: (value: string) => {
return JSON.parse(value);
},
toQuery: (value: object) => JSON.stringify(value),
},
}; };
function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) { function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
@ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
}, },
}); });
} }
function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
const type = typeof defaultValue;
const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
const storageRef = useStorage(storageName, defaultValue);
const proxyDefaultValue = transformer.toQuery(defaultValue as never);
const proxy = useRouteQuery(name, proxyDefaultValue);
const r = ref(defaultValue);
watch(r,
(value) => {
proxy.value = transformer.toQuery(value as never);
storageRef.value = value as never;
},
{ deep: true });
r.value = (proxy.value && proxy.value !== proxyDefaultValue
? transformer.fromQuery(proxy.value) as unknown as T
: storageRef.value as T) as never;
return r;
}

View file

@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter'; import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator'; import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as markdownTocGenerator } from './markdown-toc-generator';
import { tool as asciiTextDrawer } from './ascii-text-drawer'; import { tool as asciiTextDrawer } from './ascii-text-drawer';
@ -107,6 +108,7 @@ export const toolsByCategory: ToolCategory[] = [
listConverter, listConverter,
tomlToJson, tomlToJson,
tomlToYaml, tomlToYaml,
markdownTocGenerator,
], ],
}, },
{ {

View file

@ -0,0 +1,12 @@
import { Table } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Markdown toc generator',
path: '/markdown-toc-generator',
description: 'Generate a TOC from a markdown file/content',
keywords: ['markdown', 'toc', 'generator'],
component: () => import('./markdown-toc-generator.vue'),
icon: Table,
createdAt: new Date('2024-05-11'),
});

View file

@ -0,0 +1,6 @@
declare module 'markdown-contents'{
declare class MarkdownContents {
markdown(): string;
}
export default function Create(markdown: string):MarkdownContents;
}

View file

@ -0,0 +1,380 @@
import { describe, expect, it } from 'vitest';
import {
getTocMarkdown,
} from './markdown-toc-generator.service';
describe('markdown-toc-generator', () => {
it('Generate TOC correctly', async () => {
expect(getTocMarkdown({
markdown: '',
})).to.equal('');
const sourceMarkdown = `# Some main title
[TOC]
## First Title
Some text
## Second Spaced Title
Some text
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
### Title with code \`var\`
Some text
## Last Title`;
expect(getTocMarkdown({
markdown: sourceMarkdown,
anchorPrefix: 'h-',
})).to.equal(`# Some main title
<!-- TOC START -->
- [First Title](#h-first-title)
- [Second Spaced Title](#h-second-spaced-title)
* [Title with Link TOC](#h-title-with-link-toc)
* [Title with code \`var\`](#h-title-with-code-var)
- [Last Title](#h-last-title)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="first-title"></a>
## First Title
Some text
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
## Second Spaced Title
Some text
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
### Title with code \`var\`
Some text
<!-- TOC ANCHOR --><a name="last-title"></a>
## Last Title`);
expect(getTocMarkdown({
markdown: sourceMarkdown,
maxLevel: 2,
})).to.equal(`# Some main title
<!-- TOC START -->
- [First Title](#first-title)
- [Second Spaced Title](#second-spaced-title)
- [Last Title](#last-title)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="first-title"></a>
## First Title
Some text
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
## Second Spaced Title
Some text
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
### Title with code \`var\`
Some text
<!-- TOC ANCHOR --><a name="last-title"></a>
## Last Title`);
expect(getTocMarkdown({
markdown: sourceMarkdown,
commentStyle: 'liquid',
})).to.equal(`# Some main title
{%- # TOC START -%}
- [First Title](#first-title)
- [Second Spaced Title](#second-spaced-title)
* [Title with Link TOC](#title-with-link-toc)
* [Title with code \`var\`](#title-with-code-var)
- [Last Title](#last-title)
{%- # TOC END -%}
{%- # TOC ANCHOR -%}<a name="first-title"></a>
## First Title
Some text
{%- # TOC ANCHOR -%}<a name="second-spaced-title"></a>
## Second Spaced Title
Some text
{%- # TOC ANCHOR -%}<a name="title-with-link-toc"></a>
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
{%- # TOC ANCHOR -%}<a name="title-with-code-var"></a>
### Title with code \`var\`
Some text
{%- # TOC ANCHOR -%}<a name="last-title"></a>
## Last Title`);
expect(getTocMarkdown({
markdown: sourceMarkdown,
generateAnchors: false,
})).to.equal(`# Some main title
<!-- TOC START -->
- [First Title](#first-title)
- [Second Spaced Title](#second-spaced-title)
* [Title with Link TOC](#title-with-link-toc)
* [Title with code \`var\`](#title-with-code-var)
- [Last Title](#last-title)
<!-- TOC END -->
## First Title
Some text
## Second Spaced Title
Some text
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
### Title with code \`var\`
Some text
## Last Title`);
expect(getTocMarkdown({
markdown: sourceMarkdown,
indentSpaces: 4,
indentChars: '-',
concatSpaces: false,
})).to.equal(`# Some main title
<!-- TOC START -->
- [First Title](#first-title)
- [Second Spaced Title](#second--spaced--title)
- [Title with Link TOC](#title-with-link-toc)
- [Title with code \`var\`](#title-with-code-var)
- [Last Title](#last-title)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="first-title"></a>
## First Title
Some text
<!-- TOC ANCHOR --><a name="second--spaced--title"></a>
## Second Spaced Title
Some text
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
### Title with code \`var\`
Some text
<!-- TOC ANCHOR --><a name="last-title"></a>
## Last Title`);
});
it('Regenerate TOC correctly', async () => {
expect(getTocMarkdown({
markdown: `# Some main title
<!-- TOC START -->
- [First Title](#first-title)
- [Second Spaced Title](#second--spaced--title)
- [Title with Link TOC](#title-with-link-toc)
- [Title with code \`var\`](#title-with-code-var)
- [Last Title](#last-title)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="first-title"></a>
## First Title
Some text
<!-- TOC ANCHOR --><a name="second--spaced--title"></a>
## Second Spaced Title
Some text
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
### Title with code \`var\`
Some text
<!-- TOC ANCHOR --><a name="last-title"></a>
## Last Title`,
anchorPrefix: 'h-',
})).to.equal(`# Some main title
<!-- TOC START -->
- [First Title](#h-first-title)
- [Second Spaced Title](#h-second-spaced-title)
* [Title with Link TOC](#h-title-with-link-toc)
* [Title with code \`var\`](#h-title-with-code-var)
- [Last Title](#h-last-title)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="first-title"></a>
## First Title
Some text
<!-- TOC ANCHOR --><a name="second-spaced-title"></a>
## Second Spaced Title
Some text
<!-- TOC ANCHOR --><a name="title-with-link-toc"></a>
### Title with Link [TOC](http://it-tools.tech)
\`\`\`
## some bash code
echo 'test';
\`\`\`
<!-- TOC ANCHOR --><a name="title-with-code-var"></a>
### Title with code \`var\`
Some text
<!-- TOC ANCHOR --><a name="last-title"></a>
## Last Title`);
});
it('Generate distinct TOC ids', async () => {
expect(getTocMarkdown({
markdown: `# Some main title
[TOC]
## Same Title 1
Some text
## Same Title 1
Some text
### Same title 1
Some text`,
anchorPrefix: 'h-',
})).to.equal(`# Some main title
<!-- TOC START -->
- [Same Title 1](#h-same-title-1)
- [Same Title 1](#h-same-title-1-1)
* [Same title 1](#h-same-title-1-2)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="same-title-1"></a>
## Same Title 1
Some text
<!-- TOC ANCHOR --><a name="same-title-1-1"></a>
## Same Title 1
Some text
<!-- TOC ANCHOR --><a name="same-title-1-2"></a>
### Same title 1
Some text`);
});
it('Generate ids for non latin', async () => {
expect(getTocMarkdown({
markdown: `# Some main title
[TOC]
## Привет non-latin
Some text
## 😄 emoji
Some text
### Other title 1
Some text`,
anchorPrefix: 'h-',
})).to.equal(`# Some main title
<!-- TOC START -->
- [Привет non-latin ](#h--non-latin-)
- [😄 emoji](#h--emoji)
* [Other title 1](#h-other-title-1)
<!-- TOC END -->
<!-- TOC ANCHOR --><a name="-non-latin-"></a>
## Привет non-latin
Some text
<!-- TOC ANCHOR --><a name="-emoji"></a>
## 😄 emoji
Some text
<!-- TOC ANCHOR --><a name="other-title-1"></a>
### Other title 1
Some text`);
});
});

View file

@ -0,0 +1,178 @@
function stripNonLatinCharacters(text: string) {
return text.replace(/[^A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u02BB\u02EE\uA78C\d\s_-]/g, '');
};
function transformInlineCode(text: string, transform: (s: string) => string) {
return text.replace(/`(.*?)`/g, (_, p) => {
return `\`${transform(p)}\``;
});
};
function spacesToDash(text: string) {
return text.replace(/\s/g, '-');
};
function stripHtmlTags(text: string) {
return text.replace(/<.*?>/g, '');
};
function stripMarkdownLinks(text: string, replacement: string = '$1') {
return text.replace(/\[([^\]]*)\]\([^\)]*\)/g, replacement);
};
function concatDashes(text: string) {
return text.replace(/--+/g, '-');
};
function removeUnderscoreBoldAndItalics(text: string) {
const underscoreBoldAndItalicsRegexes = ['__', '_'].map((it) => {
return new RegExp(`\\b${it}([^_\\s]|[^_\\s].*?[^_\\s])${it}\\b`, 'g');
});
let result = text;
underscoreBoldAndItalicsRegexes.forEach((regex) => {
result = result.replace(regex, '$1');
});
return result;
};
function genericAnchorGenerator(text: string, concatSpaces: boolean) {
let result = text;
result = result.toLowerCase();
result = transformInlineCode(result, (s: string) => {
return stripNonLatinCharacters(s);
});
result = removeUnderscoreBoldAndItalics(result);
result = stripHtmlTags(result);
result = stripMarkdownLinks(result);
result = result.trim();
result = stripNonLatinCharacters(result);
result = spacesToDash(result);
if (concatSpaces) {
result = concatDashes(result);
}
return result;
};
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
interface Title {
level: number
id: string
name: string
md: string
}
function getTitles(markdown: string, idGenerator: (titleMarkdownContent: string) => string) {
const titles: Title[] = [];
markdown = markdown.replace(/^```[\s\S]*?\n```/mg, () => {
return '';
});
markdown = markdown.replace(/^~~~[\s\S]*?\n~~~/mg, () => {
return '';
});
[...markdown.matchAll(/^(#+)(.*$)/mg)].forEach(
([match, levelString, titleContent]) => {
const level = levelString.length;
titles.push({
md: match,
level,
id: idGenerator(titleContent),
name: titleContent.trim(),
});
});
return titles;
};
export function getTocMarkdown({
markdown,
generateAnchors = true,
indentChars = '-*+',
indentSpaces = 2,
maxLevel = -1,
anchorPrefix = '',
concatSpaces = true,
commentStyle = 'html',
}: {
markdown: string
generateAnchors?: boolean
indentChars?: string
indentSpaces?: number
maxLevel?: number
anchorPrefix?: string
concatSpaces?: boolean
commentStyle?: 'html' | 'liquid'
}) {
const allIds: { [id: string]: number } = {};
const getFinalId = (id: string) => {
if (typeof allIds[id] === 'undefined') {
allIds[id] = 0;
return id;
}
else {
allIds[id] += 1;
return `${id}-${allIds[id]}`;
}
};
const titles = getTitles(markdown, titleContent => getFinalId(genericAnchorGenerator(titleContent, concatSpaces)));
const createLink = (linkText: string, url: string) => {
return `[${linkText.replace('[', '\\[').replace(']', '\\]')}](${url.replace('(', '%28').replace(')', '%29')})`;
};
let markdownTOC = '';
let resultMarkdown = markdown;
const commentOpen = commentStyle === 'html' ? '<!--' : '{%- #';
const commentClose = commentStyle === 'html' ? '-->' : '-%}';
resultMarkdown = resultMarkdown.replace(
new RegExp(`\n${commentOpen} TOC START.*?TOC END ${commentClose}\n`, 'smg'),
'\n[TOC]\n',
);
resultMarkdown = resultMarkdown.replace(
new RegExp(`^${commentOpen} TOC ANCHOR.*?\n`, 'mg'),
'',
);
titles.forEach((title) => {
if (title.level === 1) {
return;
}
if (maxLevel > 0 && title.level > maxLevel) {
return;
}
const level = title.level - 2;
let offset = '';
if (level) {
offset = `${Array.from({ length: level * indentSpaces }).join(' ')} `;
}
const bulletChar = indentChars[level] ?? indentChars.slice(-1)[0];
const anchorName = `${anchorPrefix}${title.id}`;
markdownTOC += `${offset}${bulletChar} ${createLink(stripMarkdownLinks(title.name), `#${anchorName}`)}\n`;
if (generateAnchors) {
resultMarkdown = resultMarkdown.replace(
new RegExp(`(?<!^${commentOpen} TOC ANCHOR.*\n)^${escapeRegExp(title.md)}`, 'm'),
`${commentOpen} TOC ANCHOR ${commentClose}<a name="${title.id}"></a>\n${title.md}`,
);
}
});
resultMarkdown = resultMarkdown.replace(
/^\[TOC\]\n/mg,
`${commentOpen} TOC START ${commentClose}\n${markdownTOC}${commentOpen} TOC END ${commentClose}\n`,
);
return resultMarkdown;
}

View file

@ -0,0 +1,91 @@
<script setup lang="ts">
import { withDefaultOnError } from '../../utils/defaults';
import {
getTocMarkdown,
} from './markdown-toc-generator.service';
import { useQueryParamOrStorage } from '@/composable/queryParams';
const markdown = ref('');
const generateAnchors = useQueryParamOrStorage({ name: 'anchors', storageName: 'md-toc-gen:anchors', defaultValue: true });
const indentChars = useQueryParamOrStorage({ name: 'bullets', storageName: 'md-toc-gen:bullets', defaultValue: '-*+' });
const indentSpaces = ref(2);
const maxLevel = useQueryParamOrStorage({ name: 'max', storageName: 'md-toc-gen:max', defaultValue: -1 });
const anchorPrefix = useQueryParamOrStorage({ name: 'prefix', storageName: 'md-toc-gen:prefix', defaultValue: '' });
const concatSpaces = useQueryParamOrStorage({ name: 'concat', storageName: 'md-toc-gen:concat', defaultValue: true });
const commentStyle = useQueryParamOrStorage({ name: 'comment', storageName: 'md-toc-gen:comment', defaultValue: 'html' });
const markdownWithTOC = computed(() => withDefaultOnError(() => {
return getTocMarkdown({
markdown: markdown.value,
anchorPrefix: anchorPrefix.value,
commentStyle: commentStyle.value as ('html' | 'liquid'),
concatSpaces: concatSpaces.value,
generateAnchors: generateAnchors.value,
indentChars: indentChars.value,
indentSpaces: indentSpaces.value,
maxLevel: maxLevel.value,
});
}, ''));
</script>
<template>
<div>
<c-card title="Options" mb-2>
<n-form-item label="Generate Anchors" label-placement="left">
<n-checkbox v-model:checked="generateAnchors" mr-2 />
</n-form-item>
<c-input-text
v-model:value="indentChars"
label="Bullet Chars"
placeholder="Bullet Chars"
mb-2
/>
<n-form-item label="Indents: " label-placement="left">
<n-input-number v-model:value="indentSpaces" placeholder="Indents..." :max="10" :min="1" w-full />
</n-form-item>
<n-form-item label="Max Heading Level: " label-placement="left">
<n-input-number v-model:value="maxLevel" placeholder="Max Heading Level..." :max="6" :min="-1" w-full />
</n-form-item>
<c-input-text
v-model:value="anchorPrefix"
label="Anchors Prefix"
placeholder="Anchors Prefix"
mb-2
/>
<n-form-item label="Concat Spaces" label-placement="left">
<n-checkbox v-model:checked="concatSpaces" mr-2 />
</n-form-item>
<c-select
v-model:value="commentStyle"
label="Comment Styles"
:options="['html', 'liquid']"
placeholder="Comment Styles"
/>
</c-card>
<c-card title="Input markdown" mb-2>
<n-p>You can paste a document with existing TOC (generated by this tool) or add a <code>[TOC]</code> marker in your document (on a single line)</n-p>
<c-input-text
v-model:value="markdown"
placeholder="Put your markdown here..."
multline
rows="8"
mb-2
mt-2
/>
</c-card>
<c-card title="Output markdown with TOC" mb-2>
<textarea-copyable
language="markdown"
:value="markdownWithTOC"
/>
</c-card>
</div>
</template>