11import * as md from '../markdown' ;
22import * as notion from '../notion' ;
33import path from 'path' ;
4- import { URL } from 'url' ;
5- import { isSupportedCodeLang , LIMITS } from '../notion' ;
4+ import { URL } from 'url' ;
5+ import { isSupportedCodeLang , LIMITS } from '../notion' ;
66
77function ensureLength ( text : string , copy ?: object ) {
88 const chunks = text . match ( / [ ^ ] { 1 , 2000 } / g) || [ ] ;
@@ -54,7 +54,7 @@ function parseInline(
5454 return [ notion . richText ( element . value , copy ) ] ;
5555
5656 case 'inlineMath' :
57- return [ notion . richText ( element . value , { ...copy , type : 'equation' } ) ] ;
57+ return [ notion . richText ( element . value , { ...copy , type : 'equation' } ) ] ;
5858
5959 default :
6060 return [ ] ;
@@ -214,7 +214,7 @@ function parseBlockquote(
214214 const richText = paragraph . children . flatMap ( child =>
215215 child === firstTextNode
216216 ? textWithoutEmoji
217- ? parseInline ( { type : 'text' , value : textWithoutEmoji } )
217+ ? parseInline ( { type : 'text' , value : textWithoutEmoji } )
218218 : [ ]
219219 : parseInline ( child ) ,
220220 ) ;
@@ -229,8 +229,24 @@ function parseBlockquote(
229229 }
230230 }
231231
232+ // Parse all children as blocks
232233 const children = element . children . flatMap ( child => parseNode ( child , options ) ) ;
233- return notion . blockquote ( [ ] , children ) ;
234+
235+ // Fix Issue #70: Extract rich_text from first paragraph child to avoid "Empty quote"
236+ // If the first child is a paragraph, use its rich_text content for the blockquote
237+ // and move remaining children to the blockquote's children array
238+ if ( children . length > 0 && children [ 0 ] . type === 'paragraph' ) {
239+ const firstParagraph = children [ 0 ] as notion . Block & {
240+ paragraph : { rich_text : notion . RichText [ ] } ;
241+ } ;
242+ const richText = firstParagraph . paragraph . rich_text ;
243+ const remainingChildren = children . slice ( 1 ) ;
244+ return notion . blockquote ( richText , remainingChildren ) ;
245+ }
246+
247+ // Fallback: if no paragraph children, create a quote with placeholder text
248+ // to avoid Notion API rejecting empty rich_text arrays
249+ return notion . blockquote ( [ notion . richText ( '' ) ] , children ) ;
234250}
235251
236252function parseHeading ( element : md . Heading ) : notion . Block {
@@ -283,8 +299,19 @@ function parseTableCell(node: md.TableCell): notion.RichText[] {
283299 return node . children . flatMap ( child => parseInline ( child ) ) ;
284300}
285301
286- function parseTableRow ( node : md . TableRow ) : notion . TableRowBlock {
302+ /**
303+ * Parse a table row, padding with empty cells if needed to match table width.
304+ * Fixes Issue #71: Tables with fewer cells in subsequent rows.
305+ */
306+ function parseTableRow (
307+ node : md . TableRow ,
308+ expectedWidth : number ,
309+ ) : notion . TableRowBlock {
287310 const cells = node . children . map ( child => parseTableCell ( child ) ) ;
311+ // Pad with empty cells if this row has fewer cells than the table width
312+ while ( cells . length < expectedWidth ) {
313+ cells . push ( [ notion . richText ( '' ) ] ) ;
314+ }
288315 return notion . tableRow ( cells ) ;
289316}
290317
@@ -294,7 +321,7 @@ function parseTable(node: md.Table): notion.Block[] {
294321 ? node . children [ 0 ] . children . length
295322 : 0 ;
296323
297- const tableRows = node . children . map ( child => parseTableRow ( child ) ) ;
324+ const tableRows = node . children . map ( child => parseTableRow ( child , tableWidth ) ) ;
298325 return [ notion . table ( tableRows , tableWidth ) ] ;
299326}
300327
@@ -359,7 +386,52 @@ export interface CommonOptions {
359386export interface BlocksOptions extends CommonOptions {
360387 /** Whether to render invalid images as text */
361388 strictImageUrls ?: boolean ;
389+ /** Whether to convert blockquotes starting with emoji to callouts */
362390 enableEmojiCallouts ?: boolean ;
391+ /**
392+ * Enable LaTeX math parsing (default: false).
393+ * When false, dollar signs like $100 will be treated as plain text.
394+ * When true, $...$ will be parsed as inline math and $$...$$ as block math.
395+ */
396+ enableMath ?: boolean ;
397+ }
398+
399+ /**
400+ * Split blocks that have rich_text arrays exceeding Notion's 100-item limit.
401+ * Fixes Issue #51: Notion rich_text[] max length is 100.
402+ */
403+ function splitLargeRichTextArrays (
404+ blocks : notion . Block [ ] ,
405+ limitCallback : ( err : Error ) => void ,
406+ ) : notion . Block [ ] {
407+ const MAX_RICH_TEXT = LIMITS . RICH_TEXT_ARRAYS ;
408+
409+ return blocks . flatMap ( block => {
410+ // Handle paragraph blocks
411+ if (
412+ block . type === 'paragraph' &&
413+ 'paragraph' in block &&
414+ block . paragraph . rich_text . length > MAX_RICH_TEXT
415+ ) {
416+ limitCallback (
417+ new Error (
418+ `Paragraph rich_text array exceeds Notion limit (${ MAX_RICH_TEXT } ), splitting into multiple blocks` ,
419+ ) ,
420+ ) ;
421+ const chunks : notion . Block [ ] = [ ] ;
422+ for (
423+ let i = 0 ;
424+ i < block . paragraph . rich_text . length ;
425+ i += MAX_RICH_TEXT
426+ ) {
427+ chunks . push (
428+ notion . paragraph ( block . paragraph . rich_text . slice ( i , i + MAX_RICH_TEXT ) ) ,
429+ ) ;
430+ }
431+ return chunks ;
432+ }
433+ return [ block ] ;
434+ } ) ;
363435}
364436
365437export function parseBlocks (
@@ -369,16 +441,19 @@ export function parseBlocks(
369441 const parsed = root . children . flatMap ( item => parseNode ( item , options || { } ) ) ;
370442
371443 const truncate = ! ! ( options ?. notionLimits ?. truncate ?? true ) ,
372- limitCallback = options ?. notionLimits ?. onError ?? ( ( ) => { } ) ;
444+ limitCallback = options ?. notionLimits ?. onError ?? ( ( ) => { } ) ;
445+
446+ // Fix Issue #51: Split blocks with rich_text arrays exceeding 100 items
447+ const splitBlocks = splitLargeRichTextArrays ( parsed , limitCallback ) ;
373448
374- if ( parsed . length > LIMITS . PAYLOAD_BLOCKS )
449+ if ( splitBlocks . length > LIMITS . PAYLOAD_BLOCKS )
375450 limitCallback (
376451 new Error (
377452 `Resulting blocks array exceeds Notion limit (${ LIMITS . PAYLOAD_BLOCKS } )` ,
378453 ) ,
379454 ) ;
380455
381- return truncate ? parsed . slice ( 0 , LIMITS . PAYLOAD_BLOCKS ) : parsed ;
456+ return truncate ? splitBlocks . slice ( 0 , LIMITS . PAYLOAD_BLOCKS ) : splitBlocks ;
382457}
383458
384459export interface RichTextOptions extends CommonOptions {
@@ -404,7 +479,7 @@ export function parseRichText(
404479 } ) ;
405480
406481 const truncate = ! ! ( options ?. notionLimits ?. truncate ?? true ) ,
407- limitCallback = options ?. notionLimits ?. onError ?? ( ( ) => { } ) ;
482+ limitCallback = options ?. notionLimits ?. onError ?? ( ( ) => { } ) ;
408483
409484 if ( richTexts . length > LIMITS . RICH_TEXT_ARRAYS )
410485 limitCallback (
0 commit comments