@ampless/runtime 0.2.0-alpha.10 → 0.2.0-alpha.11
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/dist/index.d.ts +16 -6
- package/dist/index.js +138 -42
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -159,32 +159,42 @@ declare function renderBody(post: Post): string;
|
|
|
159
159
|
* Convert a tiptap doc to its HTML form. Same renderer the public
|
|
160
160
|
* site uses. Defensive: tiptap accepts an HTML string as initial
|
|
161
161
|
* content and parses it on mount, but won't fire onUpdate until the
|
|
162
|
-
* user edits
|
|
162
|
+
* user edits, so a format-switch chain (e.g. markdown -> tiptap ->
|
|
163
163
|
* markdown without editing) can still hand us a raw HTML string
|
|
164
164
|
* here. In that case, return it as-is rather than walking it as a
|
|
165
165
|
* malformed tiptap node and producing empty output.
|
|
166
166
|
*/
|
|
167
167
|
declare function tiptapToHtml(doc: unknown): string;
|
|
168
|
-
/** Convert markdown to HTML using
|
|
168
|
+
/** Convert markdown to HTML using marked + GFM. */
|
|
169
169
|
declare function markdownToHtml(md: string): string;
|
|
170
170
|
/**
|
|
171
171
|
* Walk a tiptap doc and emit Markdown. Mirrors `renderTiptap` in
|
|
172
172
|
* shape but produces markdown syntax. Loses anything markdown can't
|
|
173
173
|
* express (data attributes, image display modes, custom marks).
|
|
174
174
|
*
|
|
175
|
+
* Notes on info loss:
|
|
176
|
+
* - underline / highlight are not in GFM, so they fall back to the
|
|
177
|
+
* literal `<u>` / `<mark>` HTML tags (preserved as-is across round trips).
|
|
178
|
+
* - paragraph / heading textAlign cannot be expressed in markdown and
|
|
179
|
+
* is therefore lost on conversion.
|
|
180
|
+
*
|
|
175
181
|
* Same defensive path as tiptapToHtml: a string input means tiptap
|
|
176
182
|
* hasn't emitted JSON yet (the body is still the HTML we handed it).
|
|
177
183
|
* Route through htmlToMarkdown so the content survives.
|
|
178
184
|
*/
|
|
179
185
|
declare function tiptapToMarkdown(doc: unknown): string;
|
|
180
186
|
/**
|
|
181
|
-
* Regex-based HTML
|
|
187
|
+
* Regex-based HTML -> Markdown converter. Handles the tag set the
|
|
182
188
|
* editor produces (`<p>` `<h1>`-`<h6>` `<strong>` `<em>` `<a>`
|
|
183
189
|
* `<img>` `<ul>` `<ol>` `<li>` `<code>` `<pre>` `<blockquote>` `<hr>`
|
|
184
|
-
* `<br>`
|
|
185
|
-
*
|
|
190
|
+
* `<br>` `<u>` `<mark>` `<table>` task-list `<ul data-type="taskList">`).
|
|
191
|
+
* Decorative containers like `<div style="text-align:...">` are dropped.
|
|
192
|
+
*
|
|
193
|
+
* Tables are reduced to GFM pipe syntax via convertHtmlTable. Complex
|
|
194
|
+
* nested content inside cells (lists, other tables) is flattened to
|
|
195
|
+
* plain text.
|
|
186
196
|
*
|
|
187
|
-
* Not a full library
|
|
197
|
+
* Not a full library, there are known limits like nested formatting
|
|
188
198
|
* inside list items potentially merging. Acceptable for a v0.x
|
|
189
199
|
* format-switch convenience; complex HTML round-trips shouldn't be
|
|
190
200
|
* relied on.
|
package/dist/index.js
CHANGED
|
@@ -285,9 +285,17 @@ ${lines.join("\n")}
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
// src/rendering.ts
|
|
288
|
+
import { marked } from "marked";
|
|
288
289
|
function escape(s) {
|
|
289
290
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
290
291
|
}
|
|
292
|
+
function textAlignStyle(attrs) {
|
|
293
|
+
const v = attrs?.textAlign;
|
|
294
|
+
if (v === "left" || v === "center" || v === "right" || v === "justify") {
|
|
295
|
+
return ` style="text-align: ${v}"`;
|
|
296
|
+
}
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
291
299
|
function renderTiptap(node) {
|
|
292
300
|
if (node.type === "text") {
|
|
293
301
|
let html = escape(node.text ?? "");
|
|
@@ -296,6 +304,8 @@ function renderTiptap(node) {
|
|
|
296
304
|
else if (mark.type === "italic") html = `<em>${html}</em>`;
|
|
297
305
|
else if (mark.type === "code") html = `<code>${html}</code>`;
|
|
298
306
|
else if (mark.type === "strike") html = `<s>${html}</s>`;
|
|
307
|
+
else if (mark.type === "underline") html = `<u>${html}</u>`;
|
|
308
|
+
else if (mark.type === "highlight") html = `<mark>${html}</mark>`;
|
|
299
309
|
else if (mark.type === "link") {
|
|
300
310
|
const href = escape(String(mark.attrs?.href ?? "#"));
|
|
301
311
|
html = `<a href="${href}" target="_blank" rel="noopener">${html}</a>`;
|
|
@@ -308,10 +318,10 @@ function renderTiptap(node) {
|
|
|
308
318
|
case "doc":
|
|
309
319
|
return children;
|
|
310
320
|
case "paragraph":
|
|
311
|
-
return `<p>${children}</p>`;
|
|
321
|
+
return `<p${textAlignStyle(node.attrs)}>${children}</p>`;
|
|
312
322
|
case "heading": {
|
|
313
323
|
const level = Number(node.attrs?.level ?? 1);
|
|
314
|
-
return `<h${level}>${children}</h${level}>`;
|
|
324
|
+
return `<h${level}${textAlignStyle(node.attrs)}>${children}</h${level}>`;
|
|
315
325
|
}
|
|
316
326
|
case "bulletList":
|
|
317
327
|
return `<ul>${children}</ul>`;
|
|
@@ -336,52 +346,39 @@ function renderTiptap(node) {
|
|
|
336
346
|
const display = node.attrs?.display ? ` data-display="${escape(String(node.attrs.display))}"` : "";
|
|
337
347
|
return `<img src="${src}" alt="${alt}"${title}${display} loading="lazy" />`;
|
|
338
348
|
}
|
|
349
|
+
case "table":
|
|
350
|
+
return `<table class="tiptap-table"><tbody>${children}</tbody></table>`;
|
|
351
|
+
case "tableRow":
|
|
352
|
+
return `<tr>${children}</tr>`;
|
|
353
|
+
case "tableHeader":
|
|
354
|
+
return `<th${tableCellAttrs(node.attrs)}>${children}</th>`;
|
|
355
|
+
case "tableCell":
|
|
356
|
+
return `<td${tableCellAttrs(node.attrs)}>${children}</td>`;
|
|
357
|
+
case "taskList":
|
|
358
|
+
return `<ul data-type="taskList">${children}</ul>`;
|
|
359
|
+
case "taskItem": {
|
|
360
|
+
const checked = node.attrs?.checked === true ? "true" : "false";
|
|
361
|
+
return `<li data-type="taskItem" data-checked="${checked}">${children}</li>`;
|
|
362
|
+
}
|
|
339
363
|
default:
|
|
340
364
|
return children;
|
|
341
365
|
}
|
|
342
366
|
}
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
out.push(`<h2>${escape(line.slice(3))}</h2>`);
|
|
354
|
-
i++;
|
|
355
|
-
} else if (line.startsWith("```")) {
|
|
356
|
-
const lang = line.slice(3).trim();
|
|
357
|
-
const code = [];
|
|
358
|
-
i++;
|
|
359
|
-
while (i < lines.length && !(lines[i] ?? "").startsWith("```")) {
|
|
360
|
-
code.push(lines[i] ?? "");
|
|
361
|
-
i++;
|
|
362
|
-
}
|
|
363
|
-
i++;
|
|
364
|
-
out.push(
|
|
365
|
-
`<pre><code${lang ? ` class="language-${escape(lang)}"` : ""}>${escape(code.join("\n"))}</code></pre>`
|
|
366
|
-
);
|
|
367
|
-
} else if (line.startsWith("- ")) {
|
|
368
|
-
const items = [];
|
|
369
|
-
while (i < lines.length && (lines[i] ?? "").startsWith("- ")) {
|
|
370
|
-
items.push(`<li>${renderInlineMarkdown((lines[i] ?? "").slice(2))}</li>`);
|
|
371
|
-
i++;
|
|
372
|
-
}
|
|
373
|
-
out.push(`<ul>${items.join("")}</ul>`);
|
|
374
|
-
} else if (line.trim() === "") {
|
|
375
|
-
i++;
|
|
376
|
-
} else {
|
|
377
|
-
out.push(`<p>${renderInlineMarkdown(line)}</p>`);
|
|
378
|
-
i++;
|
|
379
|
-
}
|
|
367
|
+
function tableCellAttrs(attrs) {
|
|
368
|
+
let out = "";
|
|
369
|
+
const colspan = Number(attrs?.colspan ?? 1);
|
|
370
|
+
if (colspan > 1) out += ` colspan="${colspan}"`;
|
|
371
|
+
const rowspan = Number(attrs?.rowspan ?? 1);
|
|
372
|
+
if (rowspan > 1) out += ` rowspan="${rowspan}"`;
|
|
373
|
+
const colwidth = attrs?.colwidth;
|
|
374
|
+
if (Array.isArray(colwidth) && colwidth.length > 0) {
|
|
375
|
+
const w = Number(colwidth[0]);
|
|
376
|
+
if (Number.isFinite(w) && w > 0) out += ` style="width: ${w}px"`;
|
|
380
377
|
}
|
|
381
|
-
return out
|
|
378
|
+
return out;
|
|
382
379
|
}
|
|
383
|
-
function
|
|
384
|
-
return
|
|
380
|
+
function renderMarkdown(md) {
|
|
381
|
+
return marked.parse(md, { gfm: true, breaks: false, async: false });
|
|
385
382
|
}
|
|
386
383
|
function renderBody(post) {
|
|
387
384
|
if (post.format === "html") return String(post.body);
|
|
@@ -412,6 +409,8 @@ function tiptapNodeToMarkdown(node) {
|
|
|
412
409
|
else if (mark.type === "italic") txt = `*${txt}*`;
|
|
413
410
|
else if (mark.type === "code") txt = `\`${txt}\``;
|
|
414
411
|
else if (mark.type === "strike") txt = `~~${txt}~~`;
|
|
412
|
+
else if (mark.type === "underline") txt = `<u>${txt}</u>`;
|
|
413
|
+
else if (mark.type === "highlight") txt = `<mark>${txt}</mark>`;
|
|
415
414
|
else if (mark.type === "link") txt = `[${txt}](${String(mark.attrs?.href ?? "#")})`;
|
|
416
415
|
}
|
|
417
416
|
return txt;
|
|
@@ -449,12 +448,54 @@ function tiptapNodeToMarkdown(node) {
|
|
|
449
448
|
const alt = String(node.attrs?.alt ?? "");
|
|
450
449
|
return ``;
|
|
451
450
|
}
|
|
451
|
+
case "table":
|
|
452
|
+
return tiptapTableToMarkdown(node);
|
|
453
|
+
case "taskList":
|
|
454
|
+
return children + "\n";
|
|
455
|
+
case "taskItem": {
|
|
456
|
+
const checked = node.attrs?.checked === true ? "x" : " ";
|
|
457
|
+
const inner = children.replace(/\n+$/, "");
|
|
458
|
+
const [first, ...rest] = inner.split("\n");
|
|
459
|
+
const cont = rest.map((l) => l ? " " + l : l).join("\n");
|
|
460
|
+
return `- [${checked}] ${first ?? ""}${cont ? "\n" + cont : ""}
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
452
463
|
default:
|
|
453
464
|
return children;
|
|
454
465
|
}
|
|
455
466
|
}
|
|
467
|
+
function tiptapTableToMarkdown(node) {
|
|
468
|
+
const rows = node.content ?? [];
|
|
469
|
+
if (rows.length === 0) return "";
|
|
470
|
+
const renderedRows = [];
|
|
471
|
+
let headerIdx = -1;
|
|
472
|
+
for (let i = 0; i < rows.length; i++) {
|
|
473
|
+
const row = rows[i];
|
|
474
|
+
const cells = row.content ?? [];
|
|
475
|
+
const cellTexts = cells.map((c) => {
|
|
476
|
+
const inner = (c.content ?? []).map(tiptapNodeToMarkdown).join("");
|
|
477
|
+
return inner.replace(/\n+$/, "").replace(/\n/g, "<br>").replace(/\|/g, "\\|");
|
|
478
|
+
});
|
|
479
|
+
renderedRows.push(cellTexts);
|
|
480
|
+
if (headerIdx === -1 && cells.some((c) => c.type === "tableHeader")) headerIdx = i;
|
|
481
|
+
}
|
|
482
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
483
|
+
const header = renderedRows[headerIdx] ?? [];
|
|
484
|
+
const body = renderedRows.filter((_, i) => i !== headerIdx);
|
|
485
|
+
const cols = header.length;
|
|
486
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
487
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
488
|
+
const bodyLines = body.map((r) => {
|
|
489
|
+
const cells = Array.from({ length: cols }, (_, i) => r[i] ?? "");
|
|
490
|
+
return "| " + cells.join(" | ") + " |";
|
|
491
|
+
});
|
|
492
|
+
return "\n" + [headerLine, sepLine, ...bodyLines].join("\n") + "\n\n";
|
|
493
|
+
}
|
|
456
494
|
function htmlToMarkdown(html) {
|
|
457
495
|
let md = html;
|
|
496
|
+
md = md.replace(/<table[^>]*>([\s\S]*?)<\/table>/gi, (_, inner) => {
|
|
497
|
+
return "\n" + convertHtmlTable(String(inner)) + "\n";
|
|
498
|
+
});
|
|
458
499
|
md = md.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_, level, text) => {
|
|
459
500
|
return "\n" + "#".repeat(Number(level)) + " " + String(text).trim() + "\n\n";
|
|
460
501
|
});
|
|
@@ -467,6 +508,9 @@ function htmlToMarkdown(html) {
|
|
|
467
508
|
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, content) => {
|
|
468
509
|
return "\n" + String(content).trim().split("\n").map((l) => "> " + l).join("\n") + "\n\n";
|
|
469
510
|
});
|
|
511
|
+
md = md.replace(/<ul[^>]*data-type="taskList"[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
512
|
+
return "\n" + convertHtmlTaskList(String(items)) + "\n";
|
|
513
|
+
});
|
|
470
514
|
md = md.replace(/<ul[^>]*>([\s\S]*?)<\/ul>/gi, (_, items) => {
|
|
471
515
|
return "\n" + String(items).replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n") + "\n";
|
|
472
516
|
});
|
|
@@ -486,11 +530,63 @@ function htmlToMarkdown(html) {
|
|
|
486
530
|
md = md.replace(/<(em|i)>([\s\S]*?)<\/\1>/gi, "*$2*");
|
|
487
531
|
md = md.replace(/<s>([\s\S]*?)<\/s>/gi, "~~$1~~");
|
|
488
532
|
md = md.replace(/<code>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
533
|
+
const PH_U_OPEN = "AMP_U_OPEN";
|
|
534
|
+
const PH_U_CLOSE = "AMP_U_CLOSE";
|
|
535
|
+
const PH_MARK_OPEN = "AMP_MARK_OPEN";
|
|
536
|
+
const PH_MARK_CLOSE = "AMP_MARK_CLOSE";
|
|
537
|
+
md = md.replace(/<u>([\s\S]*?)<\/u>/gi, `${PH_U_OPEN}$1${PH_U_CLOSE}`);
|
|
538
|
+
md = md.replace(/<mark>([\s\S]*?)<\/mark>/gi, `${PH_MARK_OPEN}$1${PH_MARK_CLOSE}`);
|
|
489
539
|
md = md.replace(/<\/?[^>]+>/g, "");
|
|
540
|
+
md = md.split(PH_U_OPEN).join("<u>").split(PH_U_CLOSE).join("</u>").split(PH_MARK_OPEN).join("<mark>").split(PH_MARK_CLOSE).join("</mark>");
|
|
490
541
|
md = md.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ");
|
|
491
542
|
md = md.replace(/\n{3,}/g, "\n\n");
|
|
492
543
|
return md.trim() + "\n";
|
|
493
544
|
}
|
|
545
|
+
function convertHtmlTable(inner) {
|
|
546
|
+
const stripped = inner.replace(/<\/?(thead|tbody)[^>]*>/gi, "");
|
|
547
|
+
const rowRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
|
|
548
|
+
const rows = [];
|
|
549
|
+
let m;
|
|
550
|
+
while ((m = rowRe.exec(stripped)) !== null) {
|
|
551
|
+
const rowHtml = m[1] ?? "";
|
|
552
|
+
const cellRe = /<(th|td)[^>]*>([\s\S]*?)<\/\1>/gi;
|
|
553
|
+
const cells = [];
|
|
554
|
+
let isHeader = false;
|
|
555
|
+
let cm;
|
|
556
|
+
while ((cm = cellRe.exec(rowHtml)) !== null) {
|
|
557
|
+
if ((cm[1] ?? "").toLowerCase() === "th") isHeader = true;
|
|
558
|
+
cells.push(normalizeTableCell(cm[2] ?? ""));
|
|
559
|
+
}
|
|
560
|
+
if (cells.length > 0) rows.push({ isHeader, cells });
|
|
561
|
+
}
|
|
562
|
+
if (rows.length === 0) return "";
|
|
563
|
+
let headerIdx = rows.findIndex((r) => r.isHeader);
|
|
564
|
+
if (headerIdx === -1) headerIdx = 0;
|
|
565
|
+
const header = rows[headerIdx].cells;
|
|
566
|
+
const body = rows.filter((_, i) => i !== headerIdx).map((r) => r.cells);
|
|
567
|
+
const cols = header.length;
|
|
568
|
+
const headerLine = "| " + header.join(" | ") + " |";
|
|
569
|
+
const sepLine = "| " + Array.from({ length: cols }, () => "---").join(" | ") + " |";
|
|
570
|
+
const bodyLines = body.map((cells) => {
|
|
571
|
+
const padded = Array.from({ length: cols }, (_, i) => cells[i] ?? "");
|
|
572
|
+
return "| " + padded.join(" | ") + " |";
|
|
573
|
+
});
|
|
574
|
+
return [headerLine, sepLine, ...bodyLines].join("\n") + "\n";
|
|
575
|
+
}
|
|
576
|
+
function normalizeTableCell(html) {
|
|
577
|
+
return html.replace(/<br\s*\/?>/gi, " ").replace(/<\/?[^>]+>/g, "").replace(/\|/g, "\\|").replace(/\s+/g, " ").trim();
|
|
578
|
+
}
|
|
579
|
+
function convertHtmlTaskList(items) {
|
|
580
|
+
const liRe = /<li[^>]*data-checked="(true|false)"[^>]*>([\s\S]*?)<\/li>/gi;
|
|
581
|
+
const out = [];
|
|
582
|
+
let m;
|
|
583
|
+
while ((m = liRe.exec(items)) !== null) {
|
|
584
|
+
const checked = m[1] === "true" ? "x" : " ";
|
|
585
|
+
const inner = String(m[2] ?? "").replace(/<\/?p[^>]*>/gi, "").trim();
|
|
586
|
+
out.push(`- [${checked}] ${inner}`);
|
|
587
|
+
}
|
|
588
|
+
return out.join("\n");
|
|
589
|
+
}
|
|
494
590
|
|
|
495
591
|
// src/index.ts
|
|
496
592
|
function createAmpless(opts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.11",
|
|
4
4
|
"description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"class-variance-authority": "^0.7.1",
|
|
48
48
|
"clsx": "^2.1.1",
|
|
49
49
|
"lucide-react": "^1.16.0",
|
|
50
|
+
"marked": "^14.1.4",
|
|
50
51
|
"tailwind-merge": "^3.6.0",
|
|
51
52
|
"ampless": "0.2.0-alpha.7",
|
|
52
53
|
"@ampless/plugin-og-image": "0.2.0-alpha.7"
|