Skip to content

Commit 797999e

Browse files
committed
Add ability to copy content as markdown for llms
1 parent 022bc16 commit 797999e

3 files changed

Lines changed: 297 additions & 11 deletions

File tree

_includes/copy_page.html

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
{% comment %}
2+
Copy Page Component
3+
Provides a dropdown to copy the page as markdown or view the raw markdown
4+
{% endcomment %}
5+
6+
{% if site.gh_edit_repository and site.gh_edit_branch and page.path %}
7+
{% comment %} Extract repo path from full URL {% endcomment %}
8+
{% assign repo_parts = site.gh_edit_repository | split: "github.com/" %}
9+
{% assign repo_path = repo_parts[1] %}
10+
{% assign raw_url = "https://raw.githubusercontent.com/" | append: repo_path | append: "/" | append: site.gh_edit_branch | append: "/" | append: page.path %}
11+
{% assign view_url = site.gh_edit_repository | append: "/raw/refs/heads/" | append: site.gh_edit_branch | append: "/" | append: page.path %}
12+
13+
<div class="copy-page-dropdown">
14+
<button class="copy-page-btn" id="copyPageBtn" aria-label="Copy page options" aria-expanded="false">
15+
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
16+
<path fill="currentColor" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
17+
<path fill="currentColor" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
18+
</svg>
19+
<span>Copy page</span>
20+
<svg class="dropdown-arrow" viewBox="0 0 16 16" width="12" height="12" aria-hidden="true">
21+
<path fill="currentColor" d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"></path>
22+
</svg>
23+
</button>
24+
25+
<div class="copy-page-menu" id="copyPageMenu" role="menu" aria-hidden="true">
26+
<button class="copy-page-menu-item" id="copyMarkdownBtn" role="menuitem" data-raw-url="{{ raw_url }}">
27+
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
28+
<path fill="currentColor" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
29+
<path fill="currentColor" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
30+
</svg>
31+
<div class="menu-item-text">
32+
<div class="menu-item-title">Copy page</div>
33+
<div class="menu-item-subtitle">Copy page as Markdown for LLMs</div>
34+
</div>
35+
</button>
36+
37+
<a class="copy-page-menu-item" href="{{ view_url }}" target="_blank" rel="noopener noreferrer" role="menuitem">
38+
<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
39+
<path fill="currentColor" d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0 1 13.25 15H2.75A1.75 1.75 0 0 1 1 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25ZM4.75 5h6.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5Zm0 3h6.5a.75.75 0 0 1 0 1.5h-6.5a.75.75 0 0 1 0-1.5Zm0 3h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1 0-1.5Z"></path>
40+
</svg>
41+
<div class="menu-item-text">
42+
<div class="menu-item-title">View as Markdown →</div>
43+
<div class="menu-item-subtitle">View this page as plain text</div>
44+
</div>
45+
</a>
46+
</div>
47+
</div>
48+
49+
<script>
50+
document.addEventListener('DOMContentLoaded', function() {
51+
var copyPageBtn = document.getElementById('copyPageBtn');
52+
var copyPageMenu = document.getElementById('copyPageMenu');
53+
var copyMarkdownBtn = document.getElementById('copyMarkdownBtn');
54+
55+
if (!copyPageBtn || !copyPageMenu || !copyMarkdownBtn) {
56+
return;
57+
}
58+
59+
/* Toggle dropdown */
60+
copyPageBtn.addEventListener('click', function(e) {
61+
e.preventDefault();
62+
e.stopPropagation();
63+
64+
var isExpanded = copyPageBtn.getAttribute('aria-expanded') === 'true';
65+
copyPageBtn.setAttribute('aria-expanded', String(!isExpanded));
66+
copyPageMenu.setAttribute('aria-hidden', String(isExpanded));
67+
copyPageMenu.classList.toggle('show');
68+
});
69+
70+
/* Close dropdown when clicking outside */
71+
document.addEventListener('click', function(e) {
72+
if (!copyPageBtn.contains(e.target) && !copyPageMenu.contains(e.target)) {
73+
copyPageBtn.setAttribute('aria-expanded', 'false');
74+
copyPageMenu.setAttribute('aria-hidden', 'true');
75+
copyPageMenu.classList.remove('show');
76+
}
77+
});
78+
79+
/* Copy markdown to clipboard */
80+
copyMarkdownBtn.addEventListener('click', function(e) {
81+
e.preventDefault();
82+
e.stopPropagation();
83+
84+
var rawUrl = this.getAttribute('data-raw-url');
85+
var titleElement = copyMarkdownBtn.querySelector('.menu-item-title');
86+
var originalText = titleElement.textContent;
87+
88+
titleElement.textContent = 'Loading...';
89+
90+
fetch(rawUrl)
91+
.then(function(response) {
92+
if (!response.ok) throw new Error('Failed to fetch markdown');
93+
return response.text();
94+
})
95+
.then(function(markdown) {
96+
return navigator.clipboard.writeText(markdown);
97+
})
98+
.then(function() {
99+
titleElement.textContent = 'Copied!';
100+
setTimeout(function() {
101+
titleElement.textContent = originalText;
102+
copyPageBtn.setAttribute('aria-expanded', 'false');
103+
copyPageMenu.setAttribute('aria-hidden', 'true');
104+
copyPageMenu.classList.remove('show');
105+
}, 1500);
106+
})
107+
.catch(function(err) {
108+
console.error('Error copying markdown:', err);
109+
titleElement.textContent = 'Error!';
110+
setTimeout(function() {
111+
titleElement.textContent = originalText;
112+
}, 2000);
113+
});
114+
});
115+
116+
/* Keyboard navigation */
117+
copyPageBtn.addEventListener('keydown', function(e) {
118+
if (e.key === 'Escape') {
119+
copyPageBtn.setAttribute('aria-expanded', 'false');
120+
copyPageMenu.setAttribute('aria-hidden', 'true');
121+
copyPageMenu.classList.remove('show');
122+
}
123+
});
124+
});
125+
</script>
126+
{% endif %}
127+

