promaster 1.1.0 → 1.2.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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # promaster
2
2
 
3
- Interactive CLI to browse and download Markdown from a **public GitHub repo**,
4
- grouped into three categories: **blog**, **memory**, **books**.
3
+ Interactive CLI to browse Markdown from a **public GitHub repo**, grouped into
4
+ three categories: **blog**, **memory**, **books**. Each selected file is
5
+ rendered to a styled, standalone **HTML** page — click it to open in a browser.
5
6
 
6
7
  The package ships only the CLI. Your Markdown content lives in your own GitHub
7
8
  repo and is fetched at runtime — it is never bundled into npm.
@@ -32,7 +33,12 @@ promaster list
32
33
  1. Pick a category: `blog` / `memory` / `books`.
33
34
  2. Files are listed newest → oldest (by last git commit date).
34
35
  3. Toggle one or more with space, confirm with enter.
35
- 4. Selected files download to `./promaster-data/<category>/`, where your app reads them.
36
+ 4. Each selection is converted to HTML and saved to
37
+ `./promaster-data/<category>/<name>.html`.
38
+ 5. The CLI then offers to open the saved file(s) in your default browser.
39
+
40
+ Open any saved `.html` later by double-clicking it — it renders fully offline
41
+ (styles are inlined, with light/dark support).
36
42
 
37
43
  ## Configuration
38
44
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "promaster",
3
- "version": "1.1.0",
4
- "description": "Interactive CLI to browse and download Markdown (blog/memory/books) from a public GitHub repo.",
3
+ "version": "1.2.0",
4
+ "description": "Interactive CLI to browse Markdown (blog/memory/books) from a public GitHub repo and save it as browser-ready HTML.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "promaster": "./bin/promaster.js"
@@ -23,10 +23,12 @@
23
23
  "markdown",
24
24
  "github",
25
25
  "blog",
26
- "notes"
26
+ "notes",
27
+ "html"
27
28
  ],
28
29
  "license": "MIT",
29
30
  "dependencies": {
30
- "@inquirer/prompts": "^7.0.0"
31
+ "@inquirer/prompts": "^7.0.0",
32
+ "marked": "^14.0.0"
31
33
  }
32
34
  }
@@ -1,8 +1,9 @@
1
- import { select, checkbox } from "@inquirer/prompts";
1
+ import { select, checkbox, confirm } from "@inquirer/prompts";
2
2
  import { resolve, dirname } from "node:path";
3
3
  import { resolveRepo, CATEGORIES } from "../config.js";
4
4
  import { listMarkdown, lastCommitDate, fetchRaw, HttpError } from "../github.js";
5
5
  import { save, OUTPUT_ROOT } from "../download.js";
6
+ import { openInBrowser } from "../open.js";
6
7
 
7
8
  const CONCURRENCY = 5;
8
9
 
@@ -59,9 +60,17 @@ export async function runList() {
59
60
  saved.push(await save(category, file, content));
60
61
  }
61
62
 
62
- console.log("\nSaved:");
63
+ console.log("\nSaved (HTML — click to open in a browser):");
63
64
  for (const p of saved) console.log(` ${p}`);
64
65
  console.log(`\nOutput dir: ${resolve(process.cwd(), OUTPUT_ROOT, category)}`);
66
+
67
+ const open = await confirm({
68
+ message: `Open ${saved.length === 1 ? "it" : "them"} in your browser now?`,
69
+ default: true,
70
+ });
71
+ if (open) {
72
+ for (const p of saved) openInBrowser(p);
73
+ }
65
74
  }
66
75
 
67
76
  export { HttpError };
package/src/download.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { basename, resolve } from "node:path";
2
+ import { basename, extname, resolve } from "node:path";
3
+ import { mdToHtml } from "./render.js";
3
4
 
4
5
  export const OUTPUT_ROOT = "promaster-data";
5
6
 
@@ -15,12 +16,27 @@ export function safeName(name) {
15
16
  }
16
17
 
17
18
  /**
18
- * Save raw content under ./promaster-data/<category>/<name>. Returns absolute path.
19
+ * Safe basename with its extension replaced by ".html".
20
+ */
21
+ export function htmlName(name) {
22
+ const base = safeName(name);
23
+ const ext = extname(base);
24
+ const stem = ext ? base.slice(0, -ext.length) : base;
25
+ return `${stem}.html`;
26
+ }
27
+
28
+ /**
29
+ * Render markdown to a styled HTML document and save it under
30
+ * ./promaster-data/<category>/<name>.html. Returns the absolute path.
19
31
  */
