UNPKG

@markslides/renderer

Version:
444 lines (428 loc) 15.2 kB
// src/lib/marp/appMarp.ts import { Marp } from "@marp-team/marp-core"; import { Element as MarpitElement } from "@marp-team/marpit"; import markdownItContainer from "markdown-it-container"; import markdownItLink from "@markslides/markdown-it-link"; import markdownItMermaid from "@markslides/markdown-it-mermaid"; import markdownItTypograms from "@markslides/markdown-it-typograms"; import themes from "@markslides/themes"; // src/lib/marp/plugins/taskLists.ts var markdownItTaskLists = (md) => { const original = md.renderer.rules.text || function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.text = (tokens, idx, options, env, self) => { const token = tokens[idx]; if (!token) { return original(tokens, idx, options, env, self); } const content = token.content.trim(); if (content.startsWith("[ ] ")) { return ` <input type="checkbox" style="width: 20px; height: 20px;"> ${content.split("[ ]")[1]} </input> `; } else if (content.startsWith("[x] ")) { return ` <input type="checkbox" checked style="width: 20px; height: 20px;"> ${content.split("[x]")[1]} </input> `; } return original(tokens, idx, options, env, self); }; }; var taskLists_default = markdownItTaskLists; // src/lib/marp/plugins/copyFenceContent.ts var markdownItCopyFenceContent = (md) => { const original = md.renderer.rules.fence || function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx]; if (!token) { return original(tokens, idx, options, env, self); } const content = token.content.trim(); const escapedContent = md.utils.escapeHtml(content); const originalFenceContent = original(tokens, idx, options, env, self); const styles = ` <style> .copy-fence-container { position: relative; } button.copy-fence-content { width: 36px; height: 36px; position: absolute; top: 12px; right: 12px; display: flex; align-items: center; justify-content: center; cursor: pointer; background-color: var(--color-neutral-muted); color: var(--color-fg-default); border: 1px solid var(--color-border-default); border-radius: 4px; transition: all 0.2s ease-in-out; } button.copy-fence-content:hover { background-color: var(--color-fg-muted); color: var(--color-canvas-default); } /* Inactive */ button.copy-fence-content .lucide-copy-icon { display: block !important; } button.copy-fence-content .lucide-check-icon { display: none !important; } /* Active */ button.copy-fence-content:hover.active { color: var(--color-canvas-default); } button.copy-fence-content.active .lucide-copy-icon { display: none !important; } button.copy-fence-content.active .lucide-check-icon { display: block !important; } </style> `; const buttonHtml = ` <button class="copy-fence-content" data-content="${escapedContent}"> <svg class="lucide-copy-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect width="14" height="14" x="8" y="8" rx="2" ry="2" /> <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /> </svg> <svg class="lucide-check-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M20 6 9 17l-5-5" /> </svg> </button> `; return ` <div class="copy-fence-container"> ${styles} ${buttonHtml} </div> ${originalFenceContent} `; }; }; var copyFenceContent_default = markdownItCopyFenceContent; // src/lib/marp/plugins/fenceCodeBlockEnhancer.ts var SHOW_LINE_NUMBERS = "showLineNumbers"; var regExpLineNumbers = /{([\d,-]*)}/; var generateFenceStyles = (digits) => ` <style> pre, pre[class*="language-"] { padding: 0.75rem 1rem !important; white-space: pre-wrap; word-break: break-word; line-height: 1rem; overflow: auto; } .line-number { margin-right: ${digits * 1.4}rem; color: var(--color-fg-default); } .highlighted-line { background-color: var(--color-neutral-muted); padding: 0.1rem 12px; margin-left: -16px; margin-right: -16px; border-left: 4px solid var(--color-accent-fg); } </style> `; var markdownItFenceCodeBlockEnhancer = (md) => { const original = md.renderer.rules.fence || function(tokens, idx, options, env, self) { return self.renderToken(tokens, idx, options); }; md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx]; if (!token || !token.info) { return original(tokens, idx, options, env, self); } const tokenParts = token.info.split(" ").filter(Boolean); const langName = tokenParts[0]; if (!langName) { return original(tokens, idx, options, env, self); } const isShowLineNumber = tokenParts.includes(SHOW_LINE_NUMBERS); let lineNumbers = []; const match = regExpLineNumbers.exec(token.info); if (match && match[1]) { lineNumbers = match[1].split(",").map((v) => v.split("-").map((v2) => parseInt(v2, 10))).filter((range) => range.every((num) => !isNaN(num))); } const content = token.content; const code = options.highlight ? options.highlight(content, langName, "").trim() : content.trim(); const totalLines = code.split("\n").length; const digits = Math.max(1, Math.floor(Math.log10(totalLines)) + 1); const lines = code.split("\n").map((line, index) => { const lineNumber = index + 1; const isInHighlightRange = lineNumbers.some(([start, end]) => { if (start && end) { return lineNumber >= start && lineNumber <= end && start <= totalLines && end <= totalLines; } return lineNumber === start && start <= totalLines; }); const lineNumberStr = isShowLineNumber ? lineNumber.toString().padStart(digits, " ") : ""; const lineContent = `${isShowLineNumber ? `<span class="line-number">${lineNumberStr}</span>` : ""}${line}`; return { content: isInHighlightRange ? `<div class="highlighted-line">${lineContent}</div>` : lineContent, isHighlighted: isInHighlightRange }; }); const highlightedCode = lines.map( (line) => line.isHighlighted ? line.content : `${line.content} ` ).join(""); token.attrSet("class", langName ? `language-${langName}` : ""); const attrs = self.renderAttrs(token); const styles = generateFenceStyles(digits); return `${styles}<pre${attrs}><code${attrs}>${highlightedCode.trim()}</code></pre>`; }; }; var fenceCodeBlockEnhancer_default = markdownItFenceCodeBlockEnhancer; // src/lib/marp/appMarp.ts var appMarp = /* @__PURE__ */ function() { let instance; function createInstance(containerClassName) { const marp = new Marp({ container: [ new MarpitElement("div", { class: containerClassName ?? "marpit" }) ], // slideContainer: new MarpitElement('div', { // class: 'slide', // }), inlineSVG: true, html: true, markdown: { html: true, breaks: true } }); marp.use(markdownItContainer, "columns-2", {}); marp.use(markdownItContainer, "columns-3", {}); marp.use(markdownItContainer, "columns-4", {}); marp.use(markdownItContainer, "columns-5", {}); marp.use(markdownItContainer, "columns-6", {}); marp.use(markdownItLink); marp.use(taskLists_default); marp.use(fenceCodeBlockEnhancer_default); marp.use(markdownItMermaid); marp.use(markdownItTypograms); marp.use(copyFenceContent_default); if (themes.length > 0) { marp.themeSet.default = marp.themeSet.add(themes[0].css); themes.forEach((theme) => { marp.themeSet.add(theme.css); }); } return marp; } return { createInstance, getDefaultInstance: function() { if (!instance) { instance = createInstance(); } return instance; } }; }(); var appMarp_default = appMarp; // src/hooks/useDefaultMarpRender.ts import { useEffect, useCallback as useCallback2 } from "react"; // src/hooks/useRefreshCopyFenceContent.ts import { useCallback } from "react"; function useRefreshCopyFenceContent() { return useCallback(() => { const copyFenceContentButtonElems = Array.from( document.querySelectorAll("button.copy-fence-content") ); const handleClickCopyCodeButton = async (event) => { event.stopPropagation(); event.preventDefault(); const elem = event.currentTarget; const content = elem.dataset.content; if (!content) { return; } try { await navigator.clipboard.writeText(content); elem.classList.add("active"); const timeoutId = setTimeout(() => { elem.classList.remove("active"); }, 2e3); return () => { clearTimeout(timeoutId); }; } catch (err) { console.error("Failed to copy:", err); } }; copyFenceContentButtonElems.forEach((elem) => { elem.addEventListener("click", handleClickCopyCodeButton); }); return () => { copyFenceContentButtonElems.forEach((elem) => { elem.removeEventListener("click", handleClickCopyCodeButton); }); }; }, []); } var useRefreshCopyFenceContent_default = useRefreshCopyFenceContent; // src/lib/utils/slideConfigUtil.ts var slideConfigUtil = { generateMarpConfigFromSlideConfigState: (configState) => { return ` marp: true header: ${configState.header} footer: ${configState.footer} paginate: ${configState.paginate} class: ${configState.class} theme: ${configState.theme} size: ${configState.size} style: | pre { overflow: auto; } `.trim(); }, generateSlideConfigStateFromMarpConfig: (marpConfig) => { let slideConfigState = { header: "", footer: "![height:40px](https://www.markslides.ai/image/credit.png)", paginate: false, theme: "default", class: "normal", size: "16:9" }; marpConfig.split("\n").forEach((part) => { const separatorIndex = part.indexOf(":"); const key = part.substring(0, separatorIndex); const value = part.substring(separatorIndex + 1).trim(); if (slideConfigState[key] !== void 0) { slideConfigState[key] = value.trim(); } }); return slideConfigState; } }; var slideConfigUtil_default = slideConfigUtil; // src/hooks/useDefaultMarpRender.ts function useDefaultMarpRender(slideConfig, content) { const { html, css, comments } = (() => { if (content) { try { const config = typeof slideConfig === "string" ? slideConfig : slideConfigUtil_default.generateMarpConfigFromSlideConfigState( slideConfig ); return appMarp_default.getDefaultInstance().render(`--- ${config} --- ${content}`); } catch (error) { console.error(error); } } return { html: null, css: null, comments: null }; })(); const refreshCopyFenceContent = useRefreshCopyFenceContent_default(); const refresh = useCallback2(() => { refreshCopyFenceContent(); }, []); useEffect(() => { refresh(); }); return { html, css, comments, refresh }; } var useDefaultMarpRender_default = useDefaultMarpRender; // src/hooks/useIndependentMarpRender.ts import { useRef, useEffect as useEffect2, useCallback as useCallback3 } from "react"; function useIndependentMarpRender(containerClassName, slideConfig, content) { const containerClassNameRef = useRef(null); const marpInstanceRef = useRef(null); const { html, css, comments } = (() => { if (content) { try { const config = typeof slideConfig === "string" ? slideConfig : slideConfigUtil_default.generateMarpConfigFromSlideConfigState( slideConfig ); if (containerClassNameRef.current === null || containerClassNameRef.current !== containerClassName || !marpInstanceRef.current) { containerClassNameRef.current = containerClassName; marpInstanceRef.current = appMarp_default.createInstance(containerClassName); } return marpInstanceRef.current.render( `--- ${config} --- ${content}` ); } catch (error) { console.error(error); } } return { html: null, css: null, comments: null }; })(); const refreshCopyFenceContent = useRefreshCopyFenceContent_default(); const refresh = useCallback3(() => { refreshCopyFenceContent(); }, []); useEffect2(() => { refresh(); }); return { html, css, comments, refresh }; } var useIndependentMarpRender_default = useIndependentMarpRender; // src/lib/constants/slideConfigConst.ts import themes2 from "@markslides/themes"; var slideConfigConst = { // themes: ['default', 'gaia', 'uncover'] as const, // themes: ['default', ...themes.map((theme) => theme.name)] as const, themes: [...themes2.map((theme) => theme.name)], // classes: ['normal', 'invert', 'lead'] as const, classes: [ { label: "light", value: "normal" }, { label: "dark", value: "invert" } ], sizes: ["4:3", "16:9"] }; var slideConfigConst_default = slideConfigConst; export { appMarp_default as appMarp, slideConfigConst_default as slideConfigConst, useDefaultMarpRender_default as useDefaultMarpRender, useIndependentMarpRender_default as useIndependentMarpRender };