crawlforge-extractors 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/templates.js +57 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawlforge-extractors",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Extraction logic shared by the CrawlForge MCP server and REST API — scrape templates, charset-correct capped body reading, and structural fingerprinting. One implementation, so the two surfaces cannot drift apart.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/src/templates.js
CHANGED
|
@@ -188,6 +188,48 @@ function youtubeInteractionCount($, action) {
|
|
|
188
188
|
return count ? Number.parseInt(count, 10) : null;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// ── GitHub helpers ───────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The logged-out repo page is GitHub's React code view: the About sidebar
|
|
195
|
+
* (homepage link, license, topics, counts) is not rendered as HTML — it ships
|
|
196
|
+
* as JSON inside <script data-target="react-app.embeddedData">, and the DOM
|
|
197
|
+
* around it is skeleton placeholders. Parse that payload; the CSS selectors
|
|
198
|
+
* stay as fallbacks for older server-rendered layouts.
|
|
199
|
+
*/
|
|
200
|
+
function githubSidebarAbout($) {
|
|
201
|
+
for (const el of $('script[data-target="react-app.embeddedData"]').toArray()) {
|
|
202
|
+
try {
|
|
203
|
+
const about = JSON.parse($(el).text())?.payload?.sidebarAbout;
|
|
204
|
+
if (about) return about;
|
|
205
|
+
} catch {
|
|
206
|
+
// a different embedded payload — keep looking
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The payload's license is { spdxId: "MIT", name: "MIT License" }. The SPDX
|
|
214
|
+
* id is the short name callers want, but a repo with a non-standard licence
|
|
215
|
+
* ships spdxId "NOASSERTION", where the display name is all there is.
|
|
216
|
+
*/
|
|
217
|
+
function githubLicense(license) {
|
|
218
|
+
if (!license) return null;
|
|
219
|
+
const spdx = license.spdxId;
|
|
220
|
+
return (spdx && spdx !== 'NOASSERTION' ? spdx : license.name) || null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Repo tab counters stamp the exact number in title= ("5,102") and an
|
|
225
|
+
* abbreviated text ("5.1k"); a counter with nothing to show stamps
|
|
226
|
+
* title="Not available", so the title is only trusted when it has a digit.
|
|
227
|
+
*/
|
|
228
|
+
function githubCounter($, sel) {
|
|
229
|
+
const exact = attr($, sel, 'title');
|
|
230
|
+
return /\d/.test(exact || '') ? exact : text($, sel);
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
// ── Template definitions ─────────────────────────────────────────────────────
|
|
192
234
|
|
|
193
235
|
export const TEMPLATES = [
|
|
@@ -346,22 +388,31 @@ export const TEMPLATES = [
|
|
|
346
388
|
description: 'Scrape a GitHub repository page for stars, forks, description, language, topics, and README summary.',
|
|
347
389
|
targetPattern: /github\.com\/[^/]+\/[^/]+\/?$/i,
|
|
348
390
|
extract($) {
|
|
391
|
+
const about = githubSidebarAbout($);
|
|
349
392
|
return {
|
|
350
393
|
name: text($, 'strong[itemprop="name"] a') || text($, '.repository-content h1'),
|
|
351
394
|
description: attr($, 'meta[property="og:description"]', 'content') || text($, 'p.f4.my-3'),
|
|
352
395
|
stars: text($, '#repo-stars-counter-star') || text($, '[aria-label*="stargazers"]'),
|
|
353
396
|
forks: text($, '#repo-network-counter') || text($, '[aria-label*="forks"]'),
|
|
354
397
|
// React (logged-out) layout has no watchers aria-label; the count is
|
|
355
|
-
// the <strong> right after the single octicon-eye.
|
|
356
|
-
// client-side skeleton on that layout — unrecoverable from static
|
|
357
|
-
// HTML, so it stays null there (itemprop still works on classic).
|
|
398
|
+
// the <strong> right after the single octicon-eye.
|
|
358
399
|
watchers: text($, '.octicon-eye + strong') || text($, '[aria-label*="watchers"]'),
|
|
400
|
+
// Language and the last-push date appear nowhere in the logged-out
|
|
401
|
+
// page (verified 2026-08-26 across five User-Agents, embedded JSON
|
|
402
|
+
// included): the client fetches them after load from header-gated
|
|
403
|
+
// JSON endpoints (/_sidebar, /latest-commit). The selectors below
|
|
404
|
+
// only fire on older server-rendered layouts; otherwise these two
|
|
405
|
+
// stay null rather than guessing.
|
|
359
406
|
language: text($, 'span[itemprop="programmingLanguage"]') || text($, '.d-inline-flex[class*="language"]'),
|
|
360
407
|
topics: list($, 'a.topic-tag, a[href^="/topics/"]'),
|
|
361
|
-
|
|
408
|
+
// Never the repo's LICENSE *file* link — its text is the filename,
|
|
409
|
+
// not the licence name.
|
|
410
|
+
license: githubLicense(about?.repo?.license) || text($, '.octicon-law ~ span'),
|
|
362
411
|
last_updated: attr($, 'relative-time', 'datetime'),
|
|
363
|
-
|
|
364
|
-
|
|
412
|
+
// When the sidebar payload is present it is authoritative: an empty
|
|
413
|
+
// website means "no homepage", not "go scrape some external link".
|
|
414
|
+
homepage: about ? about.website || null : attr($, 'a[href][rel="noopener noreferrer"]', 'href'),
|
|
415
|
+
open_issues: githubCounter($, '#issues-repo-tab-count') || text($, '.Counter[aria-label*="issue"]')
|
|
365
416
|
};
|
|
366
417
|
}
|
|
367
418
|
},
|