11import fs from 'node:fs' ;
2+ import { convertMdx } from '../../src/write/convert/mdxToSource.mjs' ;
23
34let SRC = process . argv [ 2 ] ;
45if ( ! SRC ) {
@@ -14,357 +15,22 @@ if (!fs.existsSync(SRC)) {
1415if ( fs . statSync ( SRC ) . isDirectory ( ) ) SRC = SRC . replace ( / \/ $ / , '' ) + '/index.mdx' ;
1516const slug = SRC . split ( '/' ) . filter ( Boolean ) . at ( - 2 ) ?? 'post-slug' ;
1617const OUT = process . argv [ 3 ] ?? `${ slug } .write-source.json` ;
17-
18- let src = fs . readFileSync ( SRC , 'utf8' ) ;
19-
20- const fm = src . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - \n / ) ;
21- const body = fm ? src . slice ( fm [ 0 ] . length ) : src ;
22- const fmText = fm ? fm [ 1 ] : '' ;
23- const fmVal = ( key ) => {
24- const m = fmText . match ( new RegExp ( `^${ key } : (.*)$` , 'm' ) ) ;
25- return m ? m [ 1 ] . replace ( / ^ " | " $ / g, '' ) : '' ;
26- } ;
27- const tagsMatch = fmText . match ( / ^ t a g s : ( \[ .* \] ) $ / m) ;
28- const tags = tagsMatch ? JSON . parse ( tagsMatch [ 1 ] . replace ( / ' / g, '"' ) ) : [ ] ;
29-
30- let n = 0 ;
31- const id = ( p ) => `${ p } -${ ++ n } ` ;
32- const D = { backgroundColor : 'default' , textColor : 'default' , textAlignment : 'left' } ;
33-
34- const INLINE_PATTERNS = [
35- { re : / \* \* ` ( [ ^ ` ] + ) ` \* \* / g, kind : 'code' } ,
36- { re : / ` ( [ ^ ` ] + ) ` / g, kind : 'code' } ,
37- { re : / (?< ! ! ) \[ ( [ ^ \] ] + ) \] \( ( [ ^ ) \s ] + ) \) / g, kind : 'link' } ,
38- { re : / \* \* ( [ ^ * ] + (?: \* (? ! \* ) [ ^ * ] * ) * ) \* \* / g, kind : 'style' , key : 'bold' } ,
39- { re : / _ _ ( [ ^ _ ] + ) _ _ / g, kind : 'style' , key : 'bold' } ,
40- { re : / ~ ~ ( [ ^ ~ ] + ) ~ ~ / g, kind : 'style' , key : 'strike' } ,
41- { re : / \* ( [ ^ * \n ] + ) \* / g, kind : 'style' , key : 'italic' } ,
42- { re : / (?< ! [ \w ` ] ) _ ( [ ^ _ \n ] + ) _ (? ! [ \w ` ] ) / g, kind : 'style' , key : 'italic' } ,
43- ] ;
44-
45- function smartQuotes ( s ) {
46- return s
47- . replace ( / ( \w ) ' ( \w ) / g, '$1’$2' )
48- . replace ( / ( ^ | [ \s ( [ { — – - ] ) " / g, '$1“' )
49- . replace ( / " / g, '”' )
50- . replace ( / ( ^ | [ \s ( [ { — – - ] ) ' / g, '$1‘' )
51- . replace ( / ' / g, '’' ) ;
52- }
53-
54- function inline ( text , inherited = { } ) {
55- const runs = [ ] ;
56- let pos = 0 ;
57- while ( pos < text . length ) {
58- let best = null ;
59- for ( const p of INLINE_PATTERNS ) {
60- p . re . lastIndex = pos ;
61- const m = p . re . exec ( text ) ;
62- if ( m && ( ! best || m . index < best . m . index ) ) best = { p, m } ;
63- }
64- if ( ! best ) {
65- runs . push ( { type : 'text' , text : smartQuotes ( text . slice ( pos ) ) , styles : { ...inherited } } ) ;
66- break ;
67- }
68- if ( best . m . index > pos )
69- runs . push ( {
70- type : 'text' ,
71- text : smartQuotes ( text . slice ( pos , best . m . index ) ) ,
72- styles : { ...inherited } ,
73- } ) ;
74- const { p, m } = best ;
75- if ( p . kind === 'code' ) {
76- runs . push ( { type : 'text' , text : m [ 1 ] , styles : { code : true } } ) ;
77- } else if ( p . kind === 'link' ) {
78- runs . push ( {
79- type : 'link' ,
80- href : m [ 2 ] ,
81- content : inline ( m [ 1 ] , inherited ) . filter ( ( r ) => r . type === 'text' ) ,
82- } ) ;
83- } else {
84- runs . push ( ...inline ( m [ 1 ] , { ...inherited , [ p . key ] : true } ) ) ;
85- }
86- pos = m . index + m [ 0 ] . length ;
87- }
88- return runs . filter ( ( r ) => r . type !== 'text' || r . text !== '' ) ;
89- }
90-
91- const blocks = [ ] ;
92- const push = ( type , props , content , children = [ ] ) => {
93- const b = { id : id ( type ) , type, props, children } ;
94- if ( content !== undefined ) b . content = content ;
95- blocks . push ( b ) ;
96- } ;
97-
98- const cell = ( text ) => ( {
99- type : 'tableCell' ,
100- content : inline ( text ) ,
101- props : { colspan : 1 , rowspan : 1 , ...D } ,
102- } ) ;
103-
10418const DIR = SRC . replace ( / \/ i n d e x \. m d x $ / , '' ) ;
10519
106- function componentSource ( name ) {
107- const file = `${ DIR } /${ name } .tsx` ;
108- if ( fs . existsSync ( file ) ) return fs . readFileSync ( file , 'utf8' ) ;
109- console . warn ( `warning: ${ name } .tsx not found next to index.mdx — source left empty` ) ;
110- return '' ;
111- }
112-
113- function frameAttr ( attrs , name ) {
114- return (
115- attrs . match ( new RegExp ( `${ name } ="([^"]*)"` ) ) ?. [ 1 ] ??
116- JSON . parse ( attrs . match ( new RegExp ( `${ name } =\\{("(?:[^"\\\\]|\\\\.)*")\\}` ) ) ?. [ 1 ] ?? '""' )
117- ) ;
118- }
119-
120- function frameProps ( attrs ) {
121- return {
122- frameTitle : frameAttr ( attrs , 'title' ) ,
123- frameCaption : frameAttr ( attrs , 'caption' ) ,
124- frameSize : / s i z e = " w i d e " / . test ( attrs ) ? 'wide' : 'normal' ,
125- frameExpand : / ( ^ | \s ) e x p a n d ( \s | $ | = ) / . test ( attrs ) ,
126- } ;
127- }
128-
129- function unwrapJsxStyle ( svg ) {
130- return svg . replace (
131- / < s t y l e ( [ ^ > ] * ) > \{ ` ( [ \s \S ] * ?) ` \} < \/ s t y l e > / g,
132- ( _m , attrs , css ) => `<style${ attrs } >${ css . replace ( / \\ ( [ \\ ` $ ] ) / g, '$1' ) } </style>` ,
133- ) ;
134- }
135-
136- function figureSegment ( attrs , inner ) {
137- const caption = frameAttr ( attrs , 'caption' ) . replace ( / \s + / g, ' ' ) . trim ( ) ;
138- const width = Number ( attrs . match ( / w i d t h = \{ ( \d + ) \} / ) ?. [ 1 ] ?? '' ) || '' ;
139- if ( / ^ < s v g [ \s > ] / . test ( inner ) ) {
140- return { kind : 'figure' , caption, svg : unwrapJsxStyle ( inner ) } ;
141- }
142- const img =
143- inner . match ( / ^ ! \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) \s ] + ) \) $ / ) ?? inner . match ( / ^ < i m g [ ^ > ] * \s s r c = " ( [ ^ " ] + ) " [ ^ > ] * > $ / ) ;
144- if ( img ) {
145- const md = inner . startsWith ( '![' ) ;
146- return {
147- kind : 'image' ,
148- alt : md ? img [ 1 ] : ( inner . match ( / \s a l t = " ( [ ^ " ] * ) " / ) ?. [ 1 ] ?? '' ) ,
149- src : md ? img [ 2 ] : img [ 1 ] ,
150- caption,
151- width : width || 360 ,
152- } ;
153- }
154- return { kind : 'md' , text : inner } ;
155- }
156-
157- let rest = body ;
158- const pattern =
159- / ( < F i g u r e ( [ ^ > ] * ) > \s * ( [ \s \S ] * ?) \s * < \/ F i g u r e > ) | ( < N o t e > \s * ( [ \s \S ] * ?) \s * < \/ N o t e > ) | ( ` ` ` ( \w * ) \n ( [ \s \S ] * ?) ` ` ` ) | ( < I n t e r a c t i v e \b ( [ ^ > ] * ) > \s * < ( [ A - Z a - z ] \w * ) \s + c l i e n t : v i s i b l e \s * \/ > \s * < \/ I n t e r a c t i v e > ) | ( < ( [ A - Z ] \w * ) \s + c l i e n t : v i s i b l e \s * \/ > ) / g;
160-
161- let cursor = 0 ;
162- const segments = [ ] ;
163- let m ;
164- while ( ( m = pattern . exec ( rest ) ) ) {
165- if ( m . index > cursor ) segments . push ( { kind : 'md' , text : rest . slice ( cursor , m . index ) } ) ;
166- if ( m [ 1 ] ) {
167- segments . push ( figureSegment ( m [ 2 ] ?? '' , m [ 3 ] . trim ( ) ) ) ;
168- } else if ( m [ 4 ] ) segments . push ( { kind : 'note' , text : m [ 5 ] . replace ( / \s + / g, ' ' ) . trim ( ) } ) ;
169- else if ( m [ 6 ] ) {
170- const code = m [ 8 ] . replace ( / \n $ / , '' ) ;
171- if ( ( m [ 7 ] || '' ) === 'mermaid' ) segments . push ( { kind : 'mermaid' , source : code } ) ;
172- else segments . push ( { kind : 'code' , lang : m [ 7 ] || 'text' , code } ) ;
173- } else if ( m [ 9 ] ) segments . push ( { kind : 'component' , name : m [ 11 ] , frame : frameProps ( m [ 10 ] ) } ) ;
174- else if ( m [ 12 ] )
175- segments . push ( {
176- kind : 'component' ,
177- name : m [ 13 ] ,
178- frame : { frameTitle : '' , frameCaption : '' , frameSize : 'normal' , frameExpand : false } ,
179- } ) ;
180- cursor = m . index + m [ 0 ] . length ;
181- }
182- if ( cursor < rest . length ) segments . push ( { kind : 'md' , text : rest . slice ( cursor ) } ) ;
183-
184- function emitMd ( text ) {
185- const lines = text . split ( '\n' ) ;
186- let i = 0 ;
187- while ( i < lines . length ) {
188- const line = lines [ i ] ;
189- if ( ! line . trim ( ) || / ^ i m p o r t \s .+ \s f r o m \s / . test ( line ) ) {
190- i ++ ;
191- continue ;
192- }
193- const h = line . match ( / ^ ( # { 2 , 6 } ) / ) ;
194- if ( h ) {
195- push (
196- 'heading' ,
197- { ...D , level : Math . min ( h [ 1 ] . length - 1 , 3 ) , isToggleable : false } ,
198- inline ( line . slice ( h [ 1 ] . length + 1 ) ) ,
199- ) ;
200- i ++ ;
201- continue ;
202- }
203- const img = line . match ( / ^ ! \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) \s ] + ) \) $ / ) ;
204- if ( img ) {
205- push ( 'figure' , { fileName : '' , src : img [ 2 ] , alt : img [ 1 ] , caption : '' , width : 360 } ) ;
206- i ++ ;
207- continue ;
208- }
209- if ( line . startsWith ( '> ' ) ) {
210- const q = [ ] ;
211- while ( i < lines . length && lines [ i ] . startsWith ( '> ' ) ) {
212- q . push ( lines [ i ] . slice ( 2 ) ) ;
213- i ++ ;
214- }
215- push ( 'quote' , { backgroundColor : 'default' , textColor : 'default' } , inline ( q . join ( ' ' ) ) ) ;
216- continue ;
217- }
218- if ( / ^ [ - * ] \[ [ x X ] \] / . test ( line ) ) {
219- while ( i < lines . length && / ^ [ - * ] \[ [ x X ] \] / . test ( lines [ i ] ) ) {
220- push (
221- 'checkListItem' ,
222- { ...D , checked : / ^ \S + \[ [ x X ] \] / . test ( lines [ i ] ) } ,
223- inline ( lines [ i ] . replace ( / ^ [ - * ] \[ [ x X ] \] / , '' ) ) ,
224- ) ;
225- i ++ ;
226- }
227- continue ;
228- }
229- if ( / ^ [ - * ] / . test ( line ) ) {
230- while ( i < lines . length && / ^ [ - * ] / . test ( lines [ i ] ) ) {
231- push ( 'bulletListItem' , { ...D } , inline ( lines [ i ] . slice ( 2 ) ) ) ;
232- i ++ ;
233- }
234- continue ;
235- }
236- if ( / ^ \d + \. / . test ( line ) ) {
237- while ( i < lines . length && / ^ \d + \. / . test ( lines [ i ] ) ) {
238- push ( 'numberedListItem' , { ...D } , inline ( lines [ i ] . replace ( / ^ \d + \. / , '' ) ) ) ;
239- i ++ ;
240- }
241- continue ;
242- }
243- if ( / ^ ( - - - + | \* \* \* + ) $ / . test ( line . trim ( ) ) ) {
244- push ( 'separator' , { } ) ;
245- i ++ ;
246- continue ;
247- }
248- if ( / ^ < h r \b / . test ( line . trim ( ) ) ) {
249- push ( 'divider' , { } ) ;
250- i ++ ;
251- continue ;
252- }
253- if ( line . startsWith ( '|' ) ) {
254- const rows = [ ] ;
255- while ( i < lines . length && lines [ i ] . startsWith ( '|' ) ) {
256- if ( ! / ^ \| [: \- \s | ] + \| $ / . test ( lines [ i ] ) ) {
257- const cells = lines [ i ]
258- . split ( '|' )
259- . slice ( 1 , - 1 )
260- . map ( ( c ) => c . trim ( ) ) ;
261- rows . push ( { cells : cells . map ( cell ) } ) ;
262- }
263- i ++ ;
264- }
265- const cols = rows [ 0 ] . cells . length ;
266- push (
267- 'table' ,
268- { textColor : 'default' } ,
269- { type : 'tableContent' , columnWidths : Array ( cols ) . fill ( null ) , rows } ,
270- ) ;
271- continue ;
272- }
273- const para = [ ] ;
274- while (
275- i < lines . length &&
276- lines [ i ] . trim ( ) &&
277- ! / ^ ( # { 2 , 6 } | > | \| | ` ` ` | [ - * ] | \d + \. | ! \[ | < h r \b ) / . test ( lines [ i ] ) &&
278- ! / ^ ( - - - + | \* \* \* + ) $ / . test ( lines [ i ] . trim ( ) )
279- ) {
280- para . push ( lines [ i ] ) ;
281- i ++ ;
282- }
283- push ( 'paragraph' , { ...D } , inline ( para . join ( ' ' ) ) ) ;
284- }
285- }
286-
287- for ( const seg of segments ) {
288- if ( seg . kind === 'md' ) emitMd ( seg . text ) ;
289- else if ( seg . kind === 'figure' ) push ( 'svg' , { code : seg . svg , caption : seg . caption } ) ;
290- else if ( seg . kind === 'note' ) push ( 'note' , { } , inline ( seg . text ) ) ;
291- else if ( seg . kind === 'image' )
292- push ( 'figure' , {
293- fileName : '' ,
294- src : seg . src ,
295- alt : seg . alt ,
296- caption : seg . caption ,
297- width : seg . width ,
298- } ) ;
299- else if ( seg . kind === 'code' )
300- push ( 'codeBlock' , { language : seg . lang } , [ { type : 'text' , text : seg . code , styles : { } } ] ) ;
301- else if ( seg . kind === 'mermaid' ) {
302- console . warn ( 'note: mermaid block emitted without render — open the post in /write once' ) ;
303- push ( 'mermaid' , { source : seg . source , svg : '' , caption : '' } ) ;
304- } else if ( seg . kind === 'component' )
305- push ( 'customComponent' , {
306- componentName : seg . name ,
307- source : componentSource ( seg . name ) ,
308- ...seg . frame ,
309- } ) ;
310- }
311-
312- const doc = {
313- kind : 'mlsys-write-source' ,
314- version : 1 ,
315- meta : {
316- title : fmVal ( 'title' ) ,
317- summary : fmVal ( 'summary' ) ,
318- authors : ( ( ) => {
319- const list = [ ...fmText . matchAll ( / ^ { 2 } - ( .+ ) $ / gm) ]
320- . map ( ( x ) => x [ 1 ] )
321- . filter ( ( a ) => ! / ^ \d { 4 } - / . test ( a ) ) ;
322- return list . length ? list : [ 'guest' ] ;
323- } ) ( ) ,
324- writerName : 'Author Name' ,
325- topicId : fmVal ( 'topicId' ) ,
326- topicName : fmVal ( 'topic' ) ,
327- tags,
328- slug,
329- coverFileName : '' ,
330- ogCard : false ,
331- proposedTopic : '' ,
332- newAuthor : null ,
333- date : fmVal ( 'date' ) ,
20+ const { doc, warnings } = convertMdx ( fs . readFileSync ( SRC , 'utf8' ) , {
21+ slug,
22+ componentSource : ( name ) => {
23+ const file = `${ DIR } /${ name } .tsx` ;
24+ if ( fs . existsSync ( file ) ) return fs . readFileSync ( file , 'utf8' ) ;
25+ console . warn ( `warning: ${ name } .tsx not found next to index.mdx — source left empty` ) ;
26+ return '' ;
33427 } ,
335- blocks,
336- tableVariants : { } ,
337- } ;
28+ } ) ;
33829
339- const ids = blocks . map ( ( b ) => b . id ) ;
340- if ( new Set ( ids ) . size !== ids . length ) throw new Error ( 'duplicate ids' ) ;
341- const allowed = new Set ( [
342- 'paragraph' ,
343- 'heading' ,
344- 'bulletListItem' ,
345- 'numberedListItem' ,
346- 'checkListItem' ,
347- 'toggleListItem' ,
348- 'quote' ,
349- 'note' ,
350- 'codeBlock' ,
351- 'math' ,
352- 'separator' ,
353- 'divider' ,
354- 'table' ,
355- 'figure' ,
356- 'gallery' ,
357- 'video' ,
358- 'svg' ,
359- 'mermaid' ,
360- 'customComponent' ,
361- ] ) ;
362- for ( const b of blocks ) if ( ! allowed . has ( b . type ) ) throw new Error ( 'bad type ' + b . type ) ;
363- for ( const b of blocks )
364- if ( [ 'svg' , 'codeBlock' ] . includes ( b . type ) === false && b . content ) JSON . stringify ( b . content ) ;
30+ for ( const w of warnings ) console . warn ( `note: ${ w } ` ) ;
36531
36632fs . writeFileSync ( OUT , JSON . stringify ( doc , null , 2 ) + '\n' ) ;
36733const counts = { } ;
368- for ( const b of blocks ) counts [ b . type ] = ( counts [ b . type ] ?? 0 ) + 1 ;
369- console . log ( 'blocks:' , blocks . length , JSON . stringify ( counts ) ) ;
34+ for ( const b of doc . blocks ) counts [ b . type ] = ( counts [ b . type ] ?? 0 ) + 1 ;
35+ console . log ( 'blocks:' , doc . blocks . length , JSON . stringify ( counts ) ) ;
37036console . log ( 'written:' , OUT ) ;
0 commit comments