@hajimism/dek 0.3.2 → 0.4.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/CHANGELOG.md +90 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/package.json +2 -1
- package/skills/dek/SKILL.md +10 -3
- package/skills/dek-design/SKILL.md +42 -0
- package/src/cli/address.ts +84 -14
- package/src/cli/build.ts +2 -2
- package/src/cli/check.ts +2 -1
- package/src/cli/commands.ts +54 -13
- package/src/cli/contract.ts +18 -1
- package/src/cli/files.ts +9 -6
- package/src/cli/flags.ts +1 -1
- package/src/cli/goto.ts +3 -1
- package/src/cli/init.ts +10 -5
- package/src/cli/lint.ts +4 -3
- package/src/cli/main.ts +7 -5
- package/src/cli/new.ts +41 -10
- package/src/cli/pack.ts +21 -0
- package/src/cli/pptx.ts +9 -4
- package/src/cli/ref.ts +243 -9
- package/src/cli/result.ts +5 -2
- package/src/cli/scope.ts +25 -27
- package/src/cli/shot.ts +2 -1
- package/src/cli/text.ts +45 -4
- package/src/cli/theme.ts +16 -2
- package/src/cli/voice.ts +4 -3
- package/src/core/alignment.ts +320 -0
- package/src/core/annotation-rows.ts +16 -4
- package/src/core/annotations.ts +19 -1
- package/src/core/config-keys.ts +6 -2
- package/src/core/config.ts +88 -12
- package/src/core/diagnostic.ts +5 -1
- package/src/core/escape.ts +43 -0
- package/src/core/licenses/CC-BY-4.0.txt +396 -0
- package/src/core/licenses/CC0-1.0.txt +121 -0
- package/src/core/licenses.ts +20 -0
- package/src/core/line-break.ts +275 -0
- package/src/core/lint/asset-refs.ts +6 -2
- package/src/core/lint/files.ts +30 -5
- package/src/core/lint/keys.ts +7 -3
- package/src/core/lint/slide-html.ts +3 -1
- package/src/core/lint/slide-script.ts +7 -5
- package/src/core/mv.ts +30 -10
- package/src/core/pack.ts +137 -0
- package/src/core/playwright-module.d.ts +1 -0
- package/src/core/playwright-visual.ts +180 -20
- package/src/core/playwright.ts +202 -25
- package/src/core/pptx-package.ts +461 -33
- package/src/core/pptx-paint.ts +1168 -0
- package/src/core/pptx-plan.ts +879 -0
- package/src/core/pptx-text.ts +204 -47
- package/src/core/pptx-theme.ts +222 -0
- package/src/core/pptx.ts +55 -8
- package/src/core/prose.ts +23 -0
- package/src/core/pseudo-text.ts +32 -10
- package/src/core/ref-name.ts +100 -17
- package/src/core/ref.ts +318 -34
- package/src/core/resolve.ts +20 -9
- package/src/core/shot/morph.ts +1 -1
- package/src/core/shot/motion.ts +2 -3
- package/src/core/shot/still.ts +2 -2
- package/src/core/shot-cache.ts +25 -17
- package/src/core/slide-measure.ts +51 -1
- package/src/core/slide-script-eval.ts +10 -8
- package/src/core/slide-script.ts +114 -20
- package/src/core/sync.ts +3 -3
- package/src/core/visual-cache.ts +2 -0
- package/src/core/visual.ts +154 -15
- package/src/core/voice.ts +3 -5
- package/src/core/zod.ts +9 -1
- package/src/runtime/annotate-view.ts +4 -2
- package/src/server/dev.ts +15 -4
- package/src/server/lock.ts +48 -5
- package/src/server/routes.ts +7 -2
- package/src/theme/default.css +4 -0
- package/src/video/bake.ts +6 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// edit, and the Markdown Copy writes. No DOM and no file system here, so all of it is decided by
|
|
4
4
|
// these functions alone.
|
|
5
5
|
|
|
6
|
+
import { isPlainWord } from "./escape.ts";
|
|
7
|
+
|
|
6
8
|
/** A box on the slide, in logical pixels, from its top left. */
|
|
7
9
|
export type Box = { x: number; y: number; width: number; height: number };
|
|
8
10
|
|
|
@@ -115,8 +117,10 @@ export function formatNotes(
|
|
|
115
117
|
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
// The deck's folder and the notes file are the project's text, which a clone brings along.
|
|
121
|
+
const folder = shown(deck, "<deck>");
|
|
118
122
|
const lines = [
|
|
119
|
-
`## Notes on decks/${
|
|
123
|
+
`## Notes on decks/${folder}`,
|
|
120
124
|
"",
|
|
121
125
|
"Boxes and points are in the slide's own pixels, from its top left.",
|
|
122
126
|
];
|
|
@@ -129,11 +133,11 @@ export function formatNotes(
|
|
|
129
133
|
const title = slides[index]?.title;
|
|
130
134
|
lines.push(
|
|
131
135
|
"",
|
|
132
|
-
`### Slide ${index + 1} of ${slides.length}, ${first.slug}${title ? ` (${JSON.stringify(title)})` : ""}, ${beatLabel(first.step)}`,
|
|
136
|
+
`### Slide ${index + 1} of ${slides.length}, ${shown(first.slug, "<slug>")}${title ? ` (${JSON.stringify(title)})` : ""}, ${beatLabel(first.step)}`,
|
|
133
137
|
);
|
|
134
138
|
for (const row of group) {
|
|
135
139
|
const where = (target: TargetRow): string =>
|
|
136
|
-
`${describe(target)} at decks/${
|
|
140
|
+
`${describe(target)} at decks/${folder}/slides/${shown(row.slug, "<slug>")}.html:${target.line}:${target.column} (${measure(target)})${target.found ? "" : ", as written before an edit; the line may have moved"}`;
|
|
137
141
|
lines.push("");
|
|
138
142
|
if (row.targets.length === 1 && row.targets[0]) {
|
|
139
143
|
lines.push(`${row.number}. ${where(row.targets[0])}`);
|
|
@@ -159,7 +163,15 @@ function beatLabel(step: string): string {
|
|
|
159
163
|
if (step === "0") {
|
|
160
164
|
return "as it arrives";
|
|
161
165
|
}
|
|
162
|
-
|
|
166
|
+
if (/^\d+$/.test(step)) {
|
|
167
|
+
return `at beat ${step}`;
|
|
168
|
+
}
|
|
169
|
+
return isPlainWord(step) ? `at beat \`${step}\`` : "at beat <step>";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `word` as it is when it is plain, else `placeholder`: see `isPlainWord`. */
|
|
173
|
+
function shown(word: string, placeholder: string): string {
|
|
174
|
+
return isPlainWord(word) ? word : placeholder;
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
function describe(target: TargetRow): string {
|
package/src/core/annotations.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./annotation-rows.ts";
|
|
10
10
|
import { deckPaths } from "./deck-paths.ts";
|
|
11
11
|
import { DekError } from "./error.ts";
|
|
12
|
+
import { shellName } from "./escape.ts";
|
|
12
13
|
import { deckSlides, type WrittenElement, writtenElements } from "./html.ts";
|
|
13
14
|
import { type ProjectDeck, readDeckFile } from "./resolve.ts";
|
|
14
15
|
import { readSourceIfExists, writeInside } from "./safe-fs.ts";
|
|
@@ -306,11 +307,28 @@ function toRow(
|
|
|
306
307
|
text: note.text,
|
|
307
308
|
targets,
|
|
308
309
|
changed,
|
|
309
|
-
shot:
|
|
310
|
+
shot: shotCommand(deck.name, note.slug, note.step),
|
|
310
311
|
createdAt: note.createdAt,
|
|
311
312
|
};
|
|
312
313
|
}
|
|
313
314
|
|
|
315
|
+
/**
|
|
316
|
+
* The shot of a note's slide at its beat, as a command to paste. Each name is the file's, so one
|
|
317
|
+
* that no word holds alike in every shell is a placeholder, and one that starts with a dash goes
|
|
318
|
+
* after `--`, where it reads as no flag.
|
|
319
|
+
*/
|
|
320
|
+
function shotCommand(deck: string, slug: string, step: string): string {
|
|
321
|
+
const [name, slide, beat] = [
|
|
322
|
+
shellName(deck) ?? "<deck>",
|
|
323
|
+
shellName(slug) ?? "<slug>",
|
|
324
|
+
shellName(step) ?? "<step>",
|
|
325
|
+
];
|
|
326
|
+
const flag = step.startsWith("-") ? `--step=${beat}` : `--step ${beat}`;
|
|
327
|
+
return deck.startsWith("-") || slug.startsWith("-")
|
|
328
|
+
? `dekc shot ${flag} -- ${name} ${slide}`
|
|
329
|
+
: `dekc shot ${name} ${slide} ${flag}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
314
332
|
/** A hash of each of the slide's own files there is, by its extension. */
|
|
315
333
|
function slideFileHashes(deck: ProjectDeck, slug: string): Record<string, string> {
|
|
316
334
|
const paths = deckPaths(deck.dir);
|
package/src/core/config-keys.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DekToml, Voice } from "./config.ts";
|
|
1
|
+
import { DekToml, Pack, Voice } from "./config.ts";
|
|
2
2
|
import { splitLines } from "./lines.ts";
|
|
3
3
|
import { readFrontmatterYaml } from "./parse.ts";
|
|
4
4
|
import { Frontmatter } from "./schema.ts";
|
|
@@ -10,12 +10,16 @@ import { unrecognizedKeys } from "./zod.ts";
|
|
|
10
10
|
export type UnknownKey = { key: string; line: number; suggestion?: string };
|
|
11
11
|
|
|
12
12
|
/** dek.toml's schema, but a key it does not read is an issue; `refs` still takes any key. */
|
|
13
|
-
const StrictDekToml = DekToml.extend({
|
|
13
|
+
const StrictDekToml = DekToml.extend({
|
|
14
|
+
voice: Voice.strict().optional(),
|
|
15
|
+
pack: Pack.strict().optional(),
|
|
16
|
+
}).strict();
|
|
14
17
|
|
|
15
18
|
/** Every key dek.toml may hold, as dotted paths. */
|
|
16
19
|
const DEK_TOML_KEYS: readonly string[] = [
|
|
17
20
|
...Object.keys(DekToml.shape),
|
|
18
21
|
...Object.keys(Voice.shape).map((key) => `voice.${key}`),
|
|
22
|
+
...Object.keys(Pack.shape).map((key) => `pack.${key}`),
|
|
19
23
|
];
|
|
20
24
|
|
|
21
25
|
export const FRONTMATTER_KEYS: readonly string[] = Object.keys(Frontmatter.shape);
|
package/src/core/config.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { DekError } from "./error.ts";
|
|
4
|
+
import { shellQuote } from "./escape.ts";
|
|
5
|
+
import { isPackLicense, PACK_LICENSES, type PackLicense } from "./licenses.ts";
|
|
3
6
|
import { parsePublicUrl } from "./ogp.ts";
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isPinFor,
|
|
9
|
+
isPlainRefName,
|
|
10
|
+
packFile,
|
|
11
|
+
parseRefSource,
|
|
12
|
+
REF_DECK_NAME_RULE,
|
|
13
|
+
} from "./ref-name.ts";
|
|
14
|
+
import { readSourceIfExists } from "./safe-fs.ts";
|
|
5
15
|
import { DeckSettings } from "./schema.ts";
|
|
16
|
+
import { scanTomlKeys } from "./toml-keys.ts";
|
|
6
17
|
import { parseTomlWith } from "./zod.ts";
|
|
7
18
|
|
|
8
19
|
export const Voice = z.object({
|
|
@@ -11,6 +22,15 @@ export const Voice = z.object({
|
|
|
11
22
|
speed: z.number().optional(),
|
|
12
23
|
});
|
|
13
24
|
|
|
25
|
+
export const Pack = z.object({
|
|
26
|
+
license: z
|
|
27
|
+
.string()
|
|
28
|
+
.refine(isPackLicense, {
|
|
29
|
+
message: `write ${PACK_LICENSES.join(" or ")}, or delete the line and put a LICENSE at the project root`,
|
|
30
|
+
})
|
|
31
|
+
.optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
14
34
|
export const DekToml = z.object({
|
|
15
35
|
url: z
|
|
16
36
|
.string()
|
|
@@ -21,24 +41,72 @@ export const DekToml = z.object({
|
|
|
21
41
|
.optional(),
|
|
22
42
|
...DeckSettings.partial().shape,
|
|
23
43
|
voice: Voice.optional(),
|
|
44
|
+
pack: Pack.optional(),
|
|
45
|
+
// Each key is checked before its value, so a key that names no ref is never repeated.
|
|
24
46
|
refs: z
|
|
25
|
-
.record(z.string(), z.
|
|
47
|
+
.record(z.string(), z.unknown())
|
|
26
48
|
.superRefine((refs, ctx) => {
|
|
27
49
|
for (const [name, rev] of Object.entries(refs)) {
|
|
28
50
|
if (!isPlainRefName(name)) {
|
|
29
|
-
ctx.addIssue({ code: "custom", path: [name], message: "a ref is named owner/repo/deck" });
|
|
30
|
-
} else if (!isPinnedRev(rev)) {
|
|
31
51
|
ctx.addIssue({
|
|
32
52
|
code: "custom",
|
|
33
|
-
path: [
|
|
34
|
-
message:
|
|
53
|
+
path: [],
|
|
54
|
+
message: `${REFUSED_REF_KEY}; ${REFUSED_REF_HINT}`,
|
|
35
55
|
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const source = parseRefSource(name);
|
|
59
|
+
if (typeof rev === "string" && isPinFor(source, rev)) {
|
|
60
|
+
continue;
|
|
36
61
|
}
|
|
62
|
+
ctx.addIssue({
|
|
63
|
+
code: "custom",
|
|
64
|
+
path: [name],
|
|
65
|
+
message:
|
|
66
|
+
source.kind === "file"
|
|
67
|
+
? `pin a pack's ref to the 64-character sha256 of its files; run \`dekc ref ${shellQuote(packFile(source.deck))}\` to pin one`
|
|
68
|
+
: `pin a ref to a 40-character commit sha; run \`dekc ref ${shellQuote(name)}\` to pin one`,
|
|
69
|
+
});
|
|
37
70
|
}
|
|
38
71
|
})
|
|
72
|
+
.transform((refs) => refs as Record<string, string>)
|
|
39
73
|
.optional(),
|
|
40
74
|
});
|
|
41
75
|
|
|
76
|
+
const REFUSED_REF_KEY = "a key in [refs] names no ref";
|
|
77
|
+
|
|
78
|
+
const REFUSED_REF_HINT = `delete that line, and for a pack its packs/<deck>.tgz and refs/file/<deck>, then pin the deck again under a name a ref can carry: a ref is named owner/repo/deck, or file/<deck> for a pack, and ${REF_DECK_NAME_RULE}`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The first key in [refs] that names no ref, by its line when the file says it plainly. Such a
|
|
82
|
+
* key may be someone else's text, from a project cloned or a pack added before dek checked it, so
|
|
83
|
+
* the error points at it and never repeats it.
|
|
84
|
+
*/
|
|
85
|
+
function refusedRefKey(source: string): { line?: number } | undefined {
|
|
86
|
+
let parsed: unknown;
|
|
87
|
+
try {
|
|
88
|
+
parsed = Bun.TOML.parse(source);
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const refs = (parsed as { refs?: unknown } | null)?.refs;
|
|
93
|
+
const name =
|
|
94
|
+
refs !== null && typeof refs === "object"
|
|
95
|
+
? Object.keys(refs).find((key) => !isPlainRefName(key))
|
|
96
|
+
: undefined;
|
|
97
|
+
if (name === undefined) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const written = scanTomlKeys(source).filter(
|
|
101
|
+
(entry) => entry.path[0] === "refs" && entry.path.length === 2,
|
|
102
|
+
);
|
|
103
|
+
// A key written with escapes reads differently on its line; any key there that names no ref is it.
|
|
104
|
+
const entry =
|
|
105
|
+
written.find((key) => key.path[1] === name) ??
|
|
106
|
+
written.find((key) => !isPlainRefName(key.path[1] ?? ""));
|
|
107
|
+
return entry === undefined ? {} : { line: entry.line };
|
|
108
|
+
}
|
|
109
|
+
|
|
42
110
|
type VoiceDefaults = {
|
|
43
111
|
engine: string;
|
|
44
112
|
speaker: string;
|
|
@@ -54,11 +122,17 @@ export type DekConfig = {
|
|
|
54
122
|
*/
|
|
55
123
|
seed?: Partial<DeckSettings>;
|
|
56
124
|
voice?: VoiceDefaults;
|
|
57
|
-
/** `[
|
|
125
|
+
/** `[pack]`: what `dekc pack` hands a deck over with, the license it carries. */
|
|
126
|
+
pack?: { license?: PackLicense };
|
|
127
|
+
/** `[refs]`: each ref name and what it is pinned to: a commit, or a pack's digest. */
|
|
58
128
|
refs?: Record<string, string>;
|
|
59
129
|
};
|
|
60
130
|
|
|
61
131
|
export function parseDekToml(source: string, path?: string): DekConfig {
|
|
132
|
+
const refused = refusedRefKey(source);
|
|
133
|
+
if (refused) {
|
|
134
|
+
throw new DekError(REFUSED_REF_KEY, { path, ...refused, hint: REFUSED_REF_HINT });
|
|
135
|
+
}
|
|
62
136
|
const data = parseTomlWith(DekToml, source, {
|
|
63
137
|
label: "dek.toml",
|
|
64
138
|
anchor: "dek-toml",
|
|
@@ -77,13 +151,15 @@ export function parseDekToml(source: string, path?: string): DekConfig {
|
|
|
77
151
|
...(url ? { url } : {}),
|
|
78
152
|
...(Object.keys(seed).length > 0 ? { seed } : {}),
|
|
79
153
|
...(voice ? { voice } : {}),
|
|
154
|
+
...(data.pack
|
|
155
|
+
? { pack: data.pack.license ? { license: data.pack.license as PackLicense } : {} }
|
|
156
|
+
: {}),
|
|
80
157
|
...(data.refs && Object.keys(data.refs).length > 0 ? { refs: data.refs } : {}),
|
|
81
158
|
};
|
|
82
159
|
}
|
|
83
160
|
|
|
161
|
+
/** The project's dek.toml, which sits at its root; a link out of the project is an error. */
|
|
84
162
|
export function loadConfig(configPath: string): DekConfig {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
return parseDekToml(readFileSync(configPath, "utf8"), configPath);
|
|
163
|
+
const source = readSourceIfExists(configPath, dirname(configPath));
|
|
164
|
+
return source === undefined ? {} : parseDekToml(source, configPath);
|
|
89
165
|
}
|
package/src/core/diagnostic.ts
CHANGED
|
@@ -17,7 +17,9 @@ export type Scope = "project" | "deck" | "slide";
|
|
|
17
17
|
* (DEK044), an empty heading (DEK024), which a slide script may fill, and a step bound by
|
|
18
18
|
* position where the beat has an id (DEK025), which is right until a beat is inserted, and a
|
|
19
19
|
* style several slides repeat (DEK026), which works but belongs in the theme. Text under
|
|
20
|
-
* aria-hidden (DEK029) is right on a sample the talk shows as unreadable.
|
|
20
|
+
* aria-hidden (DEK029) is right on a sample the talk shows as unreadable. A line break that
|
|
21
|
+
* reads wrong (DEK035) is fixed by a choice of words or of where to break, which is the author's,
|
|
22
|
+
* and an edge a few pixels off (DEK036) can be meant, as in a picture of something out of line.
|
|
21
23
|
*/
|
|
22
24
|
export const RULES = {
|
|
23
25
|
DEK001: { severity: "error", scopes: ["slide"] },
|
|
@@ -54,6 +56,8 @@ export const RULES = {
|
|
|
54
56
|
DEK032: { severity: "error", scopes: ["slide"] },
|
|
55
57
|
DEK033: { severity: "error", scopes: ["slide"] },
|
|
56
58
|
DEK034: { severity: "error", scopes: ["slide"] },
|
|
59
|
+
DEK035: { severity: "warning", scopes: ["slide"] },
|
|
60
|
+
DEK036: { severity: "warning", scopes: ["slide"] },
|
|
57
61
|
DEK040: { severity: "warning", scopes: ["slide"] },
|
|
58
62
|
DEK041: { severity: "warning", scopes: ["deck"] },
|
|
59
63
|
DEK042: { severity: "warning", scopes: ["slide"] },
|
package/src/core/escape.ts
CHANGED
|
@@ -14,3 +14,46 @@ export function escapeAttr(text: string): string {
|
|
|
14
14
|
export function escapeRegExp(text: string): string {
|
|
15
15
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A word no shell reads anything in: letters, marks, and digits of any script, and `@%+=:,./_-`,
|
|
20
|
+
* but not first an `=`, which zsh reads as the path of the program named after it.
|
|
21
|
+
*/
|
|
22
|
+
const PLAIN_WORD = /^[\p{L}\p{M}\p{N}@%+:,./_-][\p{L}\p{M}\p{N}@%+=:,./_-]*$/u;
|
|
23
|
+
|
|
24
|
+
/** Whether `word` is plain: dek prints it as it is, in a command or in prose. */
|
|
25
|
+
export function isPlainWord(word: string): boolean {
|
|
26
|
+
return PLAIN_WORD.test(word);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A backtick, as a shell word that holds none: `printf` writes it from its octal code. */
|
|
30
|
+
const BACKTICK_WORD = `"$(printf '\\140')"`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `word` as one shell word, as dek writes it in a command to paste: as it is when it is plain,
|
|
34
|
+
* else single-quoted. A command is printed inside a code span, which a backtick would close, so a
|
|
35
|
+
* backtick is written as `printf` of its code, between quoted runs. The quoting is a POSIX shell's,
|
|
36
|
+
* as sh, bash, and zsh read it: fish reads a backslash in single quotes apart, and PowerShell and
|
|
37
|
+
* cmd quote otherwise. A name from a file goes through `shellName` instead.
|
|
38
|
+
*/
|
|
39
|
+
export function shellQuote(word: string): string {
|
|
40
|
+
if (isPlainWord(word)) {
|
|
41
|
+
return word;
|
|
42
|
+
}
|
|
43
|
+
const quoted = word
|
|
44
|
+
.split("`")
|
|
45
|
+
.map((run) => (run === "" ? "" : `'${run.replaceAll("'", "'\\''")}'`))
|
|
46
|
+
.join(BACKTICK_WORD);
|
|
47
|
+
return quoted === "" ? "''" : quoted;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A name from a file, such as a deck's folder or a slide's file, as a word in a command dek
|
|
52
|
+
* suggests, or nothing when it has none: fish reads a quote or a backslash in it apart from a
|
|
53
|
+
* POSIX shell, and a control character would start a line of its own. The command then shows a
|
|
54
|
+
* placeholder, such as `<deck>`, and says where the name is.
|
|
55
|
+
*/
|
|
56
|
+
export function shellName(name: string): string | undefined {
|
|
57
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: a control in a name is the problem.
|
|
58
|
+
return /['\\\x00-\x1f\x7f-\x9f]/.test(name) ? undefined : shellQuote(name);
|
|
59
|
+
}
|