20
32
  export async function save(category, file, content) {
21
33
  const dir = resolve(process.cwd(), OUTPUT_ROOT, safeName(category));
22
34
  await mkdir(dir, { recursive: true });
23
- const dest = resolve(dir, safeName(file.name));
24
- await writeFile(dest, content, "utf8");
35
+ const base = safeName(file.name);
36
+ const ext = extname(base);
37
+ const title = ext ? base.slice(0, -ext.length) : base;
38
+ const html = mdToHtml(content, { title });
39
+ const dest = resolve(dir, htmlName(file.name));
40
+ await writeFile(dest, html, "utf8");
25
41
  return dest;
26
42
  }
package/src/open.js ADDED
@@ -0,0 +1,30 @@
1
+ import { spawn } from "node:child_process";
2
+ import { platform } from "node:process";
3
+
4
+ /**
5
+ * Open a file/path in the OS default application (browser for .html).
6
+ * Non-blocking and best-effort: failures are reported but never throw.
7
+ */
8
+ export function openInBrowser(filePath) {
9
+ let cmd, args;
10
+ if (platform === "win32") {
11
+ // empty "" is the window title arg required by `start`
12
+ cmd = "cmd";
13
+ args = ["/c", "start", "", filePath];
14
+ } else if (platform === "darwin") {
15
+ cmd = "open";
16
+ args = [filePath];
17
+ } else {
18
+ cmd = "xdg-open";
19
+ args = [filePath];
20
+ }
21
+ try {
22
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
23
+ child.on("error", (err) => {
24
+ console.error(`Could not open ${filePath}: ${err.message}`);
25
+ });
26
+ child.unref();
27
+ } catch (err) {
28
+ console.error(`Could not open ${filePath}: ${err.message}`);
29
+ }
30
+ }
package/src/render.js ADDED
@@ -0,0 +1,77 @@
1
+ import { marked } from "marked";
2
+
3
+ const ESCAPE = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" };
4
+
5
+ function escapeHtml(str) {
6
+ return String(str).replace(/[&<>"]/g, (ch) => ESCAPE[ch]);
7
+ }
8
+
9
+ const STYLE = `
10
+ :root { color-scheme: light dark; }
11
+ body {
12
+ max-width: 760px;
13
+ margin: 2.5rem auto;
14
+ padding: 0 1.25rem;
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
16
+ line-height: 1.65;
17
+ color: #1a1a1a;
18
+ background: #ffffff;
19
+ }
20
+ h1, h2, h3, h4 { line-height: 1.25; margin-top: 1.8em; }
21
+ h1 { border-bottom: 1px solid #e1e4e8; padding-bottom: .3em; }
22
+ a { color: #0969da; }
23
+ code {
24
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
25
+ font-size: .9em;
26
+ background: rgba(127,127,127,.15);
27
+ padding: .15em .35em;
28
+ border-radius: 4px;
29
+ }
30
+ pre {
31
+ background: #f6f8fa;
32
+ padding: 1rem;
33
+ border-radius: 8px;
34
+ overflow-x: auto;
35
+ }
36
+ pre code { background: none; padding: 0; }
37
+ blockquote {
38
+ margin: 0;
39
+ padding: 0 1em;
40
+ color: #57606a;
41
+ border-left: .25em solid #d0d7de;
42
+ }
43
+ table { border-collapse: collapse; width: 100%; }
44
+ th, td { border: 1px solid #d0d7de; padding: .5em .75em; }
45
+ img { max-width: 100%; }
46
+ @media (prefers-color-scheme: dark) {
47
+ body { color: #e6edf3; background: #0d1117; }
48
+ h1 { border-bottom-color: #30363d; }
49
+ a { color: #4493f8; }
50
+ pre { background: #161b22; }
51
+ blockquote { color: #8b949e; border-left-color: #30363d; }
52
+ th, td { border-color: #30363d; }
53
+ }
54
+ `;
55
+
56
+ /**
57
+ * Convert markdown text into a standalone, styled HTML document.
58
+ * @param {string} markdown raw markdown source
59
+ * @param {{ title?: string }} [opts]
60
+ * @returns {string} full HTML document
61
+ */
62
+ export function mdToHtml(markdown, { title = "Document" } = {}) {
63
+ const body = marked.parse(String(markdown));
64
+ return `<!DOCTYPE html>
65
+ <html lang="en">
66
+ <head>
67
+ <meta charset="utf-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1">
69
+ <title>${escapeHtml(title)}</title>
70
+ <style>${STYLE}</style>
71
+ </head>
72
+ <body>
73
+ ${body}
74
+ </body>
75
+ </html>
76
+ `;
77
+ }