_layouts/default.html

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,24 @@
8080
<div id="main-content-wrap" class="main-content-wrap">
8181
{% unless page.url == "/" %}
8282
{% if page.parent %}
83-
<nav aria-label="Breadcrumb" class="breadcrumb-nav">
84-
<ol class="breadcrumb-nav-list">
85-
{% if page.grand_parent %}
86-
<li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.grand_parent }}</a></li>
87-
<li class="breadcrumb-nav-list-item"><a href="{{ second_level_url }}">{{ page.parent }}</a></li>
88-
{% else %}
89-
<li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.parent }}</a></li>
90-
{% endif %}
91-
<li class="breadcrumb-nav-list-item"><span>{{ page.title }}</span></li>
92-
</ol>
93-
</nav>
83+
<div class="breadcrumb-with-actions">
84+
<nav aria-label="Breadcrumb" class="breadcrumb-nav">
85+
<ol class="breadcrumb-nav-list">
86+
{% if page.grand_parent %}
87+
<li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.grand_parent }}</a></li>
88+
<li class="breadcrumb-nav-list-item"><a href="{{ second_level_url }}">{{ page.parent }}</a></li>
89+
{% else %}
90+
<li class="breadcrumb-nav-list-item"><a href="{{ first_level_url }}">{{ page.parent }}</a></li>
91+
{% endif %}
92+
<li class="breadcrumb-nav-list-item"><span>{{ page.title }}</span></li>
93+
</ol>
94+
</nav>
95+
{% include copy_page.html %}
96+
</div>
97+
{% else %}
98+
<div class="page-header-actions">
99+
{% include copy_page.html %}
100+
</div>
94101
{% endif %}
95102
{% endunless %}
96103

_sass/custom/custom.scss

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,155 @@ p.text-delta + ul#markdown-toc {
426426
}
427427
}
428428

