Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>MarkNest Fallback Lab</title>
<meta
name="description"
content="Upload a Markdown ZIP, inspect entry candidates, tune output controls, download debug bundles, and switch between browser and local server PDF export."
content="Upload a Markdown ZIP, folder, or single file, inspect entry candidates, tune output controls, download debug bundles, and switch between browser and local server PDF export."
/>
<link data-trunk rel="rust" href="crates/marknest-wasm/Cargo.toml" data-bindgen-target="web" />
<link data-trunk rel="css" href="web/app.css" />
Expand All @@ -16,6 +16,7 @@
<link data-trunk rel="copy-file" href="web/output_options.mjs" />
<link data-trunk rel="copy-file" href="web/remote_assets.mjs" />
<link data-trunk rel="copy-file" href="web/runtime_sync.mjs" />
<link data-trunk rel="copy-file" href="web/zip_from_files.mjs" />
<link data-trunk rel="copy-dir" href="runtime-assets" />
<script src="./runtime-assets/html2pdf/html2pdf.bundle.min.js"></script>
</head>
Expand All @@ -26,9 +27,9 @@
<p class="eyebrow">Phase 8 · Output Controls</p>
<h1>Preview in the browser, shape the output, and keep a reproducible debug bundle beside every export.</h1>
<p class="hero-summary">
MarkNest analyzes ZIP structure in WASM, keeps diagnostics visible, lets you adjust themes, CSS,
metadata, and print settings, then reuses the same option model for browser export, debug bundles,
and the local Chromium fallback service.
MarkNest analyzes ZIP archives, folders, and single Markdown files in WASM, keeps diagnostics visible,
lets you adjust themes, CSS, metadata, and print settings, then reuses the same option model for browser
export, debug bundles, and the local Chromium fallback service.
</p>
</div>
<div class="hero-meta">
Expand All @@ -43,14 +44,23 @@ <h1>Preview in the browser, shape the output, and keep a reproducible debug bund
<section class="panel upload-panel">
<div class="panel-heading">
<p class="panel-kicker">Input</p>
<h2>ZIP Upload</h2>
<h2>Upload</h2>
</div>
<label class="dropzone" for="zip-input">
<span class="dropzone-title">Choose a `.zip` workspace</span>
<span class="dropzone-body">The browser keeps the archive local, analyzes entries, and builds preview HTML in WASM.</span>
<label class="dropzone" id="dropzone-label">
<span class="dropzone-title">Choose a `.zip`, `.md` file, or drop a folder</span>
<span class="dropzone-body">Drag and drop a ZIP archive, a Markdown file, or a folder. The browser processes everything locally.</span>
</label>
<input id="zip-input" class="visually-hidden" type="file" accept=".zip,application/zip" />
<p id="file-name" class="panel-note">No archive loaded yet.</p>
<input id="zip-input" class="visually-hidden" type="file" accept=".zip,.md,.markdown,application/zip" />
<input id="folder-input" class="visually-hidden" type="file" webkitdirectory />
<div class="upload-actions">
<button id="browse-file" class="action-button action-button-secondary" type="button">Browse File</button>
<button id="browse-folder" class="action-button action-button-secondary" type="button">Browse Folder</button>
</div>
<p id="file-name" class="panel-note">No file or folder loaded yet.</p>
<p id="local-image-warning" class="panel-note is-warning" hidden>
This Markdown file references local images that cannot be resolved in the browser.
Only external (HTTP) images will be displayed. Upload the parent folder instead.
</p>
</section>

<section class="panel status-panel">
Expand Down Expand Up @@ -264,7 +274,7 @@ <h2>Controls</h2>
<h2>Markdown Candidates</h2>
</div>
<div id="entry-list" class="entry-list empty-state">
Load a ZIP archive to inspect its Markdown entry candidates.
Load a ZIP archive, folder, or Markdown file to inspect entry candidates.
</div>
</section>

