@chappibunny/repolens 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -172,6 +172,74 @@ export async function clearPage(pageId) {
172
172
  }
173
173
  }
174
174
 
175
+ function parseInlineRichText(text) {
176
+ // Parse inline markdown: **bold**, *italic*, `code` into Notion rich_text annotations
177
+ const segments = [];
178
+ let remaining = text;
179
+
180
+ while (remaining.length > 0) {
181
+ // Find the earliest inline marker
182
+ const boldIdx = remaining.indexOf("**");
183
+ const codeIdx = remaining.indexOf("`");
184
+ const italicIdx = remaining.indexOf("*");
185
+
186
+ // Collect candidate positions (only real matches)
187
+ const candidates = [];
188
+ if (boldIdx !== -1) candidates.push({ type: "bold", idx: boldIdx });
189
+ if (codeIdx !== -1) candidates.push({ type: "code", idx: codeIdx });
190
+ if (italicIdx !== -1 && italicIdx !== boldIdx) candidates.push({ type: "italic", idx: italicIdx });
191
+
192
+ if (candidates.length === 0) {
193
+ // No more markers — push rest as plain text
194
+ if (remaining) segments.push({ type: "text", text: { content: remaining } });
195
+ break;
196
+ }
197
+
198
+ // Pick the earliest marker
199
+ candidates.sort((a, b) => a.idx - b.idx);
200
+ const first = candidates[0];
201
+
202
+ // Push any text before the marker as plain
203
+ if (first.idx > 0) {
204
+ segments.push({ type: "text", text: { content: remaining.slice(0, first.idx) } });
205
+ }
206
+
207
+ if (first.type === "bold") {
208
+ const endBold = remaining.indexOf("**", first.idx + 2);
209
+ if (endBold === -1) {
210
+ // Unmatched — treat as plain text
211
+ segments.push({ type: "text", text: { content: remaining.slice(first.idx) } });
212
+ break;
213
+ }
214
+ const inner = remaining.slice(first.idx + 2, endBold);
215
+ segments.push({ type: "text", text: { content: inner }, annotations: { bold: true } });
216
+ remaining = remaining.slice(endBold + 2);
217
+ } else if (first.type === "code") {
218
+ const endCode = remaining.indexOf("`", first.idx + 1);
219
+ if (endCode === -1) {
220
+ segments.push({ type: "text", text: { content: remaining.slice(first.idx) } });
221
+ break;
222
+ }
223
+ const inner = remaining.slice(first.idx + 1, endCode);
224
+ segments.push({ type: "text", text: { content: inner }, annotations: { code: true } });
225
+ remaining = remaining.slice(endCode + 1);
226
+ } else if (first.type === "italic") {
227
+ const endItalic = remaining.indexOf("*", first.idx + 1);
228
+ if (endItalic === -1 || remaining[first.idx + 1] === "*") {
229
+ // Unmatched or actually a bold marker
230
+ segments.push({ type: "text", text: { content: remaining.slice(first.idx, first.idx + 1) } });
231
+ remaining = remaining.slice(first.idx + 1);
232
+ } else {
233
+ const inner = remaining.slice(first.idx + 1, endItalic);
234
+ segments.push({ type: "text", text: { content: inner }, annotations: { italic: true } });
235
+ remaining = remaining.slice(endItalic + 1);
236
+ }
237
+ }
238
+ }
239
+
240
+ return segments;
241
+ }
242
+
175
243
  function markdownToNotionBlocks(markdown) {
176
244
  // Safety check: handle undefined/null markdown
177
245
  if (!markdown || typeof markdown !== 'string') {
@@ -184,17 +252,18 @@ function markdownToNotionBlocks(markdown) {
184
252
  let i = 0;
185
253
 
186
254
  while (i < lines.length && blocks.length < 100) {
187
- const line = lines[i].trim();
255
+ const line = lines[i];
256
+ const trimmed = line.trim();
188
257
 
189
258
  // Skip empty lines
190
- if (!line) {
259
+ if (!trimmed) {
191
260
  i++;
192
261
  continue;
193
262
  }
194
263
 
195
264
  // Handle code blocks (```language...```)
196
- if (line.startsWith("```")) {
197
- const language = line.slice(3).trim() || "plain text";
265
+ if (trimmed.startsWith("```")) {
266
+ const language = trimmed.slice(3).trim() || "plain text";
198
267
  const codeLines = [];
199
268
  i++; // Move past opening ```
200
269
 
@@ -229,78 +298,147 @@ function markdownToNotionBlocks(markdown) {
229
298
  continue;
230
299
  }
231
300
 
232
- // Handle headings
233
- if (line.startsWith("# ")) {
301
+ // Handle dividers (--- or ***)
302
+ if (/^[-*_]{3,}$/.test(trimmed)) {
234
303
  blocks.push({
235
304
  object: "block",
236
- type: "heading_1",
237
- heading_1: {
238
- rich_text: [
239
- {
240
- type: "text",
241
- text: {
242
- content: line.replace(/^# /, "")
305
+ type: "divider",
306
+ divider: {}
307
+ });
308
+ i++;
309
+ continue;
310
+ }
311
+
312
+ // Handle tables (| header | header |)
313
+ if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
314
+ const tableRows = [];
315
+ while (i < lines.length && lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|")) {
316
+ const row = lines[i].trim();
317
+ // Skip separator rows (|---|---|)
318
+ if (/^\|[\s\-:|]+\|$/.test(row)) {
319
+ i++;
320
+ continue;
321
+ }
322
+ const cells = row.split("|").slice(1, -1).map(c => c.trim());
323
+ tableRows.push(cells);
324
+ i++;
325
+ }
326
+
327
+ if (tableRows.length > 0) {
328
+ const columnCount = tableRows[0].length;
329
+ const tableBlock = {
330
+ object: "block",
331
+ type: "table",
332
+ table: {
333
+ table_width: columnCount,
334
+ has_column_header: true,
335
+ has_row_header: false,
336
+ children: tableRows.map((row) => ({
337
+ type: "table_row",
338
+ table_row: {
339
+ cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
243
340
  }
244
- }
245
- ]
341
+ }))
342
+ }
343
+ };
344
+ // Pad rows that have fewer cells than the header
345
+ for (const child of tableBlock.table.children) {
346
+ while (child.table_row.cells.length < columnCount) {
347
+ child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
348
+ }
349
+ }
350
+ blocks.push(tableBlock);
351
+ }
352
+ continue;
353
+ }
354
+
355
+ // Handle headings
356
+ if (trimmed.startsWith("### ")) {
357
+ blocks.push({
358
+ object: "block",
359
+ type: "heading_3",
360
+ heading_3: {
361
+ rich_text: parseInlineRichText(trimmed.replace(/^### /, ""))
246
362
  }
247
363
  });
248
364
  i++;
249
365
  continue;
250
366
  }
251
367
 
252
- if (line.startsWith("## ")) {
368
+ if (trimmed.startsWith("## ")) {
253
369
  blocks.push({
254
370
  object: "block",
255
371
  type: "heading_2",
256
372
  heading_2: {
257
- rich_text: [
258
- {
259
- type: "text",
260
- text: {
261
- content: line.replace(/^## /, "")
262
- }
263
- }
264
- ]
373
+ rich_text: parseInlineRichText(trimmed.replace(/^## /, ""))
265
374
  }
266
375
  });
267
376
  i++;
268
377
  continue;
269
378
  }
270
379
 
271
- // Handle bullet lists
272
- if (line.startsWith("- ")) {
380
+ if (trimmed.startsWith("# ")) {
381
+ blocks.push({
382
+ object: "block",
383
+ type: "heading_1",
384
+ heading_1: {
385
+ rich_text: parseInlineRichText(trimmed.replace(/^# /, ""))
386
+ }
387
+ });
388
+ i++;
389
+ continue;
390
+ }
391
+
392
+ // Handle blockquotes (> text) → Notion callout block
393
+ if (trimmed.startsWith("> ")) {
394
+ const quoteLines = [];
395
+ while (i < lines.length && lines[i].trim().startsWith("> ")) {
396
+ quoteLines.push(lines[i].trim().replace(/^> /, ""));
397
+ i++;
398
+ }
399
+ blocks.push({
400
+ object: "block",
401
+ type: "callout",
402
+ callout: {
403
+ rich_text: parseInlineRichText(quoteLines.join(" ")),
404
+ icon: { emoji: "💡" }
405
+ }
406
+ });
407
+ continue;
408
+ }
409
+
410
+ // Handle numbered lists (1. text, 2. text, etc.)
411
+ if (/^\d+\.\s/.test(trimmed)) {
412
+ blocks.push({
413
+ object: "block",
414
+ type: "numbered_list_item",
415
+ numbered_list_item: {
416
+ rich_text: parseInlineRichText(trimmed.replace(/^\d+\.\s/, ""))
417
+ }
418
+ });
419
+ i++;
420
+ continue;
421
+ }
422
+
423
+ // Handle bullet lists (- text or * text)
424
+ if (/^[-*]\s/.test(trimmed)) {
273
425
  blocks.push({
274
426
  object: "block",
275
427
  type: "bulleted_list_item",
276
428
  bulleted_list_item: {
277
- rich_text: [
278
- {
279
- type: "text",
280
- text: {
281
- content: line.replace(/^- /, "")
282
- }
283
- }
284
- ]
429
+ rich_text: parseInlineRichText(trimmed.replace(/^[-*]\s/, ""))
285
430
  }
286
431
  });
287
432
  i++;
288
433
  continue;
289
434
  }
290
435
 
291
- // Handle regular paragraphs
436
+ // Handle regular paragraphs with inline rich text
292
437
  blocks.push({
293
438
  object: "block",
294
439
  type: "paragraph",
295
440
  paragraph: {
296
- rich_text: [
297
- {
298
- type: "text",
299
- text: {
300
- content: line
301
- }
302
- }
303
- ]
441
+ rich_text: parseInlineRichText(trimmed)
304
442
  }
305
443
  });
306
444
  i++;
@@ -309,6 +447,9 @@ function markdownToNotionBlocks(markdown) {
309
447
  return blocks;
310
448
  }
311
449
 
450
+ // Exported for testing
451
+ export { markdownToNotionBlocks, parseInlineRichText };
452
+
312
453
  export async function replacePageContent(pageId, markdown) {
313
454
  // Ensure page is unarchived before editing
314
455
  await unarchivePage(pageId);