Skip to content

Commit 282e445

Browse files
committed
Ship the share-this-code producer
The runner toolbar gains a quiet Copy link tool-button, injected by the inline script so no-JS pages render no dead chrome. It copies a URL embedding the editor's current code as the #code= fragment the page has decoded since the first commit, mirroring that decoder's encoding exactly; unedited code shares the clean page URL. The decoder now calls resizeEditor() so a shared payload renders fully sized in the textarea fallback. The early no-clipboard playground lesson is deliberately reversed: clipboard code is allowed, server markup still renders only Run and Reset. Verified end to end in headless Chromium against the built assets: click yields Link copied, the copied URL round-trips the edited unicode code back into the editor, unedited code copies the clean URL, and with the module CDN unreachable the share, resize, and copy-button paths all still work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJDjKbt3EyWPEThak9WQXp
1 parent 0e7f2b5 commit 282e445

6 files changed

Lines changed: 76 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.1.0
1111
- `/about` page describing how the site is made — the verified-output pipeline, the runner sandbox, the figure grammar, and the quality gates — with a design-language section that renders the live CSS tokens (color swatches, spacing scale, type specimens, and a real walkthrough cell as a specimen). Linked from the nav on every page and listed in the sitemap.
1212
- Copy buttons on read-only source cells: injected client-side onto `.cell-source` wrappers (so no-JS pages render no dead buttons), clipboard API with a hidden-textarea fallback, and a quiet token-styled treatment that sharpens on hover or focus.
1313
- Keyboard navigation on example pages: ``/`` follow the existing `rel="prev"`/`rel="next"` links, ignoring keystrokes with modifiers or focus inside the editor, inputs, or any editable surface.
14+
- "Copy link" button in the runner toolbar: copies a URL embedding the editor's current code as the `#code=` fragment the page has decoded since the first commit (the clean page URL when the code is unedited). The decoder now resizes the editor after loading a shared payload.
1415

1516
- Banner position grammar from `docs/visual-explainer-spec.md` is now production: `render_banner(slug, position)` supports `before`, `after-cell-N` (legacy anchor `cell-N`), and `after-walkthrough`, with multiple figures per position rendering as one small-multiple banner. The mutability page ships the canonical two-figure pair (aliased mutation vs. frozen tuple).
1617
- Curated pair banners on contrast cells: `positional-only-parameters` shows the `/` and `*` separator twins side by side, `metaclasses` pairs the metaclass triangle with the familiar class triangle, and `tuples` pairs the frozen tuple with the growing list on the intent-contrast cell. `iterator-vs-iterable` gains the one-pass caret figure on the exhaustion cell.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Production: <https://www.pythonbyexample.dev> (`workers.dev` remains enabled as
2323
- `/about` page describing the verification pipeline, runner sandbox, figure grammar, and live design tokens
2424
- Copy buttons on read-only source cells
2525
- Keyboard prev/next navigation (``/``) on example pages
26+
- "Copy link" sharing of the runner's current code via `#code=` URLs
2627

2728
## Attribution
2829

docs/elevation-strategy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ all aimed at the scorecard's engagement and return lines:
207207
**Now (days; mostly small, single-surface changes)**
208208

209209
1. Share-this-code link + copy buttons on code blocks.
210-
*(Copy buttons shipped 2026-07-10; the share link is still open.)*
210+
*(Both shipped 2026-07-10.)*
211211
2. `/about` page + edit-on-GitHub links.
212212
*(`/about` shipped 2026-07-10; edit-on-GitHub links are still open.)*
213213
3. `/llms.txt`, per-example Markdown endpoints, Atom feed.

src/asset_manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Generated by scripts/fingerprint_assets.py. Do not edit by hand.
22
ASSET_PATHS = {'SITE_CSS': '/site.73603216fda2.css', 'SYNTAX_JS': '/syntax-highlight.95ae4ebe5db8.js', 'EDITOR_JS': '/editor.bbf94cd1abda.js', 'SEARCH_JS': '/search.ab0effeac6ce.js', 'SEARCH_INDEX': '/search-index.f08e8599474a.json'}
3-
HTML_CACHE_VERSION = '2fa761ca8de2'
3+
HTML_CACHE_VERSION = '92063e60f2e4'

