@markslides/renderer
Version:
444 lines (428 loc) • 15.2 kB
JavaScript
// 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: "",
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
};