Expand Down Expand Up @@ -297,13 +307,13 @@ <h3>Errors</h3>
<p class="panel-kicker">Preview</p>
<h2 id="preview-title">No entry selected</h2>
</div>
<p id="preview-caption" class="preview-caption">Upload a ZIP to render the selected Markdown entry as HTML.</p>
<p id="preview-caption" class="preview-caption">Upload a ZIP, folder, or Markdown file to render the selected entry as HTML.</p>
</div>
<iframe
id="preview-frame"
class="preview-frame"
title="Rendered Markdown preview"
srcdoc="<!doctype html><html><body style='font-family: Georgia, serif; background: #fbf7ef; color: #47362b; display: grid; place-items: center; min-height: 100vh; margin: 0;'><div style='padding: 2rem; text-align: center; max-width: 32rem;'><h1 style='margin: 0 0 1rem;'>MarkNest Preview Lab</h1><p style='margin: 0;'>Choose a ZIP archive to inspect entry candidates and preview the rendered document here.</p></div></body></html>"
srcdoc="<!doctype html><html><body style='font-family: Georgia, serif; background: #fbf7ef; color: #47362b; display: grid; place-items: center; min-height: 100vh; margin: 0;'><div style='padding: 2rem; text-align: center; max-width: 32rem;'><h1 style='margin: 0 0 1rem;'>MarkNest Preview Lab</h1><p style='margin: 0;'>Choose a ZIP archive, drop a folder, or upload a Markdown file to preview the rendered document here.</p></div></body></html>"
></iframe>
</section>
</main>
Expand Down
17 changes: 17 additions & 0 deletions web/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ body::before {
box-shadow: 0 12px 26px rgba(15, 108, 124, 0.15);
}

.dropzone.is-drag-over {
transform: translateY(-2px);
border-color: var(--accent-strong);
border-style: solid;
box-shadow: 0 16px 32px rgba(15, 108, 124, 0.2);
background:
linear-gradient(135deg, rgba(15, 108, 124, 0.18), transparent),
var(--paper-strong);
}

.dropzone-title {
font-weight: 700;
font-size: 1.05rem;
Expand All @@ -191,6 +201,13 @@ body::before {
color: var(--warning);
}

.upload-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
margin-top: 0.75rem;
}

