chalknotes 0.0.26 → 0.0.28
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/package.json +1 -1
- package/src/lib/getPostBySlug.js +250 -39
package/package.json
CHANGED
package/src/lib/getPostBySlug.js
CHANGED
@@ -27,24 +27,7 @@ const getPostBySlug = async (slug) => {
|
|
27
27
|
})
|
28
28
|
let content = ""
|
29
29
|
for (const block of response.results) {
|
30
|
-
|
31
|
-
case "paragraph":
|
32
|
-
const textHTML = block.paragraph.rich_text.map(text => text.plain_text).join("");
|
33
|
-
content += `<p class="mb-4">${textHTML}</p>`
|
34
|
-
break;
|
35
|
-
case "heading_1":
|
36
|
-
const headingHTML = block.heading_1.rich_text.map(text => text.plain_text).join("");
|
37
|
-
content += `<h1 class="text-2xl font-bold mb-4">${headingHTML}</h1>`
|
38
|
-
break;
|
39
|
-
case "heading_2":
|
40
|
-
const heading2HTML = block.heading_2.rich_text.map(text => text.plain_text).join("");
|
41
|
-
content += `<h2 class="text-xl font-bold mb-4">${heading2HTML}</h2>`
|
42
|
-
break;
|
43
|
-
case "heading_3":
|
44
|
-
const heading3HTML = block.heading_3.rich_text.map(text => text.plain_text).join("");
|
45
|
-
content += `<h3 class="text-lg font-bold mb-4">${heading3HTML}</h3>`
|
46
|
-
break;
|
47
|
-
}
|
30
|
+
content += processBlock(block);
|
48
31
|
}
|
49
32
|
return {
|
50
33
|
title,
|
@@ -62,29 +45,257 @@ const getPostBySlug = async (slug) => {
|
|
62
45
|
}
|
63
46
|
}
|
64
47
|
|
65
|
-
|
66
|
-
|
48
|
+
/**
|
49
|
+
* Process individual Notion blocks and convert to HTML
|
50
|
+
* @param {Object} block - Notion block object
|
51
|
+
* @returns {string} HTML string
|
52
|
+
*/
|
53
|
+
function processBlock(block) {
|
54
|
+
switch (block.type) {
|
55
|
+
case "paragraph":
|
56
|
+
return processRichText(block.paragraph.rich_text, "p", "mb-6 leading-relaxed text-gray-700");
|
57
|
+
|
58
|
+
case "heading_1":
|
59
|
+
return processRichText(block.heading_1.rich_text, "h1", "text-3xl font-bold mb-6 mt-8 text-gray-900 border-b border-gray-200 pb-2");
|
60
|
+
|
61
|
+
case "heading_2":
|
62
|
+
return processRichText(block.heading_2.rich_text, "h2", "text-2xl font-semibold mb-4 mt-6 text-gray-900");
|
63
|
+
|
64
|
+
case "heading_3":
|
65
|
+
return processRichText(block.heading_3.rich_text, "h3", "text-xl font-medium mb-3 mt-5 text-gray-900");
|
66
|
+
|
67
|
+
case "bulleted_list_item":
|
68
|
+
return processRichText(block.bulleted_list_item.rich_text, "li", "mb-2 ml-4");
|
69
|
+
|
70
|
+
case "numbered_list_item":
|
71
|
+
return processRichText(block.numbered_list_item.rich_text, "li", "mb-2 ml-4");
|
72
|
+
|
73
|
+
case "quote":
|
74
|
+
return processRichText(block.quote.rich_text, "blockquote", "border-l-4 border-blue-500 pl-6 italic text-gray-600 mb-6 bg-blue-50 py-4 rounded-r-lg");
|
75
|
+
|
76
|
+
case "code":
|
77
|
+
const codeContent = block.code.rich_text.map(text => text.plain_text).join("");
|
78
|
+
const language = block.code.language || 'text';
|
79
|
+
return `<pre class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-6 text-sm"><code class="language-${language}">${escapeHtml(codeContent)}</code></pre>`;
|
80
|
+
|
81
|
+
case "image":
|
82
|
+
return processImage(block.image);
|
83
|
+
|
84
|
+
case "divider":
|
85
|
+
return '<hr class="my-8 border-gray-200" />';
|
86
|
+
|
87
|
+
case "callout":
|
88
|
+
return processCallout(block.callout);
|
89
|
+
|
90
|
+
case "toggle":
|
91
|
+
return processToggle(block.toggle);
|
92
|
+
|
93
|
+
case "table_of_contents":
|
94
|
+
return '<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"><p class="text-blue-800 font-medium">📋 Table of Contents</p><p class="text-blue-600 text-sm">(Generated automatically)</p></div>';
|
95
|
+
|
96
|
+
case "bookmark":
|
97
|
+
return processBookmark(block.bookmark);
|
98
|
+
|
99
|
+
case "equation":
|
100
|
+
return `<div class="bg-gray-50 p-4 rounded-lg mb-6 text-center border border-gray-200"><p class="text-gray-600 mb-2">📐 Mathematical equation</p><p class="font-mono text-sm bg-white p-2 rounded border">${block.equation.expression}</p></div>`;
|
101
|
+
|
102
|
+
default:
|
103
|
+
// For unsupported blocks, try to extract plain text
|
104
|
+
if (block[block.type]?.rich_text) {
|
105
|
+
return processRichText(block[block.type].rich_text, "p", "mb-4 text-gray-500 italic");
|
106
|
+
}
|
107
|
+
return "";
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Process image block with size, alignment, and alt text
|
113
|
+
* @param {Object} image - Notion image block
|
114
|
+
* @returns {string} HTML string
|
115
|
+
*/
|
116
|
+
function processImage(image) {
|
117
|
+
const imageUrl = image.type === 'external' ? image.external.url : image.file.url;
|
118
|
+
const caption = image.caption?.map(text => text.plain_text).join("") || "";
|
119
|
+
const altText = caption || "Image";
|
120
|
+
|
121
|
+
// Get image size from Notion properties
|
122
|
+
let sizeClass = "w-full"; // Default full width
|
123
|
+
let maxWidthClass = "max-w-full";
|
124
|
+
|
125
|
+
// Check if image has size information
|
126
|
+
if (image.width && image.height) {
|
127
|
+
// Calculate aspect ratio and apply appropriate sizing
|
128
|
+
const aspectRatio = image.width / image.height;
|
129
|
+
|
130
|
+
if (aspectRatio > 2) {
|
131
|
+
// Wide images
|
132
|
+
sizeClass = "w-full";
|
133
|
+
maxWidthClass = "max-w-4xl";
|
134
|
+
} else if (aspectRatio < 0.5) {
|
135
|
+
// Tall images
|
136
|
+
sizeClass = "w-full";
|
137
|
+
maxWidthClass = "max-w-2xl";
|
138
|
+
} else {
|
139
|
+
// Square-ish images
|
140
|
+
sizeClass = "w-full";
|
141
|
+
maxWidthClass = "max-w-3xl";
|
142
|
+
}
|
143
|
+
}
|
144
|
+
|
145
|
+
// Check for Notion's size property (if available)
|
146
|
+
if (image.size) {
|
147
|
+
switch (image.size) {
|
148
|
+
case 'small':
|
149
|
+
maxWidthClass = "max-w-md";
|
150
|
+
break;
|
151
|
+
case 'medium':
|
152
|
+
maxWidthClass = "max-w-2xl";
|
153
|
+
break;
|
154
|
+
case 'large':
|
155
|
+
maxWidthClass = "max-w-4xl";
|
156
|
+
break;
|
157
|
+
default:
|
158
|
+
maxWidthClass = "max-w-full";
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
const responsiveClasses = `${sizeClass} ${maxWidthClass} h-auto rounded-lg shadow-sm`;
|
163
|
+
|
164
|
+
return `
|
165
|
+
<figure class="my-6 text-center">
|
166
|
+
<img
|
167
|
+
src="${imageUrl}"
|
168
|
+
alt="${escapeHtml(altText)}"
|
169
|
+
class="${responsiveClasses}"
|
170
|
+
loading="lazy"
|
171
|
+
/>
|
172
|
+
${caption ? `<figcaption class="text-center text-gray-500 mt-3 text-sm italic">${escapeHtml(caption)}</figcaption>` : ''}
|
173
|
+
</figure>
|
174
|
+
`.trim();
|
67
175
|
}
|
68
|
-
// const slugify = (title) =>
|
69
|
-
// title
|
70
|
-
// .toLowerCase()
|
71
|
-
// .replace(/[^a-z0-9]+/g, "-")
|
72
|
-
// .replace(/(^-|-$)/g, "");
|
73
176
|
|
74
|
-
|
75
|
-
|
76
|
-
|
177
|
+
/**
|
178
|
+
* Process callout block
|
179
|
+
* @param {Object} callout - Notion callout block
|
180
|
+
* @returns {string} HTML string
|
181
|
+
*/
|
182
|
+
function processCallout(callout) {
|
183
|
+
const content = processRichText(callout.rich_text, "div", "");
|
184
|
+
const icon = callout.icon?.emoji || "💡";
|
185
|
+
const bgColor = callout.color || "blue";
|
186
|
+
|
187
|
+
const colorClasses = {
|
188
|
+
blue: "bg-blue-50 border-blue-200 text-blue-800",
|
189
|
+
gray: "bg-gray-50 border-gray-200 text-gray-800",
|
190
|
+
yellow: "bg-yellow-50 border-yellow-200 text-yellow-800",
|
191
|
+
red: "bg-red-50 border-red-200 text-red-800",
|
192
|
+
green: "bg-green-50 border-green-200 text-green-800",
|
193
|
+
purple: "bg-purple-50 border-purple-200 text-purple-800",
|
194
|
+
pink: "bg-pink-50 border-pink-200 text-pink-800"
|
195
|
+
};
|
196
|
+
|
197
|
+
const colorClass = colorClasses[bgColor] || colorClasses.blue;
|
198
|
+
|
199
|
+
return `
|
200
|
+
<div class="${colorClass} border-l-4 p-4 my-6 rounded-r-lg shadow-sm">
|
201
|
+
<div class="flex items-start">
|
202
|
+
<span class="mr-3 text-xl flex-shrink-0">${icon}</span>
|
203
|
+
<div class="flex-1 leading-relaxed">${content}</div>
|
204
|
+
</div>
|
205
|
+
</div>
|
206
|
+
`.trim();
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Process bookmark block
|
211
|
+
* @param {Object} bookmark - Notion bookmark block
|
212
|
+
* @returns {string} HTML string
|
213
|
+
*/
|
214
|
+
function processBookmark(bookmark) {
|
215
|
+
const url = bookmark.url;
|
216
|
+
const title = bookmark.caption?.[0]?.plain_text || "Bookmark";
|
217
|
+
|
218
|
+
return `
|
219
|
+
<div class="my-6">
|
220
|
+
<a href="${url}" target="_blank" rel="noopener noreferrer" class="block border border-gray-200 rounded-lg p-4 hover:border-gray-300 hover:shadow-md transition-all duration-200">
|
221
|
+
<div class="flex items-center">
|
222
|
+
<div class="flex-1 min-w-0">
|
223
|
+
<p class="font-medium text-gray-900 truncate">${escapeHtml(title)}</p>
|
224
|
+
<p class="text-sm text-gray-500 truncate">${url}</p>
|
225
|
+
</div>
|
226
|
+
<svg class="w-5 h-5 text-gray-400 ml-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
227
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
228
|
+
</svg>
|
229
|
+
</div>
|
230
|
+
</a>
|
231
|
+
</div>
|
232
|
+
`.trim();
|
233
|
+
}
|
234
|
+
|
235
|
+
/**
|
236
|
+
* Process toggle block
|
237
|
+
* @param {Object} toggle - Notion toggle block
|
238
|
+
* @returns {string} HTML string
|
239
|
+
*/
|
240
|
+
function processToggle(toggle) {
|
241
|
+
const content = processRichText(toggle.rich_text, "div", "");
|
242
|
+
return `
|
243
|
+
<details class="my-4">
|
244
|
+
<summary class="cursor-pointer font-medium text-gray-700 hover:text-gray-900">
|
245
|
+
${content}
|
246
|
+
</summary>
|
247
|
+
<div class="mt-2 pl-4 border-l-2 border-gray-200">
|
248
|
+
<!-- Toggle content would go here if Notion API provided it -->
|
249
|
+
<p class="text-gray-600 text-sm">Toggle content not available in current API</p>
|
250
|
+
</div>
|
251
|
+
</details>
|
252
|
+
`.trim();
|
253
|
+
}
|
77
254
|
|
78
|
-
|
255
|
+
/**
|
256
|
+
* Process rich text and apply formatting
|
257
|
+
* @param {Array} richText - Array of rich text objects
|
258
|
+
* @param {string} tag - HTML tag to wrap content
|
259
|
+
* @param {string} className - CSS classes
|
260
|
+
* @returns {string} HTML string
|
261
|
+
*/
|
262
|
+
function processRichText(richText, tag, className) {
|
263
|
+
if (!richText || richText.length === 0) return "";
|
264
|
+
|
265
|
+
const content = richText.map(text => {
|
266
|
+
let result = text.plain_text;
|
267
|
+
|
268
|
+
// Apply annotations
|
269
|
+
if (text.annotations.bold) result = `<strong>${result}</strong>`;
|
270
|
+
if (text.annotations.italic) result = `<em>${result}</em>`;
|
271
|
+
if (text.annotations.strikethrough) result = `<del>${result}</del>`;
|
272
|
+
if (text.annotations.code) result = `<code class="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono">${result}</code>`;
|
273
|
+
|
274
|
+
// Apply links
|
275
|
+
if (text.href) result = `<a href="${text.href}" class="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer">${result}</a>`;
|
276
|
+
|
277
|
+
return result;
|
278
|
+
}).join("");
|
279
|
+
|
280
|
+
return `<${tag} class="${className}">${content}</${tag}>`;
|
281
|
+
}
|
79
282
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
283
|
+
/**
|
284
|
+
* Escape HTML special characters
|
285
|
+
* @param {string} text - Text to escape
|
286
|
+
* @returns {string} Escaped text
|
287
|
+
*/
|
288
|
+
function escapeHtml(text) {
|
289
|
+
const map = {
|
290
|
+
'&': '&',
|
291
|
+
'<': '<',
|
292
|
+
'>': '>',
|
293
|
+
'"': '"',
|
294
|
+
"'": '''
|
295
|
+
};
|
296
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
297
|
+
}
|
89
298
|
|
90
|
-
|
299
|
+
module.exports = {
|
300
|
+
getPostBySlug
|
301
|
+
}
|