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'] |     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
									
									
									
								
							
							
						
						
									
										19
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -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 | ||||||
|  | |||||||
| @ -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; | ||||||
|  | } | ||||||
|  | |||||||
| @ -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, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|  | |||||||
							
								
								
									
										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