diff --git a/src/tools/index.ts b/src/tools/index.ts index 2a477ed2..28c80d15 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,6 +2,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 textToUnicode } from './text-to-unicode'; +import { tool as urlTextFragmentMaker } from './url-text-fragment-maker'; import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as macAddressGenerator } from './mac-address-generator'; @@ -111,6 +112,7 @@ export const toolsByCategory: ToolCategory[] = [ urlEncoder, htmlEntities, urlParser, + urlTextFragmentMaker, deviceInformation, basicAuthGenerator, metaTagGenerator, diff --git a/src/tools/url-text-fragment-maker/index.ts b/src/tools/url-text-fragment-maker/index.ts new file mode 100644 index 00000000..8370bfa9 --- /dev/null +++ b/src/tools/url-text-fragment-maker/index.ts @@ -0,0 +1,12 @@ +import { FileSearch } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Url Text Search Fragment Maker', + path: '/url-text-fragment-maker', + description: 'Create url that allows linking directly to a specific portion of text in a web document', + keywords: ['url', 'text', 'fragment'], + component: () => import('./url-text-fragment-maker.vue'), + icon: FileSearch, + createdAt: new Date('2024-01-17'), +}); diff --git a/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.test.ts b/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.test.ts new file mode 100644 index 00000000..48780474 --- /dev/null +++ b/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import { getUrlWithTextFragment } from './url-text-fragment-maker.service'; + +describe('url-text-fragment-maker.service', () => { + describe('getUrlWithTextFragment', () => { + describe('compute url with text fragment', () => { + it('throws on invalid url', () => { + expect(() => getUrlWithTextFragment({ + url: 'example', + textStartSearch: 'for', + })).toThrow('Invalid url'); + expect(() => getUrlWithTextFragment({ + url: 'htt://example', + textStartSearch: 'for', + })).toThrow('Url must have http:// or https:// prefix'); + expect(() => getUrlWithTextFragment({ + url: 'http:/example', + textStartSearch: 'for', + })).toThrow('Url must have http:// or https:// prefix'); + }); + + it('should handle basic cases', () => { + expect(getUrlWithTextFragment({ + url: 'https://example.com', + textStartSearch: 'for', + })) + .toBe('https://example.com#:~:text=for'); + expect(getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'human', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=human'); + }); + + it('should be url encoded', () => { + expect(getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'linked URL', + suffixSearch: '\'s format', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=linked%20URL,-\'s%20format'); + expect(getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'The Referer', + textStopSearch: 'be sent', + prefixSearch: 'downgrade:', + suffixSearch: 'to origins', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=downgrade%3A-,The%20Referer,be%20sent,-to%20origins'); + }); + + it('should handle multiple comma separated and encoded', () => { + expect( + getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'Causes,linked', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes&text=linked'); + + expect( + getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'Causes 1,linked 1', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes%201&text=linked%201'); + + expect( + getUrlWithTextFragment({ + url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a', + textStartSearch: 'Causes , linked', + })) + .toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes&text=linked'); + }); + }); + }); +}); diff --git a/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.ts b/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.ts new file mode 100644 index 00000000..48b75a2f --- /dev/null +++ b/src/tools/url-text-fragment-maker/url-text-fragment-maker.service.ts @@ -0,0 +1,35 @@ +export function getUrlWithTextFragment( + { url, textStartSearch, textStopSearch, prefixSearch, suffixSearch }: + { url: string + textStartSearch: string + textStopSearch?: string + prefixSearch?: string + suffixSearch?: string + }, +) { + const isValidUrl = (urlString: string) => { + try { + return Boolean(new URL(urlString)); + } + catch (e) { + return false; + } + }; + if (!isValidUrl(url)) { + throw new Error('Invalid url'); + } + + if (!url.match(/^https?:\/\//)) { + throw new Error('Url must have http:// or https:// prefix'); + } + + const [textStartSearchFirstText, ...textStartSearchOtherTexts] = textStartSearch.split(','); + const text = `${encodeURIComponent(prefixSearch ?? '')}-,${encodeURIComponent(textStartSearchFirstText.trim())},${encodeURIComponent(textStopSearch ?? '')},-${encodeURIComponent(suffixSearch ?? '')}` + .replace(/^-,|,(?=,)|,-$/g, '') + .replace(/,+/g, ','); + let textStartSearchOtherTextEncoded = textStartSearchOtherTexts.map(t => `text=${encodeURIComponent(t.trim())}`).join('&'); + if (textStartSearchOtherTextEncoded.length) { + textStartSearchOtherTextEncoded = `&${textStartSearchOtherTextEncoded}`; + } + return `${url.trim()}#:~:text=${text}${textStartSearchOtherTextEncoded}`; +} diff --git a/src/tools/url-text-fragment-maker/url-text-fragment-maker.vue b/src/tools/url-text-fragment-maker/url-text-fragment-maker.vue new file mode 100644 index 00000000..537fb106 --- /dev/null +++ b/src/tools/url-text-fragment-maker/url-text-fragment-maker.vue @@ -0,0 +1,94 @@ + + +