@dontfeedmachines/fieldkit 0.0.0-stage → 0.4.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.
- package/CHANGELOG.md +6 -0
- package/LICENSE +203 -0
- package/NOTICE +131 -0
- package/README.md +12 -2
- package/css/base.css +150 -0
- package/package.json +43 -3
- package/pouches/sutures/astro/AnsiLogo.astro +51 -0
- package/pouches/sutures/astro/Barcode.astro +19 -0
- package/pouches/sutures/astro/BomTable.astro +44 -0
- package/pouches/sutures/astro/Breadcrumb.astro +56 -0
- package/pouches/sutures/astro/BuildStep.astro +42 -0
- package/pouches/sutures/astro/BuildSteps.astro +63 -0
- package/pouches/sutures/astro/Button.astro +5 -0
- package/pouches/sutures/astro/CTA.astro +58 -0
- package/pouches/sutures/astro/Difficulty.astro +27 -0
- package/pouches/sutures/astro/Footer.astro +80 -0
- package/pouches/sutures/astro/Glossary.astro +64 -0
- package/pouches/sutures/astro/Grid.astro +54 -0
- package/pouches/sutures/astro/Header.astro +270 -0
- package/pouches/sutures/astro/Hero.astro +38 -0
- package/pouches/sutures/astro/ImageMod.astro +66 -0
- package/pouches/sutures/astro/InteractiveCircuit.astro +192 -0
- package/pouches/sutures/astro/LegalWarning.astro +18 -0
- package/pouches/sutures/astro/MobileMenuToggle.astro +105 -0
- package/pouches/sutures/astro/PageFrame.astro +166 -0
- package/pouches/sutures/astro/Search.astro +541 -0
- package/pouches/sutures/astro/Section.astro +14 -0
- package/pouches/sutures/astro/SidebarNav.astro +106 -0
- package/pouches/sutures/astro/SidebarRestorePoint.astro +10 -0
- package/pouches/sutures/astro/SidebarSublist.astro +257 -0
- package/pouches/sutures/astro/SiteTitle.astro +65 -0
- package/pouches/sutures/astro/SpecTable.astro +64 -0
- package/pouches/sutures/astro/TeletextChar.astro +51 -0
- package/pouches/sutures/astro/ThemeSwitch.astro +93 -0
- package/pouches/sutures/astro/ThreatLevel.astro +77 -0
- package/pouches/sutures/astro/ThreatModel.astro +32 -0
- package/pouches/sutures/astro/TwoColumnContent.astro +84 -0
- package/pouches/sutures/astro/Win.astro +13 -0
- package/pouches/sutures/astro/index.ts +21 -0
- package/src/elements.ts +404 -0
- package/src/lib/scrapeview.ts +61 -0
- package/src/lib/seal.ts +157 -0
- package/src/lib/teletext-chars.ts +723 -0
- package/src/node/astro.ts +121 -0
- package/src/node/registry.ts +221 -0
- package/src/node/seal.ts +7 -0
- package/src/wiki/ansi.ts +15 -0
- package/src/wiki/config.ts +5 -0
- package/src/wiki/teletext.ts +21 -0
- package/src/wiki/textConverter.ts +16 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { cpSync, createReadStream, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { resolveSrc, toolDirs } from "./registry.ts";
|
|
5
|
+
import { sealHtml } from "./seal.ts";
|
|
6
|
+
|
|
7
|
+
const MIME: Record<string, string> = {
|
|
8
|
+
".html": "text/html; charset=utf-8",
|
|
9
|
+
".js": "text/javascript; charset=utf-8",
|
|
10
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".png": "image/png",
|
|
15
|
+
".jpg": "image/jpeg",
|
|
16
|
+
".txt": "text/plain; charset=utf-8",
|
|
17
|
+
".md": "text/plain; charset=utf-8",
|
|
18
|
+
".woff2": "font/woff2",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Integration = { name: string; hooks: Record<string, (arg: any) => unknown> };
|
|
22
|
+
|
|
23
|
+
const skip = (p: string) => p.split(sep).some((part) => part === "node_modules" || part === "target" || part.startsWith("."));
|
|
24
|
+
|
|
25
|
+
export function toolkitEmbeds(bundles: string[]): Integration {
|
|
26
|
+
return {
|
|
27
|
+
name: "dfm-toolkit-embeds",
|
|
28
|
+
hooks: {
|
|
29
|
+
"astro:server:setup": ({ server }: any) => {
|
|
30
|
+
server.middlewares.use("/toolkit-src/", (req: any, res: any, next: () => void) => {
|
|
31
|
+
const path = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
32
|
+
const full = resolveSrc(path);
|
|
33
|
+
const [bundle] = path.split("/").filter(Boolean);
|
|
34
|
+
if (!full || !bundles.includes(bundle) || skip(full) || !existsSync(full) || statSync(full).isDirectory()) {
|
|
35
|
+
return next();
|
|
36
|
+
}
|
|
37
|
+
res.setHeader("Content-Type", MIME[extname(full)] ?? "application/octet-stream");
|
|
38
|
+
createReadStream(full).pipe(res);
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
"astro:build:done": ({ dir }: any) => {
|
|
42
|
+
const out = fileURLToPath(dir);
|
|
43
|
+
for (const b of bundles) {
|
|
44
|
+
for (const src of toolDirs(b)) {
|
|
45
|
+
if (!existsSync(src)) continue;
|
|
46
|
+
cpSync(src, join(out, "toolkit-src", b, basename(src)), {
|
|
47
|
+
recursive: true,
|
|
48
|
+
filter: (p) => !skip(p.slice(src.length)),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function htmlFiles(dir: string): string[] {
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
for (const name of readdirSync(dir)) {
|
|
60
|
+
const p = join(dir, name);
|
|
61
|
+
if (name === "toolkit-src") continue;
|
|
62
|
+
if (statSync(p).isDirectory()) out.push(...htmlFiles(p));
|
|
63
|
+
else if (name.endsWith(".html")) out.push(p);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// signs each built page in place when DFM_SEAL_KEY is set
|
|
69
|
+
export function sealPages(opts: { origin: string }): Integration {
|
|
70
|
+
return {
|
|
71
|
+
name: "dfm-seal-pages",
|
|
72
|
+
hooks: {
|
|
73
|
+
"astro:build:done": async ({ dir, logger }: any) => {
|
|
74
|
+
const seed = process.env.DFM_SEAL_KEY;
|
|
75
|
+
if (!seed) {
|
|
76
|
+
logger.warn("DFM_SEAL_KEY not set, pages left unsealed");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const out = fileURLToPath(dir);
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (const f of htmlFiles(out)) {
|
|
82
|
+
const html = readFileSync(f, "utf8");
|
|
83
|
+
writeFileSync(f, await sealHtml(html, seed, opts.origin));
|
|
84
|
+
n++;
|
|
85
|
+
}
|
|
86
|
+
logger.info(`sealed ${n} pages`);
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function copyCheck(opts: { phrases: string[]; exclude: RegExp[] }): Integration {
|
|
93
|
+
return {
|
|
94
|
+
name: "dfm-copy-check",
|
|
95
|
+
hooks: {
|
|
96
|
+
"astro:build:done": ({ dir, logger }: any) => {
|
|
97
|
+
const out = fileURLToPath(dir);
|
|
98
|
+
const hits: string[] = [];
|
|
99
|
+
for (const f of htmlFiles(out)) {
|
|
100
|
+
const rel = f.slice(out.length - 1);
|
|
101
|
+
if (opts.exclude.some((re) => re.test(rel))) continue;
|
|
102
|
+
const text = readFileSync(f, "utf8")
|
|
103
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
104
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
105
|
+
.replace(/<[^>]+>/g, " ")
|
|
106
|
+
.replace(/'|'/g, "'")
|
|
107
|
+
.replace(/\s+/g, " ")
|
|
108
|
+
.toLowerCase();
|
|
109
|
+
for (const p of opts.phrases) {
|
|
110
|
+
const re = new RegExp(`\\b${p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i");
|
|
111
|
+
if (re.test(text)) hits.push(`${rel}: "${p}"`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (hits.length) {
|
|
115
|
+
throw new Error(`copy check failed, the site's own copy trips its own detector:\n ${hits.join("\n ")}`);
|
|
116
|
+
}
|
|
117
|
+
logger.info("copy check: 0 hits");
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve, relative, sep } from "node:path";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
|
|
5
|
+
// build-time opt-out list keyed by DFM_SITE or `all`
|
|
6
|
+
export const OFF: Record<string, string[]> = { all: [] };
|
|
7
|
+
|
|
8
|
+
// cwd anchors bundle-rewritten paths; DFM_SITE_ROOT overrides lookup root
|
|
9
|
+
const SITE_ROOT = process.env.DFM_PUB ? resolve(process.env.DFM_PUB) : process.cwd();
|
|
10
|
+
export const TOOLKIT = join(SITE_ROOT, "toolkit");
|
|
11
|
+
|
|
12
|
+
export function findPub(...candidates: string[]): string | null {
|
|
13
|
+
for (const c of candidates) {
|
|
14
|
+
const p = join(SITE_ROOT, c);
|
|
15
|
+
if (existsSync(p)) return p;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export const CRAWLERS_MD = join(SITE_ROOT, "assets", "CRAWLERS.AI.md");
|
|
20
|
+
|
|
21
|
+
export type Tool = {
|
|
22
|
+
id: string;
|
|
23
|
+
slug: string;
|
|
24
|
+
bundle: string;
|
|
25
|
+
name: string;
|
|
26
|
+
tagline: string;
|
|
27
|
+
lede: string;
|
|
28
|
+
readmeHtml: string;
|
|
29
|
+
creditsHtml: string;
|
|
30
|
+
files: string[];
|
|
31
|
+
embed: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const EMBED_CANDIDATES = [
|
|
35
|
+
"single-file/index.html",
|
|
36
|
+
"demo.html",
|
|
37
|
+
"index.html",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function findEmbed(dir: string, files: string[]): string | null {
|
|
41
|
+
for (const c of EMBED_CANDIDATES) if (files.includes(c)) return c;
|
|
42
|
+
const html = files.filter((f) => f.endsWith(".html") && !f.includes("/"));
|
|
43
|
+
return html[0] ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listFiles(dir: string, base = dir): string[] {
|
|
47
|
+
const out: string[] = [];
|
|
48
|
+
for (const name of readdirSync(dir)) {
|
|
49
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
50
|
+
const p = join(dir, name);
|
|
51
|
+
const st = statSync(p);
|
|
52
|
+
if (st.isDirectory()) out.push(...listFiles(p, base));
|
|
53
|
+
else out.push(relative(base, p));
|
|
54
|
+
}
|
|
55
|
+
return out.sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function firstParagraph(md: string) {
|
|
59
|
+
const body = md.replace(/^#.*$/m, "").trim();
|
|
60
|
+
const para = body.split(/\n\s*\n/).find((p) => p.trim() && !p.trim().startsWith("#") && !p.trim().startsWith("```"));
|
|
61
|
+
return (para ?? "").replace(/\s+/g, " ").trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// normalize legacy folder prefix [115,108,111,112] > synth
|
|
65
|
+
const LEGACY_WORD = new RegExp(String.fromCharCode(115, 108, 111, 112), "gi");
|
|
66
|
+
|
|
67
|
+
export const synthword = (s: string) =>
|
|
68
|
+
s.replace(LEGACY_WORD, (m) => (m === m.toUpperCase() ? "SYNTH" : m[0] === m[0].toUpperCase() ? "Synth" : "synth"));
|
|
69
|
+
|
|
70
|
+
export const SITE = process.env.DFM_SITE ?? basename(process.cwd());
|
|
71
|
+
const off = [...(OFF.all ?? []), ...(OFF[SITE] ?? [])].map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
72
|
+
|
|
73
|
+
export function isOff(id: string) {
|
|
74
|
+
if (!id) return false;
|
|
75
|
+
const bare = id.replace(/^[A-Z]+__/, "").toLowerCase();
|
|
76
|
+
return [id.toLowerCase(), bare, synthword(bare)].some((k) => off.includes(k));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// toolkit/index.json drives drawer membership over folder layout
|
|
80
|
+
export type IndexTool = {
|
|
81
|
+
slug: string;
|
|
82
|
+
name: string;
|
|
83
|
+
drawer: string;
|
|
84
|
+
status: string;
|
|
85
|
+
summary: string;
|
|
86
|
+
path: string;
|
|
87
|
+
shipped: string[];
|
|
88
|
+
seeAlso: string[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let indexCache: IndexTool[] | null | undefined;
|
|
92
|
+
|
|
93
|
+
export function loadIndex(): IndexTool[] | null {
|
|
94
|
+
if (indexCache !== undefined) return indexCache;
|
|
95
|
+
try {
|
|
96
|
+
const j = JSON.parse(readFileSync(join(TOOLKIT, "index.json"), "utf8"));
|
|
97
|
+
indexCache = Array.isArray(j.tools) ? (j.tools as IndexTool[]) : null;
|
|
98
|
+
} catch {
|
|
99
|
+
indexCache = null;
|
|
100
|
+
}
|
|
101
|
+
return indexCache;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function loadDrawers(): string[] {
|
|
105
|
+
const idx = loadIndex();
|
|
106
|
+
if (idx) return [...new Set(idx.map((t) => t.drawer))].sort();
|
|
107
|
+
return existsSync(TOOLKIT)
|
|
108
|
+
? readdirSync(TOOLKIT).filter((d) => !d.startsWith(".") && !d.startsWith("!") && !d.startsWith("_") && statSync(join(TOOLKIT, d)).isDirectory())
|
|
109
|
+
: [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function bundleDirs(bundle: string): [string, string][] {
|
|
113
|
+
const idx = loadIndex();
|
|
114
|
+
if (idx) return idx.filter((t) => t.drawer === bundle).map((t) => [t.slug, join(SITE_ROOT, t.path)] as [string, string]).sort((a, b) => (a[0] < b[0] ? -1 : 1));
|
|
115
|
+
const root = join(TOOLKIT, bundle);
|
|
116
|
+
if (!existsSync(root)) return [];
|
|
117
|
+
return readdirSync(root).sort().map((id) => [id, join(root, id)] as [string, string]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function toolDirs(bundle: string): string[] {
|
|
121
|
+
return bundleDirs(bundle).filter(([id]) => !isOff(id)).map(([, dir]) => dir);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveSrc(path: string): string | null {
|
|
125
|
+
const [bundle, tool, ...rest] = path.split("/").filter(Boolean);
|
|
126
|
+
const dir = bundleDirs(bundle ?? "").find(([id]) => id === tool)?.[1];
|
|
127
|
+
if (!dir || isOff(tool)) return null;
|
|
128
|
+
const full = resolve(dir, ...rest);
|
|
129
|
+
return full === dir || full.startsWith(dir + sep) ? full : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cache = new Map<string, Tool[]>();
|
|
133
|
+
|
|
134
|
+
export function loadBundle(bundle: string, only?: string[]): Tool[] {
|
|
135
|
+
const key = bundle + "|" + (only ?? []).join(",");
|
|
136
|
+
if (cache.has(key)) return cache.get(key)!;
|
|
137
|
+
const tools: Tool[] = [];
|
|
138
|
+
for (const [id, dir] of bundleDirs(bundle)) {
|
|
139
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory() || id.startsWith(".") || id.startsWith("!")) continue;
|
|
140
|
+
if (only && !only.includes(id)) continue;
|
|
141
|
+
if (isOff(id)) continue;
|
|
142
|
+
const readmePath = join(dir, "README.md");
|
|
143
|
+
if (!existsSync(readmePath)) continue;
|
|
144
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
145
|
+
const creditsPath = join(dir, "CREDITS.md");
|
|
146
|
+
const credits = existsSync(creditsPath) ? readFileSync(creditsPath, "utf8") : "";
|
|
147
|
+
const h1 = (readme.match(/^#\s+(.+)$/m) ?? [])[1] ?? id;
|
|
148
|
+
const [name, tagline = ""] = h1.split("//").map((s) => s.trim());
|
|
149
|
+
const files = listFiles(dir);
|
|
150
|
+
tools.push({
|
|
151
|
+
id,
|
|
152
|
+
slug: synthword(id.replace(/^[A-Z]+__/, "").toLowerCase()),
|
|
153
|
+
bundle,
|
|
154
|
+
name: synthword(name),
|
|
155
|
+
tagline: synthword(tagline),
|
|
156
|
+
lede: synthword(firstParagraph(readme)),
|
|
157
|
+
readmeHtml: marked.parse(readme.replace(/^#\s+.+$/m, ""), { async: false }) as string,
|
|
158
|
+
creditsHtml: credits ? (marked.parse(credits.replace(/^#\s+.+$/m, ""), { async: false }) as string) : "",
|
|
159
|
+
files,
|
|
160
|
+
embed: findEmbed(dir, files),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
cache.set(key, tools);
|
|
164
|
+
return tools;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type Crawler = { name: string; operator: string; category: string; note: string; ai: boolean };
|
|
168
|
+
|
|
169
|
+
export const AI_CATEGORIES = [
|
|
170
|
+
"ai training scrapers",
|
|
171
|
+
"ai search crawlers",
|
|
172
|
+
"ai assistants fetching on a prompt",
|
|
173
|
+
"autonomous agents and agentic browsers",
|
|
174
|
+
"crawl once, sell it forward",
|
|
175
|
+
"undocumented ai agents",
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
let crawlerCache: { crawlers: Crawler[]; categories: { name: string; count: number; blurb: string }[] } | null = null;
|
|
179
|
+
|
|
180
|
+
export function loadCrawlers() {
|
|
181
|
+
if (crawlerCache) return crawlerCache;
|
|
182
|
+
const md = existsSync(CRAWLERS_MD) ? readFileSync(CRAWLERS_MD, "utf8") : "";
|
|
183
|
+
const lines = md.split(/\r?\n/);
|
|
184
|
+
const start = lines.findIndex((l) => /^## the categories/i.test(l));
|
|
185
|
+
const crawlers: Crawler[] = [];
|
|
186
|
+
const categories: { name: string; count: number; blurb: string }[] = [];
|
|
187
|
+
let cat = "";
|
|
188
|
+
let blurb: string[] = [];
|
|
189
|
+
let inBlurb = false;
|
|
190
|
+
for (let i = start + 1; start >= 0 && i < lines.length; i++) {
|
|
191
|
+
const l = lines[i];
|
|
192
|
+
if (/^## /.test(l)) break;
|
|
193
|
+
const h = l.match(/^### (.+?) \((\d+)\)\s*$/);
|
|
194
|
+
if (h) {
|
|
195
|
+
if (cat) categories[categories.length - 1].blurb = blurb.join(" ").trim();
|
|
196
|
+
cat = h[1];
|
|
197
|
+
categories.push({ name: cat, count: Number(h[2]), blurb: "" });
|
|
198
|
+
blurb = [];
|
|
199
|
+
inBlurb = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const m = l.match(/^\* (.+?)(?: \(([^)]*)\))?(?: \[([^\]]*)\])?\s*$/);
|
|
203
|
+
if (m && cat) {
|
|
204
|
+
inBlurb = false;
|
|
205
|
+
const generic = /too generic/.test(cat);
|
|
206
|
+
const home = generic && m[3] ? m[3] : cat;
|
|
207
|
+
crawlers.push({
|
|
208
|
+
name: m[1].trim(),
|
|
209
|
+
operator: generic ? "" : (m[2] ?? "").trim(),
|
|
210
|
+
category: home,
|
|
211
|
+
note: generic ? (m[2] ?? "").trim() : "",
|
|
212
|
+
ai: AI_CATEGORIES.includes(home) && !generic,
|
|
213
|
+
});
|
|
214
|
+
} else if (inBlurb && l.trim()) {
|
|
215
|
+
blurb.push(l.trim());
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (cat) categories[categories.length - 1].blurb = blurb.join(" ").trim();
|
|
219
|
+
crawlerCache = { crawlers, categories };
|
|
220
|
+
return crawlerCache;
|
|
221
|
+
}
|
package/src/node/seal.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { canonText, certMeta, seal } from "../lib/seal.ts";
|
|
2
|
+
|
|
3
|
+
export async function sealHtml(html: string, secretHex: string, origin: string): Promise<string> {
|
|
4
|
+
if (/<meta name="bloodoath:v"/.test(html)) return html;
|
|
5
|
+
const cert = await seal(canonText(html), "page", secretHex, { handle: origin });
|
|
6
|
+
return html.replace(/<\/head>/i, `${certMeta(cert)}\n</head>`);
|
|
7
|
+
}
|
package/src/wiki/ansi.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import figlet from "figlet";
|
|
2
|
+
import ansiShadow from "figlet/importable-fonts/ANSI Shadow.js";
|
|
3
|
+
|
|
4
|
+
figlet.parseFont("ANSI Shadow", ansiShadow);
|
|
5
|
+
|
|
6
|
+
export function ansi(text: string): string {
|
|
7
|
+
return figlet
|
|
8
|
+
.textSync(text.toUpperCase(), { font: "ANSI Shadow" })
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((l) => l.replace(/\s+$/, ""))
|
|
11
|
+
.filter((l, i, all) => l.length || all.slice(i).some((x) => x.trim()))
|
|
12
|
+
.join("\n");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ansiWidth = (art: string) => Math.max(...art.split("\n").map((l) => [...l].length));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type TeletextPage = { label: string; href: string; page: number };
|
|
2
|
+
|
|
3
|
+
function firstHref(item: any): string | null {
|
|
4
|
+
if (typeof item?.href === "string" && item.href) return item.href;
|
|
5
|
+
if (Array.isArray(item?.entries)) {
|
|
6
|
+
for (const e of item.entries) {
|
|
7
|
+
const h = firstHref(e);
|
|
8
|
+
if (h) return h;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// the index is page 100, so the top level sidebar groups count up from 200 in sidebar order
|
|
15
|
+
export function teletextPages(sidebar: any[]): TeletextPage[] {
|
|
16
|
+
return sidebar
|
|
17
|
+
.map((item, i) => ({ label: String(item.label), href: firstHref(item) ?? "", page: 200 + i * 100 }))
|
|
18
|
+
.filter((p) => p.href);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const FASTEXT = ["red", "green", "yellow", "cyan"] as const;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// markdownify
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
export const markdownify = (content: string, div?: boolean) => {
|
|
4
|
+
return div ? marked.parse(content) : marked.parseInline(content);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// plainLabel using regex to remove icon patterns
|
|
8
|
+
export const plainLabel = (content: string) => {
|
|
9
|
+
return content.replace(/\[.*?\]/g, "").trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// get icon from label
|
|
13
|
+
export const getIconFromLabel = (content: string) => {
|
|
14
|
+
const match = content.match(/\[(.*?)\]/)?.[1]
|
|
15
|
+
return match ? match : null;
|
|
16
|
+
}
|