11'use client' ;
22
3+ import { useMemo , useState } from 'react' ;
34import Link from 'next/link' ;
5+ import {
6+ parse ,
7+ type ContentBlock ,
8+ type Image as OSFImage ,
9+ type Link as OSFLink ,
10+ type MetaBlock ,
11+ type OSFDocument ,
12+ type TextRun
13+ } from 'omniscript-parser' ;
414
515import Terminal from '@/components/Terminal'
616import CodeBlock from '@/components/CodeBlock'
7- import { FileText , Robot , ArrowsClockwise , ChartBar , PaintBrush , Lightning , FilePdf , PresentationChart , FileXls } from 'phosphor-react' ;
17+ import { FileText , Robot , ArrowsClockwise , ChartBar , PaintBrush , Lightning , FilePdf , PresentationChart , FileXls , PuzzlePiece } from 'phosphor-react' ;
818
919export default function Home ( ) {
1020 const exampleOSF = `@meta {
@@ -50,6 +60,17 @@ export default function Home() {
5060
5161@include { path: "./sections/financial-details.osf"; }`
5262
63+ const [ exampleView , setExampleView ] = useState < 'editor' | 'preview' > ( 'editor' ) ;
64+ const examplePreview = useMemo ( ( ) => {
65+ try {
66+ const document = parse ( exampleOSF ) ;
67+ return generatePreviewHTML ( document ) ;
68+ } catch ( error ) {
69+ const message = error instanceof Error ? error . message : 'Preview error' ;
70+ return `<div class="text-red-600 font-mono">Preview error: ${ escapeHTML ( message ) } </div>` ;
71+ }
72+ } , [ exampleOSF ] ) ;
73+
5374 return (
5475 < div className = "min-h-screen" >
5576 { /* Navigation */ }
@@ -188,6 +209,34 @@ export default function Home() {
188209 </ div >
189210 </ section >
190211
212+ { /* Claude Code Plugin */ }
213+ < section className = "py-20 bg-noir-white" >
214+ < div className = "container-noir" >
215+ < div className = "border-2 border-noir-black p-8 bg-yellow-50" >
216+ < div className = "flex flex-col md:flex-row md:items-center md:justify-between gap-6" >
217+ < div >
218+ < div className = "flex items-center gap-3 mb-3" >
219+ < PuzzlePiece size = { 32 } weight = "duotone" />
220+ < h3 className = "font-mono text-2xl font-bold" > Claude Code Plugin</ h3 >
221+ </ div >
222+ < p className = "font-mono text-body-sm text-gray-700 max-w-2xl" >
223+ Install the OmniScript Claude Code plugin to generate, lint, and export OSF directly
224+ inside Claude. Perfect for rapid document workflows and agent automation.
225+ </ p >
226+ </ div >
227+ < a
228+ href = "https://github.com/OmniScriptOSF/omniscript-claude-plugin"
229+ className = "btn-secondary"
230+ target = "_blank"
231+ rel = "noopener noreferrer"
232+ >
233+ View Plugin →
234+ </ a >
235+ </ div >
236+ </ div >
237+ </ div >
238+ </ section >
239+
191240 { /* Example Section */ }
192241 < section className = "py-24 bg-noir-white" >
193242 < div className = "container-noir" >
@@ -199,11 +248,48 @@ export default function Home() {
199248 </ p >
200249
201250 < div className = "max-w-4xl mx-auto" >
202- < CodeBlock
203- code = { exampleOSF }
204- title = "business-report.osf"
205- showLineNumbers = { true }
206- />
251+ < div className = "flex items-center justify-between mb-4" >
252+ < div className = "font-mono text-sm text-gray-500" > Example OSF</ div >
253+ < div className = "inline-flex border-2 border-black bg-white" >
254+ < button
255+ type = "button"
256+ onClick = { ( ) => setExampleView ( 'editor' ) }
257+ className = { `px-4 py-2 font-mono text-xs uppercase tracking-wide ${
258+ exampleView === 'editor'
259+ ? 'bg-black text-white'
260+ : 'bg-white text-black hover:bg-gray-100'
261+ } `}
262+ >
263+ Editor
264+ </ button >
265+ < button
266+ type = "button"
267+ onClick = { ( ) => setExampleView ( 'preview' ) }
268+ className = { `px-4 py-2 font-mono text-xs uppercase tracking-wide ${
269+ exampleView === 'preview'
270+ ? 'bg-black text-white'
271+ : 'bg-white text-black hover:bg-gray-100'
272+ } `}
273+ >
274+ Preview
275+ </ button >
276+ </ div >
277+ </ div >
278+
279+ { exampleView === 'editor' ? (
280+ < CodeBlock
281+ code = { exampleOSF }
282+ title = "business-report.osf"
283+ showLineNumbers = { true }
284+ />
285+ ) : (
286+ < div className = "border-2 border-black bg-white p-6" >
287+ < div
288+ className = "prose max-w-none"
289+ dangerouslySetInnerHTML = { { __html : examplePreview } }
290+ />
291+ </ div >
292+ ) }
207293
208294 < div className = "mt-12 grid md:grid-cols-3 gap-4" >
209295 < div className = "card text-center" >
@@ -329,3 +415,260 @@ export default function Home() {
329415 </ div >
330416 )
331417}
418+
419+ function generatePreviewHTML ( doc : OSFDocument ) : string {
420+ let html = '' ;
421+ const metaBlock = doc . blocks . find ( ( block ) : block is MetaBlock => block . type === 'meta' ) ;
422+
423+ if ( metaBlock ) {
424+ html += '<div class="mb-6 pb-4 border-b-2 border-gray-200">' ;
425+ html += `<h1 class="text-3xl font-bold mb-2">${ escapeHTML ( String ( metaBlock . props . title || 'Untitled' ) ) } </h1>` ;
426+ if ( metaBlock . props . author ) {
427+ html += `<p class="text-gray-500">By ${ escapeHTML ( String ( metaBlock . props . author ) ) } </p>` ;
428+ }
429+ if ( metaBlock . props . date ) {
430+ html += `<p class="text-gray-500">${ escapeHTML ( String ( metaBlock . props . date ) ) } </p>` ;
431+ }
432+ html += '</div>' ;
433+ }
434+
435+ for ( const block of doc . blocks ) {
436+ switch ( block . type ) {
437+ case 'doc' :
438+ html += '<div class="mb-6">' ;
439+ html += convertMarkdownToHTML ( block . content ) ;
440+ html += '</div>' ;
441+ break ;
442+ case 'slide' :
443+ html += '<div class="mb-6 p-4 border-2 border-gray-200 bg-gray-50">' ;
444+ html += `<h2 class="text-xl font-bold mb-3">${ escapeHTML ( block . title || 'Slide' ) } </h2>` ;
445+ if ( block . content ) {
446+ html += renderSlideContentHTML ( block . content ) ;
447+ } else if ( block . bullets && block . bullets . length > 0 ) {
448+ html += '<ul class="list-disc pl-6 my-2">' ;
449+ html += block . bullets . map ( ( item ) => `<li>${ escapeHTML ( item ) } </li>` ) . join ( '' ) ;
450+ html += '</ul>' ;
451+ }
452+ html += '</div>' ;
453+ break ;
454+ case 'table' :
455+ html += '<div class="mb-6 overflow-x-auto">' ;
456+ if ( block . caption ) {
457+ html += `<p class="text-sm text-gray-500 italic mb-2">${ escapeHTML ( block . caption ) } </p>` ;
458+ }
459+ html += '<table class="min-w-full border border-gray-200 text-sm">' ;
460+ html += '<thead class="bg-gray-100"><tr>' ;
461+ block . headers . forEach ( ( header ) => {
462+ html += `<th class="px-3 py-2 text-left font-semibold border border-gray-200">${ escapeHTML (
463+ header
464+ ) } </th>`;
465+ } ) ;
466+ html += '</tr></thead><tbody>' ;
467+ block . rows . forEach ( ( row , rowIndex ) => {
468+ const rowClass =
469+ block . style === 'striped' && rowIndex % 2 === 1 ? ' bg-gray-50' : '' ;
470+ html += `<tr class="${ rowClass } ">` ;
471+ row . cells . forEach ( ( cell ) => {
472+ html += `<td class="px-3 py-2 border border-gray-200">${ escapeHTML (
473+ cell . text
474+ ) } </td>`;
475+ } ) ;
476+ html += '</tr>' ;
477+ } ) ;
478+ html += '</tbody></table></div>' ;
479+ break ;
480+ default :
481+ break ;
482+ }
483+ }
484+
485+ return html ;
486+ }
487+
488+ function convertMarkdownToHTML ( text : string ) : string {
489+ const lines = text . split ( / \r ? \n / ) ;
490+ let html = '' ;
491+ let paragraph : string [ ] = [ ] ;
492+ let listItems : string [ ] = [ ] ;
493+ let blockquoteLines : string [ ] = [ ] ;
494+
495+ const flushParagraph = ( ) => {
496+ if ( paragraph . length > 0 ) {
497+ html += `<p class="my-2">${ renderInlineMarkdown ( paragraph . join ( ' ' ) ) } </p>` ;
498+ paragraph = [ ] ;
499+ }
500+ } ;
501+
502+ const flushList = ( ) => {
503+ if ( listItems . length > 0 ) {
504+ html += `<ul class="list-disc pl-6 my-2">${ listItems
505+ . map ( ( item ) => `<li>${ renderInlineMarkdown ( item ) } </li>` )
506+ . join ( '' ) } </ul>`;
507+ listItems = [ ] ;
508+ }
509+ } ;
510+
511+ const flushBlockquote = ( ) => {
512+ if ( blockquoteLines . length > 0 ) {
513+ html += `<blockquote class="border-l-4 border-gray-300 pl-4 italic text-gray-600 my-3">${ blockquoteLines
514+ . map ( ( line ) => `<p>${ renderInlineMarkdown ( line ) } </p>` )
515+ . join ( '' ) } </blockquote>`;
516+ blockquoteLines = [ ] ;
517+ }
518+ } ;
519+
520+ for ( const rawLine of lines ) {
521+ const line = rawLine . trim ( ) ;
522+ if ( ! line ) {
523+ flushParagraph ( ) ;
524+ flushList ( ) ;
525+ flushBlockquote ( ) ;
526+ continue ;
527+ }
528+
529+ const headingMatch = / ^ ( # { 1 , 3 } ) \s + ( .+ ) $ / . exec ( line ) ;
530+ if ( headingMatch ) {
531+ flushParagraph ( ) ;
532+ flushList ( ) ;
533+ flushBlockquote ( ) ;
534+ const level = headingMatch [ 1 ] . length ;
535+ const size = level === 1 ? 'text-2xl' : level === 2 ? 'text-xl' : 'text-lg' ;
536+ html += `<h${ level } class="${ size } font-bold mt-4 mb-2">${ renderInlineMarkdown (
537+ headingMatch [ 2 ]
538+ ) } </h${ level } >`;
539+ continue ;
540+ }
541+
542+ const listMatch = / ^ [ - * ] \s + ( .+ ) $ / . exec ( line ) ;
543+ if ( listMatch ) {
544+ flushParagraph ( ) ;
545+ flushBlockquote ( ) ;
546+ listItems . push ( listMatch [ 1 ] ) ;
547+ continue ;
548+ }
549+
550+ const quoteMatch = / ^ > \s ? ( .+ ) $ / . exec ( line ) ;
551+ if ( quoteMatch ) {
552+ flushParagraph ( ) ;
553+ flushList ( ) ;
554+ blockquoteLines . push ( quoteMatch [ 1 ] ) ;
555+ continue ;
556+ }
557+
558+ if ( listItems . length > 0 ) {
559+ flushList ( ) ;
560+ }
561+ if ( blockquoteLines . length > 0 ) {
562+ flushBlockquote ( ) ;
563+ }
564+ paragraph . push ( line ) ;
565+ }
566+
567+ flushParagraph ( ) ;
568+ flushList ( ) ;
569+ flushBlockquote ( ) ;
570+
571+ return html ;
572+ }
573+
574+ function renderSlideContentHTML ( contentBlocks : ContentBlock [ ] ) : string {
575+ let html = '' ;
576+
577+ for ( const block of contentBlocks ) {
578+ if ( block . type === 'unordered_list' ) {
579+ html += '<ul class="list-disc pl-6 my-2">' ;
580+ for ( const item of block . items ) {
581+ html += `<li>${ renderRuns ( item . content ) } </li>` ;
582+ }
583+ html += '</ul>' ;
584+ } else if ( block . type === 'ordered_list' ) {
585+ html += '<ol class="list-decimal pl-6 my-2">' ;
586+ for ( const item of block . items ) {
587+ html += `<li>${ renderRuns ( item . content ) } </li>` ;
588+ }
589+ html += '</ol>' ;
590+ } else if ( block . type === 'blockquote' ) {
591+ html += '<blockquote class="border-l-4 border-gray-300 pl-4 italic text-gray-600 my-3">' ;
592+ for ( const paragraph of block . content ) {
593+ html += `<p>${ renderRuns ( paragraph . content ) } </p>` ;
594+ }
595+ html += '</blockquote>' ;
596+ } else if ( block . type === 'paragraph' ) {
597+ const rawText = runsToText ( block . content ) . trim ( ) ;
598+ const headingMatch = / ^ ( # { 1 , 3 } ) \s + ( .+ ) $ / . exec ( rawText ) ;
599+ if ( headingMatch ) {
600+ const level = headingMatch [ 1 ] . length ;
601+ const size = level === 1 ? 'text-2xl' : level === 2 ? 'text-xl' : 'text-lg' ;
602+ html += `<h${ level } class="${ size } font-bold mt-3 mb-2">${ renderInlineMarkdown (
603+ headingMatch [ 2 ]
604+ ) } </h${ level } >`;
605+ } else {
606+ html += `<p class="my-2">${ renderRuns ( block . content ) } </p>` ;
607+ }
608+ }
609+ }
610+
611+ return html ;
612+ }
613+
614+ function renderRuns ( runs : TextRun [ ] ) : string {
615+ return runs
616+ . map ( ( run ) => {
617+ if ( typeof run === 'string' ) {
618+ return escapeHTML ( run ) ;
619+ }
620+ if ( isLinkRun ( run ) ) {
621+ const text = escapeHTML ( run . text || '' ) ;
622+ const url = escapeHTML ( run . url || '#' ) ;
623+ return `<a class="underline text-blue-600" href="${ url } " target="_blank" rel="noopener noreferrer">${ text } </a>` ;
624+ }
625+ if ( isImageRun ( run ) ) {
626+ const alt = escapeHTML ( run . alt || '' ) ;
627+ const url = escapeHTML ( run . url || '' ) ;
628+ return `<img class="inline-block max-h-40" src="${ url } " alt="${ alt } " />` ;
629+ }
630+ let content = escapeHTML ( run . text || '' ) ;
631+ if ( run . bold ) content = `<strong>${ content } </strong>` ;
632+ if ( run . italic ) content = `<em>${ content } </em>` ;
633+ if ( run . underline ) content = `<span class="underline">${ content } </span>` ;
634+ if ( run . strike ) content = `<span class="line-through">${ content } </span>` ;
635+ return content ;
636+ } )
637+ . join ( '' ) ;
638+ }
639+
640+ function runsToText ( runs : TextRun [ ] ) : string {
641+ return runs
642+ . map ( ( run ) => {
643+ if ( typeof run === 'string' ) return run ;
644+ if ( isLinkRun ( run ) ) return run . text || '' ;
645+ if ( isImageRun ( run ) ) return run . alt || '' ;
646+ return run . text || '' ;
647+ } )
648+ . join ( '' ) ;
649+ }
650+
651+ function isLinkRun ( run : TextRun ) : run is OSFLink {
652+ return typeof run === 'object' && run !== null && 'type' in run && run . type === 'link' ;
653+ }
654+
655+ function isImageRun ( run : TextRun ) : run is OSFImage {
656+ return typeof run === 'object' && run !== null && 'type' in run && run . type === 'image' ;
657+ }
658+
659+ function renderInlineMarkdown ( text : string ) : string {
660+ let html = escapeHTML ( text ) ;
661+ html = html . replace ( / \* \* ( .+ ?) \* \* / g, '<strong>$1</strong>' ) ;
662+ html = html . replace ( / \* ( .+ ?) \* / g, '<em>$1</em>' ) ;
663+ html = html . replace ( / ` ( .+ ?) ` / g, '<code class="bg-gray-100 px-1 rounded">$1</code>' ) ;
664+ return html ;
665+ }
666+
667+ function escapeHTML ( text : string ) : string {
668+ return text
669+ . replace ( / & / g, '&' )
670+ . replace ( / < / g, '<' )
671+ . replace ( / > / g, '>' )
672+ . replace ( / " / g, '"' )
673+ . replace ( / ' / g, ''' ) ;
674+ }
0 commit comments