From 1cb4dadf13913009660685b6f2c163c939e2a51a Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Tue, 6 Jun 2023 21:19:00 -0700
Subject: [PATCH] codeblock copy

---
 quartz.config.ts                              |  4 +-
 quartz/components/Body.tsx                    | 18 +++++++++
 quartz/components/Darkmode.tsx                |  2 +-
 quartz/components/Header.tsx                  |  2 +-
 quartz/components/scripts/clipboard.inline.ts | 32 ++++++++++++++++
 quartz/components/styles/clipboard.scss       | 37 +++++++++++++++++++
 quartz/{ => components}/styles/darkmode.scss  |  0
 quartz/{ => components}/styles/header.scss    |  0
 quartz/plugins/emitters/contentPage.tsx       | 11 +++---
 quartz/plugins/index.ts                       |  2 +-
 10 files changed, 98 insertions(+), 10 deletions(-)
 create mode 100644 quartz/components/Body.tsx
 create mode 100644 quartz/components/scripts/clipboard.inline.ts
 create mode 100644 quartz/components/styles/clipboard.scss
 rename quartz/{ => components}/styles/darkmode.scss (100%)
 rename quartz/{ => components}/styles/header.scss (100%)

diff --git a/quartz.config.ts b/quartz.config.ts
index 25518d5..01412f9 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -1,4 +1,5 @@
 import { QuartzConfig } from "./quartz/cfg"