.visually-hidden {
position: absolute;
width: 1px;
Expand Down
120 changes: 113 additions & 7 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import {
runtimeDiagnosticsForEntry,
waitForFrameRenderStatus,
} from "./runtime_sync.mjs";
import {
buildZipFromFiles,
detectInputKind,
readDirectoryEntries,
} from "./zip_from_files.mjs";

const state = {
wasm: null,
Expand All @@ -39,6 +44,11 @@ const state = {

const elements = {
zipInput: document.getElementById("zip-input"),
folderInput: document.getElementById("folder-input"),
browseFile: document.getElementById("browse-file"),
browseFolder: document.getElementById("browse-folder"),
dropzoneLabel: document.getElementById("dropzone-label"),
localImageWarning: document.getElementById("local-image-warning"),
fileName: document.getElementById("file-name"),
statusChip: document.getElementById("status-chip"),
statusMessage: document.getElementById("status-message"),
Expand Down Expand Up @@ -456,6 +466,44 @@ function schedulePreviewRefresh() {
}, 180);
}

async function processInput(files) {
const inputKind = detectInputKind(files);

if (inputKind === "unsupported") {
setStatus("error", "Unsupported", "Please upload a .zip archive, a .md file, or drag a folder.");
return;
}

elements.localImageWarning.hidden = true;

if (inputKind === "zip") {
await analyzeZip(files[0]);
return;
}

if (!state.wasm) {
setStatus("waiting", "Booting", "The WASM runtime is still loading. Try again in a moment.");
return;
}

try {
setStatus("ready", "Converting", "Building an in-memory archive from the selected input.");
const result = await buildZipFromFiles(state.wasm, files);

if (result.warnings.length > 0) {
elements.localImageWarning.hidden = false;
elements.localImageWarning.textContent = result.warnings[0];
}

const syntheticFile = new File([result.zipBytes], result.fileName, {
type: "application/zip",
});
await analyzeZip(syntheticFile);
} catch (error) {
setStatus("error", "Failed", `Input processing failed: ${String(error)}`);
}
}

async function analyzeZip(file) {
if (!state.wasm) {
setStatus("waiting", "Booting", "The WASM runtime is still loading. Try again in a moment.");
Expand Down Expand Up @@ -489,7 +537,7 @@ async function analyzeZip(file) {

if (projectIndex.entry_candidates.length === 0) {
elements.previewTitle.textContent = "No preview available";
elements.previewCaption.textContent = "The uploaded archive did not expose a Markdown entry candidate.";
elements.previewCaption.textContent = "The input did not expose a Markdown entry candidate.";
setStatus("warning", "No entries", "Analysis completed, but there is no Markdown entry to preview.");
syncActionButtons();
return;
Expand All @@ -511,9 +559,9 @@ async function analyzeZip(file) {
setTextList(elements.warningList, [], "Warnings will appear after analysis.");
setTextList(elements.errorList, [String(error)], "No errors.");
elements.previewTitle.textContent = "Preview unavailable";
elements.previewCaption.textContent = "The ZIP could not be analyzed.";
elements.previewCaption.textContent = "The input could not be analyzed.";
updateTemplatePreviews();
setStatus("error", "Failed", `ZIP analysis failed: ${String(error)}`);
setStatus("error", "Failed", `Analysis failed: ${String(error)}`);
syncActionButtons();
} finally {
setBusy(false);
Expand Down Expand Up @@ -1013,7 +1061,7 @@ function connectWasmBindings() {
}

state.wasm = window.wasmBindings;
setStatus("ready", "Ready", "The WASM runtime is ready. Upload a ZIP archive to inspect it.");
setStatus("ready", "Ready", "The WASM runtime is ready. Upload a ZIP, Markdown file, or folder to inspect it.");
syncActionButtons();
}

Expand Down Expand Up @@ -1042,12 +1090,70 @@ updateScaleNote();
updateTemplatePreviews();

elements.zipInput.addEventListener("change", async (event) => {
const file = event.target.files?.[0];
if (!file) {
const files = event.target.files;
if (!files || files.length === 0) {
return;
}

await analyzeZip(file);
await processInput(Array.from(files));
});

elements.folderInput.addEventListener("change", async (event) => {
const files = event.target.files;
if (!files || files.length === 0) {
return;
}

await processInput(Array.from(files));
});

elements.browseFile.addEventListener("click", () => {
elements.zipInput.click();
});

elements.browseFolder.addEventListener("click", () => {
elements.folderInput.click();
});

// Drag-and-drop on the dropzone.
elements.dropzoneLabel.addEventListener("dragover", (event) => {
event.preventDefault();
elements.dropzoneLabel.classList.add("is-drag-over");
});

elements.dropzoneLabel.addEventListener("dragleave", () => {
elements.dropzoneLabel.classList.remove("is-drag-over");
});

elements.dropzoneLabel.addEventListener("drop", async (event) => {
event.preventDefault();
elements.dropzoneLabel.classList.remove("is-drag-over");

const items = event.dataTransfer?.items;
if (!items || items.length === 0) {
return;
}

// Check if any item is a directory.
const entries = [];
for (const item of items) {
const entry = item.webkitGetAsEntry?.();
if (entry) {
entries.push(entry);
}
}

if (entries.length === 1 && entries[0].isDirectory) {
const files = await readDirectoryEntries(entries[0]);
await processInput(files);
return;
}

// Single file drop (ZIP or .md).
const droppedFiles = event.dataTransfer.files;
if (droppedFiles.length > 0) {
await processInput(Array.from(droppedFiles));
}
});

elements.qualityMode.addEventListener("change", (event) => {
Expand Down
Loading