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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chalknotes",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "A tool that simplifies blogs.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -27,24 +27,7 @@ const getPostBySlug = async (slug) => {
27
27
  })
28
28
  let content = ""
29
29
  for (const block of response.results) {
30
- switch (block.type) {
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
- module.exports = {
66
- getPostBySlug
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
- // for (const page of response.results) {
75
- // const titleProperty = page.properties["Name"];
76
- // const title = titleProperty?.title?.[0]?.plain_text;
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
- // const pageSlug = slugify(title);
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
- // if (pageSlug === slug) {
81
- // // FOUND IT
82
- // return {
83
- // title,
84
- // slug: pageSlug,
85
- // notionPageId: page.id,
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
+ '&': '&amp;',
291
+ '<': '&lt;',
292
+ '>': '&gt;',
293
+ '"': '&quot;',
294
+ "'": '&#039;'
295
+ };
296
+ return text.replace(/[&<>"']/g, m => map[m]);
297
+ }
89
298
 
90
- // throw new Error(`No post found with slug "${slug}"`);
299
+ module.exports = {
300
+ getPostBySlug
301
+ }