promaster 0.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 +9 -3
- package/bin/promaster.js +2 -0
- package/package.json +6 -4
- package/src/banner.js +13 -0
- package/src/commands/list.js +11 -2
- package/src/download.js +20 -4
- package/src/open.js +30 -0
- package/src/render.js +77 -0
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# promaster
|
|
2
2
|
|
|
3
|
-
Interactive CLI to browse
|
|
4
|
-
|
|
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.
|
|
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/bin/promaster.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runList } from "../src/commands/list.js";
|
|
3
3
|
import { HttpError } from "../src/github.js";
|
|
4
|
+
import { printBanner } from "../src/banner.js";
|
|
4
5
|
|
|
5
6
|
const USAGE = `promaster - browse & download Markdown from a GitHub repo
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ Config:
|
|
|
12
13
|
GITHUB_TOKEN=... Optional, raises the GitHub API rate limit.`;
|
|
13
14
|
|
|
14
15
|
async function main() {
|
|
16
|
+
printBanner();
|
|
15
17
|
const cmd = process.argv[2];
|
|
16
18
|
switch (cmd) {
|
|
17
19
|
case "list":
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promaster",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Interactive CLI to browse
|
|
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
|
}
|
package/src/banner.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const BANNER = String.raw`
|
|
2
|
+
████ █████ ███ ███ █████ ███ █ █ ███ █████ █████
|
|
3
|
+
█ █ █ █ █ █ █ █ █ █ █ █ █
|
|
4
|
+
████ ████ █████ █ █ █ █ █ █ █ ████
|
|
5
|
+
█ █ █ █ █ █ █ █ █ █ █ █ █
|
|
6
|
+
█ █ █████ █ █ ███ █ ███ █ ███ █ █████
|
|
7
|
+
|
|
8
|
+
By Javid Salimov | Software Engineer | https://github.com/javidselimov
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export function printBanner() {
|
|
12
|
+
console.log(BANNER);
|
|
13
|
+
}
|
package/src/commands/list.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
24
|
-
|
|
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 = { "&": "&", "<": "<", ">": ">", '"': """ };
|
|
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
|
+
}
|