noblog 0.2.2 → 0.3.0-beta.1

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.
Files changed (43) hide show
  1. package/README.md +277 -121
  2. package/dist/client-CkOIgSvW.js +549 -0
  3. package/dist/client-CkOIgSvW.js.map +1 -0
  4. package/dist/config-check.impl-BQvIrgNe.js +14 -0
  5. package/dist/config-check.impl-BQvIrgNe.js.map +1 -0
  6. package/dist/config-csbquMhF.js +21 -0
  7. package/dist/config-csbquMhF.js.map +1 -0
  8. package/dist/config-file-BF0G2iJq.js +27 -0
  9. package/dist/config-file-BF0G2iJq.js.map +1 -0
  10. package/dist/defaults-D9BbNYsd.js +8 -0
  11. package/dist/defaults-D9BbNYsd.js.map +1 -0
  12. package/dist/errors-DgJ15MsB.js +72 -0
  13. package/dist/errors-DgJ15MsB.js.map +1 -0
  14. package/dist/frontmatter-fields-BkXq38CO.js +71 -0
  15. package/dist/frontmatter-fields-BkXq38CO.js.map +1 -0
  16. package/dist/generate.d.ts +1 -0
  17. package/dist/generate.impl-BKu-Ix4a.js +33 -0
  18. package/dist/generate.impl-BKu-Ix4a.js.map +1 -0
  19. package/dist/generate.js +197 -932
  20. package/dist/generate.js.map +1 -1
  21. package/dist/index.d.ts +209 -18
  22. package/dist/index.js +5 -0
  23. package/package.json +32 -17
  24. package/schema/config.json +173 -0
  25. package/skills/noblog-config/SKILL.md +40 -0
  26. package/skills/noblog-config/assets/nobloger.yaml +30 -0
  27. package/skills/noblog-config/assets/thought-lite.yaml +47 -0
  28. package/skills/noblog-config/references/config-schema.md +108 -0
  29. package/skills/noblog-config/references/notion-property-types.md +42 -0
  30. package/skills/noblog-config/references/theme-examples.md +39 -0
  31. package/dist/cmds/generate.d.ts +0 -3
  32. package/dist/cmds/generate.d.ts.map +0 -1
  33. package/dist/generate.cjs +0 -962
  34. package/dist/generate.cjs.map +0 -1
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/lib/noblog/format.d.ts +0 -24
  37. package/dist/lib/noblog/format.d.ts.map +0 -1
  38. package/dist/lib/noblog/noblog.d.ts +0 -31
  39. package/dist/lib/noblog/noblog.d.ts.map +0 -1
  40. package/dist/lib/noblog/utils.d.ts +0 -36
  41. package/dist/lib/noblog/utils.d.ts.map +0 -1
  42. package/dist/lib/notion/client.d.ts +0 -8
  43. package/dist/lib/notion/client.d.ts.map +0 -1
package/dist/generate.js CHANGED
@@ -1,940 +1,205 @@
1
1
  #!/usr/bin/env node