+import Body from "./quartz/components/Body"
 import Head from "./quartz/components/Head"
 import Header from "./quartz/components/Header"
 import {
@@ -68,7 +69,8 @@ const config: QuartzConfig = {
     emitters: [
       new ContentPage({
         Head: Head,
-        Header: Header
+        Header: Header,
+        Body: Body
       })
     ]
   },
diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx
new file mode 100644
index 0000000..1d9296b
--- /dev/null
+++ b/quartz/components/Body.tsx
@@ -0,0 +1,18 @@
+import { ComponentChildren } from "preact"
+import clipboardScript from './scripts/clipboard.inline'
+import clipboardStyle from './styles/clipboard.scss'
+
+export interface BodyProps {
+  title?: string
+  children: ComponentChildren
+}
+
+export default function Body({ title, children }: BodyProps) {
+  return <article>
+    {title && <h1>{title}</h1>}
+    {children}
+  </article>
+}
+
+Body.afterDOMLoaded = clipboardScript
+Body.css = clipboardStyle
diff --git a/quartz/components/Darkmode.tsx b/quartz/components/Darkmode.tsx
index ae6788c..2170253 100644
--- a/quartz/components/Darkmode.tsx
+++ b/quartz/components/Darkmode.tsx
@@ -2,7 +2,7 @@
 // modules are automatically deferred and we don't want that to happen for critical beforeDOMLoads
 // see: https://v8.dev/features/modules#defer
 import darkmodeScript from "./scripts/darkmode.inline"
-import styles from '../styles/darkmode.scss'
+import styles from './styles/darkmode.scss'
 
 export default function Darkmode() {
   return <div class="darkmode">
diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx
index cb2fae8..7005e64 100644
--- a/quartz/components/Header.tsx
+++ b/quartz/components/Header.tsx
@@ -1,6 +1,6 @@
 import { resolveToRoot } from "../path"
 import Darkmode from "./Darkmode"
-import style from '../styles/header.scss'
+import style from './styles/header.scss'
 
 export interface HeaderProps {
   title: string
diff --git a/quartz/components/scripts/clipboard.inline.ts b/quartz/components/scripts/clipboard.inline.ts
new file mode 100644
index 0000000..efddfa6
--- /dev/null
+++ b/quartz/components/scripts/clipboard.inline.ts
@@ -0,0 +1,32 @@
+const description = "Initialize copy for codeblocks"
+export default description
+
+const svgCopy =
+  '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>'
+const svgCheck =
+  '<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>'
+
+const els = document.getElementsByTagName("pre")
+for (let i = 0; i < els.length; i++) {
+  const codeBlock = els[i].getElementsByTagName("code")[0]
+  const source = codeBlock.innerText.replace(/\n\n/g, "\n")
+  const button = document.createElement("button")
+  button.className = "clipboard-button"
+  button.type = "button"
+  button.innerHTML = svgCopy
+  button.ariaLabel = "Copy source"
+  button.addEventListener("click", () => {
+    navigator.clipboard.writeText(source).then(
+      () => {
+        button.blur()
+        button.innerHTML = svgCheck
+        setTimeout(() => {
+          button.innerHTML = svgCopy
+          button.style.borderColor = ""
+        }, 2000)
+      },
+      (error) => console.error(error),
+    )
+  })
+  els[i].prepend(button)
+}
diff --git a/quartz/components/styles/clipboard.scss b/quartz/components/styles/clipboard.scss
new file mode 100644
index 0000000..1702a7b
--- /dev/null
+++ b/quartz/components/styles/clipboard.scss
@@ -0,0 +1,37 @@
+.clipboard-button {
+  position: absolute;
+  display: flex;
+  float: right;
+  right: 0;
+  padding: 0.4rem;
+  margin: -0.2rem 0.3rem;
+  color: var(--gray);
+  border-color: var(--dark);
+  background-color: var(--light);
+  border: 1px solid;
+  border-radius: 5px;
+  z-index: 1;
+  opacity: 0;
+  transition: 0.2s;
+
+  & > svg {
+    fill: var(--light);
+    filter: contrast(0.3);
+  }
+
+  &:hover {
+    cursor: pointer;
+    border-color: var(--secondary);
+  }
+
+  &:focus {
+    outline: 0;
+  }
+}
+
+pre {
+  &:hover > .clipboard-button {
+    opacity: 1;
+    transition: 0.2s;
+  }
+}
diff --git a/quartz/styles/darkmode.scss b/quartz/components/styles/darkmode.scss
similarity index 100%
rename from quartz/styles/darkmode.scss
rename to quartz/components/styles/darkmode.scss
diff --git a/quartz/styles/header.scss b/quartz/components/styles/header.scss
similarity index 100%
rename from quartz/styles/header.scss
rename to quartz/components/styles/header.scss
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index 61ddcfb..c4b357c 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -9,10 +9,12 @@ import { GlobalConfiguration } from "../../cfg"
 import { HeaderProps } from "../../components/Header"
 import { QuartzComponent } from "../../components/types"
 import { resolveToRoot } from "../../path"
+import { BodyProps } from "../../components/Body"
 
 interface Options {
   Head: QuartzComponent<HeadProps>
   Header: QuartzComponent<HeaderProps>
+  Body: QuartzComponent<BodyProps>
 }
 
 export class ContentPage extends QuartzEmitterPlugin {
@@ -31,7 +33,7 @@ export class ContentPage extends QuartzEmitterPlugin {
   async emit(cfg: GlobalConfiguration, content: ProcessedContent[], resources: StaticResources, emit: EmitCallback): Promise<string[]> {
     const fps: string[] = []
 
-    const { Head, Header } = this.opts
+    const { Head, Header, Body } = this.opts
     for (const [tree, file] of content) {
       // @ts-ignore (preact makes it angry)
       const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
@@ -42,7 +44,7 @@ export class ContentPage extends QuartzEmitterPlugin {
         js: [
           { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady" },
           ...resources.js,
-          { src: baseDir + "/postscript.js", loadTime: "afterDOMReady" }
+          { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", type: 'module' }
         ]
       }
 
@@ -56,10 +58,7 @@ export class ContentPage extends QuartzEmitterPlugin {
         <body>
           <div id="quartz-root" class="page">
             <Header title={cfg.siteTitle} slug={file.data.slug!} />
-            <article>
-              {file.data.slug !== "index" && <h1>{title}</h1>}
-              {content}
-            </article>
+            <Body title={file.data.slug === "index" ? undefined : title}>{content}</Body>
           </div>
         </body>
         {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(resource => <script key={resource.src} {...resource} />)}
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 30f77e8..e904a00 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -40,7 +40,7 @@ export function emitComponentResources(cfg: GlobalConfiguration, resources: Stat
       componentResources.beforeDOMLoaded.push(beforeDOMLoaded)
     }
     if (afterDOMLoaded) {
-      componentResources.beforeDOMLoaded.push(afterDOMLoaded)
+      componentResources.afterDOMLoaded.push(afterDOMLoaded)
     }
   }