Skip to content

Commit 0ea588e

Browse files
authored
Add markdown-copy component HTML file
> Build a web component that depends on the most popular JavaScript markdown renderer loaded from a CDN by script module imports. > ``` > <markdown-copy> > ## this is markdown > > Here's more > > - and more > - And more > </markdown> > ``` > The component renders the markdown it contains with a bit of padding and a thin border with a light border radius, and in the top right is a little tag box that says markdown which, when clicked, provides a "copy" button and a "see code" - the see code option toggles the markdown from rendered to raw text in a pre that preserves newlines but does allow line breaks.
1 parent d61ad78 commit 0ea588e

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

markdown-copy-component.html

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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>markdown-copy component</title>
7+
<style>
8+
body {
9+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10+
max-width: 720px;
11+
margin: 60px auto;
12+
padding: 0 20px;
13+
background: #f8f9fa;
14+
color: #1a1a2e;
15+
}
16+
h1 { font-weight: 600; font-size: 1.4rem; margin-bottom: 0.3em; }
17+
p.intro { color: #555; font-size: 0.95rem; margin-bottom: 2rem; }
18+
</style>
19+
</head>
20+
<body>
21+
22+
<h1>&lt;markdown-copy&gt; demo</h1>
23+
<p class="intro">A web component that renders markdown, with copy &amp; view-source controls.</p>
24+
25+
<markdown-copy>
26+
## Getting Started
27+
28+
Welcome to the project! Here's what you need to know.
29+
30+
### Installation
31+
32+
```bash
33+
npm install my-cool-library
34+
```
35+
36+
### Features
37+
38+
- **Fast** — optimized for speed
39+
- **Lightweight** — under 5kb gzipped
40+
- **Extensible** — plugin architecture
41+
42+
> "Simplicity is the ultimate sophistication." — Leonardo da Vinci
43+
44+
### Usage
45+
46+
```js
47+
import { render } from 'my-cool-library';
48+
49+
render('#app', {
50+
template: '<h1>Hello world</h1>'
51+
});
52+
```
53+
54+
That's it — you're ready to go!
55+
</markdown-copy>
56+
57+
<br>
58+
59+
<markdown-copy>
60+
## Shopping List
61+
62+
Things to pick up:
63+
64+
- Sourdough bread
65+
- Olive oil (extra virgin)
66+
- Lemons × 4
67+
- Fresh basil
68+
- Mozzarella
69+
70+
| Item | Qty | Aisle |
71+
|------|-----|-------|
72+
| Bread | 1 | Bakery |
73+
| Olive oil | 1 | 3 |
74+
| Lemons | 4 | Produce |
75+
76+
*Don't forget the reusable bags!*
77+
</markdown-copy>
78+
79+
<!-- ════════════════════════════════════════════ -->
80+
<!-- Web Component Definition -->
81+
<!-- ════════════════════════════════════════════ -->
82+
<script type="module">
83+
import { marked } from 'https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/lib/marked.esm.js';
84+
85+
class MarkdownCopy extends HTMLElement {
86+
constructor() {
87+
super();
88+
this.attachShadow({ mode: 'open' });
89+
this._showRaw = false;
90+
this._menuOpen = false;
91+
}
92+
93+
connectedCallback() {
94+
/* ── grab raw markdown, dedent it ── */
95+
this._raw = this._dedent(this.textContent);
96+
97+
/* ── render ── */
98+
this.shadowRoot.innerHTML = `
99+
<style>${MarkdownCopy.styles}</style>
100+
<div class="wrap">
101+
<!-- tag / menu -->
102+
<div class="tag-area">
103+
<button class="tag" aria-label="Markdown options">markdown</button>
104+
<div class="menu hidden">
105+
<button class="menu-btn" data-action="copy">
106+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
107+
Copy
108+
</button>
109+
<button class="menu-btn" data-action="toggle">
110+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
111+
See code
112+
</button>
113+
</div>
114+
</div>
115+
<!-- content -->
116+
<div class="rendered">${marked.parse(this._raw)}</div>
117+
<pre class="raw hidden">${this._escapeHtml(this._raw)}</pre>
118+
</div>
119+
`;
120+
121+
/* ── wire events ── */
122+
const tag = this.shadowRoot.querySelector('.tag');
123+
const menu = this.shadowRoot.querySelector('.menu');
124+
125+
tag.addEventListener('click', (e) => {
126+
e.stopPropagation();
127+
this._menuOpen = !this._menuOpen;
128+
menu.classList.toggle('hidden', !this._menuOpen);
129+
});
130+
131+
this.shadowRoot.querySelector('[data-action="copy"]').addEventListener('click', () => {
132+
navigator.clipboard.writeText(this._raw).then(() => {
133+
const btn = this.shadowRoot.querySelector('[data-action="copy"]');
134+
const orig = btn.innerHTML;
135+
btn.textContent = 'Copied!';
136+
setTimeout(() => btn.innerHTML = orig, 1200);
137+
});
138+
});
139+
140+
this.shadowRoot.querySelector('[data-action="toggle"]').addEventListener('click', () => {
141+
this._showRaw = !this._showRaw;
142+
const rendered = this.shadowRoot.querySelector('.rendered');
143+
const raw = this.shadowRoot.querySelector('.raw');
144+
const btn = this.shadowRoot.querySelector('[data-action="toggle"]');
145+
rendered.classList.toggle('hidden', this._showRaw);
146+
raw.classList.toggle('hidden', !this._showRaw);
147+
/* update label */
148+
const svgIcon = btn.querySelector('svg')?.outerHTML || '';
149+
btn.innerHTML = svgIcon + (this._showRaw ? ' See rendered' : ' See code');
150+
});
151+
152+
/* close menu on outside click */
153+
document.addEventListener('click', () => {
154+
this._menuOpen = false;
155+
menu.classList.add('hidden');
156+
});
157+
158+
this.shadowRoot.addEventListener('click', (e) => {
159+
if (!e.target.closest('.tag-area')) {
160+
this._menuOpen = false;
161+
menu.classList.add('hidden');
162+
}
163+
});
164+
}
165+
166+
/* ── helpers ── */
167+
168+
_dedent(text) {
169+
const lines = text.replace(/^\n+/, '').replace(/\n+$/, '').split('\n');
170+
const indent = lines
171+
.filter(l => l.trim().length > 0)
172+
.reduce((min, l) => {
173+
const leading = l.match(/^ */)[0].length;
174+
return Math.min(min, leading);
175+
}, Infinity);
176+
return lines.map(l => l.slice(indent)).join('\n').trim();
177+
}
178+
179+
_escapeHtml(s) {
180+
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
181+
}
182+
183+
/* ── styles ── */
184+
185+
static get styles() {
186+
return /* css */`
187+
:host { display: block; }
188+
189+
.wrap {
190+
position: relative;
191+
border: 1px solid #d0d5dd;
192+
border-radius: 10px;
193+
padding: 1.25rem 1.5rem;
194+
background: #fff;
195+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
196+
color: #1a1a2e;
197+
line-height: 1.6;
198+
}
199+
200+
/* ── rendered markdown ── */
201+
.rendered { font-size: 0.95rem; }
202+
.rendered h1, .rendered h2, .rendered h3 {
203+
margin-top: 1.1em; margin-bottom: 0.4em; font-weight: 600;
204+
}
205+
.rendered h2 { font-size: 1.25rem; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
206+
.rendered h3 { font-size: 1.05rem; }
207+
.rendered p { margin: 0.5em 0; }
208+
.rendered ul, .rendered ol { padding-left: 1.4em; margin: 0.4em 0; }
209+
.rendered li { margin: 0.2em 0; }
210+
.rendered blockquote {
211+
margin: 0.8em 0; padding: 0.4em 1em;
212+
border-left: 3px solid #c5a3ff; background: #faf7ff;
213+
color: #444; font-style: italic;
214+
}
215+
.rendered code {
216+
background: #f0f0f5; padding: 0.15em 0.4em; border-radius: 4px;
217+
font-size: 0.88em; font-family: 'SF Mono', Consolas, monospace;
218+
}
219+
.rendered pre {
220+
background: #1e1e2e; color: #cdd6f4; padding: 1em;
221+
border-radius: 8px; overflow-x: auto; font-size: 0.85rem;
222+
line-height: 1.5;
223+
}
224+
.rendered pre code {
225+
background: none; padding: 0; color: inherit;
226+
}
227+
.rendered table {
228+
border-collapse: collapse; width: 100%; margin: 0.6em 0; font-size: 0.9rem;
229+
}
230+
.rendered th, .rendered td {
231+
border: 1px solid #ddd; padding: 0.45em 0.8em; text-align: left;
232+
}
233+
.rendered th { background: #f5f5fa; font-weight: 600; }
234+
.rendered img { max-width: 100%; border-radius: 6px; }
235+
236+
/* ── raw view ── */
237+
.raw {
238+
white-space: pre-wrap;
239+
word-wrap: break-word;
240+
font-family: 'SF Mono', Consolas, monospace;
241+
font-size: 0.88rem;
242+
line-height: 1.55;
243+
margin: 0;
244+
color: #374151;
245+
background: #f9fafb;
246+
padding: 0.8em;
247+
border-radius: 6px;
248+
}
249+
250+
/* ── tag & menu ── */
251+
.tag-area {
252+
position: absolute;
253+
top: 10px; right: 12px;
254+
display: flex; flex-direction: column; align-items: flex-end;
255+
z-index: 2;
256+
user-select: none;
257+
}
258+
.tag {
259+
background: #eef0ff;
260+
color: #5b5fc7;
261+
font-size: 0.7rem;
262+
font-weight: 600;
263+
text-transform: uppercase;
264+
letter-spacing: 0.04em;
265+
padding: 3px 10px;
266+
border-radius: 6px;
267+
border: 1px solid #d4d7ff;
268+
cursor: pointer;
269+
transition: background 0.15s, transform 0.1s;
270+
font-family: inherit;
271+
}
272+
.tag:hover { background: #dde0ff; }
273+
.tag:active { transform: scale(0.96); }
274+
275+
.menu {
276+
margin-top: 6px;
277+
background: #fff;
278+
border: 1px solid #e0e0e0;
279+
border-radius: 8px;
280+
box-shadow: 0 4px 16px rgba(0,0,0,0.10);
281+
overflow: hidden;
282+
min-width: 140px;
283+
}
284+
.menu-btn {
285+
display: flex; align-items: center; gap: 8px;
286+
width: 100%;
287+
padding: 8px 14px;
288+
font-size: 0.82rem;
289+
font-family: inherit;
290+
color: #333;
291+
background: none;
292+
border: none;
293+
cursor: pointer;
294+
transition: background 0.12s;
295+
}
296+
.menu-btn:hover { background: #f3f4f6; }
297+
.menu-btn + .menu-btn { border-top: 1px solid #f0f0f0; }
298+
.menu-btn svg { flex-shrink: 0; opacity: 0.6; }
299+
300+
.hidden { display: none !important; }
301+
`;
302+
}
303+
}
304+
305+
customElements.define('markdown-copy', MarkdownCopy);
306+
</script>
307+
308+
</body>
309+
</html>

0 commit comments

Comments
 (0)