macro render

This commit is contained in:
Tienson Qin
2025-12-29 11:48:15 +08:00
parent 58f5a2ddca
commit a04050d988
3 changed files with 239 additions and 1 deletions

View File

@@ -224,6 +224,33 @@ a:hover {
font-size: 16px;
}
.macro-embed {
width: min(100%, 560px);
aspect-ratio: 16 / 9;
margin: 10px 0;
border-radius: 14px;
overflow: hidden;
background: var(--surface-strong);
box-shadow: var(--image-shadow);
}
.macro-embed iframe {
width: 100%;
height: 100%;
border: 0;
}
.macro-embed--tweet {
aspect-ratio: 4 / 5;
}
.cloze {
padding: 0 6px;
border-radius: 6px;
background: var(--surface-strong);
box-shadow: inset 0 0 0 1px rgba(40, 39, 38, 0.12);
}
.code-block {
flex: 1;
position: relative;

View File

@@ -127,9 +127,61 @@ window.toggleTheme = () => {
window.localStorage.setItem(THEME_KEY, next);
};
const initTwitterEmbeds = () => {
const tweetTargets = document.querySelectorAll(".twitter-tweet");
if (!tweetTargets.length) return;
const ensureTwitterScript = () =>
new Promise((resolve) => {
if (window.twttr?.widgets?.createTweet) {
return resolve(window.twttr);
}
let script = document.querySelector("script[data-twitter-widget]");
if (!script) {
script = document.createElement("script");
script.src = "https://platform.twitter.com/widgets.js";
script.async = true;
script.defer = true;
script.setAttribute("data-twitter-widget", "true");
document.body.appendChild(script);
}
script.addEventListener("load", () => {
resolve(window.twttr);
});
});
ensureTwitterScript().then((twttr) => {
if (!twttr?.widgets?.createTweet) return;
tweetTargets.forEach((el) => {
const a = el.querySelector("a[href*='/status/']");
if (!a) return;
const m = a.href.match(/status\/(\d+)/);
if (!m) return;
const tweetId = m[1];
// Clear fallback text
el.innerHTML = "";
// Optional: theme based on your current theme
const theme =
(document.documentElement.getAttribute("data-theme") || "light") ===
"dark"
? "dark"
: "light";
twttr.widgets.createTweet(tweetId, el, { theme });
});
});
};
const initPublish = () => {
applyTheme(preferredTheme());
initTwitterEmbeds();
document.querySelectorAll(".math-block").forEach((el) => {
const tex = el.textContent;
try {

View File

@@ -218,6 +218,154 @@
[:dd.property-value
(into [:span] (property-value->nodes v k ctx entities))]])]))
(def ^:private youtube-regex #"^((?:https?:)?//)?((?:www|m).)?((?:youtube.com|youtu.be|y2u.be|youtube-nocookie.com))(/(?:[\w-]+\?v=|embed/|v/)?)([\w-]+)([\S^\?]+)?$")
(def ^:private vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com))(/(?:video/)?)([\w-]+)(\S+)?$")
(def ^:private bilibili-regex #"^((?:https?:)?//)?((?:www).)?((?:bilibili.com))(/(?:video/)?)([\w-]+)(\?p=(\d+))?(\S+)?$")
(def ^:private loom-regex #"^((?:https?:)?//)?((?:www).)?((?:loom.com))(/(?:share/|embed/))([\w-]+)(\S+)?$")
(defn- safe-match
[re value]
(when (and (string? value) (not (string/blank? value)))
(re-find re value)))
(defn- macro-iframe
[src {:keys [class title]}]
(when (and (string? src) (not (string/blank? src)))
(let [class-name (string/join " " (remove nil? ["macro-embed" class]))]
[:div {:class class-name}
[:iframe {:src src
:title (or title "Embedded content")
:loading "lazy"
:allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
:allowfullscreen true}]])))
(defn- youtube-embed
[url]
(let [id (cond
(and (string? url) (= 11 (count url))) url
:else (nth (safe-match youtube-regex url) 5 nil))]
(when (and id (string? id))
(macro-iframe (str "https://www.youtube.com/embed/" id) {:class "macro-embed--video" :title "YouTube"}))))
(defn- vimeo-embed
[url]
(let [id (nth (safe-match vimeo-regex url) 5 nil)]
(when (and id (string? id))
(macro-iframe (str "https://player.vimeo.com/video/" id) {:class "macro-embed--video" :title "Vimeo"}))))
(defn- bilibili-embed
[url]
(let [id (if (<= (count (or url "")) 15)
url
(nth (safe-match bilibili-regex url) 5 nil))]
(when (and id (string? id) (not (string/blank? id)))
(macro-iframe (str "https://player.bilibili.com/player.html?bvid=" id "&high_quality=1&autoplay=0")
{:class "macro-embed--video" :title "Bilibili"}))))
(defn- video-embed
[url]
(when (common-util/url? url)
(let [matches (or (safe-match youtube-regex url)
(safe-match loom-regex url)
(safe-match vimeo-regex url)
(safe-match bilibili-regex url))
src (cond
(and matches (contains? #{"youtube.com" "youtu.be" "y2u.be" "youtube-nocookie.com"} (nth matches 3)))
(let [id (nth matches 5)]
(when (= 11 (count (or id "")))
(str "https://www.youtube.com/embed/" id)))
(and matches (string/ends-with? (nth matches 3) "loom.com"))
(str "https://www.loom.com/embed/" (nth matches 5))
(and matches (string/ends-with? (nth matches 3) "vimeo.com"))
(str "https://player.vimeo.com/video/" (nth matches 5))
(and matches (= (nth matches 3) "bilibili.com"))
(str "https://player.bilibili.com/player.html?bvid=" (nth matches 5) "&high_quality=1&autoplay=0")
:else
url)]
(macro-iframe src {:class "macro-embed--video" :title "Video"}))))
(defn- tweet-embed
[url]
(let [url (cond
(and (string? url) (<= (count url) 15)) (str "https://x.com/i/status/" url)
:else url)]
(when url
[:div.twitter-tweet
[:a {:href url} url]])))
(defn- tweet-embed-from-html
[html]
(let [id (last (safe-match #"/status/(\d+)" html))]
(when (and id (string? id))
(tweet-embed id))))
(defn- macro->nodes
[ctx {:keys [name arguments]}]
(let [name (string/lower-case (or name ""))
arguments (if (sequential? arguments) arguments [])
first-arg (first arguments)]
(cond
(= name "cloze")
[[:span.cloze (string/join ", " arguments)]]
(= name "youtube")
(when-let [node (youtube-embed first-arg)] [node])
(= name "vimeo")
(when-let [node (vimeo-embed first-arg)] [node])
(= name "bilibili")
(when-let [node (bilibili-embed first-arg)] [node])
(= name "video")
(when-let [node (video-embed first-arg)] [node])
(contains? #{"tweet" "twitter"} name)
(when-let [node (tweet-embed first-arg)] [node])
:else
(content->nodes (str "{{" name (when (seq arguments)
(str " " (string/join ", " arguments))) "}}")
(:uuid->title ctx)
(:graph-uuid ctx)))))
(defn- parse-macro-text
[value]
(when-let [[_ name args] (and (string? value)
(re-find #"\{\{\s*([^\s\}]+)\s*([^}]*)\}\}" value))]
(let [args (->> (string/split (or args "") #",")
(map string/trim)
(remove string/blank?)
vec)]
{:name name
:arguments args})))
(defn- normalize-macro-data
[data]
(cond
(map? data) data
(string? data) (parse-macro-text data)
(and (sequential? data) (seq data))
(let [name (first data)
args (second data)]
{:name (when (string? name) name)
:arguments (if (sequential? args) args [])})
:else nil))
(defn- macro-embed-node?
[node]
(when (vector? node)
(let [tag (first node)
attrs (second node)]
(and (= tag :div)
(map? attrs)
(string? (:class attrs))
(string/includes? (:class attrs) "macro-embed")))))
(defn inline->nodes [ctx item]
(let [[type data] item
{:keys [uuid->title name->uuid graph-uuid]} ctx]
@@ -277,6 +425,16 @@
[[:a.page-ref {:href (str "/page/" graph-uuid "/" page-uuid)} (str "#" s)]]
[(str "#" s)]))
(= "Macro" type)
(if-let [macro-data (normalize-macro-data data)]
(or (macro->nodes ctx macro-data) [])
(content->nodes (str data) uuid->title graph-uuid))
(or (= "Inline_Html" type) (= "Export_Snippet" type))
(if-let [node (tweet-embed-from-html data)]
[node]
[])
:else
(content->nodes (str data) uuid->title graph-uuid))))
@@ -291,7 +449,8 @@
content (if (seq ast)
(mapcat #(inline->nodes ctx %) ast)
(content->nodes raw (:uuid->title ctx) (:graph-uuid ctx)))]
(into [:span.block-text] content)))
(let [container (if (some macro-embed-node? content) :div :span)]
(into [(keyword (str (name container) ".block-text"))] content))))
(defn block-raw-content [block]
(or (:block/content block)