@sambitcreate/parsely-cli 2.1.0 → 2.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 +21 -7
- package/dist/app.js +45 -10
- package/dist/cli.js +10 -3
- package/dist/components/Footer.js +5 -1
- package/dist/components/LandingScreen.d.ts +2 -1
- package/dist/components/LandingScreen.js +20 -5
- package/dist/components/LoadingScreen.js +0 -2
- package/dist/components/RecipeCard.d.ts +2 -1
- package/dist/components/RecipeCard.js +134 -9
- package/dist/components/URLInput.d.ts +2 -1
- package/dist/components/URLInput.js +30 -7
- package/dist/services/scraper.d.ts +1 -0
- package/dist/services/scraper.js +170 -61
- package/dist/theme.d.ts +88 -41
- package/dist/theme.js +122 -40
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.js +10 -0
- package/dist/utils/shortcuts.d.ts +6 -0
- package/dist/utils/shortcuts.js +15 -0
- package/dist/utils/terminal.js +51 -2
- package/dist/utils/text-layout.d.ts +1 -0
- package/dist/utils/text-layout.js +63 -0
- package/package.json +2 -2
package/dist/utils/helpers.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { config } from 'dotenv';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
+
const ANSI_ESCAPE_PATTERN = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
|
|
4
|
+
const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000B-\u001A\u001C-\u001F\u007F-\u009F]/g;
|
|
3
5
|
/**
|
|
4
6
|
* Convert an ISO 8601 duration string (e.g. "PT1H30M") to total minutes.
|
|
5
7
|
* Returns -1 when the input is not parseable.
|
|
@@ -37,6 +39,11 @@ export function loadConfig() {
|
|
|
37
39
|
openaiApiKey: process.env['OPENAI_API_KEY'],
|
|
38
40
|
};
|
|
39
41
|
}
|
|
42
|
+
export function sanitizeTerminalText(input) {
|
|
43
|
+
return input
|
|
44
|
+
.replace(ANSI_ESCAPE_PATTERN, '')
|
|
45
|
+
.replace(CONTROL_CHAR_PATTERN, '');
|
|
46
|
+
}
|
|
40
47
|
export function sanitizeSingleLineInput(input) {
|
|
41
48
|
return input.replace(/[\r\n]+/g, '');
|
|
42
49
|
}
|
|
@@ -44,6 +51,9 @@ export function normalizeRecipeUrl(input) {
|
|
|
44
51
|
const trimmed = input.trim();
|
|
45
52
|
if (!trimmed)
|
|
46
53
|
return null;
|
|
54
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(trimmed) && !/^https?:\/\//i.test(trimmed)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
47
57
|
const url = /^https?:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
48
58
|
return isValidUrl(url) ? url : null;
|
|
49
59
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function toCtrlCharacter(letter) {
|
|
2
|
+
const normalized = letter.trim().toLowerCase();
|
|
3
|
+
if (!/^[a-z]$/.test(normalized)) {
|
|
4
|
+
throw new Error(`Unsupported control shortcut: ${letter}`);
|
|
5
|
+
}
|
|
6
|
+
return String.fromCharCode(normalized.charCodeAt(0) - 96);
|
|
7
|
+
}
|
|
8
|
+
export function isCtrlShortcut(input, key, letter) {
|
|
9
|
+
const normalized = letter.trim().toLowerCase();
|
|
10
|
+
return (key.ctrl === true && input.toLowerCase() === normalized) ||
|
|
11
|
+
input === toCtrlCharacter(normalized);
|
|
12
|
+
}
|
|
13
|
+
export function isThemeToggleShortcut(input, key) {
|
|
14
|
+
return isCtrlShortcut(input, key, 't');
|
|
15
|
+
}
|
package/dist/utils/terminal.js
CHANGED
|
@@ -2,6 +2,39 @@ const SYNC_OUTPUT_START = '\u001B[?2026h';
|
|
|
2
2
|
const SYNC_OUTPUT_END = '\u001B[?2026l';
|
|
3
3
|
const OSC = '\u001B]';
|
|
4
4
|
const ST = '\u001B\\';
|
|
5
|
+
const SYNCHRONIZED_OUTPUT_TERM_PROGRAMS = new Set([
|
|
6
|
+
'ghostty',
|
|
7
|
+
'WezTerm',
|
|
8
|
+
]);
|
|
9
|
+
const SYNCHRONIZED_OUTPUT_TERMS = [
|
|
10
|
+
'xterm-kitty',
|
|
11
|
+
'xterm-ghostty',
|
|
12
|
+
];
|
|
13
|
+
const DISPLAY_PALETTE_TERM_PROGRAMS = new Set([
|
|
14
|
+
'ghostty',
|
|
15
|
+
'Apple_Terminal',
|
|
16
|
+
'iTerm.app',
|
|
17
|
+
'WezTerm',
|
|
18
|
+
'WarpTerminal',
|
|
19
|
+
]);
|
|
20
|
+
const DISPLAY_PALETTE_TERMS = [
|
|
21
|
+
'alacritty',
|
|
22
|
+
'foot',
|
|
23
|
+
'foot-extra',
|
|
24
|
+
'xterm-ghostty',
|
|
25
|
+
'xterm-kitty',
|
|
26
|
+
];
|
|
27
|
+
function getTerm(env) {
|
|
28
|
+
return env['TERM']?.toLowerCase() ?? '';
|
|
29
|
+
}
|
|
30
|
+
function isMultiplexer(env) {
|
|
31
|
+
const term = getTerm(env);
|
|
32
|
+
return Boolean(env['TMUX'] || env['STY'] || term.startsWith('screen') || term.startsWith('tmux'));
|
|
33
|
+
}
|
|
34
|
+
function isPaletteBlocked(env) {
|
|
35
|
+
const term = getTerm(env);
|
|
36
|
+
return term === 'dumb' || term === 'linux' || env['TERM_PROGRAM'] === 'vscode' || env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm' || isMultiplexer(env);
|
|
37
|
+
}
|
|
5
38
|
export function getRenderableHeight(rows) {
|
|
6
39
|
if (!Number.isFinite(rows) || rows <= 1) {
|
|
7
40
|
return 1;
|
|
@@ -15,7 +48,15 @@ export function shouldUseSynchronizedOutput(env = process.env) {
|
|
|
15
48
|
if (env['PARSELY_SYNC_OUTPUT'] === '1') {
|
|
16
49
|
return true;
|
|
17
50
|
}
|
|
18
|
-
|
|
51
|
+
if (isMultiplexer(env)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const termProgram = env['TERM_PROGRAM'] ?? '';
|
|
55
|
+
if (SYNCHRONIZED_OUTPUT_TERM_PROGRAMS.has(termProgram)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const term = getTerm(env);
|
|
59
|
+
return SYNCHRONIZED_OUTPUT_TERMS.some((candidate) => term.startsWith(candidate));
|
|
19
60
|
}
|
|
20
61
|
export function shouldUseDisplayPalette(env = process.env) {
|
|
21
62
|
if (env['PARSELY_DISPLAY_PALETTE'] === '0') {
|
|
@@ -24,7 +65,15 @@ export function shouldUseDisplayPalette(env = process.env) {
|
|
|
24
65
|
if (env['PARSELY_DISPLAY_PALETTE'] === '1') {
|
|
25
66
|
return true;
|
|
26
67
|
}
|
|
27
|
-
|
|
68
|
+
if (isPaletteBlocked(env)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const termProgram = env['TERM_PROGRAM'] ?? '';
|
|
72
|
+
if (DISPLAY_PALETTE_TERM_PROGRAMS.has(termProgram)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const term = getTerm(env);
|
|
76
|
+
return DISPLAY_PALETTE_TERMS.some((candidate) => term.startsWith(candidate));
|
|
28
77
|
}
|
|
29
78
|
export function setDefaultTerminalBackground(color) {
|
|
30
79
|
return `${OSC}11;${color}${ST}`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function wrapText(text: string, width: number, initialIndent?: string, continuationIndent?: string): string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function getWidth(value) {
|
|
2
|
+
return Array.from(value).length;
|
|
3
|
+
}
|
|
4
|
+
function chunkWord(word, width) {
|
|
5
|
+
if (width <= 0) {
|
|
6
|
+
return [word];
|
|
7
|
+
}
|
|
8
|
+
const chunks = [];
|
|
9
|
+
let remaining = word;
|
|
10
|
+
while (getWidth(remaining) > width) {
|
|
11
|
+
const segment = Array.from(remaining).slice(0, width).join('');
|
|
12
|
+
chunks.push(segment);
|
|
13
|
+
remaining = Array.from(remaining).slice(width).join('');
|
|
14
|
+
}
|
|
15
|
+
if (remaining) {
|
|
16
|
+
chunks.push(remaining);
|
|
17
|
+
}
|
|
18
|
+
return chunks;
|
|
19
|
+
}
|
|
20
|
+
export function wrapText(text, width, initialIndent = '', continuationIndent = initialIndent) {
|
|
21
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return [initialIndent.trimEnd()];
|
|
24
|
+
}
|
|
25
|
+
const words = normalized.split(' ');
|
|
26
|
+
const lines = [];
|
|
27
|
+
let prefix = initialIndent;
|
|
28
|
+
let current = '';
|
|
29
|
+
const flush = () => {
|
|
30
|
+
lines.push(`${prefix}${current}`.trimEnd());
|
|
31
|
+
prefix = continuationIndent;
|
|
32
|
+
current = '';
|
|
33
|
+
};
|
|
34
|
+
for (const word of words) {
|
|
35
|
+
const available = Math.max(1, width - getWidth(prefix));
|
|
36
|
+
if (getWidth(word) > available) {
|
|
37
|
+
if (current) {
|
|
38
|
+
flush();
|
|
39
|
+
}
|
|
40
|
+
const chunks = chunkWord(word, available);
|
|
41
|
+
chunks.forEach((chunk, index) => {
|
|
42
|
+
lines.push(`${prefix}${chunk}`.trimEnd());
|
|
43
|
+
if (index < chunks.length - 1) {
|
|
44
|
+
prefix = continuationIndent;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
prefix = continuationIndent;
|
|
48
|
+
current = '';
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
52
|
+
if (getWidth(prefix) + getWidth(candidate) <= width) {
|
|
53
|
+
current = candidate;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
flush();
|
|
57
|
+
current = word;
|
|
58
|
+
}
|
|
59
|
+
if (current || lines.length === 0) {
|
|
60
|
+
lines.push(`${prefix}${current}`.trimEnd());
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sambitcreate/parsely-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A smart recipe scraper CLI with interactive TUI built on Ink",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
|
-
"public",
|
|
32
|
+
"public/parsely-logo.svg",
|
|
33
33
|
"package.json",
|
|
34
34
|
"README.md",
|
|
35
35
|
"LICENSE"
|