diff --git a/sites/labs/public/CommonMark-Demo/CommonMark/commonmark-0.27.1.jar b/sites/labs/public/CommonMark-Demo/CommonMark/commonmark-0.27.1.jar new file mode 100644 index 00000000..d673738f Binary files /dev/null and b/sites/labs/public/CommonMark-Demo/CommonMark/commonmark-0.27.1.jar differ diff --git a/sites/labs/public/CommonMark-Demo/Template/preview.css b/sites/labs/public/CommonMark-Demo/Template/preview.css new file mode 100644 index 00000000..29cd98dd --- /dev/null +++ b/sites/labs/public/CommonMark-Demo/Template/preview.css @@ -0,0 +1,189 @@ +:root { + color-scheme: light; + + /* Layout */ + --pad: 18px; + --max: 920px; + + /* Typography */ + --font: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; + --mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + + /* Colors */ + --fg: #0b1220; + --muted: #475569; + --bg: #ffffff; + --link: #2563eb; + + --surface: #f7f9fc; + --border: #e6eaf2; + + /* Radius */ + --r-sm: 8px; + --r-md: 12px; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Keep the preview readable on wide screens */ +body > * { + max-width: var(--max); + margin-left: auto; + margin-right: auto; +} + +body { + padding: var(--pad); +} + +/* Typography */ +h1, +h2, +h3, +h4, +h5, +h6 { + line-height: 1.2; + margin: 1.05em 0 0.4em; + letter-spacing: -0.01em; +} + +h1 { + font-size: 1.65em; +} +h2 { + font-size: 1.35em; +} +h3 { + font-size: 1.15em; +} + +p { + margin: 0.65em 0; +} + +small { + color: var(--muted); +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Lists */ +ul, +ol { + margin: 0.65em 0; + padding-left: 1.25em; +} + +li { + margin: 0.25em 0; +} + +li > p { + margin: 0.35em 0; +} + +/* Code */ +code, +pre, +kbd { + font-family: var(--mono); + font-size: 0.95em; +} + +code { + background: var(--surface); + border: 1px solid var(--border); + padding: 0.12em 0.38em; + border-radius: var(--r-sm); +} + +pre { + background: var(--surface); + border: 1px solid var(--border); + padding: 12px 14px; + border-radius: var(--r-md); + overflow: auto; + line-height: 1.45; +} + +/* Ensure code blocks don't double-style */ +pre code { + background: transparent; + border: none; + padding: 0; + border-radius: 0; +} + +/* Quotes */ +blockquote { + margin: 0.9em 0; + padding: 0.35em 0 0.35em 14px; + border-left: 3px solid #d3dcf3; + color: var(--muted); +} + +/* Tables */ +table { + border-collapse: collapse; + margin: 0.9em 0; + width: 100%; +} + +th, +td { + border: 1px solid var(--border); + padding: 7px 10px; + vertical-align: top; +} + +th { + background: #f1f5ff; + text-align: left; +} + +/* Images and rules */ +img { + max-width: 100%; + height: auto; + border-radius: var(--r-md); +} + +hr { + border: 0; + border-top: 1px solid var(--border); + margin: 18px 0; +} + +/* Subtle selection */ +::selection { + background: rgba(37, 99, 235, 0.18); +} diff --git a/sites/labs/public/CommonMark-Demo/Template/preview.html b/sites/labs/public/CommonMark-Demo/Template/preview.html new file mode 100644 index 00000000..e88ef3e0 --- /dev/null +++ b/sites/labs/public/CommonMark-Demo/Template/preview.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/sites/labs/public/CommonMark-Demo/index.html b/sites/labs/public/CommonMark-Demo/index.html new file mode 100644 index 00000000..1f7ecd64 --- /dev/null +++ b/sites/labs/public/CommonMark-Demo/index.html @@ -0,0 +1,152 @@ + + + + + + CommonMark – Library Mode + + + + + +
+
+
+
+
CommonMark
+
Java-powered Markdown rendering
+
+
+ +
+ + +
+ +
+
+
+
Editor
+
+ +
+ +
+
+ +
+
+
Preview
+
+ + +
+ + +
+ + +
+ + + + diff --git a/sites/labs/public/CommonMark-Demo/main.js b/sites/labs/public/CommonMark-Demo/main.js new file mode 100644 index 00000000..c580de6d --- /dev/null +++ b/sites/labs/public/CommonMark-Demo/main.js @@ -0,0 +1,235 @@ +const COMMONMARK_JAR_NAME = "commonmark-0.27.1.jar"; +const WEB_JAR_URL = new URL( + `./CommonMark/${COMMONMARK_JAR_NAME}`, + window.location.href +).toString(); +const STR_JAR_PATH = `/str/${COMMONMARK_JAR_NAME}`; + +const mdInput = document.getElementById("md-input"); +const preview = document.getElementById("preview"); + +const btnRender = document.getElementById("btn-render"); +const btnReset = document.getElementById("btn-reset"); +const btnHelp = document.getElementById("btn-help"); +const btnCloseSide = document.getElementById("btn-close-side"); + +const liveToggle = document.getElementById("live-toggle"); + +const statusLabel = document.getElementById("status-label"); +const dot = document.getElementById("dot"); + +const side = document.getElementById("side"); +const workspace = document.getElementById("workspace"); + +let lib = null; +let parser = null; +let renderer = null; + +let lastHtml = ""; +let renderTimer = null; +let previewHtmlTpl = null; +let previewCssText = null; + +function setBusy(isBusy) { + dot.classList.toggle("busy", Boolean(isBusy)); +} + +function setStatus(text, { busy = false } = {}) { + statusLabel.textContent = text; + setBusy(busy); +} + +function setSideVisible(visible) { + side.classList.toggle("side-hidden", !visible); + workspace.classList.toggle("no-side", !visible); +} + +function applyLiveUiState() { + // Only show Render button when Live is OFF + const live = liveToggle.checked; + btnRender.style.display = live ? "none" : "inline-flex"; +} + +function debounceRender(ms = 300) { + if (renderTimer) clearTimeout(renderTimer); + renderTimer = setTimeout(() => renderMarkdown({ reason: "live" }), ms); +} + +function defaultMarkdown() { + return ( + "# Hello CommonMark 👋\n\n" + + "This page uses **CheerpJ library mode** to run **Java** in the browser.\n\n" + + "## What to try\n\n" + + "- **Bold**, _italic_, `inline code`\n" + + "- Links: https://cheerpj.com\n" + + "- Code blocks:\n\n" + + "```js\n" + + "console.log('Rendered by Java (CommonMark)');\n" + + "```\n\n" + + '> Markdown is forgiving — "invalid" syntax is usually treated as text.\n' + ); +} + +async function loadJarIntoStrVfs() { + setStatus("Fetching CommonMark JAR…", { busy: true }); + + const resp = await fetch(WEB_JAR_URL); + if (!resp.ok) { + throw new Error( + `Failed to fetch ${WEB_JAR_URL}: ${resp.status} ${resp.statusText}` + ); + } + + const buf = await resp.arrayBuffer(); + const u8 = new Uint8Array(buf); + + setStatus("Injecting JAR into /str…", { busy: true }); + + try { + cheerpOSAddStringFile(STR_JAR_PATH, u8); + console.log("Jar added using cherrpOSAddStringFile"); + } catch (err) { + console.log("Could not add Jar using cherrpOSAddStringFile"); + setStatus("Failed to inject JAR into /str…", { busy: true }); + } +} + +async function init() { + applyLiveUiState(); + setSideVisible(true); + + setStatus("Starting CheerpJ…", { busy: true }); + await cheerpjInit({ version: 17 }); + + // fetch + add jar into /str at startup + await loadJarIntoStrVfs(); + + setStatus("Loading CommonMark…", { busy: true }); + + lib = await cheerpjRunLibrary(STR_JAR_PATH); + + const Parser = await lib.org.commonmark.parser.Parser; + const HtmlRenderer = await lib.org.commonmark.renderer.html.HtmlRenderer; + + parser = await (await Parser.builder()).build(); + renderer = await (await HtmlRenderer.builder()).build(); + + setStatus("Ready", { busy: false }); + + // Initial content + first render + if (!mdInput.value) mdInput.value = defaultMarkdown(); + await renderMarkdown({ reason: "init" }); +} + +async function renderMarkdown({ reason = "manual" } = {}) { + if (!parser || !renderer) return; + + try { + setStatus("Rendering…", { busy: true }); + + const md = mdInput.value ?? ""; + const docNode = await parser.parse(md); + const html = await renderer.render(docNode); + const htmlStr = await html.toString(); + + // Avoid reloading the iframe if unchanged + if (htmlStr !== lastHtml) { + lastHtml = htmlStr; + + try { + preview.srcdoc = await buildPreviewDoc(htmlStr); + } catch (e) { + console.error("Preview build failed:", e); + + // Visible error in the preview instead of a blank frame + preview.srcdoc = ` + +

Preview failed

+
${String(e)}
+ `; + } + } + setStatus("Rendered", { busy: false }); + } catch (err) { + console.error("Render failed:", err); + setStatus("Render failed — see console", { busy: false }); + } +} + +async function loadPreviewAssets() { + if (previewHtmlTpl && previewCssText) return; + + const htmlUrl = new URL( + "./Template/preview.html", + window.location.href + ).toString(); + const cssUrl = new URL( + "./Template/preview.css", + window.location.href + ).toString(); + + const [htmlResp, cssResp] = await Promise.all([ + fetch(htmlUrl), + fetch(cssUrl), + ]); + + if (!htmlResp.ok) + throw new Error( + `Failed to load preview.html: ${htmlResp.status} ${htmlResp.statusText}` + ); + if (!cssResp.ok) + throw new Error( + `Failed to load preview.css: ${cssResp.status} ${cssResp.statusText}` + ); + + previewHtmlTpl = await htmlResp.text(); + previewCssText = await cssResp.text(); +} + +async function buildPreviewDoc(htmlBody) { + await loadPreviewAssets(); + + if (!previewHtmlTpl.includes("")) { + throw new Error("preview.html missing marker"); + } + if (!previewHtmlTpl.includes("")) { + throw new Error("preview.html missing marker"); + } + + return previewHtmlTpl + .replace("", ``) + .replace("", htmlBody || ""); +} + +// Events +mdInput.addEventListener("input", () => { + if (liveToggle.checked) debounceRender(300); +}); + +liveToggle.addEventListener("change", () => { + applyLiveUiState(); + // If user turns live ON, render once immediately + if (liveToggle.checked) renderMarkdown({ reason: "manual" }); +}); + +btnRender.addEventListener("click", () => renderMarkdown({ reason: "manual" })); + +btnReset.addEventListener("click", async () => { + mdInput.value = defaultMarkdown(); + await renderMarkdown({ reason: "manual" }); +}); + +btnHelp.addEventListener("click", () => { + const isHidden = side.classList.contains("side-hidden"); + setSideVisible(isHidden); +}); + +btnCloseSide.addEventListener("click", () => { + setSideVisible(false); +}); + +// Start +init().catch((err) => { + console.error("Init failed:", err); + setStatus("Failed to start", { busy: false }); +}); diff --git a/sites/labs/public/CommonMark-Demo/style.css b/sites/labs/public/CommonMark-Demo/style.css new file mode 100644 index 00000000..8e13a52c --- /dev/null +++ b/sites/labs/public/CommonMark-Demo/style.css @@ -0,0 +1,437 @@ +:root { + /* Neutral “product” palette */ + --bg: #0b1020; + --surface: rgba(255, 255, 255, 0.06); + --border: rgba(255, 255, 255, 0.1); + --text: rgba(255, 255, 255, 0.92); + --muted: rgba(255, 255, 255, 0.6); + + --accent: #3b82f6; + --ok: #22c55e; + --warn: #f59e0b; + + --radius-xl: 18px; + --radius-lg: 14px; +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; + margin: 0; + background: radial-gradient( + 1200px 800px at 20% 0%, + #121a35 0%, + var(--bg) 55%, + #070a14 100% + ); + color: var(--text); + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; +} + +.app { + height: 100%; + display: flex; + flex-direction: column; +} + +.appbar { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 14px; + border-bottom: 1px solid var(--border); + background: rgba(10, 14, 28, 0.72); + backdrop-filter: blur(10px); +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.brand-text { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} +.brand-title { + font-weight: 700; + font-size: 0.95rem; + line-height: 1.1; +} +.brand-subtitle { + font-size: 0.78rem; + color: var(--muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.appbar-spacer { + flex: 1; +} + +.toolbar { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.btn { + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + color: var(--text); + border-radius: 999px; + padding: 7px 12px; + font-size: 0.82rem; + cursor: pointer; + transition: + transform 0.05s ease, + background 0.15s ease, + border-color 0.15s ease; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.btn:hover { + background: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.16); +} +.btn:active { + transform: translateY(1px); +} + +.btn-primary { + border-color: rgba(59, 130, 246, 0.45); + background: rgba(59, 130, 246, 0.18); +} + +.btn-primary:hover { + background: rgba(59, 130, 246, 0.25); + border-color: rgba(59, 130, 246, 0.55); +} + +.btn-ghost { + background: transparent; +} + +.toggle { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + font-size: 0.82rem; + color: var(--muted); + user-select: none; +} + +.toggle input { + display: none; +} + +.toggle-ui { + width: 34px; + height: 20px; + border-radius: 999px; + position: relative; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: background 0.15s ease; +} + +.toggle-ui::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.86); + transition: transform 0.15s ease; +} + +.toggle input:checked + .toggle-ui { + background: rgba(34, 197, 94, 0.18); + border-color: rgba(34, 197, 94, 0.28); +} + +.toggle input:checked + .toggle-ui::after { + transform: translateX(14px); + background: rgba(255, 255, 255, 0.95); +} + +.toggle-label { + color: var(--text); + opacity: 0.85; +} + +.workspace { + flex: 1; + min-height: 0; + display: grid; + grid-template-columns: 1fr 1fr 340px; + gap: 12px; + padding: 12px; +} + +.workspace.no-side { + grid-template-columns: 1fr 1fr; +} + +.panel { + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius-xl); + background: var(--surface); + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.2); +} + +.panel-title { + font-weight: 650; + font-size: 0.86rem; + letter-spacing: 0.01em; +} + +.editor-wrap { + flex: 1; + min-height: 0; + display: flex; +} + +#md-input { + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + font-size: 14px; + line-height: 20px; + padding: 12px; + flex: 1; + width: 100%; + height: 100%; +} + +#md-input { + border: none; + outline: none; + resize: none; + overflow: auto; + background: transparent; + white-space: pre-wrap; + line-height: 1.5; + color: var(--text-main); +} + +#preview { + flex: 1; + width: 100%; + min-height: 0; + border: none; + background: #ffffff; +} + +.side { + min-width: 0; + border: 1px solid var(--border); + border-radius: var(--radius-xl); + background: rgba(255, 255, 255, 0.04); + overflow: hidden; + display: flex; + flex-direction: column; +} + +.side-hidden { + display: none; +} + +.side-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.2); +} + +.side-title { + font-weight: 650; + font-size: 0.86rem; +} + +.icon-btn { + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + color: var(--text); + border-radius: 10px; + padding: 6px 8px; + cursor: pointer; +} + +.icon-btn:hover { + background: rgba(255, 255, 255, 0.07); +} + +.side-body { + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 10px; + color: var(--muted); + font-size: 0.84rem; +} + +.accordion { + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--radius-lg); + background: rgba(0, 0, 0, 0.16); + padding: 8px 10px; +} + +.accordion summary { + cursor: pointer; + color: rgba(255, 255, 255, 0.88); + font-weight: 600; +} + +.steps, +.bullets { + margin: 8px 0 0; + padding-left: 18px; +} + +.kv { + display: grid; + grid-template-columns: 90px 1fr; + gap: 6px 10px; + margin-top: 8px; + font-size: 0.82rem; +} + +.kv .k { + color: rgba(255, 255, 255, 0.55); +} +.kv .v { + color: rgba(255, 255, 255, 0.88); + font-family: ui-monospace, monospace; + font-size: 0.8rem; +} + +.side-footer { + margin-top: 6px; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.link { + display: inline-flex; + align-items: center; + gap: 6px; + text-decoration: none; + color: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.04); + padding: 6px 10px; + border-radius: 999px; + font-size: 0.8rem; +} + +.link:hover { + background: rgba(255, 255, 255, 0.07); +} + +.statusbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-top: 1px solid var(--border); + background: rgba(10, 14, 28, 0.72); + backdrop-filter: blur(10px); + font-size: 0.82rem; + color: var(--muted); +} + +.status-left { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--ok); +} + +.dot.busy { + background: var(--warn); + animation: pulse 1.1s ease-in-out infinite; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} + +.status-right { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 1100px) { + .workspace { + grid-template-columns: 1fr; + } + .workspace.no-side { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .toolbar { + flex-wrap: wrap; + justify-content: flex-end; + } + .brand-subtitle { + display: none; + } +}