From f403c1f6163265b8af38c889cd71409f9896ae6d Mon Sep 17 00:00:00 2001 From: jiaxin Date: Wed, 11 Mar 2026 16:11:54 +0800 Subject: [PATCH 01/12] feat: add copy fulltext button for docs pages Add a split button with dropdown to copy page content as Markdown or plain text, useful for pasting into LLMs. Supports i18n (zh/en). --- assets/js/copy-to-llm.js | 123 ++++++++++++++++++++++++++++++ assets/scss/_copy-to-llm.scss | 98 ++++++++++++++++++++++++ assets/scss/main.scss | 1 + i18n/en.toml | 13 ++++ i18n/zh.toml | 13 ++++ layouts/_default/content.html | 1 + layouts/partials/copy-to-llm.html | 25 ++++++ layouts/partials/scripts.html | 3 +- 8 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 assets/js/copy-to-llm.js create mode 100644 assets/scss/_copy-to-llm.scss create mode 100644 layouts/partials/copy-to-llm.html diff --git a/assets/js/copy-to-llm.js b/assets/js/copy-to-llm.js new file mode 100644 index 00000000000..f5c4b96ee3f --- /dev/null +++ b/assets/js/copy-to-llm.js @@ -0,0 +1,123 @@ +(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(); + }); + }); + + function getSourceData() { + var el = document.getElementById('copy-fulltext-source'); + if (!el) return null; + try { + return JSON.parse(el.textContent); + } catch (e) { + return null; + } + } + + function getPlainText() { + var contentEl = document.querySelector('.td-content'); + if (!contentEl) return ''; + var clone = contentEl.cloneNode(true); + var removals = clone.querySelectorAll('.copy-fulltext, .td-page-meta, script, style, .feedback--container'); + removals.forEach(function (el) { el.remove(); }); + return clone.textContent.replace(/\n{3,}/g, '\n\n').trim(); + } + + function copyContent(type) { + var data = getSourceData(); + var text = ''; + + if (type === 'markdown' && data && data.markdown) { + text = '# ' + data.title + '\n\n' + data.markdown; + if (data.url) { + text += '\n\n---\nSource: ' + data.url; + } + } else { + text = getPlainText(); + } + + copyToClipboard(text, data); + } + + function copyToClipboard(text, data) { + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + showFeedback(data); + }).catch(function () { + fallbackCopy(text, data); + }); + } else { + fallbackCopy(text, data); + } + } + + function fallbackCopy(text, data) { + 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); + } catch (e) { + // silent fail + } + document.body.removeChild(textarea); + } + + function showFeedback(data) { + var span = defaultBtn.querySelector('span'); + var original = span.textContent; + var successText = (data && data.successText) || 'Copied'; + span.textContent = '✓ ' + successText; + defaultBtn.classList.add('copy-fulltext__btn--success'); + setTimeout(function () { + span.textContent = original; + defaultBtn.classList.remove('copy-fulltext__btn--success'); + }, 2000); + } +})(); diff --git a/assets/scss/_copy-to-llm.scss b/assets/scss/_copy-to-llm.scss new file mode 100644 index 00000000000..334dcc2c4bd --- /dev/null +++ b/assets/scss/_copy-to-llm.scss @@ -0,0 +1,98 @@ +.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; + } + } + + &__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; + } + } +} diff --git a/assets/scss/main.scss b/assets/scss/main.scss index 9c024936ce8..ea4a033c3d8 100644 --- a/assets/scss/main.scss +++ b/assets/scss/main.scss @@ -33,6 +33,7 @@ @import "community"; @import "markdown"; @import "safety"; +@import "copy-to-llm"; @if $td-enable-google-fonts { @import url($web-font-path); diff --git a/i18n/en.toml b/i18n/en.toml index 240ac47caf5..5b0c4d76eb6 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -80,3 +80,16 @@ 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] +other = "Copied" diff --git a/i18n/zh.toml b/i18n/zh.toml index a753e8dd773..a02f98ee07d 100644 --- a/i18n/zh.toml +++ b/i18n/zh.toml @@ -74,3 +74,16 @@ other = "404, 访问的页面并不存在" title = '项目列表' [taxo.page.header] projects = '项目' + +# Copy full text +[copy_full_text] +other = "复制全文" + +[copy_markdown] +other = "复制 Markdown" + +[copy_plain_text] +other = "复制文本" + +[copy_success] +other = "已复制" diff --git a/layouts/_default/content.html b/layouts/_default/content.html index cc4c7dd63e9..94cbeb29f97 100644 --- a/layouts/_default/content.html +++ b/layouts/_default/content.html @@ -1,4 +1,5 @@
+ {{ partial "copy-to-llm.html" . }}

{{ .Title }}

{{ with .Params.description }}
{{ . | markdownify }}
{{ end }}