feat(userscript): add comix.to site adapter

This commit is contained in:
2026-08-03 16:57:14 +07:00
parent eeb601cbe2
commit e392ec3de0
2 changed files with 155 additions and 2 deletions
+86 -2
View File
@@ -80,6 +80,14 @@
return slug.replace(/-[0-9a-f]{8}$/, "");
}
// comix path segments are "<id>-<slug>", where the slug is a rendering of the
// current title and changes when a series is renamed. Only the id is the
// identity; seriesUrl keeps the full segment because navigation needs it.
function comixSeriesId(segment) {
const i = segment.indexOf("-");
return i === -1 ? segment : segment.slice(0, i);
}
const asura = {
site: "asura",
// asuracomic.net deep links 301 to the asurascans.com *root*, dropping the
@@ -208,7 +216,83 @@
},
};
const ADAPTERS = [asura, demonic];
const comix = {
site: "comix",
matches: (loc) => /(^|\.)comix\.to$/.test(loc.hostname),
detect(loc) {
const path = loc.pathname;
// /title/<id>-<slug>/<uploadId>-chapter-<n>. Several uploads (different
// groups or languages) share one chapter number; the number is the
// progress identity, the upload id is not.
let m = path.match(/^\/title\/([^/]+)\/[^/]*-chapter-([\d.]+)/);
if (m) {
const num = parseFloat(m[2]);
return {
type: "chapter",
site: this.site,
seriesId: comixSeriesId(m[1]),
title: cleanTitle(meta("og:title")),
cover: coverFromPage(),
seriesUrl: loc.origin + "/title/" + m[1],
chapterLabel: "Chapter " + m[2],
chapterNum: isNaN(num) ? null : num,
chapterUrl: loc.href,
};
}
// /title/<id>-<slug>
m = path.match(/^\/title\/([^/?#]+)\/?$/);
if (m) {
return {
type: "series",
site: this.site,
seriesId: comixSeriesId(m[1]),
title: cleanTitle(meta("og:title")),
cover: coverFromPage(),
seriesUrl: loc.origin + "/title/" + m[1],
chapterLabel: null,
chapterNum: null,
chapterUrl: null,
};
}
return { type: "other" };
// comix chapter og:title is "<Title> · Ch.<n>"; series is clean.
function cleanTitle(t) {
if (!t) return "";
return t.replace(/\s*·\s*Ch\.[\d.]+\s*$/i, "").trim();
}
// comix serves no og:image, so this is the one adapter that has to read
// the DOM for a cover. Matching on alt rather than a class keeps it off
// the site's styling: the cover is the image whose alt is the title.
// Do not "simplify" this into meta("og:image") — that returns null.
function coverFromPage() {
const title = cleanTitle(meta("og:title"));
if (!title || !document.querySelectorAll) return "";
for (const img of document.querySelectorAll("img[alt]")) {
if (img.getAttribute("alt") === title) return img.getAttribute("src") || "";
}
return "";
}
},
// Scoped to this series' own id prefix so a recommendation strip's links
// cannot win the maximum. seriesId is passed in because the anchors alone
// do not say which series the page belongs to.
latestChapterFromAnchors(anchors, seriesId) {
let best = null;
const re = new RegExp("^/title/" + seriesId + "-[^/]*/[^/]*-chapter-([\\d.]+)");
for (const a of anchors) {
const m = a.href.match(re);
if (!m) continue;
const num = parseFloat(m[1]);
if (isNaN(num)) continue;
if (!best || num > best.num) best = { num, label: "Chapter " + m[1] };
}
return best;
},
};
const ADAPTERS = [asura, demonic, comix];
function detect() {
const loc = window.location;
@@ -1608,7 +1692,7 @@
// Exposes pure logic only — see userscript/test/logic.test.js.
// ============================================================
if (typeof window === "undefined" && typeof module === "object" && module.exports) {
module.exports = { stripBuildHash, asura, demonic, anchorsFromHTML, statusOf };
module.exports = { stripBuildHash, comixSeriesId, asura, demonic, comix, anchorsFromHTML, statusOf };
}
// ============================================================