src/templates/example.html

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,52 @@ <h3>Example code</h3>
111111
});
112112
const hash = new URL(window.location.href).hash;
113113
if (hash.startsWith('#code=')) {
114-
try { editor().value = decodeURIComponent(escape(atob(hash.slice(6)))); } catch (error) {}
114+
try { editor().value = decodeURIComponent(escape(atob(hash.slice(6)))); resizeEditor(); } catch (error) {}
115+
}
116+
async function copyTextToClipboard(text) {
117+
if (navigator.clipboard && window.isSecureContext) {
118+
await navigator.clipboard.writeText(text);
119+
return;
120+
}
121+
const holder = document.createElement('textarea');
122+
holder.value = text;
123+
holder.setAttribute('readonly', '');
124+
holder.style.position = 'fixed';
125+
holder.style.left = '-9999px';
126+
document.body.appendChild(holder);
127+
holder.select();
128+
const copied = document.execCommand('copy');
129+
holder.remove();
130+
if (!copied) throw new Error('execCommand copy failed');
131+
}
132+
// Share the current editor code as the #code= fragment the loader
133+
// above has accepted since day one. Injected here so no-JS pages
134+
// render no dead button; unedited code shares the clean page URL.
135+
const toolbar = document.querySelector('.playground-toolbar');
136+
if (toolbar) {
137+
const shareButton = document.createElement('button');
138+
shareButton.type = 'button';
139+
shareButton.className = 'tool-button';
140+
shareButton.textContent = 'Copy link';
141+
shareButton.setAttribute('aria-live', 'polite');
142+
shareButton.setAttribute('aria-label', 'Copy a link to this example with the current code');
143+
let shareRestore = null;
144+
shareButton.addEventListener('click', async () => {
145+
if (window.pythonByExampleEditor) window.pythonByExampleEditor.syncTextarea();
146+
const code = editor().value;
147+
const pageUrl = window.location.origin + window.location.pathname;
148+
const url = code === originalCode ? pageUrl : pageUrl + '#code=' + btoa(unescape(encodeURIComponent(code)));
149+
clearTimeout(shareRestore);
150+
let feedback = 'Link copied';
151+
if (url.length > 8000) {
152+
feedback = 'Code too long to link';
153+
} else {
154+
try { await copyTextToClipboard(url); } catch (error) { feedback = 'Copy failed'; }
155+
}
156+
shareButton.textContent = feedback;
157+
shareRestore = setTimeout(() => { shareButton.textContent = 'Copy link'; }, 1600);
158+
});
159+
toolbar.append(shareButton);
115160
}
116161
document.addEventListener('keydown', (event) => {
117162
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;

tests/test_app.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,12 @@ def test_cf_workers_design_system_and_playground_lessons(self):
369369
self.assertIn('.cm-editor', css)
370370
self.assertIn("function resetCode", html)
371371
self.assertIn('class="syntax-inline">print()</code>', html)
372-
self.assertNotIn("navigator.clipboard", html)
372+
# The 2026-07 share-link work deliberately reversed the early
373+
# no-clipboard playground lesson. Clipboard code is now allowed,
374+
# but copy/share affordances stay JS-injected: server markup
375+
# renders only the Run and Reset buttons.
376+
self.assertEqual(html.count("<button"), 2)
377+
self.assertIn("copyTextToClipboard", html)
373378
self.assertIn("fetch(form.action", html)
374379
self.assertIn("new URLSearchParams(formData)", html)
375380
self.assertIn("application/x-www-form-urlencoded", html)
@@ -811,6 +816,26 @@ def test_arrow_navigation_skips_editable_and_modified_keys(self):
811816
self.assertIn("event.metaKey", page)
812817
self.assertIn("event.defaultPrevented", page)
813818

819+
def test_share_button_copies_a_code_fragment_link(self):
820+
page = render_example_page(get_example("values"))
821+
self.assertIn("Copy link", page)
822+
self.assertIn("btoa(unescape(encodeURIComponent(code)))", page)
823+
self.assertIn("code === originalCode ? pageUrl : pageUrl + '#code='", page)
824+
self.assertIn("'.playground-toolbar'", page)
825+
self.assertIn('aria-live', page)
826+
827+
def test_share_encoder_mirrors_the_day_one_decoder(self):
828+
page = render_example_page(get_example("values"))
829+
self.assertIn("decodeURIComponent(escape(atob(hash.slice(6))))", page)
830+
self.assertIn("btoa(unescape(encodeURIComponent(code)))", page)
831+
832+
def test_hash_decode_resizes_the_editor(self):
833+
page = render_example_page(get_example("values"))
834+
self.assertIn(
835+
"editor().value = decodeURIComponent(escape(atob(hash.slice(6)))); resizeEditor();",
836+
page,
837+
)
838+
814839
def test_arrow_navigation_guards_missing_neighbors_at_catalog_edges(self):
815840
examples = list_examples()
816841
first_page = render_example_page(get_example(examples[0]["slug"]))

0 commit comments

Comments
 (0)