try quote button js
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 11s
Some checks failed
Build and publish CMBA rulebooks (Gitea) / build-release (push) Failing after 11s
This commit is contained in:
13
.github/workflows/release-docs.yml
vendored
13
.github/workflows/release-docs.yml
vendored
@@ -24,6 +24,18 @@ jobs:
|
|||||||
sudo apt-get install -y pandoc \
|
sudo apt-get install -y pandoc \
|
||||||
jq \
|
jq \
|
||||||
curl
|
curl
|
||||||
|
- name: Build pandoc after-body include (inline quote.js)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p build
|
||||||
|
{
|
||||||
|
echo '<script>'
|
||||||
|
cat quote.js
|
||||||
|
echo '</script>'
|
||||||
|
echo '<style>'
|
||||||
|
echo '.quote-btn{margin-left:.5rem;font-size:.8em;vertical-align:middle;}'
|
||||||
|
echo '</style>'
|
||||||
|
} > build/after-body.html
|
||||||
|
|
||||||
- name: Build artifacts
|
- name: Build artifacts
|
||||||
run: |
|
run: |
|
||||||
@@ -41,6 +53,7 @@ jobs:
|
|||||||
--metadata-file src/metadata.yml \
|
--metadata-file src/metadata.yml \
|
||||||
--embed-resources \
|
--embed-resources \
|
||||||
--metadata title="CMBA ${doc}" \
|
--metadata title="CMBA ${doc}" \
|
||||||
|
--include-after-body=build/after-body.html \
|
||||||
--css src/style.css \
|
--css src/style.css \
|
||||||
-o "dist/${doc}.html"
|
-o "dist/${doc}.html"
|
||||||
|
|
||||||
|
|||||||
182
src/quote.js
Normal file
182
src/quote.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
(() => {
|
||||||
|
// Inject Quote buttons into Pandoc-style headings and copy a section-range quote to clipboard.
|
||||||
|
//
|
||||||
|
// Assumptions (matches your sample):
|
||||||
|
// - Headings are h1/h2 with stable ids (anchors).
|
||||||
|
// - A "section" is: the heading + all following sibling elements until the next heading
|
||||||
|
// of the same or higher level (H2 stops at next H2 or H1; H1 stops at next H1).
|
||||||
|
//
|
||||||
|
// Primary target: GroupMe. We optimize text/plain to be readable and "quote-like".
|
||||||
|
// We still provide text/html for rich paste targets (Docs/Email).
|
||||||
|
//
|
||||||
|
// No dependencies.
|
||||||
|
|
||||||
|
const HEADING_SELECTOR = "h1[id], h2[id]"; // change to "h2[id]" if you only want section-level
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
injectQuoteButtons();
|
||||||
|
});
|
||||||
|
|
||||||
|
function injectQuoteButtons() {
|
||||||
|
document.querySelectorAll(HEADING_SELECTOR).forEach((heading) => {
|
||||||
|
// Avoid double-injection
|
||||||
|
if (heading.querySelector(":scope > .quote-btn")) return;
|
||||||
|
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "quote-btn";
|
||||||
|
btn.textContent = "Quote";
|
||||||
|
btn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onQuoteClick(btn, heading);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a little separation from heading text
|
||||||
|
heading.appendChild(document.createTextNode(" "));
|
||||||
|
heading.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onQuoteClick(btn, heading) {
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
btn.textContent = "Copying…";
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
await copyQuotedSection(heading);
|
||||||
|
|
||||||
|
btn.textContent = "Copied";
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 800);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
btn.textContent = "Failed";
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyQuotedSection(heading) {
|
||||||
|
if (!heading.id) throw new Error("Heading missing id.");
|
||||||
|
|
||||||
|
const level = heading.tagName; // H1 or H2
|
||||||
|
|
||||||
|
// Clone heading + subsequent siblings for the quote range
|
||||||
|
const nodes = [];
|
||||||
|
nodes.push(heading.cloneNode(true));
|
||||||
|
|
||||||
|
let el = heading.nextElementSibling;
|
||||||
|
while (el) {
|
||||||
|
if (isBoundary(level, el)) break;
|
||||||
|
nodes.push(el.cloneNode(true));
|
||||||
|
el = el.nextElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove injected button from cloned heading (so it doesn't show up in pasted content)
|
||||||
|
nodes[0].querySelector(".quote-btn")?.remove();
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
nodes.forEach((n) => container.appendChild(n));
|
||||||
|
|
||||||
|
const href = buildAnchorUrl(heading.id);
|
||||||
|
|
||||||
|
// HTML (for rich paste targets)
|
||||||
|
const html = buildHtmlQuote(container.innerHTML, href);
|
||||||
|
|
||||||
|
// Plain text (optimized for GroupMe)
|
||||||
|
const plain = buildGroupMePlainQuote(container, href);
|
||||||
|
|
||||||
|
await writeClipboardMultiFormat({ html, plain, url: href });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBoundary(level, el) {
|
||||||
|
// Stop at next heading of same or higher rank.
|
||||||
|
// H2 stops at next H1 or H2.
|
||||||
|
// H1 stops at next H1.
|
||||||
|
if (level === "H1") return el.tagName === "H1";
|
||||||
|
if (level === "H2") return el.tagName === "H1" || el.tagName === "H2";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAnchorUrl(id) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.hash = id;
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtmlQuote(innerHtml, href) {
|
||||||
|
const safeHref = escapeHtmlAttr(href);
|
||||||
|
const safeText = escapeHtml(href);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<blockquote>
|
||||||
|
${innerHtml}
|
||||||
|
<p><a href="${safeHref}">${safeText}</a></p>
|
||||||
|
</blockquote>
|
||||||
|
`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroupMePlainQuote(container, href) {
|
||||||
|
// Create a readable quote-like block:
|
||||||
|
// - Prefix lines with "> " to visually indicate quoting (works even if not rendered specially)
|
||||||
|
// - Include Source link
|
||||||
|
const text = normalizePlainText(container);
|
||||||
|
|
||||||
|
const quoted = text
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => (line.trim().length ? `> ${line}` : `>`))
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return `${quoted}\n> \n> Source: ${href}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlainText(container) {
|
||||||
|
// Use innerText to approximate what the user sees (captures list numbering reasonably in many browsers).
|
||||||
|
// Then normalize spacing.
|
||||||
|
return (container.innerText || "")
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.replace(/\n{3,}/g, "\n\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeClipboardMultiFormat({ html, plain, url }) {
|
||||||
|
// Best path: multi-format clipboard write (Chromium/Edge, etc.)
|
||||||
|
if (navigator.clipboard?.write) {
|
||||||
|
const item = new ClipboardItem({
|
||||||
|
"text/html": new Blob([html], { type: "text/html" }),
|
||||||
|
"text/plain": new Blob([plain], { type: "text/plain" }),
|
||||||
|
// Best-effort URL type; some paste targets prefer this
|
||||||
|
"text/uri-list": new Blob([url], { type: "text/uri-list" }),
|
||||||
|
});
|
||||||
|
await navigator.clipboard.write([item]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: plain text only
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(plain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Clipboard API unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtmlAttr(s) {
|
||||||
|
return escapeHtml(s);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user