75 lines
3.5 KiB
JavaScript
75 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
const PUBLIC_MODES = new Set(['missing', 'latest', 'last', 'first', 'chapter', 'after', 'volume']);
|
|
|
|
function chapterNumber(value, field = 'chapter') {
|
|
const raw = String(value ?? '').trim();
|
|
if (!/^\d+(?:\.\d+)?$/.test(raw)) throw new Error(`${field} must be a numeric chapter`);
|
|
const [whole, decimal = ''] = raw.split('.');
|
|
const normalizedWhole = whole.replace(/^0+(?=\d)/, '') || '0';
|
|
const normalizedDecimal = decimal.replace(/0+$/, '');
|
|
return normalizedDecimal ? `${normalizedWhole}.${normalizedDecimal}` : normalizedWhole;
|
|
}
|
|
|
|
function positiveCount(value) {
|
|
const count = Number(value);
|
|
if (!Number.isInteger(count) || count < 1 || count > 10000) throw new Error('count must be an integer from 1 to 10000');
|
|
return count;
|
|
}
|
|
|
|
function normalizeScope(input = {}) {
|
|
const raw = input && typeof input === 'object' ? input : {};
|
|
const mode = String(raw.mode || 'missing').toLowerCase();
|
|
if (mode === 'ids') {
|
|
if (!Array.isArray(raw.ids) || !raw.ids.length || raw.ids.length > 10000) throw new Error('ids scope requires 1 to 10000 source chapter ids');
|
|
return { mode, ids: [...new Set(raw.ids.map(value => String(value).trim()).filter(Boolean))] };
|
|
}
|
|
if (!PUBLIC_MODES.has(mode)) throw new Error(`Unsupported queue scope: ${mode}`);
|
|
if (mode === 'last' || mode === 'first') return { mode, count: positiveCount(raw.count) };
|
|
if (mode === 'chapter' || mode === 'after') return { mode, chapter: chapterNumber(raw.chapter) };
|
|
if (mode === 'volume') {
|
|
const volume = String(raw.volume ?? '').trim();
|
|
if (!/^\d+(?:\.\d+)?$/.test(volume)) throw new Error('volume must be numeric');
|
|
return { mode, volume: chapterNumber(volume, 'volume') };
|
|
}
|
|
return { mode };
|
|
}
|
|
|
|
function volumeOf(ref) {
|
|
const value = ref.volume ?? ref.book?.number;
|
|
if (value === undefined || value === null || value === '') return null;
|
|
try { return chapterNumber(value, 'volume'); } catch { return null; }
|
|
}
|
|
|
|
function selectByScope(chapters, input) {
|
|
const scope = normalizeScope(input);
|
|
const list = [...chapters];
|
|
if (scope.mode === 'missing') return list;
|
|
if (scope.mode === 'latest') return list.length ? [list[list.length - 1]] : [];
|
|
if (scope.mode === 'last') return list.slice(-scope.count);
|
|
if (scope.mode === 'first') return list.slice(0, scope.count);
|
|
const numbered = list.filter(ref => /^\d+(?:\.\d+)?$/.test(String(ref.number)));
|
|
if (scope.mode === 'chapter') return numbered.filter(ref => chapterNumber(ref.number) === scope.chapter);
|
|
if (scope.mode === 'after') return numbered.filter(ref => Number(chapterNumber(ref.number)) > Number(scope.chapter));
|
|
if (scope.mode === 'volume') return list.filter(ref => volumeOf(ref) === scope.volume);
|
|
if (scope.mode === 'ids') {
|
|
const ids = new Set(scope.ids);
|
|
return list.filter(ref => ids.has(String(ref.id)));
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function scopeLabel(input) {
|
|
const scope = normalizeScope(input);
|
|
if (scope.mode === 'missing') return 'all missing chapters';
|
|
if (scope.mode === 'latest') return 'newest chapter';
|
|
if (scope.mode === 'last') return `last ${scope.count} chapters`;
|
|
if (scope.mode === 'first') return `first ${scope.count} chapters`;
|
|
if (scope.mode === 'chapter') return `chapter ${scope.chapter}`;
|
|
if (scope.mode === 'after') return `chapters after ${scope.chapter}`;
|
|
if (scope.mode === 'volume') return `volume ${scope.volume}`;
|
|
return `${scope.ids.length} discovered chapters`;
|
|
}
|
|
|
|
module.exports = { normalizeScope, selectByScope, scopeLabel, chapterNumber, volumeOf };
|