@franken-suite/franken-markdown 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/demo/demo.js +215 -0
- package/demo/index.html +395 -0
- package/franken_markdown.d.ts +102 -0
- package/franken_markdown.js +295 -0
- package/package.json +50 -0
- package/pkg/franken_markdown.d.ts +172 -0
- package/pkg/franken_markdown.js +811 -0
- package/pkg/franken_markdown_bg.wasm +0 -0
- package/pkg/franken_markdown_bg.wasm.d.ts +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# franken_markdown WASM Package
|
|
2
|
+
|
|
3
|
+
This directory contains the source wrapper for the browser package. Generated
|
|
4
|
+
`wasm-bindgen` glue is intentionally not committed here; `scripts/check-wasm-package.sh`
|
|
5
|
+
builds it into `target/fmd-checks/wasm-package/`.
|
|
6
|
+
|
|
7
|
+
## Build
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
rustup target add wasm32-unknown-unknown
|
|
11
|
+
cargo install wasm-bindgen-cli --version 0.2.126 --locked
|
|
12
|
+
scripts/check-wasm-package.sh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The assembled package contains:
|
|
16
|
+
|
|
17
|
+
- `franken_markdown.js` - ergonomic hand-written ESM wrapper,
|
|
18
|
+
- `franken_markdown.d.ts` - exact TypeScript API contract,
|
|
19
|
+
- `demo/` - static browser demo that uses the public package wrapper,
|
|
20
|
+
- `package.json` - package metadata and exports,
|
|
21
|
+
- `pkg/` - generated wasm-bindgen glue and `.wasm` binary.
|
|
22
|
+
|
|
23
|
+
## Demo
|
|
24
|
+
|
|
25
|
+
After `scripts/check-wasm-package.sh`, serve the assembled package directory
|
|
26
|
+
with any static file server and open `demo/index.html`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 -m http.server 8787 --directory target/fmd-checks/wasm-package
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then open `http://127.0.0.1:8787/demo/`. The demo is plain HTML/CSS/ESM and
|
|
33
|
+
does not require a bundler or network access for normal rendering. It imports
|
|
34
|
+
`../franken_markdown.js`, renders the HTML preview through `renderHtml`, and
|
|
35
|
+
downloads PDF bytes through `renderPdf`.
|
|
36
|
+
|
|
37
|
+
## Browser Usage
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { createRenderer } from "./franken_markdown.js";
|
|
41
|
+
|
|
42
|
+
const fmd = await createRenderer();
|
|
43
|
+
const html = await fmd.renderHtml("# Hello", { font: "sans", darkMode: "auto" });
|
|
44
|
+
document.body.innerHTML = html.text();
|
|
45
|
+
|
|
46
|
+
const pdf = await fmd.renderPdf("# Report", {
|
|
47
|
+
title: "Report",
|
|
48
|
+
metadataEpochSeconds: 1700000000
|
|
49
|
+
});
|
|
50
|
+
const url = URL.createObjectURL(pdf.blob());
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every render result has:
|
|
54
|
+
|
|
55
|
+
- `format`: `html` or `pdf`,
|
|
56
|
+
- `mimeType`: browser Blob MIME type,
|
|
57
|
+
- `extension`: default download extension,
|
|
58
|
+
- `sourceLength`: Markdown source byte length,
|
|
59
|
+
- `bytes`: `Uint8Array`,
|
|
60
|
+
- `diagnostics`: recoverable parser diagnostics,
|
|
61
|
+
- `text()`, `blob()`, and `filename()` helpers.
|
package/demo/demo.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { createRenderer } from "../franken_markdown.js";
|
|
2
|
+
|
|
3
|
+
const sampleMarkdown = `# franken_markdown demo
|
|
4
|
+
|
|
5
|
+
This preview is rendered by the same browser package API that applications use.
|
|
6
|
+
|
|
7
|
+
| Item | Status | Notes |
|
|
8
|
+
|---|:---:|---|
|
|
9
|
+
| HTML | ready | Self-contained preview with embedded subset fonts |
|
|
10
|
+
| PDF | ready | Deterministic bytes, tagged text, links, and outlines |
|
|
11
|
+
| WASM | first-class | No filesystem, process, threads, or network in the core |
|
|
12
|
+
|
|
13
|
+
> Blockquotes, tables, links, lists, and code blocks use the default theme.
|
|
14
|
+
|
|
15
|
+
\`\`\`rust
|
|
16
|
+
fn main() {
|
|
17
|
+
println!("render markdown");
|
|
18
|
+
}
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
- Toggle the serif font for long-form reading.
|
|
22
|
+
- Disable dark CSS if the host page needs a fixed light palette.
|
|
23
|
+
- Download a PDF generated from the same Markdown source.
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
const els = {
|
|
27
|
+
markdown: requiredElement("#markdown"),
|
|
28
|
+
render: requiredElement("#render"),
|
|
29
|
+
downloadPdf: requiredElement("#download-pdf"),
|
|
30
|
+
preview: requiredElement("#preview"),
|
|
31
|
+
diagnostics: requiredElement("#diagnostics"),
|
|
32
|
+
status: requiredElement("#status"),
|
|
33
|
+
sourceSize: requiredElement("#source-size"),
|
|
34
|
+
previewMeta: requiredElement("#preview-meta"),
|
|
35
|
+
font: requiredElement("#font"),
|
|
36
|
+
darkMode: requiredElement("#dark-mode"),
|
|
37
|
+
title: requiredElement("#title"),
|
|
38
|
+
author: requiredElement("#author"),
|
|
39
|
+
customCss: requiredElement("#custom-css"),
|
|
40
|
+
allowHtml: requiredElement("#allow-html"),
|
|
41
|
+
lineNumbers: requiredElement("#line-numbers")
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let renderer = null;
|
|
45
|
+
let renderTimer = 0;
|
|
46
|
+
let lastPdfUrl = null;
|
|
47
|
+
|
|
48
|
+
function requiredElement(selector) {
|
|
49
|
+
const element = document.querySelector(selector);
|
|
50
|
+
if (element === null) {
|
|
51
|
+
throw new Error(`franken_markdown demo is missing required element ${selector}`);
|
|
52
|
+
}
|
|
53
|
+
return element;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
els.markdown.value = sampleMarkdown;
|
|
57
|
+
setBusy(true, "loading wasm package");
|
|
58
|
+
createRenderer()
|
|
59
|
+
.then((created) => {
|
|
60
|
+
renderer = created;
|
|
61
|
+
setBusy(false, "ready");
|
|
62
|
+
schedulePreview();
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
setBusy(false, "wasm load failed");
|
|
66
|
+
showError(error);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
els.render.addEventListener("click", () => {
|
|
70
|
+
void renderPreview();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
els.downloadPdf.addEventListener("click", () => {
|
|
74
|
+
void downloadPdf();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
for (const input of [
|
|
78
|
+
els.markdown,
|
|
79
|
+
els.font,
|
|
80
|
+
els.darkMode,
|
|
81
|
+
els.title,
|
|
82
|
+
els.author,
|
|
83
|
+
els.customCss,
|
|
84
|
+
els.allowHtml,
|
|
85
|
+
els.lineNumbers
|
|
86
|
+
]) {
|
|
87
|
+
input.addEventListener("input", schedulePreview);
|
|
88
|
+
input.addEventListener("change", schedulePreview);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function schedulePreview() {
|
|
92
|
+
window.clearTimeout(renderTimer);
|
|
93
|
+
updateSourceSize();
|
|
94
|
+
renderTimer = window.setTimeout(() => {
|
|
95
|
+
void renderPreview();
|
|
96
|
+
}, 180);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function renderPreview() {
|
|
100
|
+
if (renderer === null) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const markdown = els.markdown.value;
|
|
104
|
+
const options = renderOptions();
|
|
105
|
+
if (markdown.trim() === "") {
|
|
106
|
+
els.preview.srcdoc = "";
|
|
107
|
+
els.previewMeta.textContent = "empty source";
|
|
108
|
+
renderDiagnostics([]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
setBusy(true, "rendering html");
|
|
112
|
+
try {
|
|
113
|
+
const output = await renderer.renderHtml(markdown, options);
|
|
114
|
+
els.preview.srcdoc = output.text();
|
|
115
|
+
els.previewMeta.textContent = `${output.sourceLength} source bytes -> ${output.bytes.byteLength} html bytes`;
|
|
116
|
+
renderDiagnostics(output.diagnostics);
|
|
117
|
+
setBusy(false, "ready");
|
|
118
|
+
} catch (error) {
|
|
119
|
+
setBusy(false, "render failed");
|
|
120
|
+
showError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function downloadPdf() {
|
|
125
|
+
if (renderer === null) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const markdown = els.markdown.value;
|
|
129
|
+
if (markdown.trim() === "") {
|
|
130
|
+
showError(new Error("Markdown input is empty; add content before downloading PDF."));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setBusy(true, "rendering pdf");
|
|
134
|
+
try {
|
|
135
|
+
const output = await renderer.renderPdf(markdown, renderOptions());
|
|
136
|
+
if (lastPdfUrl !== null) {
|
|
137
|
+
URL.revokeObjectURL(lastPdfUrl);
|
|
138
|
+
}
|
|
139
|
+
lastPdfUrl = URL.createObjectURL(output.blob());
|
|
140
|
+
const link = document.createElement("a");
|
|
141
|
+
link.href = lastPdfUrl;
|
|
142
|
+
link.download = output.filename(filenameBase());
|
|
143
|
+
document.body.appendChild(link);
|
|
144
|
+
link.click();
|
|
145
|
+
link.remove();
|
|
146
|
+
els.previewMeta.textContent = `${output.sourceLength} source bytes -> ${output.bytes.byteLength} pdf bytes`;
|
|
147
|
+
renderDiagnostics(output.diagnostics);
|
|
148
|
+
setBusy(false, "pdf ready");
|
|
149
|
+
} catch (error) {
|
|
150
|
+
setBusy(false, "pdf failed");
|
|
151
|
+
showError(error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderOptions() {
|
|
156
|
+
const customCss = els.customCss.value.trim() === "" ? undefined : els.customCss.value;
|
|
157
|
+
return {
|
|
158
|
+
font: els.font.value,
|
|
159
|
+
darkMode: els.darkMode.value,
|
|
160
|
+
title: textOrUndefined(els.title.value),
|
|
161
|
+
author: textOrUndefined(els.author.value),
|
|
162
|
+
customCss,
|
|
163
|
+
allowRawHtml: els.allowHtml.checked,
|
|
164
|
+
codeLineNumbers: els.lineNumbers.checked,
|
|
165
|
+
metadataEpochSeconds: 1700000000
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function textOrUndefined(value) {
|
|
170
|
+
const text = String(value).trim();
|
|
171
|
+
return text === "" ? undefined : text;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function filenameBase() {
|
|
175
|
+
return textOrUndefined(els.title.value)
|
|
176
|
+
?.toLowerCase()
|
|
177
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
178
|
+
.replace(/^-|-$/g, "")
|
|
179
|
+
|| "franken-markdown";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderDiagnostics(diagnostics) {
|
|
183
|
+
els.diagnostics.replaceChildren();
|
|
184
|
+
if (diagnostics.length === 0) {
|
|
185
|
+
const item = document.createElement("li");
|
|
186
|
+
item.className = "empty";
|
|
187
|
+
item.textContent = "No diagnostics.";
|
|
188
|
+
els.diagnostics.appendChild(item);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
for (const diagnostic of diagnostics) {
|
|
192
|
+
const item = document.createElement("li");
|
|
193
|
+
item.className = diagnostic.severity === "error" ? "error" : "";
|
|
194
|
+
item.textContent = `${diagnostic.severity} ${diagnostic.start}-${diagnostic.end}: ${diagnostic.message}`;
|
|
195
|
+
els.diagnostics.appendChild(item);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function showError(error) {
|
|
200
|
+
els.diagnostics.replaceChildren();
|
|
201
|
+
const item = document.createElement("li");
|
|
202
|
+
item.className = "error";
|
|
203
|
+
item.textContent = error instanceof Error ? error.message : String(error);
|
|
204
|
+
els.diagnostics.appendChild(item);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function updateSourceSize() {
|
|
208
|
+
els.sourceSize.textContent = `${new TextEncoder().encode(els.markdown.value).byteLength} bytes`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setBusy(busy, message) {
|
|
212
|
+
els.status.textContent = message;
|
|
213
|
+
els.render.disabled = busy;
|
|
214
|
+
els.downloadPdf.disabled = busy;
|
|
215
|
+
}
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>franken_markdown WASM Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light;
|
|
10
|
+
--bg: #f6f7f9;
|
|
11
|
+
--panel: #ffffff;
|
|
12
|
+
--panel-2: #f0f3f7;
|
|
13
|
+
--text: #16181d;
|
|
14
|
+
--muted: #626a78;
|
|
15
|
+
--line: #d9dee7;
|
|
16
|
+
--accent: #1769aa;
|
|
17
|
+
--accent-2: #0f7f68;
|
|
18
|
+
--danger: #a61b3c;
|
|
19
|
+
--shadow: 0 14px 30px rgba(33, 42, 55, 0.10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
background: var(--bg);
|
|
30
|
+
color: var(--text);
|
|
31
|
+
font-family:
|
|
32
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
33
|
+
"Segoe UI", sans-serif;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
button,
|
|
37
|
+
input,
|
|
38
|
+
select,
|
|
39
|
+
textarea {
|
|
40
|
+
font: inherit;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.app {
|
|
44
|
+
min-height: 100vh;
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-rows: auto 1fr;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.topbar {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: minmax(210px, 1fr) auto;
|
|
52
|
+
gap: 12px;
|
|
53
|
+
align-items: center;
|
|
54
|
+
padding: 12px 16px;
|
|
55
|
+
border-bottom: 1px solid var(--line);
|
|
56
|
+
background: var(--panel);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.brand {
|
|
60
|
+
min-width: 0;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-wrap: wrap;
|
|
63
|
+
gap: 8px 12px;
|
|
64
|
+
align-items: baseline;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.brand strong {
|
|
68
|
+
font-size: 15px;
|
|
69
|
+
letter-spacing: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.brand span,
|
|
73
|
+
.status {
|
|
74
|
+
color: var(--muted);
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.actions {
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 8px;
|
|
81
|
+
align-items: center;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.button {
|
|
85
|
+
border: 1px solid var(--line);
|
|
86
|
+
border-radius: 6px;
|
|
87
|
+
background: var(--panel);
|
|
88
|
+
color: var(--text);
|
|
89
|
+
min-height: 34px;
|
|
90
|
+
padding: 0 12px;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.button.primary {
|
|
95
|
+
border-color: var(--accent);
|
|
96
|
+
background: var(--accent);
|
|
97
|
+
color: #ffffff;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.button.secondary {
|
|
101
|
+
border-color: var(--accent-2);
|
|
102
|
+
color: var(--accent-2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.button:disabled {
|
|
106
|
+
cursor: wait;
|
|
107
|
+
opacity: 0.62;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.workspace {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: minmax(280px, 0.88fr) minmax(320px, 1.12fr);
|
|
113
|
+
gap: 12px;
|
|
114
|
+
min-height: 0;
|
|
115
|
+
padding: 12px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.pane {
|
|
119
|
+
min-width: 0;
|
|
120
|
+
min-height: 0;
|
|
121
|
+
display: grid;
|
|
122
|
+
grid-template-rows: auto 1fr;
|
|
123
|
+
border: 1px solid var(--line);
|
|
124
|
+
border-radius: 8px;
|
|
125
|
+
background: var(--panel);
|
|
126
|
+
box-shadow: var(--shadow);
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.pane header {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: space-between;
|
|
134
|
+
gap: 10px;
|
|
135
|
+
padding: 10px 12px;
|
|
136
|
+
border-bottom: 1px solid var(--line);
|
|
137
|
+
background: var(--panel-2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.pane h2 {
|
|
141
|
+
margin: 0;
|
|
142
|
+
font-size: 13px;
|
|
143
|
+
font-weight: 650;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.editor-body {
|
|
147
|
+
display: grid;
|
|
148
|
+
grid-template-rows: auto 1fr;
|
|
149
|
+
min-height: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.controls {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
|
155
|
+
gap: 8px;
|
|
156
|
+
padding: 10px;
|
|
157
|
+
border-bottom: 1px solid var(--line);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.field {
|
|
161
|
+
min-width: 0;
|
|
162
|
+
display: grid;
|
|
163
|
+
gap: 4px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.field.wide {
|
|
167
|
+
grid-column: span 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
label,
|
|
171
|
+
.check span {
|
|
172
|
+
color: var(--muted);
|
|
173
|
+
font-size: 11px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
input,
|
|
177
|
+
select,
|
|
178
|
+
textarea {
|
|
179
|
+
width: 100%;
|
|
180
|
+
border: 1px solid var(--line);
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
background: #ffffff;
|
|
183
|
+
color: var(--text);
|
|
184
|
+
outline-color: var(--accent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
input,
|
|
188
|
+
select {
|
|
189
|
+
height: 34px;
|
|
190
|
+
padding: 0 8px;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
textarea {
|
|
194
|
+
min-height: 0;
|
|
195
|
+
resize: none;
|
|
196
|
+
line-height: 1.45;
|
|
197
|
+
padding: 10px;
|
|
198
|
+
tab-size: 2;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#markdown {
|
|
202
|
+
border: 0;
|
|
203
|
+
border-radius: 0;
|
|
204
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
205
|
+
font-size: 13px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#custom-css {
|
|
209
|
+
min-height: 72px;
|
|
210
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
211
|
+
font-size: 12px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.checks {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
align-items: end;
|
|
218
|
+
gap: 8px 14px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.check {
|
|
222
|
+
display: inline-flex;
|
|
223
|
+
gap: 6px;
|
|
224
|
+
align-items: center;
|
|
225
|
+
min-height: 34px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.check input {
|
|
229
|
+
width: 16px;
|
|
230
|
+
height: 16px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.preview-wrap {
|
|
234
|
+
display: grid;
|
|
235
|
+
grid-template-rows: 1fr minmax(94px, 0.26fr);
|
|
236
|
+
min-height: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#preview {
|
|
240
|
+
width: 100%;
|
|
241
|
+
height: 100%;
|
|
242
|
+
min-height: 320px;
|
|
243
|
+
border: 0;
|
|
244
|
+
background: #ffffff;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.diagnostics {
|
|
248
|
+
border-top: 1px solid var(--line);
|
|
249
|
+
background: #fbfcfd;
|
|
250
|
+
overflow: auto;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.diagnostics h3 {
|
|
254
|
+
margin: 10px 12px 4px;
|
|
255
|
+
color: var(--muted);
|
|
256
|
+
font-size: 11px;
|
|
257
|
+
font-weight: 650;
|
|
258
|
+
text-transform: uppercase;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.diagnostics ul {
|
|
262
|
+
margin: 0;
|
|
263
|
+
padding: 0 12px 12px;
|
|
264
|
+
list-style: none;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.diagnostics li {
|
|
268
|
+
border-left: 3px solid var(--accent);
|
|
269
|
+
margin-top: 7px;
|
|
270
|
+
padding: 4px 8px;
|
|
271
|
+
color: var(--text);
|
|
272
|
+
font-size: 12px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.diagnostics li.error {
|
|
276
|
+
border-left-color: var(--danger);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.empty {
|
|
280
|
+
color: var(--muted);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@media (max-width: 980px) {
|
|
284
|
+
.topbar,
|
|
285
|
+
.workspace {
|
|
286
|
+
grid-template-columns: 1fr;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.actions {
|
|
290
|
+
justify-content: start;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.controls {
|
|
294
|
+
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@media (max-width: 620px) {
|
|
299
|
+
.controls {
|
|
300
|
+
grid-template-columns: 1fr;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.field.wide {
|
|
304
|
+
grid-column: auto;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.actions {
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
</style>
|
|
312
|
+
</head>
|
|
313
|
+
<body>
|
|
314
|
+
<main class="app">
|
|
315
|
+
<div class="topbar">
|
|
316
|
+
<div class="brand">
|
|
317
|
+
<strong>franken_markdown</strong>
|
|
318
|
+
<span id="status" class="status">loading wasm package</span>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="actions">
|
|
321
|
+
<button id="render" class="button primary" type="button">Render</button>
|
|
322
|
+
<button id="download-pdf" class="button secondary" type="button">Download PDF</button>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<section class="workspace" aria-label="Markdown demo workspace">
|
|
327
|
+
<section class="pane" aria-label="Markdown input">
|
|
328
|
+
<header>
|
|
329
|
+
<h2>Markdown</h2>
|
|
330
|
+
<span id="source-size" class="status">0 bytes</span>
|
|
331
|
+
</header>
|
|
332
|
+
<div class="editor-body">
|
|
333
|
+
<div class="controls">
|
|
334
|
+
<label class="field">
|
|
335
|
+
Font
|
|
336
|
+
<select id="font">
|
|
337
|
+
<option value="sans">Sans</option>
|
|
338
|
+
<option value="serif">Serif</option>
|
|
339
|
+
</select>
|
|
340
|
+
</label>
|
|
341
|
+
<label class="field">
|
|
342
|
+
Dark CSS
|
|
343
|
+
<select id="dark-mode">
|
|
344
|
+
<option value="auto">Auto</option>
|
|
345
|
+
<option value="disabled">Disabled</option>
|
|
346
|
+
</select>
|
|
347
|
+
</label>
|
|
348
|
+
<label class="field">
|
|
349
|
+
Title
|
|
350
|
+
<input id="title" value="franken_markdown demo" />
|
|
351
|
+
</label>
|
|
352
|
+
<label class="field">
|
|
353
|
+
Author
|
|
354
|
+
<input id="author" value="FMD" />
|
|
355
|
+
</label>
|
|
356
|
+
<label class="field wide">
|
|
357
|
+
Custom CSS
|
|
358
|
+
<textarea id="custom-css" spellcheck="false" placeholder="body { max-width: 860px; }"></textarea>
|
|
359
|
+
</label>
|
|
360
|
+
<div class="checks">
|
|
361
|
+
<label class="check">
|
|
362
|
+
<input id="allow-html" type="checkbox" />
|
|
363
|
+
<span>Allow raw HTML</span>
|
|
364
|
+
</label>
|
|
365
|
+
<label class="check">
|
|
366
|
+
<input id="line-numbers" type="checkbox" checked />
|
|
367
|
+
<span>PDF line numbers</span>
|
|
368
|
+
</label>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
<textarea id="markdown" spellcheck="false"></textarea>
|
|
372
|
+
</div>
|
|
373
|
+
</section>
|
|
374
|
+
|
|
375
|
+
<section class="pane" aria-label="Rendered output">
|
|
376
|
+
<header>
|
|
377
|
+
<h2>Preview</h2>
|
|
378
|
+
<span id="preview-meta" class="status">waiting</span>
|
|
379
|
+
</header>
|
|
380
|
+
<div class="preview-wrap">
|
|
381
|
+
<iframe id="preview" title="Rendered HTML preview" sandbox=""></iframe>
|
|
382
|
+
<aside class="diagnostics" aria-live="polite">
|
|
383
|
+
<h3>Diagnostics</h3>
|
|
384
|
+
<ul id="diagnostics">
|
|
385
|
+
<li class="empty">No diagnostics yet.</li>
|
|
386
|
+
</ul>
|
|
387
|
+
</aside>
|
|
388
|
+
</div>
|
|
389
|
+
</section>
|
|
390
|
+
</section>
|
|
391
|
+
</main>
|
|
392
|
+
|
|
393
|
+
<script type="module" src="./demo.js"></script>
|
|
394
|
+
</body>
|
|
395
|
+
</html>
|