Skip to content

Commit a93113f

Browse files
committed
feat: enable OSF API backend and playground exports
1 parent 51a43eb commit a93113f

2 files changed

Lines changed: 68 additions & 233 deletions

File tree

app/playground/page.tsx

Lines changed: 64 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77

88
import { useState } from 'react';
99
import dynamic from 'next/dynamic';
10-
import Link from 'next/link';
1110
import {
1211
parse,
1312
type ContentBlock,
13+
type MetaBlock,
1414
type OSFDocument,
1515
type TextRun,
16-
type Image as OSFImage,
17-
type Link as OSFLink,
18-
type MetaBlock
1916
} from 'omniscript-parser';
2017
import Navigation from '@/components/Navigation';
2118

@@ -93,9 +90,9 @@ export default function PlaygroundPage() {
9390
} else if (output === 'preview') {
9491
setResult(generatePreviewHTML(doc));
9592
}
96-
} catch (error: unknown) {
93+
} catch (error) {
94+
const message = error instanceof Error ? error.message : 'Failed to parse document';
9795
setOutput('errors');
98-
const message = error instanceof Error ? error.message : 'Unexpected error';
9996
setResult(message);
10097
}
10198
};
@@ -134,8 +131,8 @@ export default function PlaygroundPage() {
134131
a.click();
135132
window.URL.revokeObjectURL(url);
136133
document.body.removeChild(a);
137-
} catch (error: unknown) {
138-
const message = error instanceof Error ? error.message : 'Unexpected error';
134+
} catch (error) {
135+
const message = error instanceof Error ? error.message : 'Export failed';
139136
alert(`Export error: ${message}`);
140137
} finally {
141138
setIsExporting(false);
@@ -167,12 +164,9 @@ export default function PlaygroundPage() {
167164
<pre className="font-mono text-xs bg-black text-green-400 p-2">
168165
{apiBase || '/api/convert/{format}'}
169166
</pre>
170-
<Link
171-
href="/docs/getting-started/installation"
172-
className="font-mono text-xs text-blue-600 hover:underline font-bold"
173-
>
167+
<a href="/docs/getting-started/installation" className="font-mono text-xs text-blue-600 hover:underline font-bold">
174168
→ CLI still supported for batch exports
175-
</Link>
169+
</a>
176170
</div>
177171
</div>
178172

@@ -210,7 +204,12 @@ export default function PlaygroundPage() {
210204

211205
<select
212206
value={output}
213-
onChange={(e) => setOutput(e.target.value as 'preview' | 'ast' | 'errors')}
207+
onChange={(e) => {
208+
const nextValue = e.target.value;
209+
if (nextValue === 'preview' || nextValue === 'ast' || nextValue === 'errors') {
210+
setOutput(nextValue);
211+
}
212+
}}
214213
className="px-4 py-2 bg-white text-black border-2 border-black"
215214
>
216215
<option value="preview">HTML Preview</option>
@@ -269,7 +268,7 @@ export default function PlaygroundPage() {
269268
)}
270269
{!result && (
271270
<div className="text-gray-400 text-center py-20 font-mono">
272-
Click &quot;Parse &amp; Preview&quot; to see output
271+
Click &quot;Parse & Preview&quot; to see output
273272
</div>
274273
)}
275274
</div>
@@ -286,18 +285,20 @@ function generatePreviewHTML(doc: OSFDocument): string {
286285
const metaBlock = doc.blocks.find((block): block is MetaBlock => block.type === 'meta');
287286
if (metaBlock) {
288287
html += '<div class="mb-8 pb-4 border-b-2 border-gray-700">';
289-
html += `<h1 class="text-4xl font-bold mb-2">${metaBlock.props.title || 'Untitled'}</h1>`;
288+
html += `<h1 class="text-4xl font-bold mb-2">${escapeHTML(
289+
String(metaBlock.props.title || 'Untitled')
290+
)}</h1>`;
290291
if (metaBlock.props.author) {
291-
html += `<p class="text-gray-400">By ${metaBlock.props.author}</p>`;
292+
html += `<p class="text-gray-400">By ${escapeHTML(String(metaBlock.props.author))}</p>`;
292293
}
293294
if (metaBlock.props.date) {
294-
html += `<p class="text-gray-400">${metaBlock.props.date}</p>`;
295+
html += `<p class="text-gray-400">${escapeHTML(String(metaBlock.props.date))}</p>`;
295296
}
296297
html += '</div>';
297298
}
298299

299300
// Render other blocks
300-
doc.blocks.forEach((block) => {
301+
doc.blocks.forEach(block => {
301302
switch (block.type) {
302303
case 'doc':
303304
html += '<div class="mb-8 prose prose-invert max-w-none">';
@@ -309,11 +310,7 @@ function generatePreviewHTML(doc: OSFDocument): string {
309310
html += '<div class="mb-8 p-6 border-2 border-blue-500 bg-blue-900 bg-opacity-20 rounded">';
310311
html += `<h2 class="text-2xl font-bold mb-4">${block.title || 'Slide'}</h2>`;
311312
if (block.content && block.content.length > 0) {
312-
html += renderSlideContentHTML(block.content);
313-
} else if (block.bullets && block.bullets.length > 0) {
314-
html += '<ul class="list-disc pl-6 my-2">';
315-
html += block.bullets.map((item) => `<li>${escapeHTML(item)}</li>`).join('');
316-
html += '</ul>';
313+
html += convertMarkdownToHTML(contentToMarkdown(block.content));
317314
}
318315
html += '</div>';
319316
break;
@@ -339,33 +336,6 @@ function generatePreviewHTML(doc: OSFDocument): string {
339336
html += `<pre class="text-sm bg-black bg-opacity-50 p-4 rounded overflow-x-auto">${block.code}</pre>`;
340337
html += '</div>';
341338
break;
342-
343-
case 'table':
344-
html += '<div class="mb-8 overflow-x-auto">';
345-
if (block.caption) {
346-
html += `<p class="text-sm text-gray-400 italic mb-2">${block.caption}</p>`;
347-
}
348-
html += '<table class="min-w-full border border-gray-700 text-sm">';
349-
html += '<thead class="bg-gray-800"><tr>';
350-
block.headers.forEach((header) => {
351-
html += `<th class="px-3 py-2 text-left font-semibold border border-gray-700">${escapeHTML(
352-
header
353-
)}</th>`;
354-
});
355-
html += '</tr></thead><tbody>';
356-
block.rows.forEach((row, rowIndex) => {
357-
const rowClass =
358-
block.style === 'striped' && rowIndex % 2 === 1 ? ' bg-gray-800/60' : '';
359-
html += `<tr class="${rowClass}">`;
360-
row.cells.forEach((cell) => {
361-
html += `<td class="px-3 py-2 border border-gray-700">${escapeHTML(
362-
cell.text
363-
)}</td>`;
364-
});
365-
html += '</tr>';
366-
});
367-
html += '</tbody></table></div>';
368-
break;
369339

370340
case 'osfcode':
371341
html += '<div class="mb-8">';
@@ -375,201 +345,64 @@ function generatePreviewHTML(doc: OSFDocument): string {
375345
html += `<pre class="bg-gray-800 p-4 rounded overflow-x-auto text-sm"><code class="language-${block.language}">${escapeHTML(block.code)}</code></pre>`;
376346
html += '</div>';
377347
break;
378-
379-
default:
380-
break;
381348
}
382349
});
383350

384351
return html;
385352
}
386353

387-
function convertMarkdownToHTML(text: string): string {
388-
const lines = text.split(/\r?\n/);
389-
let html = '';
390-
let paragraph: string[] = [];
391-
let listItems: string[] = [];
392-
let blockquoteLines: string[] = [];
393-
394-
const flushParagraph = () => {
395-
if (paragraph.length > 0) {
396-
html += `<p class="my-2">${renderInlineMarkdown(paragraph.join(' '))}</p>`;
397-
paragraph = [];
398-
}
399-
};
400-
401-
const flushList = () => {
402-
if (listItems.length > 0) {
403-
html += `<ul class="list-disc pl-6 my-2">${listItems
404-
.map((item) => `<li>${renderInlineMarkdown(item)}</li>`)
405-
.join('')}</ul>`;
406-
listItems = [];
407-
}
408-
};
409-
410-
const flushBlockquote = () => {
411-
if (blockquoteLines.length > 0) {
412-
html += `<blockquote class="border-l-4 border-gray-600 pl-4 italic text-gray-300 my-3">${blockquoteLines
413-
.map((line) => `<p>${renderInlineMarkdown(line)}</p>`)
414-
.join('')}</blockquote>`;
415-
blockquoteLines = [];
416-
}
417-
};
418-
419-
for (const rawLine of lines) {
420-
const line = rawLine.trim();
421-
if (!line) {
422-
flushParagraph();
423-
flushList();
424-
flushBlockquote();
425-
continue;
426-
}
427-
428-
const headingMatch = /^(#{1,3})\s+(.+)$/.exec(line);
429-
if (headingMatch) {
430-
flushParagraph();
431-
flushList();
432-
flushBlockquote();
433-
const level = headingMatch[1].length;
434-
const size =
435-
level === 1 ? 'text-3xl' : level === 2 ? 'text-2xl' : 'text-xl';
436-
html += `<h${level} class="${size} font-bold mt-6 mb-3">${renderInlineMarkdown(
437-
headingMatch[2]
438-
)}</h${level}>`;
439-
continue;
440-
}
441-
442-
const listMatch = /^[-*]\s+(.+)$/.exec(line);
443-
if (listMatch) {
444-
flushParagraph();
445-
flushBlockquote();
446-
listItems.push(listMatch[1]);
447-
continue;
448-
}
449-
450-
const quoteMatch = /^>\s?(.+)$/.exec(line);
451-
if (quoteMatch) {
452-
flushParagraph();
453-
flushList();
454-
blockquoteLines.push(quoteMatch[1]);
455-
continue;
456-
}
457-
458-
if (listItems.length > 0) {
459-
flushList();
460-
}
461-
if (blockquoteLines.length > 0) {
462-
flushBlockquote();
463-
}
464-
paragraph.push(line);
465-
}
466-
467-
flushParagraph();
468-
flushList();
469-
flushBlockquote();
470-
471-
return html;
472-
}
473-
474-
function renderSlideContentHTML(contentBlocks: ContentBlock[]): string {
475-
let html = '';
476-
477-
for (const block of contentBlocks) {
478-
if (block.type === 'unordered_list') {
479-
html += '<ul class="list-disc pl-6 my-2">';
480-
for (const item of block.items) {
481-
html += `<li>${renderRuns(item.content)}</li>`;
482-
}
483-
html += '</ul>';
484-
} else if (block.type === 'ordered_list') {
485-
html += '<ol class="list-decimal pl-6 my-2">';
486-
for (const item of block.items) {
487-
html += `<li>${renderRuns(item.content)}</li>`;
488-
}
489-
html += '</ol>';
490-
} else if (block.type === 'blockquote') {
491-
html += '<blockquote class="border-l-4 border-blue-500 pl-4 italic text-gray-300 my-3">';
492-
for (const paragraph of block.content) {
493-
html += `<p>${renderRuns(paragraph.content)}</p>`;
494-
}
495-
html += '</blockquote>';
496-
} else if (block.type === 'paragraph') {
497-
const rawText = runsToText(block.content).trim();
498-
const headingMatch = /^(#{1,3})\s+(.+)$/.exec(rawText);
499-
if (headingMatch) {
500-
const level = headingMatch[1].length;
501-
const size =
502-
level === 1 ? 'text-3xl' : level === 2 ? 'text-2xl' : 'text-xl';
503-
html += `<h${level} class="${size} font-bold mt-4 mb-2">${renderInlineMarkdown(
504-
headingMatch[2]
505-
)}</h${level}>`;
506-
} else {
507-
html += `<p class="my-2">${renderRuns(block.content)}</p>`;
508-
}
509-
} else if (block.type === 'code') {
510-
html += `<pre class="bg-gray-800 p-4 rounded overflow-x-auto text-sm">${escapeHTML(
511-
block.content
512-
)}</pre>`;
513-
} else if (block.type === 'image') {
514-
html += `<img class="inline-block max-h-64" src="${escapeHTML(
515-
block.url
516-
)}" alt="${escapeHTML(block.alt)}" />`;
517-
}
518-
}
519-
520-
return html;
521-
}
522-
523-
function renderRuns(runs: TextRun[]): string {
524-
return runs
525-
.map((run) => {
526-
if (typeof run === 'string') {
527-
return escapeHTML(run);
528-
}
529-
if (isLinkRun(run)) {
530-
const text = escapeHTML(run.text || '');
531-
const url = escapeHTML(run.url || '#');
532-
return `<a class="underline text-blue-400" href="${url}" target="_blank" rel="noopener noreferrer">${text}</a>`;
533-
}
534-
if (isImageRun(run)) {
535-
const alt = escapeHTML(run.alt || '');
536-
const url = escapeHTML(run.url || '');
537-
return `<img class="inline-block max-h-32" src="${url}" alt="${alt}" />`;
354+
function contentToMarkdown(content: ContentBlock[]): string {
355+
return content
356+
.map(block => {
357+
switch (block.type) {
358+
case 'paragraph':
359+
return block.content.map(extractText).join('');
360+
case 'unordered_list':
361+
return block.items.map(item => `- ${item.content.map(extractText).join('')}`).join('\n');
362+
case 'ordered_list':
363+
return block.items
364+
.map((item, index) => `${index + 1}. ${item.content.map(extractText).join('')}`)
365+
.join('\n');
366+
case 'blockquote':
367+
return block.content
368+
.map(paragraph => `> ${paragraph.content.map(extractText).join('')}`)
369+
.join('\n');
370+
case 'code':
371+
return `\`\`\`\n${block.content}\n\`\`\``;
372+
case 'image':
373+
return block.alt ? `![${block.alt}](${block.url})` : `![](${block.url})`;
374+
default:
375+
return '';
538376
}
539-
let content = escapeHTML(run.text || '');
540-
if (run.bold) content = `<strong>${content}</strong>`;
541-
if (run.italic) content = `<em>${content}</em>`;
542-
if (run.underline) content = `<span class="underline">${content}</span>`;
543-
if (run.strike) content = `<span class="line-through">${content}</span>`;
544-
return content;
545377
})
546-
.join('');
378+
.filter(Boolean)
379+
.join('\n');
547380
}
548381

549-
function runsToText(runs: TextRun[]): string {
550-
return runs
551-
.map((run) => {
552-
if (typeof run === 'string') return run;
553-
if (isLinkRun(run)) return run.text || '';
554-
if (isImageRun(run)) return run.alt || '';
555-
return run.text || '';
556-
})
557-
.join('');
558-
}
559-
560-
function isLinkRun(run: TextRun): run is OSFLink {
561-
return typeof run === 'object' && run !== null && 'type' in run && run.type === 'link';
562-
}
563-
564-
function isImageRun(run: TextRun): run is OSFImage {
565-
return typeof run === 'object' && run !== null && 'type' in run && run.type === 'image';
382+
function extractText(run: TextRun): string {
383+
if (typeof run === 'string') return run;
384+
if ('type' in run) {
385+
if (run.type === 'link') return run.text;
386+
if (run.type === 'image') return run.alt || '';
387+
}
388+
if ('text' in run) return run.text;
389+
return '';
566390
}
567391

568-
function renderInlineMarkdown(text: string): string {
569-
let html = escapeHTML(text);
392+
function convertMarkdownToHTML(text: string): string {
393+
let html = text;
394+
395+
html = html.replace(/^### (.+)$/gm, '<h3 class="text-xl font-bold mt-4 mb-2">$1</h3>');
396+
html = html.replace(/^## (.+)$/gm, '<h2 class="text-2xl font-bold mt-6 mb-3">$1</h2>');
397+
html = html.replace(/^# (.+)$/gm, '<h1 class="text-3xl font-bold mt-8 mb-4">$1</h1>');
398+
570399
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
571400
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
572401
html = html.replace(/`(.+?)`/g, '<code class="bg-gray-800 px-1 rounded">$1</code>');
402+
403+
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
404+
html = html.replace(/(<li>[\s\S]*<\/li>)/, '<ul class="list-disc pl-6 my-2">$1</ul>');
405+
573406
return html;
574407
}
575408

0 commit comments

Comments
 (0)