Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions assets/js/copy-to-llm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
(function () {
'use strict';

var container = document.getElementById('copy-fulltext');
if (!container) return;

var defaultBtn = document.getElementById('copy-fulltext-default');
var options = container.querySelectorAll('.dropdown-item');
var toast = document.getElementById('copy-fulltext-toast');

// Default button copies markdown (most useful for LLM)
defaultBtn.addEventListener('click', function () {
copyContent('markdown');
});

options.forEach(function (opt) {
opt.addEventListener('click', function () {
copyContent(this.getAttribute('data-copy-type'));
});
});

function getSourceData() {
var mdEl = document.getElementById('copy-fulltext-markdown');
return {
title: container.getAttribute('data-title') || '',
url: container.getAttribute('data-url') || '',
successMarkdown: container.getAttribute('data-success-markdown') || 'Copied as Markdown',
successText: container.getAttribute('data-success-text') || 'Copied as plain text',
markdown: mdEl ? mdEl.textContent : ''
};
}

function getPlainText() {
var contentEl = document.querySelector('.td-content');
if (!contentEl) return '';
var clone = contentEl.cloneNode(true);

// Remove UI elements that shouldn't be copied
var removals = clone.querySelectorAll('.copy-fulltext, .td-page-meta, script, style, .feedback--container');
removals.forEach(function (el) { el.remove(); });

// Replace mermaid SVGs with placeholder (SVG textContent is gibberish)
clone.querySelectorAll('pre.mermaid, .mermaid').forEach(function (el) {
var placeholder = document.createElement('p');
placeholder.textContent = '[diagram]';
el.replaceWith(placeholder);
});

// Replace images with their alt text
clone.querySelectorAll('img').forEach(function (img) {
var alt = img.getAttribute('alt');
if (alt) {
var text = document.createElement('span');
text.textContent = '[image: ' + alt + ']';
img.replaceWith(text);
} else {
img.remove();
}
});

return clone.textContent.replace(/\n{3,}/g, '\n\n').trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return clone.textContent.replace(/\n{3,}/g, '\n\n').trim();
return clone.textContent.replace(/(\s*\n){3,}/g, '\n\n').trim();

好点。比如 section类型 的页面,复制会有有大量空格

}

function copyContent(type) {
var data = getSourceData();
var text = '';
var actualType = type;

if (type === 'markdown' && data && data.markdown) {
text = '# ' + data.title + '\n\n' + data.markdown;
if (data.url) {
text += '\n\n---\nSource: ' + data.url;
}
} else {
actualType = 'text';
text = getPlainText();
}

copyToClipboard(text, data, actualType);
}

function copyToClipboard(text, data, type) {
navigator.clipboard.writeText(text).then(function () {
showFeedback(data, type);
});
}

function showFeedback(data, type) {
// Swap icon to checkmark
var icon = defaultBtn.querySelector('i');
icon.classList.replace('fa-copy', 'fa-check');
defaultBtn.classList.add('copy-fulltext__btn--success');

// Show toast
var toastText = type === 'markdown' ? data.successMarkdown : data.successText;
toast.textContent = '✅ ' + toastText;
toast.classList.add('copy-fulltext__toast--visible');

setTimeout(function () {
icon.classList.replace('fa-check', 'fa-copy');
defaultBtn.classList.remove('copy-fulltext__btn--success');
toast.classList.remove('copy-fulltext__toast--visible');
}, 2000);
}
})();
30 changes: 30 additions & 0 deletions assets/scss/_copy-to-llm.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.copy-fulltext {
float: right;
margin-top: 0.25rem;

&__btn--success {
color: $success !important;
}

&__toast {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 0.5rem;
padding: 0.5rem 0.75rem;
background: $white;
border: 1px solid $gray-300;
border-radius: $border-radius;
box-shadow: 0 4px 12px rgba($black, 0.1);
font-size: 0.875rem;
color: $gray-700;
white-space: nowrap;
z-index: 11;

&--visible {
display: block;
}
}
}
1 change: 1 addition & 0 deletions assets/scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
@import "community";
@import "markdown";
@import "safety";
@import "copy-to-llm";

