Skip to content

Commit daa72d9

Browse files
simonwclaude
andauthored
Add link extractor tool for extracting links from pasted content (#116)
* Add link extractor tool for extracting links from pasted content Similar to the alt text extractor, this tool allows users to paste rich text content from web pages and extract all links. Features include: - Renders extracted links on the page with title and URL - Copy as HTML (unordered list with anchor tags) - Copy as Markdown (bullet list with link syntax) - Copy as plain text (title followed by newline and URL) - Preview sections showing the output format before copying - Deduplication of identical links * Move copy buttons next to their relevant output sections Each export format (HTML, Markdown, Plain Text) now has its own copy button in the header row next to the format title, making it clearer which button copies which format. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4286738 commit daa72d9

File tree

1 file changed

+374
-0
lines changed

1 file changed

+374
-0
lines changed

link-extractor.html

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Link Extractor</title>
7+
<style>
8+
* {
9+
box-sizing: border-box;
10+
}
11+
12+
body {
13+
font-family: Helvetica, Arial, sans-serif;
14+
max-width: 800px;
15+
margin: 0 auto;
16+
padding: 20px;
17+
background-color: #f5f5f5;
18+
}
19+
20+
h1 {
21+
color: #333;
22+
text-align: center;
23+
margin-bottom: 30px;
24+
}
25+
26+
.paste-area {
27+
width: 100%;
28+
min-height: 150px;
29+
max-height: 250px;
30+
padding: 15px;
31+
border: 2px dashed #ccc;
32+
border-radius: 8px;
33+
background-color: white;
34+
font-size: 16px;
35+
margin-bottom: 20px;
36+
outline: none;
37+
overflow-y: auto;
38+
}
39+
40+
.paste-area:focus {
41+
border-color: #007bff;
42+
}
43+
44+
.instructions {
45+
text-align: center;
46+
color: #666;
47+
margin-bottom: 20px;
48+
font-size: 14px;
49+
}
50+
51+
.results {
52+
margin-top: 20px;
53+
}
54+
55+
.export-header {
56+
display: flex;
57+
justify-content: space-between;
58+
align-items: center;
59+
margin-bottom: 10px;
60+
}
61+
62+
.link-count {
63+
background-color: white;
64+
border-radius: 8px;
65+
padding: 15px;
66+
margin-bottom: 20px;
67+
border: 1px solid #ddd;
68+
text-align: center;
69+
font-weight: bold;
70+
color: #333;
71+
}
72+
73+
.link-item {
74+
background-color: white;
75+
border-radius: 8px;
76+
padding: 15px;
77+
margin-bottom: 10px;
78+
border: 1px solid #ddd;
79+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
80+
}
81+
82+
.link-title {
83+
font-weight: bold;
84+
color: #333;
85+
margin-bottom: 5px;
86+
word-wrap: break-word;
87+
}
88+
89+
.link-url {
90+
color: #007bff;
91+
font-size: 14px;
92+
word-wrap: break-word;
93+
word-break: break-all;
94+
}
95+
96+
.link-url a {
97+
color: #007bff;
98+
text-decoration: none;
99+
}
100+
101+
.link-url a:hover {
102+
text-decoration: underline;
103+
}
104+
105+
.copy-button {
106+
background-color: #007bff;
107+
color: white;
108+
border: none;
109+
padding: 10px 16px;
110+
border-radius: 4px;
111+
cursor: pointer;
112+
font-size: 14px;
113+
transition: background-color 0.2s;
114+
}
115+
116+
.copy-button:hover {
117+
background-color: #0056b3;
118+
}
119+
120+
.copy-button:disabled {
121+
background-color: #6c757d;
122+
cursor: not-allowed;
123+
}
124+
125+
.copy-success {
126+
background-color: #28a745 !important;
127+
}
128+
129+
.clear-button {
130+
background-color: #6c757d;
131+
color: white;
132+
border: none;
133+
padding: 10px 20px;
134+
border-radius: 4px;
135+
cursor: pointer;
136+
font-size: 14px;
137+
margin-bottom: 20px;
138+
}
139+
140+
.clear-button:hover {
141+
background-color: #545b62;
142+
}
143+
144+
.no-links {
145+
text-align: center;
146+
color: #6c757d;
147+
font-style: italic;
148+
padding: 20px;
149+
}
150+
151+
.export-section {
152+
background-color: white;
153+
border-radius: 8px;
154+
padding: 15px;
155+
margin-bottom: 20px;
156+
border: 1px solid #ddd;
157+
}
158+
159+
.export-section h3 {
160+
margin: 0;
161+
color: #333;
162+
font-size: 14px;
163+
}
164+
165+
.export-section .copy-button {
166+
padding: 6px 12px;
167+
font-size: 13px;
168+
}
169+
170+
.export-preview {
171+
background-color: #f8f9fa;
172+
border: 1px solid #e9ecef;
173+
border-radius: 4px;
174+
padding: 10px;
175+
font-size: 13px;
176+
font-family: monospace;
177+
white-space: pre-wrap;
178+
word-wrap: break-word;
179+
max-height: 150px;
180+
overflow-y: auto;
181+
margin-bottom: 10px;
182+
}
183+
</style>
184+
</head>
185+
<body>
186+
<h1>Link Extractor</h1>
187+
188+
<div class="instructions">
189+
Paste rich text content from web pages below to extract links
190+
</div>
191+
192+
<div contenteditable="true" class="paste-area" id="pasteArea" placeholder="Paste your content here..."></div>
193+
194+
<button class="clear-button" id="clearButton">Clear</button>
195+
196+
<div class="results" id="results"></div>
197+
198+
<script type="module">
199+
const pasteArea = document.getElementById('pasteArea');
200+
const results = document.getElementById('results');
201+
const clearButton = document.getElementById('clearButton');
202+
203+
let extractedLinks = [];
204+
205+
pasteArea.addEventListener('paste', handlePaste);
206+
clearButton.addEventListener('click', clearAll);
207+
results.addEventListener('click', handleCopyClick);
208+
209+
function handlePaste(event) {
210+
setTimeout(() => {
211+
extractLinks();
212+
}, 10);
213+
}
214+
215+
function extractLinks() {
216+
const links = pasteArea.querySelectorAll('a');
217+
218+
extractedLinks = [];
219+
220+
links.forEach((link) => {
221+
const href = link.href;
222+
const title = link.textContent.trim() || href;
223+
224+
if (href && href.startsWith('http')) {
225+
// Avoid duplicates
226+
const exists = extractedLinks.some(l => l.url === href && l.title === title);
227+
if (!exists) {
228+
extractedLinks.push({ title, url: href });
229+
}
230+
}
231+
});
232+
233+
if (extractedLinks.length === 0) {
234+
results.innerHTML = '<div class="no-links">No links found in pasted content</div>';
235+
return;
236+
}
237+
238+
renderResults();
239+
}
240+
241+
function renderResults() {
242+
let html = `<div class="link-count">${extractedLinks.length} link${extractedLinks.length === 1 ? '' : 's'} found</div>`;
243+
244+
// Export previews
245+
html += `
246+
<div class="export-section">
247+
<div class="export-header">
248+
<h3>HTML</h3>
249+
<button class="copy-button" data-format="html">Copy</button>
250+
</div>
251+
<div class="export-preview">${escapeHtml(generateHtml())}</div>
252+
</div>
253+
<div class="export-section">
254+
<div class="export-header">
255+
<h3>Markdown</h3>
256+
<button class="copy-button" data-format="markdown">Copy</button>
257+
</div>
258+
<div class="export-preview">${escapeHtml(generateMarkdown())}</div>
259+
</div>
260+
<div class="export-section">
261+
<div class="export-header">
262+
<h3>Plain Text</h3>
263+
<button class="copy-button" data-format="plain">Copy</button>
264+
</div>
265+
<div class="export-preview">${escapeHtml(generatePlain())}</div>
266+
</div>
267+
`;
268+
269+
// Individual links
270+
extractedLinks.forEach((link, index) => {
271+
html += `
272+
<div class="link-item">
273+
<div class="link-title">${escapeHtml(link.title)}</div>
274+
<div class="link-url"><a href="${escapeHtml(link.url)}" target="_blank" rel="noopener">${escapeHtml(link.url)}</a></div>
275+
</div>
276+
`;
277+
});
278+
279+
results.innerHTML = html;
280+
}
281+
282+
function generateHtml() {
283+
let html = '<ul>\n';
284+
extractedLinks.forEach(link => {
285+
html += ` <li><a href="${link.url}">${escapeHtml(link.title)}</a></li>\n`;
286+
});
287+
html += '</ul>';
288+
return html;
289+
}
290+
291+
function generateMarkdown() {
292+
return extractedLinks.map(link => `- [${link.title}](${link.url})`).join('\n');
293+
}
294+
295+
function generatePlain() {
296+
return extractedLinks.map(link => `${link.title}\n${link.url}`).join('\n\n');
297+
}
298+
299+
function escapeHtml(text) {
300+
const div = document.createElement('div');
301+
div.textContent = text;
302+
return div.innerHTML;
303+
}
304+
305+
function handleCopyClick(event) {
306+
const button = event.target;
307+
if (button.classList.contains('copy-button') && button.dataset.format) {
308+
copyAsFormat(button.dataset.format, button);
309+
}
310+
}
311+
312+
async function copyAsFormat(format, button) {
313+
let text;
314+
315+
switch (format) {
316+
case 'html':
317+
text = generateHtml();
318+
break;
319+
case 'markdown':
320+
text = generateMarkdown();
321+
break;
322+
case 'plain':
323+
text = generatePlain();
324+
break;
325+
}
326+
327+
try {
328+
await navigator.clipboard.writeText(text);
329+
330+
const originalText = button.textContent;
331+
button.textContent = 'Copied!';
332+
button.classList.add('copy-success');
333+
334+
setTimeout(() => {
335+
button.textContent = originalText;
336+
button.classList.remove('copy-success');
337+
}, 2000);
338+
339+
} catch (err) {
340+
console.error('Failed to copy: ', err);
341+
342+
// Fallback
343+
const textArea = document.createElement('textarea');
344+
textArea.value = text;
345+
document.body.appendChild(textArea);
346+
textArea.select();
347+
try {
348+
document.execCommand('copy');
349+
button.textContent = 'Copied!';
350+
button.classList.add('copy-success');
351+
352+
setTimeout(() => {
353+
button.textContent = button.dataset.original || 'Copy';
354+
button.classList.remove('copy-success');
355+
}, 2000);
356+
} catch (fallbackErr) {
357+
console.error('Fallback copy failed: ', fallbackErr);
358+
}
359+
document.body.removeChild(textArea);
360+
}
361+
}
362+
363+
function clearAll() {
364+
pasteArea.innerHTML = '';
365+
results.innerHTML = '';
366+
extractedLinks = [];
367+
pasteArea.focus();
368+
}
369+
370+
// Set initial focus
371+
pasteArea.focus();
372+
</script>
373+
</body>
374+
</html>

0 commit comments

Comments
 (0)