mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
105 lines
2.8 KiB
TypeScript
105 lines
2.8 KiB
TypeScript
/**
|
|
* Pure helper: expand a human-friendly page specification into a Set of
|
|
* 1-based page numbers. Handles single pages (N), ranges (N-M), reverse
|
|
* indices (rN = total - N + 1), the "z" alias for the last page, and
|
|
* comma-separated combinations.
|
|
*
|
|
* Throws on out-of-range references or empty specs.
|
|
*/
|
|
export function parsePageSpec(spec: string, total: number): Set<number> {
|
|
if (total < 1) throw new Error("Total page count must be at least 1");
|
|
|
|
const result = new Set<number>();
|
|
const parts = spec
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
if (parts.length === 0) {
|
|
throw new Error("Empty page specification");
|
|
}
|
|
|
|
for (const part of parts) {
|
|
if (part.includes("-")) {
|
|
const [startRaw, endRaw] = part.split("-", 2);
|
|
const start = resolveToken(startRaw, total);
|
|
const end = resolveToken(endRaw, total);
|
|
const lo = Math.min(start, end);
|
|
const hi = Math.max(start, end);
|
|
for (let i = lo; i <= hi; i++) {
|
|
result.add(i);
|
|
}
|
|
} else {
|
|
result.add(resolveToken(part, total));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Collapse a sorted-ascending array of page numbers into the most compact
|
|
* qpdf range string by merging consecutive runs into N-M parts.
|
|
*
|
|
* Examples:
|
|
* [2,3,4,...,80] -> "2-80"
|
|
* [1,3,5,7] -> "1,3,5,7"
|
|
* [1,2,4,5,6,9] -> "1-2,4-6,9"
|
|
* [] -> ""
|
|
* [3] -> "3"
|
|
*
|
|
* The input MUST be sorted ascending with no duplicates (as produced by
|
|
* iterating 1..total and filtering).
|
|
*/
|
|
export function compressPageRuns(pages: number[]): string {
|
|
if (pages.length === 0) return "";
|
|
|
|
const parts: string[] = [];
|
|
let runStart = pages[0];
|
|
let runEnd = pages[0];
|
|
|
|
for (let i = 1; i < pages.length; i++) {
|
|
if (pages[i] === runEnd + 1) {
|
|
runEnd = pages[i];
|
|
} else {
|
|
parts.push(runStart === runEnd ? `${runStart}` : `${runStart}-${runEnd}`);
|
|
runStart = pages[i];
|
|
runEnd = pages[i];
|
|
}
|
|
}
|
|
|
|
parts.push(runStart === runEnd ? `${runStart}` : `${runStart}-${runEnd}`);
|
|
return parts.join(",");
|
|
}
|
|
|
|
function resolveToken(token: string, total: number): number {
|
|
const t = token.trim().toLowerCase();
|
|
let page: number;
|
|
|
|
if (t === "z") {
|
|
page = total;
|
|
} else if (t.startsWith("r")) {
|
|
const n = Number.parseInt(t.slice(1), 10);
|
|
if (!Number.isFinite(n) || n < 1) {
|
|
throw new Error(`Invalid reverse page index: ${token}`);
|
|
}
|
|
page = total - n + 1;
|
|
if (page < 1) {
|
|
throw new Error(
|
|
`Reverse index ${token} resolves to page ${page}, out of range for ${total} pages`,
|
|
);
|
|
}
|
|
} else {
|
|
page = Number.parseInt(t, 10);
|
|
if (!Number.isFinite(page) || page < 1) {
|
|
throw new Error(`Invalid page number: ${token}`);
|
|
}
|
|
}
|
|
|
|
if (page < 1 || page > total) {
|
|
throw new Error(`Page ${page} out of range for a ${total}-page document`);
|
|
}
|
|
|
|
return page;
|
|
}
|