Skip to content

Commit cb0349a

Browse files
committed
Merge branch 'feat/url-fragment-generator' into chore/all-my-stuffs
# Conflicts: # src/tools/index.ts
2 parents 30f1c4e + b779beb commit cb0349a

File tree

6 files changed

+220
-2
lines changed

6 files changed

+220
-2
lines changed

pnpm-lock.yaml

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import { tool as portNumbers } from './port-numbers';
134134
import { tool as rsaEncryption } from './rsa-encryption';
135135
import { tool as urlCleaner } from './url-cleaner';
136136
import { tool as urlFanger } from './url-fanger';
137+
import { tool as urlTextFragmentMaker } from './url-text-fragment-maker';
137138
import { tool as pdfSignatureChecker } from './pdf-signature-checker';
138139
import { tool as numeronymGenerator } from './numeronym-generator';
139140
import { tool as macAddressGenerator } from './mac-address-generator';
@@ -340,6 +341,7 @@ export const toolsByCategory: ToolCategory[] = [
340341
urlParser,
341342
urlCleaner,
342343
urlFanger,
344+
urlTextFragmentMaker,
343345
deviceInformation,
344346
basicAuthGenerator,
345347
htpasswdGenerator,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FileSearch } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Url Text Search Fragment Maker',
6+
path: '/url-text-fragment-maker',
7+
description: 'Create url that allows linking directly to a specific portion of text in a web document',
8+
keywords: ['url', 'text', 'fragment'],
9+
component: () => import('./url-text-fragment-maker.vue'),
10+
icon: FileSearch,
11+
createdAt: new Date('2024-01-17'),
12+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getUrlWithTextFragment } from './url-text-fragment-maker.service';
3+
4+
describe('url-text-fragment-maker.service', () => {
5+
describe('getUrlWithTextFragment', () => {
6+
describe('compute url with text fragment', () => {
7+
it('throws on invalid url', () => {
8+
expect(() => getUrlWithTextFragment({
9+
url: 'example',
10+
textStartSearch: 'for',
11+
})).toThrow('Invalid url');
12+
expect(() => getUrlWithTextFragment({
13+
url: 'htt://example',
14+
textStartSearch: 'for',
15+
})).toThrow('Url must have http:// or https:// prefix');
16+
expect(() => getUrlWithTextFragment({
17+
url: 'http:/example',
18+
textStartSearch: 'for',
19+
})).toThrow('Url must have http:// or https:// prefix');
20+
});
21+
22+
it('should handle basic cases', () => {
23+
expect(getUrlWithTextFragment({
24+
url: 'https://example.com',
25+
textStartSearch: 'for',
26+
}))
27+
.toBe('https://example.com#:~:text=for');
28+
expect(getUrlWithTextFragment({
29+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
30+
textStartSearch: 'human',
31+
}))
32+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=human');
33+
});
34+
35+
it('should be url encoded', () => {
36+
expect(getUrlWithTextFragment({
37+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
38+
textStartSearch: 'linked URL',
39+
suffixSearch: '\'s format',
40+
}))
41+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=linked%20URL,-\'s%20format');
42+
expect(getUrlWithTextFragment({
43+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
44+
textStartSearch: 'The Referer',
45+
textStopSearch: 'be sent',
46+
prefixSearch: 'downgrade:',
47+
suffixSearch: 'to origins',
48+
}))
49+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=downgrade%3A-,The%20Referer,be%20sent,-to%20origins');
50+
});
51+
52+
it('should handle multiple comma separated and encoded', () => {
53+
expect(
54+
getUrlWithTextFragment({
55+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
56+
textStartSearch: 'Causes,linked',
57+
}))
58+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes&text=linked');
59+
60+
expect(
61+
getUrlWithTextFragment({
62+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
63+
textStartSearch: 'Causes 1,linked 1',
64+
}))
65+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes%201&text=linked%201');
66+
67+
expect(
68+
getUrlWithTextFragment({
69+
url: 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a',
70+
textStartSearch: 'Causes , linked',
71+
}))
72+
.toBe('https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#:~:text=Causes&text=linked');
73+
});
74+
});
75+
});
76+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export function getUrlWithTextFragment(
2+
{ url, textStartSearch, textStopSearch, prefixSearch, suffixSearch }:
3+
{ url: string
4+
textStartSearch: string
5+
textStopSearch?: string
6+
prefixSearch?: string
7+
suffixSearch?: string
8+
},
9+
) {
10+
const isValidUrl = (urlString: string) => {
11+
try {
12+
return Boolean(new URL(urlString));
13+
}
14+
catch (e) {
15+
return false;
16+
}
17+
};
18+
if (!isValidUrl(url)) {
19+
throw new Error('Invalid url');
20+
}
21+
22+
if (!url.match(/^https?:\/\//)) {
23+
throw new Error('Url must have http:// or https:// prefix');
24+
}
25+
26+
const [textStartSearchFirstText, ...textStartSearchOtherTexts] = textStartSearch.split(',');
27+
const text = `${encodeURIComponent(prefixSearch ?? '')}-,${encodeURIComponent(textStartSearchFirstText.trim())},${encodeURIComponent(textStopSearch ?? '')},-${encodeURIComponent(suffixSearch ?? '')}`
28+
.replace(/^-,|,(?=,)|,-$/g, '')
29+
.replace(/,+/g, ',');
30+
let textStartSearchOtherTextEncoded = textStartSearchOtherTexts.map(t => `text=${encodeURIComponent(t.trim())}`).join('&');
31+
if (textStartSearchOtherTextEncoded.length) {
32+
textStartSearchOtherTextEncoded = `&${textStartSearchOtherTextEncoded}`;
33+
}
34+
return `${url.trim()}#:~:text=${text}${textStartSearchOtherTextEncoded}`;
35+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script setup lang="ts">
2+
import { getUrlWithTextFragment } from './url-text-fragment-maker.service';
3+
import TextareaCopyable from '@/components/TextareaCopyable.vue';
4+
5+
const url = ref('');
6+
const prefixSearch = ref('');
7+
const textStartSearch = ref('');
8+
const textStopSearch = ref('');
9+
const suffixSearch = ref('');
10+
11+
const searchableUrl = computed(() => {
12+
try {
13+
return getUrlWithTextFragment({
14+
url: url.value,
15+
textStartSearch: textStartSearch.value,
16+
textStopSearch: textStopSearch.value,
17+
prefixSearch: prefixSearch.value,
18+
suffixSearch: suffixSearch.value,
19+
});
20+
}
21+
catch (e: any) {
22+
return e.toString();
23+
}
24+
});
25+
</script>
26+
27+
<template>
28+
<div>
29+
<n-p>
30+
Url with Text Fragments allows to make link to content that has no anchor or @id.
31+
<n-a href="https://developer.mozilla.org/en-US/docs/Web/Text_fragments" target="blank" rel="noopener">
32+
See MDN for more info
33+
</n-a>
34+
</n-p>
35+
<div>
36+
<c-input-text
37+
v-model:value="url"
38+
label="Base url:"
39+
placeholder="Base url..."
40+
type="url"
41+
clearable raw-text mb-5
42+
/>
43+
</div>
44+
45+
<div flex justify-center gap-2>
46+
<c-input-text
47+
v-model:value="textStartSearch"
48+
label="Start Search(es) (comma separated)"
49+
placeholder="Start Search(es) (comma separated)..."
50+
clearable
51+
raw-text
52+
mb-2
53+
/>
54+
<c-input-text
55+
v-model:value="textStopSearch"
56+
label="Stop Search"
57+
placeholder="Stop Search text..."
58+
clearable
59+
raw-text
60+
mb-2
61+
/>
62+
</div>
63+
64+
<div flex justify-center gap-2>
65+
<c-input-text
66+
v-model:value="prefixSearch"
67+
label="Prefix"
68+
placeholder="Prefix search"
69+
clearable
70+
raw-text
71+
mb-2
72+
/>
73+
<c-input-text
74+
v-model:value="suffixSearch"
75+
label="Suffix"
76+
placeholder="Suffix search"
77+
clearable
78+
raw-text
79+
mb-2
80+
/>
81+
</div>
82+
83+
<n-divider />
84+
85+
<n-form-item label="Searchable Url:">
86+
<TextareaCopyable :value="searchableUrl" />
87+
</n-form-item>
88+
<div flex justify-center>
89+
<n-a :href="searchableUrl" target="blank" rel="noopener">
90+
Test Searchable Url
91+
</n-a>
92+
</div>
93+
</div>
94+
</template>

0 commit comments

Comments
 (0)