429+
// =============================================================================
430+
// Copy Page Dropdown
431+
// =============================================================================
432+
433+
.breadcrumb-with-actions {
434+
display: flex;
435+
justify-content: space-between;
436+
align-items: center;
437+
gap: 1rem;
438+
margin-bottom: 1rem;
439+
440+
.breadcrumb-nav {
441+
flex: 1;
442+
margin-bottom: 0 !important;
443+
}
444+
}
445+
446+
.page-header-actions {
447+
display: flex;
448+
justify-content: flex-end;
449+
margin-bottom: 1rem;
450+
margin-top: -0.5rem;
451+
}
452+
453+
.copy-page-dropdown {
454+
position: relative;
455+
display: inline-block;
456+
457+
@media (max-width: 800px) {
458+
display: none; // Hide on mobile to save space
459+
}
460+
}
461+
462+
.copy-page-btn {
463+
display: flex;
464+
align-items: center;
465+
gap: 0.375rem;
466+
padding: 0.375rem 0.75rem;
467+
font-size: 0.875rem;
468+
font-weight: 500;
469+
color: $grey-dk-200;
470+
background-color: white;
471+
border: 1px solid $grey-lt-200;
472+
border-radius: 6px;
473+
cursor: pointer;
474+
transition: all 0.15s ease;
475+
white-space: nowrap;
476+
477+
&:hover {
478+
background-color: $grey-lt-000;
479+
border-color: $grey-lt-100;
480+
color: $grey-dk-300;
481+
}
482+
483+
&:focus {
484+
outline: 2px solid $link-color;
485+
outline-offset: 2px;
486+
}
487+
488+
&[aria-expanded="true"] {
489+
background-color: $grey-lt-000;
490+
border-color: $grey-lt-100;
491+
492+
.dropdown-arrow {
493+
transform: rotate(180deg);
494+
}
495+
}
496+
497+
svg {
498+
flex-shrink: 0;
499+
}
500+
501+
.dropdown-arrow {
502+
transition: transform 0.2s ease;
503+
}
504+
}
505+
506+
.copy-page-menu {
507+
position: absolute;
508+
top: calc(100% + 0.5rem);
509+
right: 0;
510+
min-width: 280px;
511+
background-color: white;
512+
border: 1px solid $grey-lt-200;
513+
border-radius: 8px;
514+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.1);
515+
z-index: 1000;
516+
opacity: 0;
517+
visibility: hidden;
518+
transform: translateY(-8px);
519+
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
520+
overflow: hidden;
521+
522+
&.show {
523+
opacity: 1;
524+
visibility: visible;
525+
transform: translateY(0);
526+
}
527+
}
528+
529+
.copy-page-menu-item {
530+
display: flex;
531+
align-items: flex-start;
532+
gap: 0.75rem;
533+
padding: 0.75rem 1rem;
534+
width: 100%;
535+
background: none;
536+
border: none;
537+
text-align: left;
538+
cursor: pointer;
539+
transition: background-color 0.15s ease;
540+
text-decoration: none;
541+
color: inherit;
542+
543+
&:hover {
544+
background-color: $grey-lt-000;
545+
}
546+
547+
&:active {
548+
background-color: $grey-lt-100;
549+
}
550+
551+
&:not(:last-child) {
552+
border-bottom: 1px solid $grey-lt-100;
553+
}
554+
555+
svg {
556+
flex-shrink: 0;
557+
margin-top: 0.125rem;
558+
color: $grey-dk-100;
559+
}
560+
}
561+
562+
.menu-item-text {
563+
flex: 1;
564+
min-width: 0;
565+
}
566+
567+
.menu-item-title {
568+
font-size: 0.875rem;
569+
font-weight: 500;
570+
color: $grey-dk-300;
571+
line-height: 1.4;
572+
margin-bottom: 0.125rem;
573+
}
574+
575+
.menu-item-subtitle {
576+
font-size: 0.75rem;
577+
color: $grey-dk-100;
578+
line-height: 1.3;
579+
}
580+

0 commit comments

Comments
 (0)