Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
156 changes: 156 additions & 0 deletions assets/js/copy-to-llm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
(function () {
'use strict';

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

var toggleBtn = document.getElementById('copy-fulltext-toggle');
var defaultBtn = document.getElementById('copy-fulltext-default');
var dropdown = document.getElementById('copy-fulltext-dropdown');
var options = dropdown.querySelectorAll('.copy-fulltext__option');

toggleBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (dropdown.classList.contains('copy-fulltext__dropdown--open')) {
closeDropdown();
} else {
openDropdown();
}
});

function openDropdown() {
dropdown.classList.add('copy-fulltext__dropdown--open');
toggleBtn.querySelector('i').classList.replace('fa-chevron-up', 'fa-chevron-down');
}

function closeDropdown() {
dropdown.classList.remove('copy-fulltext__dropdown--open');
toggleBtn.querySelector('i').classList.replace('fa-chevron-down', 'fa-chevron-up');
}

document.addEventListener('click', function (e) {
if (!container.contains(e.target)) {
closeDropdown();
}
});

// 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'));
closeDropdown();
});
});

var toast = document.getElementById('copy-fulltext-toast');

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();
Comment thread
solarhell marked this conversation as resolved.
Outdated
}

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) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
showFeedback(data, type);
}).catch(function () {
fallbackCopy(text, data, type);
});
} else {
fallbackCopy(text, data, type);
}
}

function fallbackCopy(text, data, type) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showFeedback(data, type);
} catch (e) {
// silent fail
}
document.body.removeChild(textarea);
}

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);
}
})();
120 changes: 120 additions & 0 deletions assets/scss/_copy-to-llm.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
.copy-fulltext {
position: relative;
display: inline-flex;
float: right;
margin-top: 0.25rem;

&__main {
display: inline-flex;
align-items: stretch;
border: 1px solid $gray-300;
border-radius: $border-radius;
background: $white;
overflow: hidden;
}

&__btn {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border: none;
background: transparent;
color: $gray-700;
font-size: 0.875rem;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s, color 0.15s;

&:hover {
background-color: $gray-100;
color: $gray-900;
}

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

&__toggle {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.5rem;
border: none;
border-left: 1px solid $gray-300;
background: transparent;
color: $gray-500;
cursor: pointer;
transition: background-color 0.15s;

&:hover {
background-color: $gray-100;
}

i {
font-size: 0.625rem;
}
}

&__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;
}
}

&__dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 0.25rem;
min-width: 140px;
background: $white;
border: 1px solid $gray-300;
border-radius: $border-radius;
box-shadow: 0 4px 12px rgba($black, 0.1);
z-index: 10;
overflow: hidden;

&--open {
display: block;
}
}

&__option {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: $gray-700;
font-size: 0.875rem;
text-align: left;
cursor: pointer;
transition: background-color 0.15s;

&:hover {
background-color: $gray-100;
}

& + & {
border-top: 1px solid $gray-200;
}
}
}
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
28 changes: 28 additions & 0 deletions layouts/partials/copy-to-llm.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{/* Copy full text button with dropdown for markdown / plain text */}}
<div class="copy-fulltext" id="copy-fulltext"
data-title="{{ .Title }}"
data-url="{{ .Permalink }}"
data-success-markdown="{{ T "copy_success_markdown" }}"
data-success-text="{{ T "copy_success_text" }}">
<div class="copy-fulltext__main">
<button class="copy-fulltext__btn" id="copy-fulltext-default" title="{{ T "copy_full_text" }}">
<i class="fa-regular fa-copy"></i>
<span>{{ T "copy_full_text" }}</span>
</button>
<button class="copy-fulltext__toggle" id="copy-fulltext-toggle" aria-label="More copy options">
<i class="fa-solid fa-chevron-up"></i>
</button>
</div>
<div class="copy-fulltext__toast" id="copy-fulltext-toast"></div>
<div class="copy-fulltext__dropdown" id="copy-fulltext-dropdown">
<button class="copy-fulltext__option" data-copy-type="markdown">
{{ T "copy_markdown" }}
</button>
<button class="copy-fulltext__option" data-copy-type="text">
{{ T "copy_plain_text" }}
</button>
</div>
</div>

{{/* Embed raw markdown for copy-as-markdown */}}
<script id="copy-fulltext-markdown" type="text/plain">{{ .RawContent }}</script>
Comment thread
solarhell marked this conversation as resolved.
Outdated
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
Binary file added screenshots/en-copy-toast.png
Comment thread
solarhell marked this conversation as resolved.
Outdated
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/zh-copy-toast.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.