77
88import { useState } from 'react' ;
99import dynamic from 'next/dynamic' ;
10- import Link from 'next/link' ;
1110import {
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' ;
2017import 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 "Parse & Preview" to see output
271+ Click "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 ? `` : `` ;
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 ( / ( < l i > [ \s \S ] * < \/ l i > ) / , '<ul class="list-disc pl-6 my-2">$1</ul>' ) ;
405+
573406 return html ;
574407}
575408
0 commit comments