2
- import * as path from 'path';
3
- import { dirname, join } from 'path';
4
- import * as fs from 'fs/promises';
5
- import fetch from 'node-fetch';
6
- import { markdownTable } from 'markdown-table';
7
- import 'dotenv/config';
8
- import retry from 'async-retry';
9
- import { Client, isNotionClientError } from '@notionhq/client';
10
- import { readFileSync } from 'fs';
11
- import { fileURLToPath } from 'url';
12
-
13
- const suffixInlineCode = "`";
14
- const prefixInlineCode = "`";
15
- const suffixBold = "**";
16
- const prefixBold = "**";
17
- const suffixItalic = "_";
18
- const prefixItalic = "_";
19
- const suffixStrikethrough = "~~";
20
- const prefixStrikethrough = "~~";
21
- const suffixUnderline = "<u>";
22
- const prefixUnderline = "</u>";
23
- function GetPrefixSuffix(type) {
24
- switch (type) {
25
- case "code":
26
- return [prefixInlineCode, suffixInlineCode];
27
- case "bold":
28
- return [prefixBold, suffixBold];
29
- case "italic":
30
- return [prefixItalic, suffixItalic];
31
- case "strikethrough":
32
- return [prefixStrikethrough, suffixStrikethrough];
33
- case "underline":
34
- return [prefixUnderline, suffixUnderline];
35
- default:
36
- return ["", ""];
37
- }
38
- }
39
- const inlineEquation = (text) => {
40
- return `$${text}$`;
41
- };
42
- const link = (text, href) => {
43
- return `[${text}](${href})`;
44
- };
45
- const codeBlock = (text, language) => {
46
- if (language === "plain text")
47
- language = "text";
48
- return `\`\`\`${language}
49
- ${text}
50
- \`\`\``;
51
- };
52
- const equation = (text) => {
53
- return `$$
54
- ${text}
55
- $$`;
56
- };
57
- const heading1 = (text) => {
58
- return `# ${text}`;
59
- };
60
- const heading2 = (text) => {
61
- return `## ${text}`;
62
- };
63
- const heading3 = (text) => {
64
- return `### ${text}`;
65
- };
66
- const quote = (text) => {
67
- // the replace is done to handle multiple lines
68
- return `> ${text.replace(/\n/g, " \n> ")}`;
69
- };
70
- const callout = (text, icon) => {
71
- let emoji;
72
- if (icon?.type === "emoji") {
73
- emoji = icon.emoji;
74
- }
75
- // the replace is done to handle multiple lines
76
- return `> ${emoji ? emoji + " " : ""}${text.replace(/\n/g, " \n> ")}`;
77
- };
78
- const bullet = (text, count) => {
79
- const renderText = text.trim();
80
- return count ? `${count}. ${renderText}` : `- ${renderText}`;
81
- };
82
- const todo = (text, checked) => {
83
- return checked ? `- [x] ${text}` : `- [ ] ${text}`;
84
- };
85
- const image = async (alt, href, convertToBase64 = false) => {
86
- // In case the user does not want to convert the images to Base64
87
- // or the image is already base64
88
- if (!convertToBase64 || href.startsWith("data:")) {
89
- if (href.startsWith("data:")) {
90
- // Extract base64 data, i.e. the string after 'data:mime/type;base64,'
91
- const base64 = href.split(",").pop();
92
- // This overrides incorrect data: string format to png
93
- // so that browsers can correctly render the data
94
- return `![${alt}](data:image/png;base64,${base64})`;
95
- }
96
- return `![${alt}](${href})`;
97
- }
98
- else {
99
- // Otherwise, download the image and convert it to base64
100
- const res = await fetch(href);
101
- const buf = await res.arrayBuffer();
102
- const base64 = Buffer.from(buf).toString("base64");
103
- return `![${alt}](data:image/png;base64,${base64})`;
104
- }
105
- };
106
- const addTabSpace = (text, n = 0) => {
107
- const tab = " ";
108
- for (let i = 0; i < n; i++) {
109
- if (text.includes("\n")) {
110
- const multiLineText = text.split(/(?:^|\n)/).join(`\n${tab}`);
111
- text = tab + multiLineText;
112
- }
113
- else
114
- text = tab + text;
115
- }
116
- return text;
117
- };
118
- const divider = () => {
119
- return "---";
120
- };
121
- const toggle = (summary, children) => {
122
- if (!summary)
123
- return children || "";
124
- return `<details>
125
- <summary>${summary}</summary>
126
- ${children || ""}
127
- </details>\n\n`;
128
- };
129
- const table = (cells) => {
130
- return markdownTable(cells);
131
- };
132
-
133
- const NOTION_API_SECRET = process.env.NOTION_API_SECRET ?? "";
134
- const NOBLOG_DATABASE_ID = process.env.NOBLOG_DATABASE_ID ?? "";
135
- const client = new Client({
136
- auth: NOTION_API_SECRET,
2
+ import { t as ConfigurationError } from "./config-csbquMhF.js";
3
+ import { a as renderPages, n as resolveStoreLayoutPath, r as writePages, t as createNotionSource } from "./client-CkOIgSvW.js";
4
+ import { config } from "dotenv";
5
+ import { ExitCode, buildApplication, buildCommand, buildRouteMap, help, run, version } from "@stricli/core";
6
+ const app = buildApplication(buildRouteMap({
7
+ routes: {
8
+ config: buildRouteMap({
9
+ routes: { check: buildCommand({
10
+ loader: async () => import("./config-check.impl-BQvIrgNe.js"),
11
+ parameters: {
12
+ flags: { config: {
13
+ kind: "parsed",
14
+ brief: "YAML configuration file",
15
+ placeholder: "PATH",
16
+ parse: (input) => input
17
+ } },
18
+ positional: {
19
+ kind: "tuple",
20
+ parameters: []
21
+ }
22
+ },
23
+ docs: { brief: "Validate a YAML configuration without network access" }
24
+ }) },
25
+ docs: { brief: "Inspect Noblog configuration" }
26
+ }),
27
+ generate: buildCommand({
28
+ loader: async () => import("./generate.impl-BKu-Ix4a.js"),
29
+ parameters: {
30
+ flags: {
31
+ verbose: {
32
+ kind: "boolean",
33
+ brief: "Print diagnostic information",
34
+ default: false,
35
+ withNegated: false
36
+ },
37
+ config: {
38
+ kind: "parsed",
39
+ brief: "YAML configuration file",
40
+ placeholder: "PATH",
41
+ parse: (input) => input,
42
+ optional: true
43
+ },
44
+ layout: {
45
+ kind: "parsed",
46
+ brief: "Layout path relative to SAVE_PATH",
47
+ placeholder: "PATH",
48
+ parse: (input) => input,
49
+ optional: true
50
+ },
51
+ store: {
52
+ kind: "parsed",
53
+ brief: "Directory for canonical Markdown files",
54
+ placeholder: "PATH",
55
+ parse: (input) => input,
56
+ optional: true
57
+ }
58
+ },
59
+ aliases: { v: "verbose" },
60
+ positional: {
61
+ kind: "tuple",
62
+ parameters: [{
63
+ brief: "Public view directory for database posts",
64
+ placeholder: "SAVE_PATH",
65
+ parse: (input) => input,
66
+ optional: true
67
+ }, {
68
+ brief: "Deprecated compatibility argument; ignored",
69
+ placeholder: "SUB_PATH",
70
+ parse: (input) => input,
71
+ optional: true
72
+ }]
73
+ }
74
+ },
75
+ docs: {
76
+ brief: "Generate blog content from Notion",
77
+ fullDescription: "Fetch published pages from Notion and write Astro-compatible Markdown files. Only database posts receive public symlinks; child pages live in the canonical store. The legacy SUB_PATH argument is accepted but ignored."
78
+ }
79
+ })
80
+ },
81
+ docs: {
82
+ brief: "Generate static blog content from Notion",
83
+ fullDescription: "Running noblog without a subcommand is equivalent to 'noblog generate'.\nThe legacy 'help' and 'version' commands are also supported.\n\nGeneration requires environment variables:\n NOTION_API_SECRET Your Notion API secret\n NOBLOG_DATABASE_ID Your Notion database ID"
84
+ }
85
+ }), {
86
+ name: "noblog",
87
+ scanner: { allowArgumentEscapeSequence: true },
88
+ determineExitCode: (error) => error instanceof ConfigurationError ? 2 : ExitCode.CommandRunError
89
+ }, {
90
+ help: help({
91
+ brief: "Show help information",
92
+ defaultForRouteMap: true,
93
+ formatting: {
94
+ caseStyle: "original",
95
+ useAliasInUsageLine: false,
96
+ onlyRequiredInUsageLine: false
97
+ }
98
+ }),
99
+ version: version({
100
+ brief: "Show version information",
101
+ info: { currentVersion: `noblog v0.3.0-beta.1` },
102
+ alias: false
103
+ })
137
104
  });
138
- const numberOfRetry = 2;
139
- async function ListPages() {
140
- const respDatabase = (await client.databases.retrieve({
141
- database_id: NOBLOG_DATABASE_ID,
142
- }));
143
- const reqDataSources = respDatabase.data_sources.map((source) => ({
144
- data_source_id: source.id,
145
- filter: {
146
- and: [
147
- {
148
- property: "publish",
149
- checkbox: { equals: true },
150
- },
151
- ],
152
- },
153
- sorts: [
154
- {
155
- property: "date",
156
- direction: "descending",
157
- },
158
- ],
159
- page_size: 100,
160
- }));
161
- let results = [];
162
- for (const req of reqDataSources) {
163
- let params = req;
164
- while (true) {
165
- const resp = await retry(async (bail) => {
166
- try {
167
- return client.dataSources.query(params);
168
- }
169
- catch (error) {
170
- if (isNotionClientError(error)) {
171
- bail(error);
172
- }
173
- throw error;
174
- }
175
- }, { retries: numberOfRetry });
176
- results = results.concat(resp.results);
177
- if (!resp.has_more) {
178
- break;
179
- }
180
- params.start_cursor = resp.next_cursor;
181
- }
182
- }
183
- return results;
105
+ const PATH_FLAGS = /* @__PURE__ */ new Set([
106
+ "--config",
107
+ "--layout",
108
+ "--store"
109
+ ]);
110
+ async function runCli(inputs, context) {
111
+ const escapeIndex = inputs.indexOf("--");
112
+ const commandInputs = escapeIndex === -1 ? inputs : inputs.slice(0, escapeIndex);
113
+ const escapedInputs = escapeIndex === -1 ? [] : inputs.slice(escapeIndex);
114
+ let normalizedInputs;
115
+ if (hasLegacyControl(commandInputs, "help")) normalizedInputs = ["--help"];
116
+ else if (hasLegacyControl(commandInputs, "version") || hasLegacyControl(commandInputs, "--version")) normalizedInputs = ["--version"];
117
+ else {
118
+ const remainingInputs = [];
119
+ for (let index = 0; index < commandInputs.length; index++) {
120
+ const input = commandInputs[index];
121
+ if (input === void 0) continue;
122
+ if (input === "generate" && index !== 0) continue;
123
+ remainingInputs.push(input);
124
+ const flagValue = getFlagValue(commandInputs, index);
125
+ if (PATH_FLAGS.has(input) && flagValue !== void 0) {
126
+ remainingInputs.push(flagValue);
127
+ index++;
128
+ }
129
+ }
130
+ normalizedInputs = [...remainingInputs, ...escapedInputs];
131
+ if (normalizedInputs[0] !== "generate" && normalizedInputs[0] !== "config" && normalizedInputs[0] !== "-h" && normalizedInputs[0] !== "--help" && normalizedInputs[0] !== "--version") normalizedInputs = ["generate", ...normalizedInputs];
132
+ }
133
+ await run(app, normalizedInputs, context);
134
+ const exitCode = context.process.exitCode;
135
+ if (typeof exitCode === "number" && exitCode < 0) context.process.exitCode = exitCode === ExitCode.InvalidArgument || exitCode === ExitCode.UnknownCommand ? 2 : 1;
184
136
  }
185
- async function GetPage(page_id) {
186
- const result = await retry(async (bail) => {
187
- try {
188
- return (await client.pages.retrieve({
189
- page_id: page_id,
190
- }));
191
- }
192
- catch (error) {
193
- if (isNotionClientError(error)) {
194
- bail(error);
195
- }
196
- throw error;
197
- }
198
- }, { retries: numberOfRetry });
199
- return result;
137
+ function hasLegacyControl(inputs, control) {
138
+ for (let index = 0; index < inputs.length; index++) {
139
+ const input = inputs[index];
140
+ if (input === void 0) continue;
141
+ if (PATH_FLAGS.has(input) && getFlagValue(inputs, index) !== void 0) {
142
+ index++;
143
+ continue;
144
+ }
145
+ if (input === control) return true;
146
+ }
147
+ return false;
200
148
  }
201
- async function GetBlockChildren(block_id, totalPage) {
202
- const results = [];
203
- let pageCount = 0;
204
- let start_cursor = undefined;
205
- do {
206
- const response = (await client.blocks.children.list({
207
- start_cursor: start_cursor,
208
- block_id: block_id,
209
- }));
210
- results.push(...response.results);
211
- start_cursor = response?.next_cursor;
212
- pageCount += 1;
213
- } while (start_cursor != null &&
214
- (totalPage == null || pageCount < totalPage));
215
- ModifyNumberedListObject(results);
216
- return results;
149
+ function getFlagValue(inputs, flagIndex) {
150
+ const value = inputs[flagIndex + 1];
151
+ return value !== void 0 && !value.startsWith("-") ? value : void 0;
217
152
  }
218
- function ModifyNumberedListObject(blocks) {
219
- let numberedListIndex = 0;
220
- for (const block of blocks) {
221
- if ("type" in block && block.type === "numbered_list_item") {
222
- // add numbers
223
- // @ts-ignore
224
- block.numbered_list_item.number = ++numberedListIndex;
225
- }
226
- else {
227
- numberedListIndex = 0;
228
- }
229
- }
153
+ //#endregion
154
+ //#region src/workflows/generate.ts
155
+ async function generateBlog(config, options, output) {
156
+ const notion = createNotionSource(config);
157
+ const storeLayoutPath = resolveStoreLayoutPath(options);
158
+ if (options.verbose) output.stderr.write("Fetching posts from Notion...\n");
159
+ const posts = await notion.listPages();
160
+ if (options.verbose) output.stderr.write(`Found ${posts.length} pages\nSave path: ${options.savePath}\nStore path: ${options.storePath}\nLayout path: ${options.layoutPath}\n`);
161
+ await writePages(await renderPages(posts, notion, {
162
+ layoutPath: storeLayoutPath,
163
+ recursive: options.recursive,
164
+ ...options.propertyMap !== void 0 ? { propertyMap: options.propertyMap } : {},
165
+ ...options.frontmatter !== void 0 ? { frontmatter: options.frontmatter } : {}
166
+ }), {
167
+ savePath: options.savePath,
168
+ storePath: options.storePath
169
+ });
170
+ output.stdout.write("✅ Successfully generated blog content\n");
230
171
  }
231
-
232
- const BLOCK_TO_DO = "to_do";
233
- const BLOCK_TOGGLE = "toggle";
234
- const BLOCK_CALLOUT = "callout";
235
- const BLOCK_CHILD_PAGE = "child_page";
236
- const BLOCK_UNSUPPORTED = "supported";
237
- const BLOCK_SYNCED_BLOCK = "synced_block";
238
- const BLOCK_NUMBERED_LIST_ITEM = "numbered_list_item";
239
- const BLOCK_BULLETED_LIST_ITEM = "bulleted_list_item";
240
- const BLOCK_IMAGE = "image";
241
- const BLOCK_DIVIDER = "divider";
242
- const BLOCK_EQUATION = "equation";
243
- const BLOCK_PDF = "pdf";
244
- const BLOCK_FILE = "file";
245
- const BLOCK_VIDEO = "video";
246
- const BLOCK_EMBED = "embed";
247
- const BLOCK_BOOKMARK = "bookmark";
248
- const BLOCK_LINK_PREVIEW = "link_preview";
249
- const BLOCK_LINK_TO_PAGE = "link_to_page";
250
- const BLOCK_CHILD_DATABASE = "child_database";
251
- const BLOCK_TABLE = "table";
252
- const BLOCK_CODE = "code";
253
- const BLOCK_HEADING_1 = "heading_1";
254
- const BLOCK_HEADING_2 = "heading_2";
255
- const BLOCK_HEADING_3 = "heading_3";
256
- const BLOCK_QUOTE = "quote";
257
- const BLOCK_PARAGRAPH = "paragraph";
258
- const LINEWRAP_NULL = "";
259
- const LINEWRAP_NEWLINE = "\n";
260
-
261
- async function SaveJelly(fname, dir, jelly, dry_run = false) {
262
- if (jelly === undefined)
263
- return `failed: ${fname} has empty jelly: `;
264
- try {
265
- if (dry_run) {
266
- console.log(jelly.content);
267
- return `success: ${fname} saved to ${dir}`;
268
- }
269
- await fs.mkdir(dir, { recursive: true });
270
- const filePath = path.join(dir, fname);
271
- await fs.writeFile(filePath, jelly.content, "utf-8");
272
- return `success: ${fname} saved to ${filePath}`;
273
- }
274
- catch (error) {
275
- if (error instanceof Error) {
276
- return `failed: ${fname} - ${error.message}`;
277
- }
278
- else {
279
- return "failed: an unknown error occurred";
280
- }
281
- }
172
+ //#endregion
173
+ //#region src/main.ts
174
+ const nodeProcess = {
175
+ env: process.env,
176
+ stdout: process.stdout,
177
+ stderr: process.stderr,
178
+ get exitCode() {
179
+ const exitCode = process.exitCode;
180
+ return typeof exitCode === "number" ? exitCode : null;
181
+ },
182
+ set exitCode(value) {
183
+ process.exitCode = value ?? void 0;
184
+ }
185
+ };
186
+ async function main(inputs = process.argv.slice(2), context = {
187
+ process: nodeProcess,
188
+ generateBlog
189
+ }) {
190
+ const environment = context.process.env ?? {};
191
+ const processEnv = Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0));
192
+ config({
193
+ processEnv,
194
+ quiet: true
195
+ });
196
+ Object.assign(environment, processEnv);
197
+ await runCli(inputs, context);
282
198
  }
283
- class Noblog {
284
- Posts = [];
285
- CurrPage = "";
286
- SavePath = "./src/pages/posts/";
287
- ChildPath = "nob_children/";
288
- LayoutPath = "../../layouts/MarkdownPostLayout.astro";
289
- MdCollection = {};
290
- CustomTransformers = {};
291
- constructor(posts, save_path, child_path, layout_path) {
292
- this.Posts = posts;
293
- if (child_path != undefined) {
294
- this.ChildPath = child_path;
295
- }
296
- if (save_path != undefined) {
297
- this.SavePath = save_path;
298
- }
299
- if (layout_path != undefined) {
300
- this.LayoutPath = layout_path;
301
- }
302
- this.CustomTransformers[BLOCK_CHILD_PAGE] = async (block) => {
303
- const page = block;
304
- if (!page?.id) {
305
- return false;
306
- }
307
- // if (curr_page.id in this.Post) and (page.id in this.Post) ||
308
- // (curr_page.id not in this.Post) and (page.id not in this.Post)
309
- // * path should be "../pageid"
310
- // if (curr_page.id in this.Post) and (page.id not in this.Post)
311
- // * path should be "../save_path/pageid"
312
- // if (curr_page.id not in this.Post) and (page.id in this.Post)
313
- // * path should be "../../pageid"
314
- let sub_path;
315
- const parent_surface = this.Posts.filter((post) => post.id === this.CurrPage).length > 0;
316
- const child_surface = this.Posts.filter((post) => post.id === page.id).length > 0;
317
- if (parent_surface === child_surface) {
318
- sub_path = "";
319
- }
320
- else if (parent_surface) {
321
- sub_path = this.ChildPath;
322
- }
323
- else {
324
- sub_path = "..";
325
- }
326
- const full_path = path.join("../", sub_path, page.id);
327
- return `(」゚ロ゚)」 [${page.child_page.title}](${full_path})`;
328
- };
329
- }
330
- // Collect converts all pages in database to markdown files
331
- async Collect(recursive = true) {
332
- const save_dir = this.SavePath;
333
- const sub_dir = path.join(save_dir, this.ChildPath);
334
- for (const page of this.Posts) {
335
- await this.FromPageId(page.id, recursive);
336
- }
337
- try {
338
- for (const page_id of Object.keys(this.MdCollection)) {
339
- let fpath = sub_dir;
340
- if (this.Posts.find((page) => page.id === page_id))
341
- fpath = save_dir;
342
- await SaveJelly(page_id + ".md", fpath, this.MdCollection[page_id]);
343
- }
344
- console.log(`success: finish dump all files to "${save_dir}"`);
345
- }
346
- catch (error) {
347
- if (error instanceof Error) {
348
- console.log(`failed: consider run verbose with v=true - ${error.message}`);
349
- }
350
- else {
351
- console.log("failed: an unknown error occurred");
352
- }
353
- }
354
- }
355
- // FromBlocks converts blocks fetched from notion api to
356
- // rendered chunk (which is called jelly). Content in jelly
357
- // will be collected to form the final markdown. If recursive
358
- // is true, it will also fetch child pages exists in the given
359
- // blocks.
360
- async FromBlocks(blocks, recursive) {
361
- const jelly = { content: "", children: [] };
362
- for (const block of blocks) {
363
- const subjelly = await this.FromBlocks(block.children, recursive);
364
- let line_wrap = LINEWRAP_NULL;
365
- if (block.type !== BLOCK_TO_DO &&
366
- block.type !== BLOCK_BULLETED_LIST_ITEM &&
367
- block.type !== BLOCK_NUMBERED_LIST_ITEM) {
368
- line_wrap = LINEWRAP_NEWLINE;
369
- }
370
- switch (block.type) {
371
- case BLOCK_TOGGLE:
372
- jelly.content += toggle(block.parent, subjelly.content);
373
- jelly.children = [...jelly.children, ...subjelly.children];
374
- continue;
375
- default:
376
- jelly.content +=
377
- line_wrap + `${addTabSpace(block.parent, 0)}\n` + line_wrap;
378
- }
379
- if (block.type === BLOCK_CHILD_PAGE) {
380
- jelly.children = [...jelly.children, block.blockId];
381
- if (recursive)
382
- await this.FromPageId(block.blockId, recursive);
383
- }
384
- else {
385
- jelly.content += subjelly.content;
386
- jelly.children = [...jelly.children, ...subjelly.children];
387
- }
388
- }
389
- return jelly;
390
- }
391
- // FromPageid converts a page id to rendered chunk (which is
392
- // called jelly). Content in jelly will be collected to form
393
- // the final markdown. If recursive is true, it will also
394
- // fetch child pages exists in the given page.
395
- async FromPageId(page_id, recursive = true) {
396
- // preserve the curr page info
397
- const prev_page = this.CurrPage;
398
- this.CurrPage = page_id;
399
- if (this.MdCollection.hasOwnProperty(page_id)) {
400
- return this.MdCollection[page_id];
401
- }
402
- const blocks = await this.PageToMarkdown(page_id);
403
- const jelly = await this.FromBlocks(blocks, recursive);
404
- const meta = await GetPage(page_id);
405
- const astro_meta = await this.AssembleAstroFrontmatter(meta);
406
- jelly.content = astro_meta + jelly.content;
407
- this.MdCollection[page_id] = jelly;
408
- // restore the prev page info
409
- this.CurrPage = prev_page;
410
- return jelly;
411
- }
412
- async PageToMarkdown(id, totalPage) {
413
- if (!client) {
414
- throw new Error("notion client is not provided");
415
- }
416
- const blocks = await GetBlockChildren(id, totalPage);
417
- const parsedData = await this.BlocksToMarkdown(blocks);
418
- return parsedData;
419
- }
420
- async BlocksToMarkdown(blocks, totalPage = null, mdBlocks = []) {
421
- if (!client) {
422
- throw new Error("notion client is not provided");
423
- }
424
- if (!blocks)
425
- return mdBlocks;
426
- for (const block of blocks) {
427
- // @ts-ignore
428
- if (block.type === BLOCK_UNSUPPORTED) {
429
- continue;
430
- }
431
- if (!("has_children" in block) || !block.has_children) {
432
- mdBlocks.push({
433
- // @ts-ignore
434
- type: block.type,
435
- blockId: block.id,
436
- children: [],
437
- parent: await this.BlockToMarkdown(block),
438
- });
439
- continue;
440
- }
441
- const block_id = block.type == BLOCK_SYNCED_BLOCK &&
442
- block.synced_block?.synced_from?.block_id
443
- ? block.synced_block.synced_from.block_id
444
- : block.id;
445
- // Get children of this block.
446
- const child_blocks = await GetBlockChildren(block_id, totalPage);
447
- // Push this block to mdBlocks.
448
- mdBlocks.push({
449
- type: block.type,
450
- blockId: block.id,
451
- parent: await this.BlockToMarkdown(block),
452
- children: [],
453
- });
454
- if (block.type === BLOCK_CALLOUT) {
455
- continue;
456
- }
457
- if (!(block.type in this.CustomTransformers)) {
458
- continue;
459
- }
460
- // Recursively call BlocksToMarkdown to collect children. check for custom transformer before parsing child
461
- if (mdBlocks !== undefined) {
462
- await this.BlocksToMarkdown(child_blocks, totalPage, mdBlocks[mdBlocks.length - 1]?.children);
463
- }
464
- }
465
- return mdBlocks;
466
- }
467
- async BlockToMarkdown(block) {
468
- if (typeof block !== "object" || !("type" in block))
469
- return "";
470
- let renderedData = "";
471
- const { type } = block;
472
- if (type in this.CustomTransformers && !!this.CustomTransformers[type]) {
473
- const transformer = this.CustomTransformers[type];
474
- const customTransformerValue = await transformer(block);
475
- if (typeof customTransformerValue === "string")
476
- return customTransformerValue;
477
- }
478
- switch (type) {
479
- case BLOCK_IMAGE:
480
- const blockContent = block.image;
481
- let image_title = "image";
482
- const image_caption_plain = blockContent.caption
483
- .map((item) => item.plain_text)
484
- .join("");
485
- let link$1 = "";
486
- const image_type = blockContent.type;
487
- if (image_type === "external") {
488
- link$1 = blockContent.external.url;
489
- }
490
- if (image_type === "file") {
491
- link$1 = blockContent.file.url;
492
- }
493
- // image caption with high priority
494
- if (image_caption_plain.trim().length > 0) {
495
- image_title = image_caption_plain;
496
- }
497
- else if (image_type === "file" || image_type === "external") {
498
- const matches = link$1.match(/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/);
499
- image_title = matches ? matches[0] : image_title;
500
- }
501
- return await image(image_title, link$1, false);
502
- case BLOCK_DIVIDER:
503
- return divider();
504
- case BLOCK_EQUATION:
505
- return equation(block.equation.expression);
506
- case BLOCK_PDF:
507
- case BLOCK_FILE:
508
- case BLOCK_VIDEO:
509
- {
510
- let blockContent;
511
- let title = type;
512
- if (type === BLOCK_PDF)
513
- blockContent = block.pdf;
514
- if (type === BLOCK_FILE)
515
- blockContent = block.file;
516
- if (type === BLOCK_VIDEO)
517
- blockContent = block.video;
518
- const caption = blockContent?.caption
519
- .map((item) => item.plain_text)
520
- .join("");
521
- if (blockContent) {
522
- const file_type = blockContent.type;
523
- let link$1 = "";
524
- if (file_type === "file")
525
- link$1 = blockContent.file.url;
526
- if (file_type === "external")
527
- link$1 = blockContent.external.url;
528
- if (caption && caption.trim().length > 0) {
529
- title = caption;
530
- }
531
- else if (link$1) {
532
- const matches = link$1.match(/[^\/\\&\?]+\.\w{3,4}(?=([\?&].*$|$))/);
533
- title = matches ? matches[0] : type;
534
- }
535
- return link(title, link$1);
536
- }
537
- }
538
- break;
539
- case BLOCK_EMBED:
540
- case BLOCK_BOOKMARK:
541
- case BLOCK_LINK_PREVIEW:
542
- case BLOCK_LINK_TO_PAGE:
543
- {
544
- let blockContent;
545
- const title = type;
546
- if (type === BLOCK_EMBED)
547
- blockContent = block.embed;
548
- if (type === BLOCK_BOOKMARK)
549
- blockContent = block.bookmark;
550
- if (type === BLOCK_LINK_PREVIEW)
551
- blockContent = block.link_preview;
552
- if (type === BLOCK_LINK_TO_PAGE &&
553
- block.link_to_page.type === "page_id") {
554
- blockContent = {
555
- url: `https://www.notion.so/${block.link_to_page.page_id}`,
556
- };
557
- }
558
- if (blockContent)
559
- return link(title, blockContent.url);
560
- }
561
- break;
562
- case BLOCK_CHILD_PAGE: {
563
- const pageTitle = block.child_page.title;
564
- return heading2(pageTitle);
565
- }
566
- case BLOCK_CHILD_DATABASE: {
567
- const pageTitle = block.child_database.title || `child_database`;
568
- return heading2(pageTitle);
569
- }
570
- case BLOCK_TABLE: {
571
- const { id, has_children } = block;
572
- const tableArr = [];
573
- if (has_children) {
574
- const tableRows = await GetBlockChildren(id, 100);
575
- const rowsPromise = tableRows?.map(async (row) => {
576
- const { type } = row;
577
- const cells = row[type]["cells"];
578
- /**
579
- * this is more like a hack since matching the type text was
580
- * difficult. So converting each cell to paragraph type to
581
- * reuse the BlockToMarkdown function
582
- */
583
- const cellStringPromise = cells.map(async (cell) => await this.BlockToMarkdown({
584
- type: BLOCK_PARAGRAPH,
585
- paragraph: { rich_text: cell },
586
- }));
587
- const cellStringArr = await Promise.all(cellStringPromise);
588
- tableArr.push(cellStringArr);
589
- });
590
- await Promise.all(rowsPromise || []);
591
- }
592
- return table(tableArr);
593
- }
594
- // Rest of the types:
595
- // "paragraph" "heading_1" "heading_2" "heading_3"
596
- // "bulleted_list_item" "numbered_list_item"
597
- // "quote" "to_do" "template" "synced_block"
598
- // "child_page" "child_database" "code" "callout" "breadcrumb"
599
- // "table_of_contents" "link_to_page" "audio" "unsupported"
600
- default: {
601
- // @ts-ignore
602
- const blockContent = block[type].text || block[type].rich_text || [];
603
- const bucket = [];
604
- const code_range = [];
605
- const bold_range = [];
606
- const italic_range = [];
607
- const underline_range = [];
608
- const strikethrough_range = [];
609
- const push_or_merge = (arr, idx) => {
610
- if (arr.length == 0) {
611
- return;
612
- }
613
- // @ts-ignore
614
- if (arr[arr.length - 1][1] === idx) {
615
- // @ts-ignore
616
- arr[arr.length - 1][1] += 1;
617
- }
618
- else {
619
- arr.push([idx, idx + 1]);
620
- }
621
- };
622
- for (let i = 0; i < blockContent.length; i++) {
623
- const content = blockContent[i];
624
- if (content.type === "equation") {
625
- bucket.push(inlineEquation(content.equation.expression));
626
- continue;
627
- }
628
- let plain_text = content.plain_text;
629
- if (content["href"])
630
- plain_text = link(plain_text, content["href"]);
631
- bucket.push(plain_text);
632
- const annotations = content.annotations;
633
- if (annotations.code)
634
- push_or_merge(code_range, i);
635
- if (annotations.bold)
636
- push_or_merge(bold_range, i);
637
- if (annotations.italic)
638
- push_or_merge(italic_range, i);
639
- if (annotations.underline)
640
- push_or_merge(underline_range, i);
641
- if (annotations.strikethrough)
642
- push_or_merge(strikethrough_range, i);
643
- }
644
- const annoed_bucket = [];
645
- for (const range of code_range)
646
- annoed_bucket.push({ range: range, type: "code" });
647
- for (const range of bold_range)
648
- annoed_bucket.push({ range: range, type: "bold" });
649
- for (const range of italic_range)
650
- annoed_bucket.push({ range: range, type: "italic" });
651
- for (const range of underline_range)
652
- annoed_bucket.push({ range: range, type: "underline" });
653
- for (const range of strikethrough_range)
654
- annoed_bucket.push({ range: range, type: "strikethrough" });
655
- annoed_bucket.sort(
656
- // @ts-ignore
657
- (a, b) => a.range[1] - a.range[0] - (b.range[1] - b.range[0]));
658
- for (const annoed of annoed_bucket) {
659
- const type = annoed.type;
660
- const [from, to] = annoed.range;
661
- let panno, sanno = GetPrefixSuffix(type);
662
- // @ts-ignore
663
- bucket[from] = panno + bucket[from];
664
- // @ts-ignore
665
- bucket[to - 1] = bucket[to - 1] + sanno;
666
- }
667
- bucket.forEach((x) => (renderedData += x));
668
- }
669
- }
670
- switch (type) {
671
- case BLOCK_CODE:
672
- renderedData = codeBlock(renderedData, block[type].language);
673
- break;
674
- case BLOCK_HEADING_1:
675
- renderedData = heading1(renderedData);
676
- break;
677
- case BLOCK_HEADING_2:
678
- renderedData = heading2(renderedData);
679
- break;
680
- case BLOCK_HEADING_3:
681
- renderedData = heading3(renderedData);
682
- break;
683
- case BLOCK_QUOTE:
684
- renderedData = quote(renderedData);
685
- break;
686
- case BLOCK_CALLOUT:
687
- const { id, has_children } = block;
688
- let callout_string = "";
689
- if (!has_children) {
690
- return callout(renderedData, block.callout.icon);
691
- }
692
- const callout_children_object = await GetBlockChildren(id, 100);
693
- const callout_children = await this.BlocksToMarkdown(callout_children_object);
694
- callout_string += `${renderedData}\n`;
695
- callout_string += (await this.FromBlocks(callout_children, true))
696
- .content;
697
- renderedData = callout(callout_string.trim(), block.callout.icon);
698
- break;
699
- case BLOCK_BULLETED_LIST_ITEM:
700
- renderedData = bullet(renderedData);
701
- break;
702
- case BLOCK_NUMBERED_LIST_ITEM:
703
- renderedData = bullet(renderedData, block.numbered_list_item.number);
704
- break;
705
- case BLOCK_TO_DO:
706
- renderedData = todo(renderedData, block.to_do.checked);
707
- break;
708
- }
709
- return renderedData;
710
- }
711
- async AssembleAstroFrontmatter(page) {
712
- let frontmatter = "";
713
- // Default layout
714
- const ischild = this.Posts.filter((post) => post.id === page.id).length > 0 ? "" : "../";
715
- frontmatter += "layout: " + ischild + this.LayoutPath + "\n";
716
- let title = "";
717
- let description = "";
718
- let tags = [];
719
- let pubDate = "";
720
- let archived = "";
721
- Object.entries(page.properties).forEach(([key, property]) => {
722
- switch (property.type) {
723
- case "title": // Get title
724
- title = property.title
725
- ?.map((title) => title.plain_text)
726
- .reduce((a, b) => a + b, "");
727
- break;
728
- case "multi_select": // Get tags
729
- tags = property.multi_select?.map((tag) => tag.name);
730
- break;
731
- case "date": // Get date
732
- pubDate =
733
- property.date?.start ?? new Date().toISOString().split("T")[0];
734
- break;
735
- default: // Customized properties from the template given in the Github repo
736
- switch (key) {
737
- case "archived": // Get archived
738
- archived =
739
- property.type === "checkbox" && property.checkbox
740
- ? "true"
741
- : "false";
742
- break;
743
- case "description": // Get description
744
- description =
745
- property.type === "rich_text"
746
- ? property.rich_text
747
- ?.map((text) => text.plain_text)
748
- .reduce((a, b) => a + b, "")
749
- : "";
750
- break;
751
- }
752
- }
753
- });
754
- frontmatter += "title: " + JSON.stringify(title) + "\n";
755
- frontmatter += "description: " + JSON.stringify(description) + "\n";
756
- frontmatter += "tags: " + JSON.stringify(tags) + "\n";
757
- frontmatter += "pubDate: " + JSON.stringify(pubDate) + "\n";
758
- frontmatter += "archived: " + JSON.stringify(archived) + "\n";
759
- return "---\n" + frontmatter + "---\n";
760
- }
761
- }
762
-
763
- function getVersion() {
764
- try {
765
- const __filename = fileURLToPath(import.meta.url);
766
- const __dirname = dirname(__filename);
767
- const packagePath = join(__dirname, "../../package.json");
768
- const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
769
- return packageJson.version;
770
- }
771
- catch {
772
- return "unknown";
773
- }
774
- }
775
- function help() {
776
- console.log(`
777
- noblog - Generate static blog content from Notion.so
778
-
779
- Usage: noblog [command] [options] [SAVE_PATH] [SUB_PATH]
780
-
781
- Options:
782
- -v, --verbose Print more messages for debugging
783
- -h, --help Show help message
784
- --version Show version information
785
- --layout PATH Custom layout path (default: ../../layouts/MarkdownPostLayout.astro)
786
-
787
- Commands:
788
- help Show this help message
789
- version Show version information
790
- generate Generate blog content (default command)
199
+ //#endregion
200
+ //#region src/cli.ts
201
+ await main();
202
+ //#endregion
203
+ export {};
791
204
 
792
- Options:
793
- -v, --verbose Print more messages for debugging
794
- -h, --help Show help message
795
- --version Show version information
796
-
797
- Arguments:
798
- SAVE_PATH Directory to save main posts (default: ./src/pages/posts/)
799
- SUB_PATH Subdirectory for nested content (default: nob_children/)
800
-
801
- Layout Options:
802
- --layout PATH Custom layout path relative to save directory (default: ../../layouts/MarkdownPostLayout.astro)
803
-
804
- Examples:
805
- noblog help
806
- noblog version
807
- noblog generate
808
- noblog -v
809
- noblog src/pages/posts/ nob_children/
810
- noblog --layout ../../layouts/BlogLayout.astro
811
- noblog src/posts/ children/ --layout ../../../layouts/CustomLayout.astro -v
812
-
813
- Environment Variables:
814
- NOTION_API_SECRET Your Notion API secret
815
- NOBLOG_DATABASE_ID Your Notion database ID
816
- `);
817
- }
818
- function parseArgs(argv) {
819
- const options = {
820
- verbose: false,
821
- help: false,
822
- version: false,
823
- savePath: "./src/pages/posts/",
824
- subPath: "nob_children/",
825
- layoutPath: "../../layouts/MarkdownPostLayout.astro",
826
- };
827
- const positionalArgs = [];
828
- for (let i = 0; i < argv.length; i++) {
829
- const arg = argv[i];
830
- switch (arg) {
831
- case "-v":
832
- case "--verbose":
833
- options.verbose = true;
834
- break;
835
- case "-h":
836
- case "--help":
837
- case "help":
838
- options.help = true;
839
- break;
840
- case "--version":
841
- case "version":
842
- options.version = true;
843
- break;
844
- case "--layout":
845
- if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
846
- options.layoutPath = argv[++i];
847
- }
848
- else {
849
- console.error("Error: --layout requires a path argument");
850
- help();
851
- process.exit(1);
852
- }
853
- break;
854
- case "generate":
855
- // Explicit generate command, skip it
856
- break;
857
- default:
858
- if (arg.startsWith("-")) {
859
- console.error(`Unknown option: ${arg}`);
860
- help();
861
- process.exit(1);
862
- }
863
- positionalArgs.push(arg);
864
- break;
865
- }
866
- }
867
- // Handle positional arguments
868
- if (positionalArgs.length > 0) {
869
- options.savePath = positionalArgs[0];
870
- }
871
- if (positionalArgs.length > 1) {
872
- options.subPath = positionalArgs[1];
873
- }
874
- if (positionalArgs.length > 2) {
875
- console.error("Error: Too many arguments.");
876
- help();
877
- process.exit(1);
878
- }
879
- return options;
880
- }
881
- async function generateBlog(options) {
882
- try {
883
- if (options.verbose) {
884
- console.log("Fetching posts from Notion...");
885
- }
886
- const pages = await ListPages();
887
- if (options.verbose) {
888
- console.log(`Found ${pages.length} pages`);
889
- console.log(`Save path: ${options.savePath}`);
890
- console.log(`Sub path: ${options.subPath}`);
891
- console.log(`Layout path: ${options.layoutPath}`);
892
- }
893
- const nob = new Noblog(pages, options.savePath, options.subPath, options.layoutPath);
894
- await nob.Collect(true);
895
- console.log(`✅ Successfully generated blog content`);
896
- }
897
- catch (error) {
898
- console.error(`❌ Error generating blog content:`);
899
- if (error instanceof Error) {
900
- console.error(` ${error.message}`);
901
- if (options.verbose) {
902
- console.error(error.stack);
903
- }
904
- }
905
- else {
906
- console.error(` An unknown error occurred`);
907
- }
908
- process.exit(1);
909
- }
910
- }
911
- async function main() {
912
- const argv = process.argv.slice(2);
913
- if (argv.length === 0) {
914
- // Default behavior: generate with default options
915
- await generateBlog({
916
- verbose: false,
917
- help: false,
918
- version: false,
919
- savePath: "./src/pages/posts/",
920
- subPath: "nob_children/",
921
- layoutPath: "../../layouts/MarkdownPostLayout.astro",
922
- });
923
- return;
924
- }
925
- const options = parseArgs(argv);
926
- if (options.help) {
927
- help();
928
- return;
929
- }
930
- if (options.version) {
931
- console.log(`noblog v${getVersion()}`);
932
- return;
933
- }
934
- await generateBlog(options);
935
- }
936
- main().catch((error) => {
937
- console.error(`❌ Unexpected error:`, error);
938
- process.exit(1);
939
- });
940
- //# sourceMappingURL=generate.js.map
205
+ //# sourceMappingURL=generate.js.map