docs: split CLAUDE.md into per-directory guidance #14

Merged
sulthan merged 19 commits from docs/split-claude-md into main 2026-08-04 20:35:17 +07:00
2 changed files with 71 additions and 0 deletions
Showing only changes of commit 2fcf882c49 - Show all commits
+25
View File
@@ -25,6 +25,15 @@ var asuraSlugRe = regexp.MustCompile(`/comics/([^/?#]+)`)
// through. Both the raw "&" and the HTML-escaped "&" forms occur. // through. Both the raw "&" and the HTML-escaped "&" forms occur.
var demonicChapterRe = regexp.MustCompile(`chaptered\.php\?manga=\d+&(?:amp;)?chapter=([0-9.]+)`) var demonicChapterRe = regexp.MustCompile(`chaptered\.php\?manga=\d+&(?:amp;)?chapter=([0-9.]+)`)
// comixSlugRe pulls the "<id>-<slug>" segment out of a stored series_url.
// Only the id prefix is stable; the slug tail follows the title.
var comixSlugRe = regexp.MustCompile(`/title/([^/?#]+)`)
// kaganeChapterRe matches the chapter numbers in a kagane API response. This
// branch is fed by the browser fetcher, so the body is JSON rather than HTML —
// there are no anchors to scan.
var kaganeChapterRe = regexp.MustCompile(`"chapter_no":"([0-9.]+)"`)
// latestChapterFrom returns the highest chapter number body advertises for this // latestChapterFrom returns the highest chapter number body advertises for this
// series. ok is false when the body yields nothing usable — an unknown site, an // series. ok is false when the body yields nothing usable — an unknown site, an
// empty body, a Cloudflare challenge page, and a site redesign all land here, // empty body, a Cloudflare challenge page, and a site redesign all land here,
@@ -61,6 +70,22 @@ func latestChapterFrom(site, seriesURL, body string) (latestChapter, bool) {
re = regexp.MustCompile(`/comics/` + regexp.QuoteMeta(slug) + `(?:-[0-9a-f]{8})?/chapter/([0-9.]+)`) re = regexp.MustCompile(`/comics/` + regexp.QuoteMeta(slug) + `(?:-[0-9a-f]{8})?/chapter/([0-9.]+)`)
case "demonic": case "demonic":
re = demonicChapterRe re = demonicChapterRe
case "comix":
m := comixSlugRe.FindStringSubmatch(seriesURL)
if m == nil {
return latestChapter{}, false
}
// comix ships an SPA: the served HTML carries a JSON state blob instead
// of chapter anchors, and latestChapterUrl is the only place the newest
// chapter appears. Scoping to this series' id prefix keeps a
// "recommended" strip's entries from winning the maximum.
id := m[1]
if i := strings.Index(id, "-"); i != -1 {
id = id[:i]
}
re = regexp.MustCompile(`"latestChapterUrl":"/title/` + regexp.QuoteMeta(id) + `-[^"]*-chapter-([0-9.]+)"`)
case "kagane":
re = kaganeChapterRe
default: default:
return latestChapter{}, false return latestChapter{}, false
} }
+46
View File
@@ -34,6 +34,24 @@ const challengeFixture = `<!DOCTYPE html><html><head><title>Just a moment...</ti
<script src="/cdn-cgi/challenge-platform/h/b/orchestrate/chl_page/v1"></script></head> <script src="/cdn-cgi/challenge-platform/h/b/orchestrate/chl_page/v1"></script></head>
<body><div id="challenge-running">Checking your browser</div></body></html>` <body><div id="challenge-running">Checking your browser</div></body></html>`
// Trimmed from the server-rendered HTML of
// https://comix.to/title/n8we-dungeons-and-crayons fetched 2026-08-03. comix is
// an SPA: the page ships a JSON state blob rather than a list of chapter
// anchors, and latestChapterUrl is where the newest chapter actually lives.
const comixSeriesFixture = `
{"firstChapterUrl":"/title/n8we-dungeons-and-crayons/5038739-chapter-1","latestChapterUrl":"/title/n8we-dungeons-and-crayons/11139891-chapter-80"},
{""manga","recommended","n8we",1]":{"items":[{"latestChapterUrl":"/title/qqwrm-full-time-awakening/99999999-chapter-999"}]}
`
// The kagane branch is fed by the browser fetcher, so the body is API JSON, not
// HTML. Trimmed from GET /api/v2/series/<uuid> on 2026-08-03.
const kaganeAPIFixture = `
{"series_id":"019f84bc-9ba0-7ed9-86f5-8b905ec7c28b","title":"Infinite Decryption",
"series_books":[{"book_id":"a","title":"Episode 1","chapter_no":"1","sort_no":1},
{"book_id":"b","title":"Episode 41","chapter_no":"41","sort_no":41},
{"book_id":"c","title":"Episode 40.5","chapter_no":"40.5","sort_no":40}]}
`
func TestLatestChapterFrom(t *testing.T) { func TestLatestChapterFrom(t *testing.T) {
const asuraURL = "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af" const asuraURL = "https://asurascans.com/comics/chronicles-of-the-demon-faction-f886a8af"
const demonicURL = "https://demonicscans.org/manga/Catastrophic-Necromancer" const demonicURL = "https://demonicscans.org/manga/Catastrophic-Necromancer"
@@ -100,6 +118,34 @@ func TestLatestChapterFrom(t *testing.T) {
site: "mangadex", seriesURL: "https://example.com/x", body: asuraSeriesFixture, site: "mangadex", seriesURL: "https://example.com/x", body: asuraSeriesFixture,
wantOK: false, wantOK: false,
}, },
{
name: "comix reads latestChapterUrl, scoped to this series",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: comixSeriesFixture,
wantOK: true, wantNum: 80, wantLabel: "Chapter 80",
},
{
name: "comix yields nothing on a challenge page",
site: "comix",
seriesURL: "https://comix.to/title/n8we-dungeons-and-crayons",
body: challengeFixture,
wantOK: false,
},
{
name: "kagane takes the max chapter_no from API json",
site: "kagane",
seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b",
body: kaganeAPIFixture,
wantOK: true, wantNum: 41, wantLabel: "Chapter 41",
},
{
name: "kagane yields nothing on a challenge page",
site: "kagane",
seriesURL: "https://kagane.to/series/019f84bc-9ba0-7ed9-86f5-8b905ec7c28b",
body: challengeFixture,
wantOK: false,
},
} }
for _, tt := range tests { for _, tt := range tests {