@if $td-enable-google-fonts {
@import url($web-font-path);
Expand Down
16 changes: 16 additions & 0 deletions i18n/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,19 @@ other = "404, Page not found"
title = 'Out projects'
[taxo.page.header]
projects = 'Projects'

# Copy full text
[copy_full_text]
other = "Copy Full Text"

[copy_markdown]
other = "Copy Markdown"

[copy_plain_text]
other = "Copy Text"

[copy_success_markdown]
other = "Copied as Markdown"

[copy_success_text]
other = "Copied as plain text"
16 changes: 16 additions & 0 deletions i18n/zh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,19 @@ other = "404, 访问的页面并不存在"
title = '项目列表'
[taxo.page.header]
projects = '项目'

# Copy full text
[copy_full_text]
other = "复制全文"

[copy_markdown]
other = "复制 Markdown"

[copy_plain_text]
other = "复制文本"

[copy_success_markdown]
other = "已复制 Markdown 格式"

[copy_success_text]
other = "已复制纯文本"
1 change: 1 addition & 0 deletions layouts/_default/content.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<div class="td-content">
{{ partial "copy-to-llm.html" . }}
<h1>{{ .Title }}</h1>
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
<header class="article-meta">
Expand Down
1 change: 1 addition & 0 deletions layouts/docs/list.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{{ define "main" }}
<div class="td-content">
{{ partial "copy-to-llm.html" . }}
<h1>{{ .Title }}</h1>
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
<header class="article-meta">
Expand Down
27 changes: 27 additions & 0 deletions layouts/partials/copy-to-llm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{/* Copy full text button with split dropdown for markdown / plain text */}}
<div class="copy-fulltext btn-group" id="copy-fulltext"
data-title="{{ .Title }}"
data-url="{{ .Permalink }}"
data-success-markdown="{{ T "copy_success_markdown" }}"
data-success-text="{{ T "copy_success_text" }}">
<button type="button" class="btn btn-sm btn-outline-secondary" id="copy-fulltext-default" title="{{ T "copy_full_text" }}">
<i class="fa-regular fa-copy"></i>
<span>{{ T "copy_full_text" }}</span>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right">
<button class="dropdown-item" data-copy-type="markdown">
{{ T "copy_markdown" }}
</button>
<button class="dropdown-item" data-copy-type="text">
{{ T "copy_plain_text" }}
</button>
</div>
<div class="copy-fulltext__toast" id="copy-fulltext-toast"></div>
</div>

{{/* Embed raw markdown for copy-as-markdown */}}
<script id="copy-fulltext-markdown" type="text/plain">{{ .RawContent }}</script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<script id="copy-fulltext-markdown" type="text/plain">{{ .RawContent }}</script>
<script id="copy-fulltext-markdown" type="text/plain">{{ .RawContent | safeHTML }}</script>

safeHTML 会好点,有标签的页面复制出来会有很多符号

3 changes: 2 additions & 1 deletion layouts/partials/scripts.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@
{{ $jsSearch := resources.Get "js/search.js" | resources.ExecuteAsTemplate "js/search.js" .Site.Home }}
{{ $jsMermaid := resources.Get "js/mermaid.js" | resources.ExecuteAsTemplate "js/mermaid.js" . }}
{{ $jsPlantuml := resources.Get "js/plantuml.js" | resources.ExecuteAsTemplate "js/plantuml.js" . }}
{{ $jsCopyToLLM := resources.Get "js/copy-to-llm.js" }}
{{ if .Site.Params.offlineSearch }}
{{ $jsSearch = resources.Get "js/offline-search.js" }}
{{ end }}
{{ $js := (slice $jsBase $security $jsAnchor $jsSearch $jsMermaid $jsPlantuml) | resources.Concat "js/main.js" }}
{{ $js := (slice $jsBase $security $jsAnchor $jsSearch $jsMermaid $jsPlantuml $jsCopyToLLM) | resources.Concat "js/main.js" }}
{{ if hugo.IsServer }}
<script src="{{ $js.RelPermalink }}"></script>
{{ else }}
Expand Down