This commit is contained in:
sharevb 2024-03-03 09:14:50 +00:00 committed by GitHub
commit cf693f09c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 219 additions and 0 deletions

View file

@ -2,6 +2,7 @@ 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 textToUnicode } from './text-to-unicode'; 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 pdfSignatureChecker } from './pdf-signature-checker';
import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as numeronymGenerator } from './numeronym-generator';
import { tool as macAddressGenerator } from './mac-address-generator'; import { tool as macAddressGenerator } from './mac-address-generator';
@ -111,6 +112,7 @@ export const toolsByCategory: ToolCategory[] = [
urlEncoder, urlEncoder,
htmlEntities, htmlEntities,
urlParser, urlParser,
urlTextFragmentMaker,
deviceInformation, deviceInformation,
basicAuthGenerator, basicAuthGenerator,
metaTagGenerator, metaTagGenerator,

View file

@ -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'),
});

View file

@ -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');
});
});
});
});

View file

@ -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}`;
}

View file

@ -0,0 +1,94 @@
<script setup lang="ts">
import { getUrlWithTextFragment } from './url-text-fragment-maker.service';
import TextareaCopyable from '@/components/TextareaCopyable.vue';
const url = ref('');
const prefixSearch = ref('');
const textStartSearch = ref('');
const textStopSearch = ref('');
const suffixSearch = ref('');
const searchableUrl = computed(() => {
try {
return getUrlWithTextFragment({
url: url.value,
textStartSearch: textStartSearch.value,
textStopSearch: textStopSearch.value,
prefixSearch: prefixSearch.value,
suffixSearch: suffixSearch.value,
});
}
catch (e: any) {
return e.toString();
}
});
</script>
<template>
<div>
<n-p>
Url with Text Fragments allows to make link to content that has no anchor or @id.
<n-a href="https://developer.mozilla.org/en-US/docs/Web/Text_fragments" target="blank" rel="noopener">
See MDN for more info
</n-a>
</n-p>
<div>
<c-input-text
v-model:value="url"
label="Base url:"
placeholder="Base url..."
type="url"
clearable raw-text mb-5
/>
</div>
<div flex justify-center gap-2>
<c-input-text
v-model:value="textStartSearch"
label="Start Search(es) (comma separated)"
placeholder="Start Search(es) (comma separated)..."
clearable
raw-text
mb-2
/>
<c-input-text
v-model:value="textStopSearch"
label="Stop Search"
placeholder="Stop Search text..."
clearable
raw-text
mb-2
/>
</div>
<div flex justify-center gap-2>
<c-input-text
v-model:value="prefixSearch"
label="Prefix"
placeholder="Prefix search"
clearable
raw-text
mb-2
/>
<c-input-text
v-model:value="suffixSearch"
label="Suffix"
placeholder="Suffix search"
clearable
raw-text
mb-2
/>
</div>
<n-divider />
<n-form-item label="Searchable Url:">
<TextareaCopyable :value="searchableUrl" />
</n-form-item>
<div flex justify-center>
<n-a :href="searchableUrl" target="blank" rel="noopener">
Test Searchable Url
</n-a>
</div>
</div>
</template>