Skip to content

Commit 36f50f2

Browse files
authored
feat: add copy markdowm/text button for docs pages (#1521)
1 parent 0f9acaf commit 36f50f2

9 files changed

Lines changed: 199 additions & 1 deletion

File tree

assets/js/copy-to-llm.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
(function () {
2+
'use strict';
3+
4+
var container = document.getElementById('copy-fulltext');
5+
if (!container) return;
6+
7+
var defaultBtn = document.getElementById('copy-fulltext-default');
8+
var options = container.querySelectorAll('.dropdown-item');
9+
var toast = document.getElementById('copy-fulltext-toast');
10+
11+
// Default button copies markdown (most useful for LLM)
12+
defaultBtn.addEventListener('click', function () {
13+
copyContent('markdown');
14+
});
15+
16+
options.forEach(function (opt) {
17+
opt.addEventListener('click', function () {
18+
copyContent(this.getAttribute('data-copy-type'));
19+
});
20+
});
21+
22+
function getSourceData() {
23+
var mdEl = document.getElementById('copy-fulltext-markdown');
24+
return {
25+
title: container.getAttribute('data-title') || '',
26+
url: container.getAttribute('data-url') || '',
27+
successMarkdown: container.getAttribute('data-success-markdown') || 'Copied as Markdown',
28+
successText: container.getAttribute('data-success-text') || 'Copied as plain text',
29+
markdown: mdEl ? mdEl.textContent : ''
30+
};
31+
}
32+
33+
function getPlainText() {
34+
var contentEl = document.querySelector('.td-content');
35+
if (!contentEl) return '';
36+
var clone = contentEl.cloneNode(true);
37+
38+
// Remove UI elements that shouldn't be copied
39+
var removals = clone.querySelectorAll('.copy-fulltext, .td-page-meta, script, style, .feedback--container');
40+
removals.forEach(function (el) { el.remove(); });
41+
42+
// Replace mermaid SVGs with placeholder (SVG textContent is gibberish)
43+
clone.querySelectorAll('pre.mermaid, .mermaid').forEach(function (el) {
44+
var placeholder = document.createElement('p');
45+
placeholder.textContent = '[diagram]';
46+
el.replaceWith(placeholder);
47+
});
48+
49+
// Replace images with their alt text
50+
clone.querySelectorAll('img').forEach(function (img) {
51+
var alt = img.getAttribute('alt');
52+
if (alt) {
53+
var text = document.createElement('span');
54+
text.textContent = '[image: ' + alt + ']';
55+
img.replaceWith(text);
56+
} else {
57+
img.remove();
58+
}
59+
});
60+
61+
return clone.textContent.replace(/(\s*\n){3,}/g, '\n\n').trim();
62+
}
63+
64+
function copyContent(type) {
65+
var data = getSourceData();
66+
var text = '';
67+
var actualType = type;
68+
69+
if (type === 'markdown' && data && data.markdown) {
70+
text = '# ' + data.title + '\n\n' + data.markdown;
71+
if (data.url) {
72+
text += '\n\n---\nSource: ' + data.url;
73+
}
74+
} else {
75+
actualType = 'text';
76+
text = getPlainText();
77+
}
78+
79+
copyToClipboard(text, data, actualType);
80+
}
81+
82+
function copyToClipboard(text, data, type) {
83+
navigator.clipboard.writeText(text).then(function () {
84+
showFeedback(data, type);
85+
});
86+
}
87+
88+
function showFeedback(data, type) {
89+
// Swap icon to checkmark
90+
var icon = defaultBtn.querySelector('i');
91+
icon.classList.replace('fa-copy', 'fa-check');
92+
defaultBtn.classList.add('copy-fulltext__btn--success');
93+
94+
// Show toast
95+
var toastText = type === 'markdown' ? data.successMarkdown : data.successText;
96+
toast.textContent = '✅ ' + toastText;
97+
toast.classList.add('copy-fulltext__toast--visible');
98+
99+
setTimeout(function () {
100+
icon.classList.replace('fa-check', 'fa-copy');
101+
defaultBtn.classList.remove('copy-fulltext__btn--success');
102+
toast.classList.remove('copy-fulltext__toast--visible');
103+
}, 2000);
104+
}
105+
})();

assets/scss/_copy-to-llm.scss

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.copy-fulltext {
2+
float: right;
3+
margin-top: 0.25rem;
4+
5+
&__btn--success {
6+
color: $success !important;
7+
}
8+
9+
&__toast {
10+
display: none;
11+
position: absolute;
12+
bottom: 100%;
13+
left: 50%;
14+
transform: translateX(-50%);
15+
margin-bottom: 0.5rem;
16+
padding: 0.5rem 0.75rem;
17+
background: $white;
18+
border: 1px solid $gray-300;
19+
border-radius: $border-radius;
20+
box-shadow: 0 4px 12px rgba($black, 0.1);
21+
font-size: 0.875rem;
22+
color: $gray-700;
23+
white-space: nowrap;
24+
z-index: 11;
25+
26+
&--visible {
27+
display: block;
28+
}
29+
}
30+
}

assets/scss/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
@import "community";
3434
@import "markdown";
3535
@import "safety";
36+
@import "copy-to-llm";
3637

3738
@if $td-enable-google-fonts {
3839
@import url($web-font-path);

i18n/en.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,19 @@ other = "404, Page not found"
8080
title = 'Out projects'
8181
[taxo.page.header]
8282
projects = 'Projects'
83+
84+
# Copy full text
85+
[copy_full_text]
86+
other = "Copy Full Text"
87+
88+
[copy_markdown]
89+
other = "Copy Markdown"
90+
91+
[copy_plain_text]
92+
other = "Copy Text"
93+
94+
[copy_success_markdown]
95+
other = "Copied as Markdown"
96+
97+
[copy_success_text]
98+
other = "Copied as plain text"

i18n/zh.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,19 @@ other = "404, 访问的页面并不存在"
7474
title = '项目列表'
7575
[taxo.page.header]
7676
projects = '项目'
77+
78+
# Copy full text
79+
[copy_full_text]
80+
other = "复制全文"
81+
82+
[copy_markdown]
83+
other = "复制 Markdown"
84+
85+
[copy_plain_text]
86+
other = "复制文本"
87+
88+
[copy_success_markdown]
89+
other = "已复制 Markdown 格式"
90+
91+
[copy_success_text]
92+
other = "已复制纯文本"

layouts/_default/content.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<div class="td-content">
2+
{{ partial "copy-to-llm.html" . }}
23
<h1>{{ .Title }}</h1>
34
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
45
<header class="article-meta">

layouts/docs/list.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{{ define "main" }}
22
<div class="td-content">
3+
{{ partial "copy-to-llm.html" . }}
34
<h1>{{ .Title }}</h1>
45
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
56
<header class="article-meta">

layouts/partials/copy-to-llm.html

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{{/* Copy full text button with split dropdown for markdown / plain text */}}
2+
<div class="copy-fulltext btn-group" id="copy-fulltext"
3+
data-title="{{ .Title }}"
4+
data-url="{{ .Permalink }}"
5+
data-success-markdown="{{ T "copy_success_markdown" }}"
6+
data-success-text="{{ T "copy_success_text" }}">
7+
<button type="button" class="btn btn-sm btn-outline-secondary" id="copy-fulltext-default" title="{{ T "copy_full_text" }}">
8+
<i class="fa-regular fa-copy"></i>
9+
<span>{{ T "copy_full_text" }}</span>
10+
</button>
11+
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split"
12+
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
13+
<span class="sr-only">Toggle Dropdown</span>
14+
</button>
15+
<div class="dropdown-menu dropdown-menu-right">
16+
<button class="dropdown-item" data-copy-type="markdown">
17+
{{ T "copy_markdown" }}
18+
</button>
19+
<button class="dropdown-item" data-copy-type="text">
20+
{{ T "copy_plain_text" }}
21+
</button>
22+
</div>
23+
<div class="copy-fulltext__toast" id="copy-fulltext-toast"></div>
24+
</div>
25+
26+
{{/* Embed raw markdown for copy-as-markdown */}}
27+
<script id="copy-fulltext-markdown" type="text/plain">{{ .RawContent | safeHTML }}</script>

layouts/partials/scripts.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@
3131
{{ $jsSearch := resources.Get "js/search.js" | resources.ExecuteAsTemplate "js/search.js" .Site.Home }}
3232
{{ $jsMermaid := resources.Get "js/mermaid.js" | resources.ExecuteAsTemplate "js/mermaid.js" . }}
3333
{{ $jsPlantuml := resources.Get "js/plantuml.js" | resources.ExecuteAsTemplate "js/plantuml.js" . }}
34+
{{ $jsCopyToLLM := resources.Get "js/copy-to-llm.js" }}
3435
{{ if .Site.Params.offlineSearch }}
3536
{{ $jsSearch = resources.Get "js/offline-search.js" }}
3637
{{ end }}
37-
{{ $js := (slice $jsBase $security $jsAnchor $jsSearch $jsMermaid $jsPlantuml) | resources.Concat "js/main.js" }}
38+
{{ $js := (slice $jsBase $security $jsAnchor $jsSearch $jsMermaid $jsPlantuml $jsCopyToLLM) | resources.Concat "js/main.js" }}
3839
{{ if hugo.IsServer }}
3940
<script src="{{ $js.RelPermalink }}"></script>
4041
{{ else }}

0 commit comments

Comments
 (0)