Skip to content

Commit e654c1b

Browse files
committed
Fix issues tryfabric#48, tryfabric#51, tryfabric#70, tryfabric#71 on PR tryfabric#76 from alephpiece:chore/bump-deps
Bug fixes applied on top of ESM migration (unified v11, vitest): - tryfabric#48: Make LaTeX math parsing opt-in via enableMath option (default: false) - tryfabric#51: Split rich_text arrays exceeding Notion's 100-item limit - tryfabric#70: Extract blockquote text from first paragraph instead of empty - tryfabric#71: Pad table rows with empty cells when fewer than table width All 42 tests passing.
1 parent f38baea commit e654c1b

3 files changed

Lines changed: 174 additions & 37 deletions

File tree

src/index.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {unified} from 'unified';
1+
import { unified } from 'unified';
22
import markdown from 'remark-parse';
33
import type * as notion from './notion';
44
import {
@@ -15,14 +15,20 @@ import remarkMath from 'remark-math';
1515
* Parses Markdown content into Notion Blocks.
1616
*
1717
* @param body Any Markdown or GFM content
18-
* @param options Any additional option
18+
* @param options Any additional options
19+
* @param options.enableMath Enable LaTeX math parsing (default: false).
20+
* When false, dollar signs like $100 will be treated as plain text.
21+
* When true, $...$ will be parsed as inline math and $$...$$ as block math.
1922
*/
2023
export function markdownToBlocks(
2124
body: string,
22-
2325
options?: BlocksOptions,
2426
): notion.Block[] {
25-
const root = unified().use(markdown).use(gfm).use(remarkMath).parse(body);
27+
// Build the unified processor with optional math support
28+
// Parse in separate branches to avoid TypeScript type union issues
29+
const root = options?.enableMath
30+
? unified().use(markdown).use(gfm).use(remarkMath).parse(body)
31+
: unified().use(markdown).use(gfm).parse(body);
2632
return parseBlocks(root as unknown as md.Root, options);
2733
}
2834

src/parser/internal.ts

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as md from '../markdown';
22
import * as notion from '../notion';
33
import 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

77
function 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

236252
function 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 {
359386
export 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

365437
export 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

384459
export 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

Comments
 (0)