parent
							
								
									e876d03608
								
							
						
					
					
						commit
						e32d38c057
					
				
							
								
								
									
										4
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -119,6 +119,7 @@ declare module '@vue/runtime-core' { | ||||
|     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'] | ||||
|     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'] | ||||
|     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'] | ||||
| @ -129,15 +130,18 @@ declare module '@vue/runtime-core' { | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NColorPicker: typeof import('naive-ui')['NColorPicker'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NForm: typeof import('naive-ui')['NForm'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NGi: typeof import('naive-ui')['NGi'] | ||||
|     NGrid: typeof import('naive-ui')['NGrid'] | ||||
|     NH1: typeof import('naive-ui')['NH1'] | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NImage: typeof import('naive-ui')['NImage'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
|     NLabel: typeof import('naive-ui')['NLabel'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|  | ||||
							
								
								
									
										19
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -3351,7 +3351,7 @@ packages: | ||||
|     dependencies: | ||||
|       '@unhead/dom': 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 | ||||
|       vue: 3.3.4 | ||||
|     transitivePeerDependencies: | ||||
| @ -3984,19 +3984,19 @@ packages: | ||||
|       - vue | ||||
|     dev: false | ||||
| 
 | ||||
|   /@vueuse/shared@10.3.0(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} | ||||
|   /@vueuse/shared@10.11.0(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==} | ||||
|     dependencies: | ||||
|       vue-demi: 0.14.5(vue@3.3.4) | ||||
|       vue-demi: 0.14.8(vue@3.3.4) | ||||
|     transitivePeerDependencies: | ||||
|       - '@vue/composition-api' | ||||
|       - vue | ||||
|     dev: false | ||||
| 
 | ||||
|   /@vueuse/shared@10.7.2(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} | ||||
|   /@vueuse/shared@10.3.0(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} | ||||
|     dependencies: | ||||
|       vue-demi: 0.14.6(vue@3.3.4) | ||||
|       vue-demi: 0.14.5(vue@3.3.4) | ||||
|     transitivePeerDependencies: | ||||
|       - '@vue/composition-api' | ||||
|       - vue | ||||
| @ -9151,8 +9151,8 @@ packages: | ||||
|       vue: 3.3.4 | ||||
|     dev: false | ||||
| 
 | ||||
|   /vue-demi@0.14.6(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} | ||||
|   /vue-demi@0.14.8(vue@3.3.4): | ||||
|     resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==} | ||||
|     engines: {node: '>=12'} | ||||
|     hasBin: true | ||||
|     requiresBuild: true | ||||
| @ -9442,6 +9442,7 @@ packages: | ||||
| 
 | ||||
|   /workbox-google-analytics@7.0.0: | ||||
|     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: | ||||
|       workbox-background-sync: 7.0.0 | ||||
|       workbox-core: 7.0.0 | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import { useRouteQuery } from '@vueuse/router'; | ||||
| import { computed } from 'vue'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| 
 | ||||
| export { useQueryParam }; | ||||
| export { useQueryParam, useQueryParamOrStorage }; | ||||
| 
 | ||||
| const transformers = { | ||||
|   number: { | ||||
| @ -16,6 +17,12 @@ const transformers = { | ||||
|     fromQuery: (value: string) => value.toLowerCase() === 'true', | ||||
|     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 }) { | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { tool as base64FileConverter } from './base64-file-converter'; | ||||
| import { tool as base64StringConverter } from './base64-string-converter'; | ||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||
| import { tool as markdownTocGenerator } from './markdown-toc-generator'; | ||||
| 
 | ||||
| import { tool as asciiTextDrawer } from './ascii-text-drawer'; | ||||
| 
 | ||||
| @ -107,6 +108,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       listConverter, | ||||
|       tomlToJson, | ||||
|       tomlToYaml, | ||||
|       markdownTocGenerator, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|  | ||||
							
								
								
									
										12
									
								
								src/tools/markdown-toc-generator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/markdown-toc-generator/index.ts
									
									
									
									
									
										Normal 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'), | ||||
| }); | ||||
							
								
								
									
										6
									
								
								src/tools/markdown-toc-generator/markdown-contents.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/tools/markdown-toc-generator/markdown-contents.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| declare module 'markdown-contents'{ | ||||
|     declare class MarkdownContents { | ||||
|         markdown(): string; | ||||
|       } | ||||
|     export default function Create(markdown: string):MarkdownContents; | ||||
| } | ||||
| @ -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`);
 | ||||
|   }); | ||||
| }); | ||||
| @ -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; | ||||
| } | ||||
							
								
								
									
										91
									
								
								src/tools/markdown-toc-generator/markdown-toc-generator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/tools/markdown-toc-generator/markdown-toc-generator.vue
									
									
									
									
									
										Normal 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> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user