126 lines
6.5 KiB
TypeScript
126 lines
6.5 KiB
TypeScript
// Tiny synthetic inputs for the importer tests. Nothing here is real course material.
|
|
import { existsSync } from "node:fs";
|
|
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { run } from "../src/util/exec";
|
|
|
|
// PyMuPDF is needed for PDF tests: set STC_PYTHON, or this machine's venv is used when present.
|
|
const VENV_PY = "/home/user/.venvs/pdf/bin/python";
|
|
export const PYTHON = process.env.STC_PYTHON ?? (existsSync(VENV_PY) ? VENV_PY : "python3");
|
|
|
|
export async function tmp(prefix = "stc-imp-"): Promise<string> {
|
|
return mkdtemp(join(tmpdir(), prefix));
|
|
}
|
|
|
|
export async function makeVideo(path: string, seconds = 2): Promise<void> {
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await run(["ffmpeg", "-v", "error", "-y", "-f", "lavfi", "-i", `testsrc=duration=${seconds}:size=160x90:rate=10`, "-f", "lavfi", "-i", `sine=frequency=440:duration=${seconds}`,
|
|
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", path]);
|
|
}
|
|
|
|
/** Minimal valid PDF with one line of text per page. */
|
|
export function pdfBytes(pages: string[]): Uint8Array {
|
|
const objs: string[] = [];
|
|
const kids = pages.map((_, i) => `${4 + i * 2} 0 R`).join(" ");
|
|
objs.push("<< /Type /Catalog /Pages 2 0 R >>");
|
|
objs.push(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>`);
|
|
objs.push("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
|
|
pages.forEach((text, i) => {
|
|
const content = `BT /F1 24 Tf 40 150 Td (${text.replace(/[()\\]/g, "")}) Tj ET`;
|
|
objs.push(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 320 200] /Resources << /Font << /F1 3 0 R >> >> /Contents ${5 + i * 2} 0 R >>`);
|
|
objs.push(`<< /Length ${content.length} >>\nstream\n${content}\nendstream`);
|
|
});
|
|
let out = "%PDF-1.4\n";
|
|
const offsets: number[] = [];
|
|
objs.forEach((o, i) => { offsets.push(out.length); out += `${i + 1} 0 obj\n${o}\nendobj\n`; });
|
|
const xref = out.length;
|
|
out += `xref\n0 ${objs.length + 1}\n0000000000 65535 f \n` + offsets.map((o) => `${String(o).padStart(10, "0")} 00000 n \n`).join("");
|
|
out += `trailer\n<< /Size ${objs.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
|
|
return new TextEncoder().encode(out);
|
|
}
|
|
|
|
export async function makePdf(path: string, pages = ["First slide alpha", "Second slide beta"]): Promise<void> {
|
|
await mkdir(dirname(path), { recursive: true });
|
|
await writeFile(path, pdfBytes(pages));
|
|
}
|
|
|
|
export async function zipDir(srcDir: string, zipPath: string): Promise<void> {
|
|
await run([PYTHON, "-I", "-c",
|
|
"import sys,os,zipfile\nsrc,out=sys.argv[1:3]\nz=zipfile.ZipFile(out,'w',zipfile.ZIP_STORED)\nfor r,d,f in os.walk(src):\n for n in f:\n p=os.path.join(r,n); z.write(p, os.path.relpath(p,src))\nz.close()",
|
|
srcDir, zipPath]);
|
|
}
|
|
|
|
const li = (t: string) => `<li>${t}</li>`;
|
|
const clipHtml = (s: number, m: number, title: string) => `
|
|
<li>
|
|
<img src="images/image.jpg" class="image-holder"/>
|
|
<h1>${s}.${m} ${title}</h1>
|
|
<span>In this video, ${title}, you'll learn it.</span>
|
|
<div id="video${s}_${m}" class="btn btn-small btn-primary watch" data-video="videos/video${s}_${m}.mp4" data-poster="images/videoImage.jpg">Watch Video</div>
|
|
<a href="videos/video${s}_${m}.mp4" class="btn download">Download</a>
|
|
</li>`;
|
|
|
|
export const PACKT_SECTIONS = [
|
|
{ title: "Intro to Docker", clips: ["Course Introduction", "Docker & Friends"] },
|
|
{ title: "Images (Sample)", clips: ["Topic Introduction - Images"] },
|
|
];
|
|
|
|
/** Mimics the real package layout: summary lists without "Video N.M:" prefixes, then chapter blocks. */
|
|
export function packtIndexHtml(opts: { dropChapter?: number } = {}): string {
|
|
const summary = PACKT_SECTIONS.map((s, i) => `
|
|
<li>
|
|
<img src="images/image.jpg" class="image-holder"/>
|
|
<h1>Section ${i + 1}: ${s.title}</h1>
|
|
<p>Video ${i + 1}.1: ${s.clips[0]}</p>
|
|
<ul>${s.clips.map(li).join("\n")}</ul>
|
|
<div class="btn btn-primary browse" id="#content-${i + 1}">Browse Videos</div>
|
|
</li>`).join("");
|
|
const chapters = PACKT_SECTIONS.map((s, i) => i + 1 === opts.dropChapter ? "" : `
|
|
<div class="chapter" id="content-${i + 1}" style="display: none;">
|
|
<ul class="move">${s.clips.map((c, j) => clipHtml(i + 1, j + 1, c)).join("")}
|
|
</ul>
|
|
</div>`).join("");
|
|
return `<!doctype html><html><head><title>Fake</title></head><body>
|
|
<div class="intro"><div class="inner"><h1>Fake Docker Course</h1></div></div>
|
|
<div id="content-main"><ul class="move">${summary}</ul>${chapters}</div></body></html>`;
|
|
}
|
|
|
|
export async function makePacktTree(root: string): Promise<void> {
|
|
await mkdir(join(root, "package", "videos"), { recursive: true });
|
|
await writeFile(join(root, "package", "index.html"), packtIndexHtml());
|
|
await writeFile(join(root, "Video Package_User Guide.pdf"), "not used");
|
|
await makeVideo(join(root, "package/videos/video1_1.mp4"), 2);
|
|
for (const n of ["video1_2", "video2_1"]) await Bun.write(join(root, "package/videos", `${n}.mp4`), Bun.file(join(root, "package/videos/video1_1.mp4")));
|
|
}
|
|
|
|
export async function makePacktZip(dir: string): Promise<string> {
|
|
const tree = join(dir, "packt-tree");
|
|
await makePacktTree(tree);
|
|
const zip = join(dir, "video_zip.zip");
|
|
await zipDir(tree, zip);
|
|
return zip;
|
|
}
|
|
|
|
/** Companion repo with folders that have spaces; S01 has a deck and a cheat sheet, S02 has code. */
|
|
export async function makeRepoTree(root: string): Promise<string> {
|
|
const top = join(root, "Course-Repo-main");
|
|
await makePdf(join(top, "S01 - Intro to Docker", "Slides_Intro.pdf"), ["Repo deck one", "Repo deck two", "Repo deck three"]);
|
|
await makePdf(join(top, "S01 - Intro to Docker", "Docker_CheatSheet.pdf"), ["cheats"]);
|
|
await mkdir(join(top, "S02 - Images (Sample)", "app"), { recursive: true });
|
|
await writeFile(join(top, "S02 - Images (Sample)", "app", "main.py"), "print('hi')\n");
|
|
await writeFile(join(top, "S02 - Images (Sample)", "Ünïcode notes.txt"), "notes\n");
|
|
await mkdir(join(top, "Bonus Extras"), { recursive: true });
|
|
await writeFile(join(top, "Bonus Extras", "x.txt"), "x");
|
|
await writeFile(join(top, "README.md"), "# repo\n");
|
|
return top;
|
|
}
|
|
|
|
export async function makeRepoZip(dir: string): Promise<string> {
|
|
const tree = join(dir, "repo-tree");
|
|
await makeRepoTree(tree);
|
|
const zip = join(dir, "repo.zip");
|
|
await zipDir(tree, zip);
|
|
return zip;
|
|
}
|