11import { NextRequest , NextResponse } from 'next/server' ;
2- import { Document , Packer , Paragraph , TextRun , HeadingLevel , AlignmentType } from 'docx' ;
32import { getUserFromRequest } from '@/lib/auth-helpers' ;
3+ import { generateDocx } from '@/lib/export/docx' ;
4+ import { generatePdfBuffer } from '@/lib/export/pdf' ;
5+ import { MAX_CONTENT_LENGTH , sanitizeFilename , type ExportRequest } from '@/lib/export/types' ;
46
57export async function POST ( request : NextRequest ) {
68 try {
@@ -9,192 +11,40 @@ export async function POST(request: NextRequest) {
911 return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } ) ;
1012 }
1113
12- const body = await request . json ( ) ;
14+ const body = ( await request . json ( ) ) as Partial < ExportRequest > ;
1315 const { type, content, title } = body ;
1416
15- if ( type === 'docx' ) {
16- return await generateDocx ( content , title ) ;
17- } else if ( type === 'pdf' ) {
18- return await generatePdf ( content , title ) ;
17+ if ( ! type || ! content ) {
18+ return NextResponse . json ( { error : 'Type and content are required' } , { status : 400 } ) ;
1919 }
20-
21- return NextResponse . json ( { error : 'Invalid export type' } , { status : 400 } ) ;
22- } catch ( error ) {
23- console . error ( 'Export error:' , error ) ;
24- return NextResponse . json ( { error : 'Failed to generate export' } , { status : 500 } ) ;
25- }
26- }
27-
28- async function generateDocx ( content : string , title : string ) {
29- const lines = content . split ( '\n' ) ;
30- const children : Paragraph [ ] = [ ] ;
31-
32- // Add title
33- children . push ( new Paragraph ( {
34- children : [ new TextRun ( { text : title || 'Document' , bold : true , size : 36 } ) ] ,
35- heading : HeadingLevel . HEADING_1 ,
36- alignment : AlignmentType . CENTER ,
37- } ) ) ;
38-
39- children . push ( new Paragraph ( { text : '' } ) ) ;
40-
41- for ( const line of lines ) {
42- const trimmed = line . trim ( ) ;
43- if ( ! trimmed ) {
44- children . push ( new Paragraph ( { text : '' } ) ) ;
45- continue ;
20+ if ( type !== 'docx' && type !== 'pdf' ) {
21+ return NextResponse . json ( { error : 'Invalid export type' } , { status : 400 } ) ;
4622 }
47-
48- // Detect headings (lines starting with #)
49- if ( trimmed . startsWith ( '### ' ) ) {
50- children . push ( new Paragraph ( {
51- children : [ new TextRun ( { text : trimmed . replace ( '### ' , '' ) , bold : true , size : 22 } ) ] ,
52- heading : HeadingLevel . HEADING_3 ,
53- } ) ) ;
54- } else if ( trimmed . startsWith ( '## ' ) ) {
55- children . push ( new Paragraph ( {
56- children : [ new TextRun ( { text : trimmed . replace ( '## ' , '' ) , bold : true , size : 26 } ) ] ,
57- heading : HeadingLevel . HEADING_2 ,
58- } ) ) ;
59- } else if ( trimmed . startsWith ( '# ' ) ) {
60- children . push ( new Paragraph ( {
61- children : [ new TextRun ( { text : trimmed . replace ( '# ' , '' ) , bold : true , size : 28 } ) ] ,
62- heading : HeadingLevel . HEADING_1 ,
63- } ) ) ;
64- } else if ( trimmed . startsWith ( '- ' ) || trimmed . startsWith ( '• ' ) ) {
65- children . push ( new Paragraph ( {
66- children : [ new TextRun ( { text : `\u2022 ${ trimmed . replace ( / ^ [ - • ] \s * / , '' ) } ` , size : 22 } ) ] ,
67- indent : { left : 360 } ,
68- } ) ) ;
69- } else {
70- children . push ( new Paragraph ( {
71- children : [ new TextRun ( { text : trimmed , size : 22 } ) ] ,
72- } ) ) ;
23+ if ( content . length > MAX_CONTENT_LENGTH ) {
24+ return NextResponse . json ( { error : 'Content is too long' } , { status : 400 } ) ;
7325 }
74- }
75-
76- const doc = new Document ( {
77- sections : [ { properties : { } , children } ] ,
78- } ) ;
79-
80- const buffer = await Packer . toBuffer ( doc ) ;
81-
82- return new NextResponse ( new Uint8Array ( buffer ) , {
83- headers : {
84- 'Content-Type' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
85- 'Content-Disposition' : `attachment; filename="${ ( title || 'document' ) . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) } .docx"` ,
86- } ,
87- } ) ;
88- }
89-
90- async function generatePdf ( content : string , title : string ) {
91- // Generate PDF manually to avoid pdfkit font resolution issues in Next.js bundled environment.
92- const lines = content . split ( '\n' ) ;
93- const allLines : Array < { text : string ; size : number ; bold : boolean ; indent : number ; color : string } > = [ ] ;
9426
95- // Title
96- allLines . push ( { text : title || 'Document' , size : 24 , bold : true , indent : 0 , color : '0 0 0' } ) ;
97- allLines . push ( { text : '' , size : 8 , bold : false , indent : 0 , color : '0 0 0' } ) ;
27+ const filename = sanitizeFilename ( title ) ;
9828
99- for ( const line of lines ) {
100- const trimmed = line . trim ( ) ;
101- if ( ! trimmed ) {
102- allLines . push ( { text : '' , size : 4 , bold : false , indent : 0 , color : '0 0 0' } ) ;
103- continue ;
104- }
105-
106- if ( trimmed . startsWith ( '### ' ) ) {
107- allLines . push ( { text : trimmed . replace ( '### ' , '' ) , size : 12 , bold : true , indent : 0 , color : '0 0 0' } ) ;
108- } else if ( trimmed . startsWith ( '## ' ) ) {
109- allLines . push ( { text : trimmed . replace ( '## ' , '' ) , size : 14 , bold : true , indent : 0 , color : '0 0 0' } ) ;
110- } else if ( trimmed . startsWith ( '# ' ) ) {
111- allLines . push ( { text : trimmed . replace ( '# ' , '' ) , size : 16 , bold : true , indent : 0 , color : '0 0 0' } ) ;
112- } else if ( trimmed . startsWith ( '- ' ) || trimmed . startsWith ( '• ' ) ) {
113- allLines . push ( { text : `\u2022 ${ trimmed . replace ( / ^ [ - • ] \s * / , '' ) } ` , size : 11 , bold : false , indent : 20 , color : '0 0 0' } ) ;
114- } else {
115- allLines . push ( { text : trimmed , size : 11 , bold : false , indent : 0 , color : '0 0 0' } ) ;
29+ if ( type === 'docx' ) {
30+ const buffer = await generateDocx ( content , title ?? 'Document' ) ;
31+ return new NextResponse ( new Uint8Array ( buffer ) , {
32+ headers : {
33+ 'Content-Type' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
34+ 'Content-Disposition' : `attachment; filename="${ filename } .docx"` ,
35+ } ,
36+ } ) ;
11637 }
117- }
118-
119- const pdfContent = buildSimplePdf ( allLines ) ;
12038
121- return new NextResponse ( new Uint8Array ( pdfContent ) , {
122- headers : {
123- 'Content-Type' : 'application/pdf' ,
124- 'Content-Disposition' : `attachment; filename="${ ( title || 'document' ) . replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) } .pdf"` ,
125- } ,
126- } ) ;
127- }
128-
129- // Build a minimal valid PDF manually (no external font files needed)
130- function buildSimplePdf ( allLines : Array < { text : string ; size : number ; bold : boolean ; indent : number ; color : string } > ) : Buffer {
131- // Build PDF structure
132- const objects : string [ ] = [ ] ;
133- let objectCount = 0 ;
134-
135- const addObject = ( content : string ) : number => {
136- objectCount ++ ;
137- objects . push ( `${ objectCount } 0 obj\n${ content } \nendobj` ) ;
138- return objectCount ;
139- } ;
140-
141- // Object 1: Catalog
142- const catalogObj = addObject ( '<< /Type /Catalog /Pages 2 0 R >>' ) ;
143-
144- // Object 2: Pages (will reference page objects)
145- addObject ( '<< /Type /Pages /Kids [3 0 R] /Count 1 >>' ) ;
146-
147- // Build page content stream
148- let streamContent = '' ;
149- let y = 770 ; // Start near top of page
150-
151- for ( const item of allLines ) {
152- if ( y < 50 ) break ; // Stop if we run off the page
153-
154- const fontName = item . bold ? '/F2' : '/F1' ;
155- const x = 50 + item . indent ;
156-
157- // Escape special PDF characters
158- const escapedText = item . text
159- . replace ( / \\ / g, '\\\\' )
160- . replace ( / \( / g, '\\(' )
161- . replace ( / \) / g, '\\)' ) ;
162-
163- streamContent += `BT\n${ fontName } ${ item . size } Tf\n${ item . color } rg\n${ x } ${ y } Td\n(${ escapedText } ) Tj\nET\n` ;
164- y -= item . size + 4 ;
165- }
166-
167- // Object 3: Stream
168- const streamObjId = addObject ( `<< /Length ${ streamContent . length } >>\nstream\n${ streamContent } \nendstream` ) ;
169-
170- // Object 4: Page
171- addObject ( `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents ${ streamObjId } 0 R /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> >>` ) ;
172-
173- // Object 5: Font (Courier)
174- addObject ( '<< /Type /Font /Subtype /Type1 /BaseFont /Courier /Encoding /WinAnsiEncoding >>' ) ;
175-
176- // Object 6: Font (Courier-Bold)
177- addObject ( '<< /Type /Font /Subtype /Type1 /BaseFont /Courier-Bold /Encoding /WinAnsiEncoding >>' ) ;
178-
179- // Build PDF
180- let pdf = '%PDF-1.4\n' ;
181- const offsets : number [ ] = [ ] ;
182-
183- for ( let i = 0 ; i < objects . length ; i ++ ) {
184- offsets . push ( pdf . length ) ;
185- pdf += `${ objects [ i ] } \n` ;
186- }
187-
188- // Cross-reference table
189- const xrefOffset = pdf . length ;
190- pdf += `xref\n0 ${ objectCount + 1 } \n` ;
191- pdf += '0000000000 65535 f \n' ;
192- for ( const offset of offsets ) {
193- pdf += `${ offset . toString ( ) . padStart ( 10 , '0' ) } 00000 n \n` ;
39+ const buffer = generatePdfBuffer ( content , title ?? 'Document' ) ;
40+ return new NextResponse ( new Uint8Array ( buffer ) , {
41+ headers : {
42+ 'Content-Type' : 'application/pdf' ,
43+ 'Content-Disposition' : `attachment; filename="${ filename } .pdf"` ,
44+ } ,
45+ } ) ;
46+ } catch ( error ) {
47+ console . error ( 'Export error:' , error ) ;
48+ return NextResponse . json ( { error : 'Failed to generate export' } , { status : 500 } ) ;
19449 }
195-
196- pdf += `trailer\n<< /Size ${ objectCount + 1 } /Root ${ catalogObj } 0 R >>\n` ;
197- pdf += `startxref\n${ xrefOffset } \n%%EOF` ;
198-
199- return Buffer . from ( pdf , 'binary' ) ;
20050}
0 commit comments