@stoneforge/quarry 1.12.0 → 1.14.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.
- package/README.md +2 -0
- package/dist/api/quarry-api.d.ts +9 -1
- package/dist/api/quarry-api.d.ts.map +1 -1
- package/dist/api/quarry-api.js +21 -2
- package/dist/api/quarry-api.js.map +1 -1
- package/dist/api/types.d.ts +8 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/cli/commands/auto-link-helper.d.ts +33 -0
- package/dist/cli/commands/auto-link-helper.d.ts.map +1 -0
- package/dist/cli/commands/auto-link-helper.js +74 -0
- package/dist/cli/commands/auto-link-helper.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +3 -0
- package/dist/cli/commands/crud.d.ts.map +1 -1
- package/dist/cli/commands/crud.js +144 -15
- package/dist/cli/commands/crud.js.map +1 -1
- package/dist/cli/commands/docs.js +2 -2
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/document.js +1 -1
- package/dist/cli/commands/document.js.map +1 -1
- package/dist/cli/commands/entity.js +1 -1
- package/dist/cli/commands/entity.js.map +1 -1
- package/dist/cli/commands/external-sync.d.ts +18 -0
- package/dist/cli/commands/external-sync.d.ts.map +1 -0
- package/dist/cli/commands/external-sync.js +2499 -0
- package/dist/cli/commands/external-sync.js.map +1 -0
- package/dist/cli/commands/library.js +1 -1
- package/dist/cli/commands/library.js.map +1 -1
- package/dist/cli/commands/message.js +2 -2
- package/dist/cli/commands/message.js.map +1 -1
- package/dist/cli/commands/serve.d.ts.map +1 -1
- package/dist/cli/commands/serve.js +2 -0
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/task.d.ts.map +1 -1
- package/dist/cli/commands/task.js +7 -4
- package/dist/cli/commands/task.js.map +1 -1
- package/dist/cli/commands/team.js +1 -1
- package/dist/cli/commands/team.js.map +1 -1
- package/dist/cli/commands/workflow.js +1 -1
- package/dist/cli/commands/workflow.js.map +1 -1
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +3 -0
- package/dist/cli/runner.js.map +1 -1
- package/dist/cli/utils/progress.d.ts +30 -0
- package/dist/cli/utils/progress.d.ts.map +1 -0
- package/dist/cli/utils/progress.js +47 -0
- package/dist/cli/utils/progress.js.map +1 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +34 -0
- package/dist/config/config.js.map +1 -1
- package/dist/config/defaults.d.ts +13 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +22 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/file.d.ts.map +1 -1
- package/dist/config/file.js +71 -0
- package/dist/config/file.js.map +1 -1
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +2 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/merge.d.ts.map +1 -1
- package/dist/config/merge.js +52 -1
- package/dist/config/merge.js.map +1 -1
- package/dist/config/types.d.ts +68 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +33 -0
- package/dist/config/types.js.map +1 -1
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +64 -1
- package/dist/config/validation.js.map +1 -1
- package/dist/external-sync/adapters/document-sync-adapter.d.ts +150 -0
- package/dist/external-sync/adapters/document-sync-adapter.d.ts.map +1 -0
- package/dist/external-sync/adapters/document-sync-adapter.js +325 -0
- package/dist/external-sync/adapters/document-sync-adapter.js.map +1 -0
- package/dist/external-sync/adapters/task-sync-adapter.d.ts +177 -0
- package/dist/external-sync/adapters/task-sync-adapter.d.ts.map +1 -0
- package/dist/external-sync/adapters/task-sync-adapter.js +353 -0
- package/dist/external-sync/adapters/task-sync-adapter.js.map +1 -0
- package/dist/external-sync/auto-link.d.ts +66 -0
- package/dist/external-sync/auto-link.d.ts.map +1 -0
- package/dist/external-sync/auto-link.js +98 -0
- package/dist/external-sync/auto-link.js.map +1 -0
- package/dist/external-sync/conflict-resolver.d.ts +170 -0
- package/dist/external-sync/conflict-resolver.d.ts.map +1 -0
- package/dist/external-sync/conflict-resolver.js +580 -0
- package/dist/external-sync/conflict-resolver.js.map +1 -0
- package/dist/external-sync/index.d.ts +23 -0
- package/dist/external-sync/index.d.ts.map +1 -0
- package/dist/external-sync/index.js +24 -0
- package/dist/external-sync/index.js.map +1 -0
- package/dist/external-sync/provider-registry.d.ts +113 -0
- package/dist/external-sync/provider-registry.d.ts.map +1 -0
- package/dist/external-sync/provider-registry.js +205 -0
- package/dist/external-sync/provider-registry.js.map +1 -0
- package/dist/external-sync/providers/folder/folder-document-adapter.d.ts +97 -0
- package/dist/external-sync/providers/folder/folder-document-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/folder/folder-document-adapter.js +261 -0
- package/dist/external-sync/providers/folder/folder-document-adapter.js.map +1 -0
- package/dist/external-sync/providers/folder/folder-fs.d.ts +146 -0
- package/dist/external-sync/providers/folder/folder-fs.d.ts.map +1 -0
- package/dist/external-sync/providers/folder/folder-fs.js +300 -0
- package/dist/external-sync/providers/folder/folder-fs.js.map +1 -0
- package/dist/external-sync/providers/folder/folder-provider.d.ts +28 -0
- package/dist/external-sync/providers/folder/folder-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/folder/folder-provider.js +87 -0
- package/dist/external-sync/providers/folder/folder-provider.js.map +1 -0
- package/dist/external-sync/providers/folder/index.d.ts +11 -0
- package/dist/external-sync/providers/folder/index.d.ts.map +1 -0
- package/dist/external-sync/providers/folder/index.js +13 -0
- package/dist/external-sync/providers/folder/index.js.map +1 -0
- package/dist/external-sync/providers/github/github-api.d.ts +271 -0
- package/dist/external-sync/providers/github/github-api.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-api.js +366 -0
- package/dist/external-sync/providers/github/github-api.js.map +1 -0
- package/dist/external-sync/providers/github/github-field-map.d.ts +76 -0
- package/dist/external-sync/providers/github/github-field-map.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-field-map.js +157 -0
- package/dist/external-sync/providers/github/github-field-map.js.map +1 -0
- package/dist/external-sync/providers/github/github-provider.d.ts +36 -0
- package/dist/external-sync/providers/github/github-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-provider.js +212 -0
- package/dist/external-sync/providers/github/github-provider.js.map +1 -0
- package/dist/external-sync/providers/github/github-task-adapter.d.ts +135 -0
- package/dist/external-sync/providers/github/github-task-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/github/github-task-adapter.js +374 -0
- package/dist/external-sync/providers/github/github-task-adapter.js.map +1 -0
- package/dist/external-sync/providers/github/index.d.ts +12 -0
- package/dist/external-sync/providers/github/index.d.ts.map +1 -0
- package/dist/external-sync/providers/github/index.js +15 -0
- package/dist/external-sync/providers/github/index.js.map +1 -0
- package/dist/external-sync/providers/index.d.ts +13 -0
- package/dist/external-sync/providers/index.d.ts.map +1 -0
- package/dist/external-sync/providers/index.js +15 -0
- package/dist/external-sync/providers/index.js.map +1 -0
- package/dist/external-sync/providers/linear/index.d.ts +19 -0
- package/dist/external-sync/providers/linear/index.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/index.js +19 -0
- package/dist/external-sync/providers/linear/index.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-api.d.ts +252 -0
- package/dist/external-sync/providers/linear/linear-api.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-api.js +522 -0
- package/dist/external-sync/providers/linear/linear-api.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-field-map.d.ts +135 -0
- package/dist/external-sync/providers/linear/linear-field-map.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-field-map.js +338 -0
- package/dist/external-sync/providers/linear/linear-field-map.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-provider.d.ts +52 -0
- package/dist/external-sync/providers/linear/linear-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-provider.js +169 -0
- package/dist/external-sync/providers/linear/linear-provider.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.d.ts +190 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.js +521 -0
- package/dist/external-sync/providers/linear/linear-task-adapter.js.map +1 -0
- package/dist/external-sync/providers/linear/linear-types.d.ts +114 -0
- package/dist/external-sync/providers/linear/linear-types.d.ts.map +1 -0
- package/dist/external-sync/providers/linear/linear-types.js +10 -0
- package/dist/external-sync/providers/linear/linear-types.js.map +1 -0
- package/dist/external-sync/providers/notion/index.d.ts +19 -0
- package/dist/external-sync/providers/notion/index.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/index.js +20 -0
- package/dist/external-sync/providers/notion/index.js.map +1 -0
- package/dist/external-sync/providers/notion/notion-api.d.ts +253 -0
- package/dist/external-sync/providers/notion/notion-api.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/notion-api.js +492 -0
- package/dist/external-sync/providers/notion/notion-api.js.map +1 -0
- package/dist/external-sync/providers/notion/notion-blocks.d.ts +93 -0
- package/dist/external-sync/providers/notion/notion-blocks.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/notion-blocks.js +773 -0
- package/dist/external-sync/providers/notion/notion-blocks.js.map +1 -0
- package/dist/external-sync/providers/notion/notion-document-adapter.d.ts +176 -0
- package/dist/external-sync/providers/notion/notion-document-adapter.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/notion-document-adapter.js +413 -0
- package/dist/external-sync/providers/notion/notion-document-adapter.js.map +1 -0
- package/dist/external-sync/providers/notion/notion-provider.d.ts +57 -0
- package/dist/external-sync/providers/notion/notion-provider.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/notion-provider.js +159 -0
- package/dist/external-sync/providers/notion/notion-provider.js.map +1 -0
- package/dist/external-sync/providers/notion/notion-types.d.ts +388 -0
- package/dist/external-sync/providers/notion/notion-types.d.ts.map +1 -0
- package/dist/external-sync/providers/notion/notion-types.js +47 -0
- package/dist/external-sync/providers/notion/notion-types.js.map +1 -0
- package/dist/external-sync/sync-engine.d.ts +364 -0
- package/dist/external-sync/sync-engine.d.ts.map +1 -0
- package/dist/external-sync/sync-engine.js +1154 -0
- package/dist/external-sync/sync-engine.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.js +8 -8
- package/dist/server/index.js.map +1 -1
- package/dist/services/inbox.js +1 -1
- package/dist/sync/hash.d.ts +5 -0
- package/dist/sync/hash.d.ts.map +1 -1
- package/dist/sync/hash.js +21 -2
- package/dist/sync/hash.js.map +1 -1
- package/package.json +10 -12
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion Blocks ↔ Markdown Converter
|
|
3
|
+
*
|
|
4
|
+
* Bidirectional converter between markdown text and Notion block format.
|
|
5
|
+
* Internal module with no external dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Supported conversions:
|
|
8
|
+
* - Paragraphs ↔ paragraph blocks
|
|
9
|
+
* - Headings (# ## ###) ↔ heading_1, heading_2, heading_3 blocks
|
|
10
|
+
* - Bulleted lists (- or *) ↔ bulleted_list_item blocks
|
|
11
|
+
* - Numbered lists (1. 2. 3.) ↔ numbered_list_item blocks
|
|
12
|
+
* - Code blocks (```) ↔ code blocks (with language annotation)
|
|
13
|
+
* - Blockquotes (>) ↔ quote blocks
|
|
14
|
+
* - Checkboxes (- [ ] / - [x]) ↔ to_do blocks
|
|
15
|
+
* - Rich text: **bold**, *italic*, `inline code`, [links](url)
|
|
16
|
+
*/
|
|
17
|
+
import { DEFAULT_ANNOTATIONS } from './notion-types.js';
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Rich Text Chunking (Notion 2000-character limit)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Maximum length for a single rich_text element's text.content in the Notion API.
|
|
23
|
+
* @see https://developers.notion.com/reference/block
|
|
24
|
+
*/
|
|
25
|
+
export const NOTION_MAX_TEXT_LENGTH = 2000;
|
|
26
|
+
/**
|
|
27
|
+
* Maximum number of elements in a single rich_text array in the Notion API.
|
|
28
|
+
* When a block's rich_text array exceeds this limit, the block must be split
|
|
29
|
+
* into multiple blocks of the same type.
|
|
30
|
+
* @see https://developers.notion.com/reference/block
|
|
31
|
+
*/
|
|
32
|
+
export const NOTION_MAX_RICH_TEXT_ARRAY_LENGTH = 100;
|
|
33
|
+
/**
|
|
34
|
+
* Split a text string into chunks of at most `maxLength` characters,
|
|
35
|
+
* preferring word boundaries when possible.
|
|
36
|
+
*/
|
|
37
|
+
function splitTextAtWordBoundaries(text, maxLength = NOTION_MAX_TEXT_LENGTH) {
|
|
38
|
+
if (text.length <= maxLength) {
|
|
39
|
+
return [text];
|
|
40
|
+
}
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let remaining = text;
|
|
43
|
+
while (remaining.length > 0) {
|
|
44
|
+
if (remaining.length <= maxLength) {
|
|
45
|
+
chunks.push(remaining);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
// Split at last space before maxLength
|
|
49
|
+
let splitAt = remaining.lastIndexOf(' ', maxLength);
|
|
50
|
+
if (splitAt <= 0)
|
|
51
|
+
splitAt = maxLength; // No space found, hard split
|
|
52
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
53
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
54
|
+
}
|
|
55
|
+
return chunks;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Split a plain text string into multiple NotionRichText elements,
|
|
59
|
+
* each with text.content at most `maxLength` characters.
|
|
60
|
+
* Splits at word boundaries when possible.
|
|
61
|
+
*/
|
|
62
|
+
export function chunkRichText(text, maxLength = NOTION_MAX_TEXT_LENGTH) {
|
|
63
|
+
if (text.length <= maxLength) {
|
|
64
|
+
return [createPlainRichText(text)];
|
|
65
|
+
}
|
|
66
|
+
return splitTextAtWordBoundaries(text, maxLength).map((chunk) => createPlainRichText(chunk));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Split a single NotionRichText element into multiple elements if its content
|
|
70
|
+
* exceeds `maxLength`, preserving annotations and link information.
|
|
71
|
+
*/
|
|
72
|
+
function chunkRichTextElement(rt, maxLength = NOTION_MAX_TEXT_LENGTH) {
|
|
73
|
+
const content = rt.text?.content ?? rt.plain_text;
|
|
74
|
+
if (content.length <= maxLength) {
|
|
75
|
+
return [rt];
|
|
76
|
+
}
|
|
77
|
+
const textChunks = splitTextAtWordBoundaries(content, maxLength);
|
|
78
|
+
return textChunks.map((chunk) => ({
|
|
79
|
+
type: rt.type,
|
|
80
|
+
text: rt.text ? { content: chunk, link: rt.text.link } : undefined,
|
|
81
|
+
annotations: { ...rt.annotations },
|
|
82
|
+
plain_text: chunk,
|
|
83
|
+
href: rt.href,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if two NotionRichText elements have identical annotations.
|
|
88
|
+
*/
|
|
89
|
+
function annotationsEqual(a, b) {
|
|
90
|
+
return (a.bold === b.bold &&
|
|
91
|
+
a.italic === b.italic &&
|
|
92
|
+
a.strikethrough === b.strikethrough &&
|
|
93
|
+
a.underline === b.underline &&
|
|
94
|
+
a.code === b.code &&
|
|
95
|
+
a.color === b.color);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Check if two NotionRichText elements can be merged (same annotations and link).
|
|
99
|
+
*/
|
|
100
|
+
function canMergeRichText(a, b) {
|
|
101
|
+
if (a.type !== 'text' || b.type !== 'text')
|
|
102
|
+
return false;
|
|
103
|
+
if (!annotationsEqual(a.annotations, b.annotations))
|
|
104
|
+
return false;
|
|
105
|
+
// Both must have same link (both null, or both same URL)
|
|
106
|
+
const aLink = a.text?.link?.url ?? null;
|
|
107
|
+
const bLink = b.text?.link?.url ?? null;
|
|
108
|
+
return aLink === bLink;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Merge consecutive rich_text elements that share the same annotations and link.
|
|
112
|
+
* This reduces array length when inline-heavy markdown produces many alternating
|
|
113
|
+
* plain/formatted segments that happen to be adjacent with identical formatting.
|
|
114
|
+
* Respects the per-element character limit to avoid undoing character chunking.
|
|
115
|
+
*/
|
|
116
|
+
function mergeAdjacentRichText(richTexts, maxLength = NOTION_MAX_TEXT_LENGTH) {
|
|
117
|
+
if (richTexts.length <= 1)
|
|
118
|
+
return richTexts;
|
|
119
|
+
const merged = [richTexts[0]];
|
|
120
|
+
for (let i = 1; i < richTexts.length; i++) {
|
|
121
|
+
const prev = merged[merged.length - 1];
|
|
122
|
+
const curr = richTexts[i];
|
|
123
|
+
const prevLen = (prev.text?.content ?? prev.plain_text).length;
|
|
124
|
+
const currLen = (curr.text?.content ?? curr.plain_text).length;
|
|
125
|
+
if (canMergeRichText(prev, curr) && prevLen + currLen <= maxLength) {
|
|
126
|
+
// Merge: concatenate content (stays within character limit)
|
|
127
|
+
const content = (prev.text?.content ?? prev.plain_text) + (curr.text?.content ?? curr.plain_text);
|
|
128
|
+
merged[merged.length - 1] = {
|
|
129
|
+
type: prev.type,
|
|
130
|
+
text: prev.text ? { content, link: prev.text.link } : undefined,
|
|
131
|
+
annotations: { ...prev.annotations },
|
|
132
|
+
plain_text: content,
|
|
133
|
+
href: prev.href,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
merged.push(curr);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return merged;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Ensure all elements in a rich_text array respect both:
|
|
144
|
+
* 1. The Notion 2000-character-per-element limit (splits oversized elements)
|
|
145
|
+
* 2. Reduced array length via merging adjacent same-formatted elements
|
|
146
|
+
*
|
|
147
|
+
* Merge first (to reduce count), then chunk (to enforce character limits).
|
|
148
|
+
*/
|
|
149
|
+
function ensureRichTextWithinLimits(richTexts, maxLength = NOTION_MAX_TEXT_LENGTH) {
|
|
150
|
+
// First merge adjacent elements with same formatting to reduce count
|
|
151
|
+
const merged = mergeAdjacentRichText(richTexts, maxLength);
|
|
152
|
+
// Then split any elements that exceed the character limit
|
|
153
|
+
return merged.flatMap((rt) => chunkRichTextElement(rt, maxLength));
|
|
154
|
+
}
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Markdown → Notion Blocks
|
|
157
|
+
// ============================================================================
|
|
158
|
+
/**
|
|
159
|
+
* Convert markdown text to an array of Notion blocks.
|
|
160
|
+
*
|
|
161
|
+
* Parses markdown line-by-line, recognizing headings, lists, code blocks,
|
|
162
|
+
* blockquotes, checkboxes, and paragraphs. Rich text formatting (bold,
|
|
163
|
+
* italic, inline code, links) is preserved within each block.
|
|
164
|
+
*/
|
|
165
|
+
export function markdownToNotionBlocks(markdown) {
|
|
166
|
+
if (!markdown || markdown.trim() === '') {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const lines = markdown.split('\n');
|
|
170
|
+
const blocks = [];
|
|
171
|
+
let i = 0;
|
|
172
|
+
while (i < lines.length) {
|
|
173
|
+
const line = lines[i];
|
|
174
|
+
// Code block (fenced with ```)
|
|
175
|
+
if (line.trimStart().startsWith('```')) {
|
|
176
|
+
const indent = line.indexOf('```');
|
|
177
|
+
const langMatch = line.slice(indent + 3).trim();
|
|
178
|
+
const language = langMatch || 'plain text';
|
|
179
|
+
const codeLines = [];
|
|
180
|
+
i++;
|
|
181
|
+
while (i < lines.length && !lines[i].trimStart().startsWith('```')) {
|
|
182
|
+
codeLines.push(lines[i]);
|
|
183
|
+
i++;
|
|
184
|
+
}
|
|
185
|
+
// Skip closing ```
|
|
186
|
+
if (i < lines.length) {
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
blocks.push(...createCodeBlocks(codeLines.join('\n'), language));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// Empty line — skip (paragraph breaks are implicit)
|
|
193
|
+
if (line.trim() === '') {
|
|
194
|
+
i++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// Heading (# ## ###)
|
|
198
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
199
|
+
if (headingMatch) {
|
|
200
|
+
const level = headingMatch[1].length;
|
|
201
|
+
const text = headingMatch[2];
|
|
202
|
+
blocks.push(...createHeadingBlock(level, text));
|
|
203
|
+
i++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// Checkbox (- [ ] or - [x] or * [ ] or * [x])
|
|
207
|
+
const checkboxMatch = line.match(/^[-*]\s+\[([ xX])\]\s+(.*)$/);
|
|
208
|
+
if (checkboxMatch) {
|
|
209
|
+
const checked = checkboxMatch[1].toLowerCase() === 'x';
|
|
210
|
+
const text = checkboxMatch[2];
|
|
211
|
+
blocks.push(...createToDoBlock(text, checked));
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Bulleted list (- or *)
|
|
216
|
+
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
217
|
+
if (bulletMatch) {
|
|
218
|
+
const text = bulletMatch[1];
|
|
219
|
+
blocks.push(...createBulletedListItemBlock(text));
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Numbered list (1. 2. etc.)
|
|
224
|
+
const numberedMatch = line.match(/^\d+\.\s+(.+)$/);
|
|
225
|
+
if (numberedMatch) {
|
|
226
|
+
const text = numberedMatch[1];
|
|
227
|
+
blocks.push(...createNumberedListItemBlock(text));
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Blockquote (>)
|
|
232
|
+
const quoteMatch = line.match(/^>\s?(.*)$/);
|
|
233
|
+
if (quoteMatch) {
|
|
234
|
+
// Collect consecutive blockquote lines
|
|
235
|
+
const quoteLines = [quoteMatch[1]];
|
|
236
|
+
i++;
|
|
237
|
+
while (i < lines.length && lines[i].match(/^>\s?(.*)/)) {
|
|
238
|
+
const nextQuoteMatch = lines[i].match(/^>\s?(.*)$/);
|
|
239
|
+
if (nextQuoteMatch) {
|
|
240
|
+
quoteLines.push(nextQuoteMatch[1]);
|
|
241
|
+
}
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
blocks.push(...createQuoteBlock(quoteLines.join('\n')));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
// Paragraph (default)
|
|
248
|
+
// Collect consecutive non-empty, non-special lines as a single paragraph
|
|
249
|
+
const paragraphLines = [line];
|
|
250
|
+
i++;
|
|
251
|
+
while (i < lines.length) {
|
|
252
|
+
const nextLine = lines[i];
|
|
253
|
+
// Stop paragraph on empty line or any special syntax
|
|
254
|
+
if (nextLine.trim() === '' ||
|
|
255
|
+
nextLine.match(/^#{1,3}\s+/) ||
|
|
256
|
+
nextLine.match(/^[-*]\s+/) ||
|
|
257
|
+
nextLine.match(/^\d+\.\s+/) ||
|
|
258
|
+
nextLine.match(/^>\s?/) ||
|
|
259
|
+
nextLine.trimStart().startsWith('```')) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
paragraphLines.push(nextLine);
|
|
263
|
+
i++;
|
|
264
|
+
}
|
|
265
|
+
blocks.push(...createParagraphBlock(paragraphLines.join('\n')));
|
|
266
|
+
}
|
|
267
|
+
return blocks;
|
|
268
|
+
}
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// Notion Blocks → Markdown
|
|
271
|
+
// ============================================================================
|
|
272
|
+
/**
|
|
273
|
+
* Convert Notion blocks back to a markdown string.
|
|
274
|
+
*
|
|
275
|
+
* Handles all supported block types. Unsupported block types produce
|
|
276
|
+
* a fallback `[Unsupported: {type}]` text.
|
|
277
|
+
*/
|
|
278
|
+
export function notionBlocksToMarkdown(blocks) {
|
|
279
|
+
if (!blocks || blocks.length === 0) {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
const lines = [];
|
|
283
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
284
|
+
const block = blocks[i];
|
|
285
|
+
const prevBlock = i > 0 ? blocks[i - 1] : null;
|
|
286
|
+
// Add blank line between blocks of different types,
|
|
287
|
+
// but not between consecutive list items of the same type
|
|
288
|
+
if (prevBlock && i > 0) {
|
|
289
|
+
const needsBlankLine = !isSameListType(prevBlock.type, block.type);
|
|
290
|
+
if (needsBlankLine) {
|
|
291
|
+
lines.push('');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const markdown = blockToMarkdown(block);
|
|
295
|
+
if (markdown !== null) {
|
|
296
|
+
lines.push(markdown);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Block Creation Helpers (markdown → Notion)
|
|
303
|
+
// ============================================================================
|
|
304
|
+
/**
|
|
305
|
+
* Split a rich_text array into chunks respecting the Notion 100-element limit.
|
|
306
|
+
* Each chunk contains at most NOTION_MAX_RICH_TEXT_ARRAY_LENGTH elements.
|
|
307
|
+
*/
|
|
308
|
+
function splitRichTextArray(richTexts, maxArrayLength = NOTION_MAX_RICH_TEXT_ARRAY_LENGTH) {
|
|
309
|
+
if (richTexts.length <= maxArrayLength) {
|
|
310
|
+
return [richTexts];
|
|
311
|
+
}
|
|
312
|
+
const chunks = [];
|
|
313
|
+
for (let i = 0; i < richTexts.length; i += maxArrayLength) {
|
|
314
|
+
chunks.push(richTexts.slice(i, i + maxArrayLength));
|
|
315
|
+
}
|
|
316
|
+
return chunks;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Prepare rich_text for a block: parse inline markdown, enforce character limits,
|
|
320
|
+
* merge adjacent elements, and split into chunks if array exceeds 100 elements.
|
|
321
|
+
*/
|
|
322
|
+
function prepareRichTextChunks(text) {
|
|
323
|
+
const richTexts = ensureRichTextWithinLimits(parseInlineMarkdown(text));
|
|
324
|
+
return splitRichTextArray(richTexts);
|
|
325
|
+
}
|
|
326
|
+
function createParagraphBlock(text) {
|
|
327
|
+
return prepareRichTextChunks(text).map((richText) => ({
|
|
328
|
+
type: 'paragraph',
|
|
329
|
+
paragraph: { rich_text: richText },
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
function createHeadingBlock(level, text) {
|
|
333
|
+
const type = `heading_${level}`;
|
|
334
|
+
const chunks = prepareRichTextChunks(text);
|
|
335
|
+
// First chunk keeps the heading type; overflow chunks become paragraphs
|
|
336
|
+
// (Notion doesn't support multiple consecutive headings for a single logical heading)
|
|
337
|
+
return chunks.map((richText, i) => {
|
|
338
|
+
if (i === 0) {
|
|
339
|
+
return { type, [type]: { rich_text: richText } };
|
|
340
|
+
}
|
|
341
|
+
return { type: 'paragraph', paragraph: { rich_text: richText } };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
function createBulletedListItemBlock(text) {
|
|
345
|
+
return prepareRichTextChunks(text).map((richText) => ({
|
|
346
|
+
type: 'bulleted_list_item',
|
|
347
|
+
bulleted_list_item: { rich_text: richText },
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
function createNumberedListItemBlock(text) {
|
|
351
|
+
return prepareRichTextChunks(text).map((richText) => ({
|
|
352
|
+
type: 'numbered_list_item',
|
|
353
|
+
numbered_list_item: { rich_text: richText },
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Create one or more code blocks from a code string.
|
|
358
|
+
* If the code exceeds NOTION_MAX_TEXT_LENGTH, it is split into multiple
|
|
359
|
+
* consecutive code blocks with the same language annotation.
|
|
360
|
+
* Code splits prefer line boundaries, then word boundaries.
|
|
361
|
+
*/
|
|
362
|
+
function createCodeBlocks(code, language) {
|
|
363
|
+
const normalizedLang = normalizeLanguage(language);
|
|
364
|
+
if (code.length <= NOTION_MAX_TEXT_LENGTH) {
|
|
365
|
+
return [
|
|
366
|
+
{
|
|
367
|
+
type: 'code',
|
|
368
|
+
code: {
|
|
369
|
+
rich_text: [createPlainRichText(code)],
|
|
370
|
+
language: normalizedLang,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
// Split code at line boundaries when possible
|
|
376
|
+
const chunks = splitCodeAtLineBoundaries(code, NOTION_MAX_TEXT_LENGTH);
|
|
377
|
+
return chunks.map((chunk) => ({
|
|
378
|
+
type: 'code',
|
|
379
|
+
code: {
|
|
380
|
+
rich_text: [createPlainRichText(chunk)],
|
|
381
|
+
language: normalizedLang,
|
|
382
|
+
},
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Split code text into chunks, preferring line boundaries (\n) over word boundaries.
|
|
387
|
+
*/
|
|
388
|
+
function splitCodeAtLineBoundaries(text, maxLength) {
|
|
389
|
+
if (text.length <= maxLength) {
|
|
390
|
+
return [text];
|
|
391
|
+
}
|
|
392
|
+
const chunks = [];
|
|
393
|
+
let remaining = text;
|
|
394
|
+
while (remaining.length > 0) {
|
|
395
|
+
if (remaining.length <= maxLength) {
|
|
396
|
+
chunks.push(remaining);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
// Prefer splitting at a newline before maxLength
|
|
400
|
+
let splitAt = remaining.lastIndexOf('\n', maxLength);
|
|
401
|
+
if (splitAt <= 0) {
|
|
402
|
+
// No newline found; try word boundary
|
|
403
|
+
splitAt = remaining.lastIndexOf(' ', maxLength);
|
|
404
|
+
}
|
|
405
|
+
if (splitAt <= 0) {
|
|
406
|
+
// No boundary found; hard split
|
|
407
|
+
splitAt = maxLength;
|
|
408
|
+
}
|
|
409
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
410
|
+
// For newline splits, skip the newline character itself
|
|
411
|
+
if (remaining[splitAt] === '\n') {
|
|
412
|
+
remaining = remaining.slice(splitAt + 1);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return chunks;
|
|
419
|
+
}
|
|
420
|
+
function createQuoteBlock(text) {
|
|
421
|
+
return prepareRichTextChunks(text).map((richText) => ({
|
|
422
|
+
type: 'quote',
|
|
423
|
+
quote: { rich_text: richText },
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
function createToDoBlock(text, checked) {
|
|
427
|
+
const chunks = prepareRichTextChunks(text);
|
|
428
|
+
return chunks.map((richText, i) => ({
|
|
429
|
+
type: 'to_do',
|
|
430
|
+
to_do: {
|
|
431
|
+
rich_text: richText,
|
|
432
|
+
// Only the first block carries the checked state; overflow blocks are unchecked
|
|
433
|
+
checked: i === 0 ? checked : false,
|
|
434
|
+
},
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// Block → Markdown Helpers
|
|
439
|
+
// ============================================================================
|
|
440
|
+
function blockToMarkdown(block) {
|
|
441
|
+
switch (block.type) {
|
|
442
|
+
case 'paragraph': {
|
|
443
|
+
const b = block;
|
|
444
|
+
return richTextToMarkdown(b.paragraph.rich_text);
|
|
445
|
+
}
|
|
446
|
+
case 'heading_1': {
|
|
447
|
+
const b = block;
|
|
448
|
+
return `# ${richTextToMarkdown(b.heading_1.rich_text)}`;
|
|
449
|
+
}
|
|
450
|
+
case 'heading_2': {
|
|
451
|
+
const b = block;
|
|
452
|
+
return `## ${richTextToMarkdown(b.heading_2.rich_text)}`;
|
|
453
|
+
}
|
|
454
|
+
case 'heading_3': {
|
|
455
|
+
const b = block;
|
|
456
|
+
return `### ${richTextToMarkdown(b.heading_3.rich_text)}`;
|
|
457
|
+
}
|
|
458
|
+
case 'bulleted_list_item': {
|
|
459
|
+
const b = block;
|
|
460
|
+
return `- ${richTextToMarkdown(b.bulleted_list_item.rich_text)}`;
|
|
461
|
+
}
|
|
462
|
+
case 'numbered_list_item': {
|
|
463
|
+
const b = block;
|
|
464
|
+
return `1. ${richTextToMarkdown(b.numbered_list_item.rich_text)}`;
|
|
465
|
+
}
|
|
466
|
+
case 'code': {
|
|
467
|
+
const b = block;
|
|
468
|
+
const lang = b.code.language === 'plain text' ? '' : b.code.language;
|
|
469
|
+
const codeText = richTextToPlainText(b.code.rich_text);
|
|
470
|
+
return `\`\`\`${lang}\n${codeText}\n\`\`\``;
|
|
471
|
+
}
|
|
472
|
+
case 'quote': {
|
|
473
|
+
const b = block;
|
|
474
|
+
const quoteText = richTextToMarkdown(b.quote.rich_text);
|
|
475
|
+
// Prefix each line with >
|
|
476
|
+
return quoteText
|
|
477
|
+
.split('\n')
|
|
478
|
+
.map((line) => `> ${line}`)
|
|
479
|
+
.join('\n');
|
|
480
|
+
}
|
|
481
|
+
case 'to_do': {
|
|
482
|
+
const b = block;
|
|
483
|
+
const checkbox = b.to_do.checked ? '[x]' : '[ ]';
|
|
484
|
+
return `- ${checkbox} ${richTextToMarkdown(b.to_do.rich_text)}`;
|
|
485
|
+
}
|
|
486
|
+
default:
|
|
487
|
+
// Unsupported block type fallback
|
|
488
|
+
return `[Unsupported: ${block.type}]`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Check if two block types belong to the same list category
|
|
493
|
+
* (so we don't insert blank lines between consecutive items).
|
|
494
|
+
*/
|
|
495
|
+
function isSameListType(typeA, typeB) {
|
|
496
|
+
const listTypes = new Set([
|
|
497
|
+
'bulleted_list_item',
|
|
498
|
+
'numbered_list_item',
|
|
499
|
+
'to_do',
|
|
500
|
+
]);
|
|
501
|
+
// Same exact type (e.g., both bulleted_list_item)
|
|
502
|
+
if (typeA === typeB && listTypes.has(typeA)) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
// ============================================================================
|
|
508
|
+
// URL Validation
|
|
509
|
+
// ============================================================================
|
|
510
|
+
/**
|
|
511
|
+
* Check whether a URL string is valid for use in a Notion link block.
|
|
512
|
+
* Notion only accepts absolute http: or https: URLs. Relative paths,
|
|
513
|
+
* fragment-only references (#section), workspace element IDs (el-xxxx),
|
|
514
|
+
* empty strings, and malformed URLs are all rejected.
|
|
515
|
+
*/
|
|
516
|
+
export function isValidNotionUrl(url) {
|
|
517
|
+
try {
|
|
518
|
+
const parsed = new URL(url);
|
|
519
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// ============================================================================
|
|
526
|
+
// Rich Text Parsing (markdown inline → Notion rich text)
|
|
527
|
+
// ============================================================================
|
|
528
|
+
/**
|
|
529
|
+
* Parse inline markdown formatting into an array of NotionRichText objects.
|
|
530
|
+
*
|
|
531
|
+
* Supports:
|
|
532
|
+
* - **bold** or __bold__
|
|
533
|
+
* - *italic* or _italic_
|
|
534
|
+
* - `inline code`
|
|
535
|
+
* - [link text](url)
|
|
536
|
+
* - Combinations thereof
|
|
537
|
+
*/
|
|
538
|
+
export function parseInlineMarkdown(text) {
|
|
539
|
+
if (!text) {
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
const richTexts = [];
|
|
543
|
+
// Regex for matching inline markdown tokens
|
|
544
|
+
// Order matters: bold before italic to avoid ambiguity with ** vs *
|
|
545
|
+
const inlinePattern = /(\*\*(.+?)\*\*|__(.+?)__|\*(.+?)\*|_([^_]+?)_|`([^`]+?)`|\[([^\]]+?)\]\(([^)]+?)\))/g;
|
|
546
|
+
let lastIndex = 0;
|
|
547
|
+
let match;
|
|
548
|
+
while ((match = inlinePattern.exec(text)) !== null) {
|
|
549
|
+
// Add plain text before this match
|
|
550
|
+
if (match.index > lastIndex) {
|
|
551
|
+
const plainText = text.slice(lastIndex, match.index);
|
|
552
|
+
richTexts.push(createPlainRichText(plainText));
|
|
553
|
+
}
|
|
554
|
+
const fullMatch = match[0];
|
|
555
|
+
if (match[2] !== undefined || match[3] !== undefined) {
|
|
556
|
+
// **bold** or __bold__
|
|
557
|
+
const boldText = match[2] ?? match[3];
|
|
558
|
+
richTexts.push(createAnnotatedRichText(boldText, { bold: true }));
|
|
559
|
+
}
|
|
560
|
+
else if (match[4] !== undefined || match[5] !== undefined) {
|
|
561
|
+
// *italic* or _italic_
|
|
562
|
+
const italicText = match[4] ?? match[5];
|
|
563
|
+
richTexts.push(createAnnotatedRichText(italicText, { italic: true }));
|
|
564
|
+
}
|
|
565
|
+
else if (match[6] !== undefined) {
|
|
566
|
+
// `inline code`
|
|
567
|
+
richTexts.push(createAnnotatedRichText(match[6], { code: true }));
|
|
568
|
+
}
|
|
569
|
+
else if (match[7] !== undefined && match[8] !== undefined) {
|
|
570
|
+
// [link text](url) — validate URL before creating a link block
|
|
571
|
+
if (isValidNotionUrl(match[8])) {
|
|
572
|
+
richTexts.push(createLinkRichText(match[7], match[8]));
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Invalid URL — render as plain text to avoid Notion rejection
|
|
576
|
+
richTexts.push(createPlainRichText(match[7]));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
lastIndex = match.index + fullMatch.length;
|
|
580
|
+
}
|
|
581
|
+
// Add remaining plain text after last match
|
|
582
|
+
if (lastIndex < text.length) {
|
|
583
|
+
richTexts.push(createPlainRichText(text.slice(lastIndex)));
|
|
584
|
+
}
|
|
585
|
+
// If no matches found at all, return the text as plain
|
|
586
|
+
if (richTexts.length === 0) {
|
|
587
|
+
richTexts.push(createPlainRichText(text));
|
|
588
|
+
}
|
|
589
|
+
return richTexts;
|
|
590
|
+
}
|
|
591
|
+
// ============================================================================
|
|
592
|
+
// Rich Text → Markdown (Notion rich text → inline markdown)
|
|
593
|
+
// ============================================================================
|
|
594
|
+
/**
|
|
595
|
+
* Convert an array of Notion rich text objects back to inline markdown.
|
|
596
|
+
*/
|
|
597
|
+
export function richTextToMarkdown(richTexts) {
|
|
598
|
+
if (!richTexts || richTexts.length === 0) {
|
|
599
|
+
return '';
|
|
600
|
+
}
|
|
601
|
+
return richTexts.map(richTextElementToMarkdown).join('');
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Convert rich text array to plain text (no formatting).
|
|
605
|
+
* Used for code blocks where we want raw content.
|
|
606
|
+
*/
|
|
607
|
+
function richTextToPlainText(richTexts) {
|
|
608
|
+
if (!richTexts || richTexts.length === 0) {
|
|
609
|
+
return '';
|
|
610
|
+
}
|
|
611
|
+
return richTexts.map((rt) => rt.plain_text).join('');
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Convert a single rich text element to its markdown representation.
|
|
615
|
+
*/
|
|
616
|
+
function richTextElementToMarkdown(rt) {
|
|
617
|
+
let text = rt.plain_text;
|
|
618
|
+
const { annotations } = rt;
|
|
619
|
+
const link = rt.text?.link;
|
|
620
|
+
// Apply inline code first (code takes precedence, no nesting inside code)
|
|
621
|
+
if (annotations.code) {
|
|
622
|
+
text = `\`${text}\``;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
// Apply bold and italic (can be combined)
|
|
626
|
+
if (annotations.bold && annotations.italic) {
|
|
627
|
+
text = `***${text}***`;
|
|
628
|
+
}
|
|
629
|
+
else if (annotations.bold) {
|
|
630
|
+
text = `**${text}**`;
|
|
631
|
+
}
|
|
632
|
+
else if (annotations.italic) {
|
|
633
|
+
text = `*${text}*`;
|
|
634
|
+
}
|
|
635
|
+
// Apply link wrapping
|
|
636
|
+
if (link?.url) {
|
|
637
|
+
text = `[${text}](${link.url})`;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return text;
|
|
641
|
+
}
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// Rich Text Construction Helpers
|
|
644
|
+
// ============================================================================
|
|
645
|
+
/**
|
|
646
|
+
* Create a plain (unformatted) rich text element.
|
|
647
|
+
*/
|
|
648
|
+
function createPlainRichText(content) {
|
|
649
|
+
return {
|
|
650
|
+
type: 'text',
|
|
651
|
+
text: {
|
|
652
|
+
content,
|
|
653
|
+
link: null,
|
|
654
|
+
},
|
|
655
|
+
annotations: { ...DEFAULT_ANNOTATIONS },
|
|
656
|
+
plain_text: content,
|
|
657
|
+
href: null,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Create a rich text element with specific annotations.
|
|
662
|
+
*/
|
|
663
|
+
function createAnnotatedRichText(content, annotationOverrides) {
|
|
664
|
+
const annotations = {
|
|
665
|
+
...DEFAULT_ANNOTATIONS,
|
|
666
|
+
...annotationOverrides,
|
|
667
|
+
};
|
|
668
|
+
return {
|
|
669
|
+
type: 'text',
|
|
670
|
+
text: {
|
|
671
|
+
content,
|
|
672
|
+
link: null,
|
|
673
|
+
},
|
|
674
|
+
annotations,
|
|
675
|
+
plain_text: content,
|
|
676
|
+
href: null,
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Create a rich text element with a link.
|
|
681
|
+
*/
|
|
682
|
+
function createLinkRichText(content, url) {
|
|
683
|
+
return {
|
|
684
|
+
type: 'text',
|
|
685
|
+
text: {
|
|
686
|
+
content,
|
|
687
|
+
link: { url },
|
|
688
|
+
},
|
|
689
|
+
annotations: { ...DEFAULT_ANNOTATIONS },
|
|
690
|
+
plain_text: content,
|
|
691
|
+
href: url,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// ============================================================================
|
|
695
|
+
// Language Normalization
|
|
696
|
+
// ============================================================================
|
|
697
|
+
/**
|
|
698
|
+
* The complete set of language identifiers accepted by the Notion API
|
|
699
|
+
* for code blocks. Any language not in this set will be rejected.
|
|
700
|
+
* @see https://developers.notion.com/reference/block#code
|
|
701
|
+
*/
|
|
702
|
+
export const NOTION_LANGUAGES = new Set([
|
|
703
|
+
'abap', 'abc', 'agda', 'arduino', 'ascii art', 'assembly', 'bash', 'basic',
|
|
704
|
+
'bnf', 'c', 'c#', 'c++', 'clojure', 'coffeescript', 'coq', 'css', 'dart',
|
|
705
|
+
'dhall', 'diff', 'docker', 'ebnf', 'elixir', 'elm', 'erlang', 'f#', 'flow',
|
|
706
|
+
'fortran', 'gherkin', 'glsl', 'go', 'graphql', 'groovy', 'haskell', 'hcl',
|
|
707
|
+
'html', 'idris', 'java', 'javascript', 'json', 'julia', 'kotlin', 'latex',
|
|
708
|
+
'less', 'lisp', 'livescript', 'llvm ir', 'lua', 'makefile', 'markdown',
|
|
709
|
+
'markup', 'matlab', 'mathematica', 'mermaid', 'nix', 'notion formula',
|
|
710
|
+
'objective-c', 'ocaml', 'pascal', 'perl', 'php', 'plain text', 'powershell',
|
|
711
|
+
'prolog', 'protobuf', 'purescript', 'python', 'r', 'racket', 'reason',
|
|
712
|
+
'ruby', 'rust', 'sass', 'scala', 'scheme', 'scss', 'shell', 'smalltalk',
|
|
713
|
+
'solidity', 'sql', 'swift', 'toml', 'typescript', 'vb.net', 'verilog',
|
|
714
|
+
'vhdl', 'visual basic', 'webassembly', 'xml', 'yaml', 'java/c/c++/c#',
|
|
715
|
+
]);
|
|
716
|
+
/**
|
|
717
|
+
* Map common code fence language aliases to their Notion-accepted equivalents.
|
|
718
|
+
* These cover short names, file extensions, and alternate spellings that
|
|
719
|
+
* markdown authors commonly use but Notion does not recognize.
|
|
720
|
+
*/
|
|
721
|
+
export const LANGUAGE_ALIASES = {
|
|
722
|
+
'tsx': 'typescript',
|
|
723
|
+
'ts': 'typescript',
|
|
724
|
+
'jsx': 'javascript',
|
|
725
|
+
'js': 'javascript',
|
|
726
|
+
'sh': 'shell',
|
|
727
|
+
'zsh': 'shell',
|
|
728
|
+
'yml': 'yaml',
|
|
729
|
+
'py': 'python',
|
|
730
|
+
'rb': 'ruby',
|
|
731
|
+
'rs': 'rust',
|
|
732
|
+
'cs': 'c#',
|
|
733
|
+
'cpp': 'c++',
|
|
734
|
+
'objc': 'objective-c',
|
|
735
|
+
'dockerfile': 'docker',
|
|
736
|
+
'tf': 'hcl',
|
|
737
|
+
'hs': 'haskell',
|
|
738
|
+
'ex': 'elixir',
|
|
739
|
+
'exs': 'elixir',
|
|
740
|
+
'kt': 'kotlin',
|
|
741
|
+
'fs': 'f#',
|
|
742
|
+
'fsharp': 'f#',
|
|
743
|
+
'csharp': 'c#',
|
|
744
|
+
'jsonc': 'json',
|
|
745
|
+
'md': 'markdown',
|
|
746
|
+
'text': 'plain text',
|
|
747
|
+
'txt': 'plain text',
|
|
748
|
+
'plaintext': 'plain text',
|
|
749
|
+
'': 'plain text',
|
|
750
|
+
};
|
|
751
|
+
/**
|
|
752
|
+
* Map a code fence language identifier to a Notion-accepted language value.
|
|
753
|
+
*
|
|
754
|
+
* 1. If the lowercased input is already in NOTION_LANGUAGES, return it.
|
|
755
|
+
* 2. If it matches a known alias, return the mapped value.
|
|
756
|
+
* 3. Otherwise, fall back to 'plain text' (always accepted by Notion).
|
|
757
|
+
*/
|
|
758
|
+
export function mapLanguageToNotion(lang) {
|
|
759
|
+
const lower = lang.toLowerCase().trim();
|
|
760
|
+
if (NOTION_LANGUAGES.has(lower))
|
|
761
|
+
return lower;
|
|
762
|
+
if (LANGUAGE_ALIASES[lower])
|
|
763
|
+
return LANGUAGE_ALIASES[lower];
|
|
764
|
+
return 'plain text'; // safe fallback
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Normalize a language identifier to Notion's format.
|
|
768
|
+
* Delegates to mapLanguageToNotion for full allowlist validation.
|
|
769
|
+
*/
|
|
770
|
+
function normalizeLanguage(lang) {
|
|
771
|
+
return mapLanguageToNotion(lang);
|
|
772
|
+
}
|
|
773
|
+
//# sourceMappingURL=notion-blocks.js.map
|