Skip to content

Commit 3446272

Browse files
Add Clipboard HTML → Markdown tool (#111)
### Motivation - Provide a simple standalone tool that lets users paste rich HTML from the clipboard and get equivalent Markdown, with one-click paste and copy actions. ### Description - Add `html-to-markdown-clipboard.html`, a self-contained UI that uses the `turndown` CDN to convert HTML to Markdown and exposes `Paste from clipboard` and `Copy Markdown` buttons that use `navigator.clipboard.read` and `navigator.clipboard.writeText` respectively. - Add `html-to-markdown-clipboard.docs.md` with a short description for the new tool. ------ [Codex Task](https://chatgpt.com/codex/tasks/task_e_6968b207730c8325a2a4be59b4a4fabf)
1 parent ea6b99a commit 3446272

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

html-to-markdown-clipboard.docs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Turn rich HTML from your clipboard into clean Markdown. Click the paste button to pull HTML or plain text from the clipboard, and the tool immediately converts it to Markdown with a one-click copy button.

html-to-markdown-clipboard.html

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Clipboard HTML to Markdown</title>
8+
<link rel="stylesheet" href="styles.css">
9+
<style>
10+
body {
11+
max-width: 960px;
12+
margin: 0 auto;
13+
padding: 24px 20px 48px;
14+
}
15+
16+
.page-header {
17+
display: flex;
18+
flex-direction: column;
19+
gap: 0.5rem;
20+
}
21+
22+
.site-link {
23+
font-weight: 600;
24+
color: var(--foreground-subtle);
25+
text-decoration: none;
26+
}
27+
28+
.site-link:hover,
29+
.site-link:focus-visible {
30+
color: var(--foreground);
31+
}
32+
33+
main {
34+
display: grid;
35+
gap: 1.5rem;
36+
}
37+
38+
.tool-card {
39+
padding: clamp(1.25rem, 3vw, 2rem);
40+
}
41+
42+
.tool-actions {
43+
display: flex;
44+
flex-wrap: wrap;
45+
gap: 0.75rem;
46+
}
47+
48+
.grid {
49+
display: grid;
50+
gap: 1rem;
51+
}
52+
53+
@media (min-width: 860px) {
54+
.grid {
55+
grid-template-columns: repeat(2, minmax(0, 1fr));
56+
}
57+
}
58+
59+
textarea {
60+
font-family: var(--font-mono);
61+
min-height: 220px;
62+
resize: vertical;
63+
}
64+
65+
.status {
66+
font-size: 0.95rem;
67+
color: var(--foreground-subtle);
68+
}
69+
70+
.status.error {
71+
color: var(--re);
72+
}
73+
74+
@media (max-width: 720px) {
75+
body {
76+
padding: 20px 16px 40px;
77+
}
78+
}
79+
</style>
80+
</head>
81+
82+
<body>
83+
<header class="page-header">
84+
<a class="site-link" href="https://tools.mathspp.com/" aria-label="Back to tools.mathspp.com">← tools.mathspp.com</a>
85+
<h1>Clipboard HTML to Markdown</h1>
86+
<p class="lead">Paste rich HTML from your clipboard and get clean Markdown that you can copy instantly.</p>
87+
</header>
88+
89+
<main>
90+
<section class="surface tool-card">
91+
<div class="tool-actions">
92+
<button type="button" id="paste-button">Paste from clipboard</button>
93+
<button type="button" id="copy-button">Copy Markdown</button>
94+
</div>
95+
<p class="status" id="status">Waiting for clipboard input.</p>
96+
</section>
97+
98+
<section class="surface tool-card">
99+
<div class="grid">
100+
<div>
101+
<label for="source-input">Clipboard contents (HTML or text)</label>
102+
<textarea id="source-input" placeholder="Paste rich HTML here or click the button to pull from your clipboard."></textarea>
103+
</div>
104+
<div>
105+
<label for="markdown-output">Markdown output</label>
106+
<textarea id="markdown-output" readonly placeholder="Markdown will appear here."></textarea>
107+
</div>
108+
</div>
109+
</section>
110+
</main>
111+
112+
<script src="https://cdn.jsdelivr.net/npm/turndown@7.1.2/dist/turndown.js"></script>
113+
<script>
114+
(function () {
115+
const pasteButton = document.getElementById('paste-button');
116+
const copyButton = document.getElementById('copy-button');
117+
const sourceInput = document.getElementById('source-input');
118+
const markdownOutput = document.getElementById('markdown-output');
119+
const status = document.getElementById('status');
120+
121+
const turndownService = new TurndownService({
122+
codeBlockStyle: 'fenced',
123+
headingStyle: 'atx'
124+
});
125+
126+
const isProbablyHtml = (text) => /<\s*\/?[a-z][\s\S]*>/i.test(text);
127+
128+
const setStatus = (message, isError = false) => {
129+
status.textContent = message;
130+
status.classList.toggle('error', isError);
131+
};
132+
133+
const updateMarkdown = (source, preferHtml = false) => {
134+
if (!source) {
135+
markdownOutput.value = '';
136+
setStatus('Waiting for clipboard input.');
137+
return;
138+
}
139+
140+
const treatAsHtml = preferHtml || isProbablyHtml(source);
141+
if (treatAsHtml) {
142+
markdownOutput.value = turndownService.turndown(source);
143+
setStatus('Converted HTML to Markdown.');
144+
} else {
145+
markdownOutput.value = source;
146+
setStatus('Using plain text from clipboard.');
147+
}
148+
};
149+
150+
pasteButton.addEventListener('click', async () => {
151+
try {
152+
if (!navigator.clipboard || !navigator.clipboard.read) {
153+
setStatus('Clipboard read is not supported in this browser.', true);
154+
return;
155+
}
156+
const items = await navigator.clipboard.read();
157+
let html = '';
158+
let text = '';
159+
160+
for (const item of items) {
161+
if (item.types.includes('text/html')) {
162+
const blob = await item.getType('text/html');
163+
html = await blob.text();
164+
break;
165+
}
166+
if (item.types.includes('text/plain') && !text) {
167+
const blob = await item.getType('text/plain');
168+
text = await blob.text();
169+
}
170+
}
171+
172+
const source = html || text;
173+
sourceInput.value = source;
174+
updateMarkdown(source, Boolean(html));
175+
} catch (error) {
176+
setStatus(`Clipboard read failed: ${error.message}`, true);
177+
}
178+
});
179+
180+
copyButton.addEventListener('click', async () => {
181+
const text = markdownOutput.value;
182+
if (!text) {
183+
setStatus('Nothing to copy yet.', true);
184+
return;
185+
}
186+
try {
187+
await navigator.clipboard.writeText(text);
188+
const original = copyButton.textContent;
189+
copyButton.textContent = 'Copied!';
190+
setStatus('Markdown copied to clipboard.');
191+
setTimeout(() => {
192+
copyButton.textContent = original;
193+
}, 1600);
194+
} catch (error) {
195+
setStatus(`Copy failed: ${error.message}`, true);
196+
}
197+
});
198+
199+
sourceInput.addEventListener('paste', (event) => {
200+
const html = event.clipboardData?.getData('text/html');
201+
const text = event.clipboardData?.getData('text/plain');
202+
if (html || text) {
203+
event.preventDefault();
204+
const source = html || text;
205+
sourceInput.value = source;
206+
updateMarkdown(source, Boolean(html));
207+
}
208+
});
209+
210+
sourceInput.addEventListener('input', () => {
211+
updateMarkdown(sourceInput.value, false);
212+
});
213+
})();
214+
</script>
215+
216+
<footer class="page-footer">
217+
<p>Built with ❤️, 🤖, and 🐍, by <a href="https://mathspp.com/">Rodrigo Girão Serrão</a></p>
218+
</footer>
219+
</body>
220+
221+
</html>

0 commit comments

Comments
 (0)