@just-every/mcp-read-website-fast 0.1.11 → 0.1.13
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/README.md +2 -0
- package/dist/index.js +2 -4
- package/dist/internal/fetchMarkdown.js +2 -4
- package/package.json +3 -9
- package/dist/cache/disk.d.ts +0 -12
- package/dist/cache/disk.js +0 -54
- package/dist/cache/normalize.d.ts +0 -2
- package/dist/cache/normalize.js +0 -31
- package/dist/crawler/fetch.d.ts +0 -8
- package/dist/crawler/fetch.js +0 -43
- package/dist/crawler/queue.d.ts +0 -14
- package/dist/crawler/queue.js +0 -148
- package/dist/crawler/robots.d.ts +0 -8
- package/dist/crawler/robots.js +0 -47
- package/dist/parser/article.d.ts +0 -4
- package/dist/parser/article.js +0 -125
- package/dist/parser/dom.d.ts +0 -3
- package/dist/parser/dom.js +0 -60
- package/dist/parser/markdown.d.ts +0 -9
- package/dist/parser/markdown.js +0 -147
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Existing MCP web crawlers are slow and consume large quantities of tokens. This
|
|
|
11
11
|
|
|
12
12
|
This MCP package fetches web pages locally, strips noise, and converts content to clean Markdown while preserving links. Designed for Claude Code, IDEs and LLM pipelines with minimal token footprint. Crawl sites locally with minimal dependencies.
|
|
13
13
|
|
|
14
|
+
**Note:** This package now uses [@just-every/crawl](https://www.npmjs.com/package/@just-every/crawl) for its core crawling and markdown conversion functionality.
|
|
15
|
+
|
|
14
16
|
## Features
|
|
15
17
|
|
|
16
18
|
- **Fast startup** using official MCP SDK with lazy loading for optimal performance
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
3
|
+
import { fetch } from '@just-every/crawl';
|
|
4
4
|
import { readFileSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { dirname, join } from 'path';
|
|
@@ -34,10 +34,8 @@ program
|
|
|
34
34
|
cacheDir: options.cacheDir,
|
|
35
35
|
timeout: parseInt(options.timeout, 10),
|
|
36
36
|
};
|
|
37
|
-
const queue = new CrawlQueue(crawlOptions);
|
|
38
|
-
await queue.init();
|
|
39
37
|
console.error(`Fetching ${url}...`);
|
|
40
|
-
const results = await
|
|
38
|
+
const results = await fetch(url, crawlOptions);
|
|
41
39
|
if (options.output === 'json') {
|
|
42
40
|
console.log(JSON.stringify(results, null, 2));
|
|
43
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetch } from '@just-every/crawl';
|
|
2
2
|
export async function fetchMarkdown(url, options = {}) {
|
|
3
3
|
try {
|
|
4
4
|
const crawlOptions = {
|
|
@@ -10,9 +10,7 @@ export async function fetchMarkdown(url, options = {}) {
|
|
|
10
10
|
cacheDir: options.cacheDir ?? '.cache',
|
|
11
11
|
timeout: options.timeout ?? 30000,
|
|
12
12
|
};
|
|
13
|
-
const
|
|
14
|
-
await queue.init();
|
|
15
|
-
const results = await queue.crawl(url);
|
|
13
|
+
const results = await fetch(url, crawlOptions);
|
|
16
14
|
const mainResult = results[0];
|
|
17
15
|
if (!mainResult) {
|
|
18
16
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@just-every/mcp-read-website-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Markdown Content Preprocessor - Fetch web pages, extract content, convert to clean Markdown",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,15 +50,9 @@
|
|
|
50
50
|
"homepage": "https://github.com/just-every/mcp-read-website-fast#readme",
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@just-every/crawl": "^1.0.2",
|
|
53
54
|
"@modelcontextprotocol/sdk": "^1.12.3",
|
|
54
|
-
"
|
|
55
|
-
"commander": "^14.0.0",
|
|
56
|
-
"jsdom": "^26.1.0",
|
|
57
|
-
"p-limit": "^6.2.0",
|
|
58
|
-
"robots-parser": "^3.0.1",
|
|
59
|
-
"turndown": "^7.1.3",
|
|
60
|
-
"turndown-plugin-gfm": "^1.0.2",
|
|
61
|
-
"undici": "^7.10.0"
|
|
55
|
+
"commander": "^14.0.0"
|
|
62
56
|
},
|
|
63
57
|
"devDependencies": {
|
|
64
58
|
"@types/jsdom": "^21.1.6",
|
package/dist/cache/disk.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { CacheEntry } from '../types.js';
|
|
2
|
-
export declare class DiskCache {
|
|
3
|
-
private cacheDir;
|
|
4
|
-
constructor(cacheDir?: string);
|
|
5
|
-
init(): Promise<void>;
|
|
6
|
-
private getCacheKey;
|
|
7
|
-
private getCachePath;
|
|
8
|
-
has(url: string): Promise<boolean>;
|
|
9
|
-
get(url: string): Promise<CacheEntry | null>;
|
|
10
|
-
put(url: string, markdown: string, title?: string): Promise<void>;
|
|
11
|
-
getAge(url: string): Promise<number | null>;
|
|
12
|
-
}
|
package/dist/cache/disk.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import { mkdir, readFile, writeFile, access } from 'fs/promises';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
export class DiskCache {
|
|
5
|
-
cacheDir;
|
|
6
|
-
constructor(cacheDir = '.cache') {
|
|
7
|
-
this.cacheDir = cacheDir;
|
|
8
|
-
}
|
|
9
|
-
async init() {
|
|
10
|
-
await mkdir(this.cacheDir, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
getCacheKey(url) {
|
|
13
|
-
return createHash('sha256').update(url).digest('hex');
|
|
14
|
-
}
|
|
15
|
-
getCachePath(url) {
|
|
16
|
-
const key = this.getCacheKey(url);
|
|
17
|
-
return join(this.cacheDir, `${key}.json`);
|
|
18
|
-
}
|
|
19
|
-
async has(url) {
|
|
20
|
-
try {
|
|
21
|
-
await access(this.getCachePath(url));
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async get(url) {
|
|
29
|
-
try {
|
|
30
|
-
const path = this.getCachePath(url);
|
|
31
|
-
const data = await readFile(path, 'utf-8');
|
|
32
|
-
return JSON.parse(data);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async put(url, markdown, title) {
|
|
39
|
-
const entry = {
|
|
40
|
-
url,
|
|
41
|
-
markdown,
|
|
42
|
-
timestamp: Date.now(),
|
|
43
|
-
title,
|
|
44
|
-
};
|
|
45
|
-
const path = this.getCachePath(url);
|
|
46
|
-
await writeFile(path, JSON.stringify(entry, null, 2));
|
|
47
|
-
}
|
|
48
|
-
async getAge(url) {
|
|
49
|
-
const entry = await this.get(url);
|
|
50
|
-
if (!entry)
|
|
51
|
-
return null;
|
|
52
|
-
return Date.now() - entry.timestamp;
|
|
53
|
-
}
|
|
54
|
-
}
|
package/dist/cache/normalize.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export function normalizeUrl(url) {
|
|
2
|
-
try {
|
|
3
|
-
const parsed = new URL(url);
|
|
4
|
-
if (parsed.pathname !== '/' && parsed.pathname.endsWith('/')) {
|
|
5
|
-
parsed.pathname = parsed.pathname.slice(0, -1);
|
|
6
|
-
}
|
|
7
|
-
const params = Array.from(parsed.searchParams.entries());
|
|
8
|
-
params.sort(([a], [b]) => a.localeCompare(b));
|
|
9
|
-
parsed.search = '';
|
|
10
|
-
params.forEach(([key, value]) => parsed.searchParams.append(key, value));
|
|
11
|
-
if ((parsed.protocol === 'http:' && parsed.port === '80') ||
|
|
12
|
-
(parsed.protocol === 'https:' && parsed.port === '443')) {
|
|
13
|
-
parsed.port = '';
|
|
14
|
-
}
|
|
15
|
-
parsed.hash = '';
|
|
16
|
-
return parsed.href;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return url;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export function isSameOrigin(url1, url2) {
|
|
23
|
-
try {
|
|
24
|
-
const u1 = new URL(url1);
|
|
25
|
-
const u2 = new URL(url2);
|
|
26
|
-
return u1.origin === u2.origin;
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
package/dist/crawler/fetch.d.ts
DELETED
package/dist/crawler/fetch.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { fetch } from 'undici';
|
|
2
|
-
export async function fetchStream(url, options = {}) {
|
|
3
|
-
const { userAgent = 'MCP/0.1 (+https://github.com/just-every/mcp-read-website-fast)', timeout = 30000, maxRedirections = 5, } = options;
|
|
4
|
-
try {
|
|
5
|
-
const response = await fetch(url, {
|
|
6
|
-
headers: {
|
|
7
|
-
'User-Agent': userAgent,
|
|
8
|
-
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
9
|
-
'Accept-Language': 'en-US,en;q=0.5',
|
|
10
|
-
DNT: '1',
|
|
11
|
-
Connection: 'keep-alive',
|
|
12
|
-
'Upgrade-Insecure-Requests': '1',
|
|
13
|
-
},
|
|
14
|
-
redirect: maxRedirections > 0 ? 'follow' : 'manual',
|
|
15
|
-
signal: AbortSignal.timeout(timeout),
|
|
16
|
-
});
|
|
17
|
-
if (!response.ok) {
|
|
18
|
-
throw new Error(`HTTP ${response.status} for ${url}`);
|
|
19
|
-
}
|
|
20
|
-
const contentType = response.headers.get('content-type');
|
|
21
|
-
if (contentType &&
|
|
22
|
-
!contentType.includes('text/html') &&
|
|
23
|
-
!contentType.includes('application/xhtml+xml')) {
|
|
24
|
-
throw new Error(`Non-HTML content type: ${contentType} for ${url}`);
|
|
25
|
-
}
|
|
26
|
-
return await response.text();
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (error instanceof Error) {
|
|
30
|
-
throw new Error(`Failed to fetch ${url}: ${error.message}`);
|
|
31
|
-
}
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
export function isValidUrl(url) {
|
|
36
|
-
try {
|
|
37
|
-
const parsed = new URL(url);
|
|
38
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
package/dist/crawler/queue.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { CrawlOptions, CrawlResult } from '../types.js';
|
|
2
|
-
export declare class CrawlQueue {
|
|
3
|
-
private visited;
|
|
4
|
-
private queue;
|
|
5
|
-
private limit;
|
|
6
|
-
private cache;
|
|
7
|
-
private options;
|
|
8
|
-
private results;
|
|
9
|
-
constructor(options?: CrawlOptions);
|
|
10
|
-
init(): Promise<void>;
|
|
11
|
-
crawl(startUrl: string): Promise<CrawlResult[]>;
|
|
12
|
-
private processQueue;
|
|
13
|
-
private processUrl;
|
|
14
|
-
}
|
package/dist/crawler/queue.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import pLimit from 'p-limit';
|
|
2
|
-
import { normalizeUrl, isSameOrigin } from '../cache/normalize.js';
|
|
3
|
-
import { DiskCache } from '../cache/disk.js';
|
|
4
|
-
import { fetchStream, isValidUrl } from './fetch.js';
|
|
5
|
-
import { isAllowedByRobots, getCrawlDelay } from './robots.js';
|
|
6
|
-
import { htmlToDom, extractLinks } from '../parser/dom.js';
|
|
7
|
-
import { extractArticle } from '../parser/article.js';
|
|
8
|
-
import { formatArticleMarkdown } from '../parser/markdown.js';
|
|
9
|
-
export class CrawlQueue {
|
|
10
|
-
visited = new Set();
|
|
11
|
-
queue = [];
|
|
12
|
-
limit;
|
|
13
|
-
cache;
|
|
14
|
-
options;
|
|
15
|
-
results = [];
|
|
16
|
-
constructor(options = {}) {
|
|
17
|
-
this.options = {
|
|
18
|
-
depth: options.depth ?? 0,
|
|
19
|
-
maxConcurrency: options.maxConcurrency ?? 3,
|
|
20
|
-
respectRobots: options.respectRobots ?? true,
|
|
21
|
-
sameOriginOnly: options.sameOriginOnly ?? true,
|
|
22
|
-
userAgent: options.userAgent ?? 'MCP/0.1',
|
|
23
|
-
cacheDir: options.cacheDir ?? '.cache',
|
|
24
|
-
timeout: options.timeout ?? 30000,
|
|
25
|
-
};
|
|
26
|
-
this.limit = pLimit(this.options.maxConcurrency);
|
|
27
|
-
this.cache = new DiskCache(this.options.cacheDir);
|
|
28
|
-
}
|
|
29
|
-
async init() {
|
|
30
|
-
await this.cache.init();
|
|
31
|
-
}
|
|
32
|
-
async crawl(startUrl) {
|
|
33
|
-
const normalizedUrl = normalizeUrl(startUrl);
|
|
34
|
-
if (!isValidUrl(normalizedUrl)) {
|
|
35
|
-
throw new Error(`Invalid URL: ${startUrl}`);
|
|
36
|
-
}
|
|
37
|
-
this.queue.push(normalizedUrl);
|
|
38
|
-
await this.processQueue(0);
|
|
39
|
-
return this.results;
|
|
40
|
-
}
|
|
41
|
-
async processQueue(currentDepth) {
|
|
42
|
-
if (currentDepth > this.options.depth)
|
|
43
|
-
return;
|
|
44
|
-
const urls = [...this.queue];
|
|
45
|
-
this.queue = [];
|
|
46
|
-
const tasks = urls.map(url => this.limit(() => this.processUrl(url, currentDepth)));
|
|
47
|
-
await Promise.all(tasks);
|
|
48
|
-
if (this.queue.length > 0) {
|
|
49
|
-
await this.processQueue(currentDepth + 1);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async processUrl(url, depth) {
|
|
53
|
-
const normalizedUrl = normalizeUrl(url);
|
|
54
|
-
if (this.visited.has(normalizedUrl))
|
|
55
|
-
return;
|
|
56
|
-
this.visited.add(normalizedUrl);
|
|
57
|
-
try {
|
|
58
|
-
const cached = await this.cache.get(normalizedUrl);
|
|
59
|
-
if (cached) {
|
|
60
|
-
this.results.push({
|
|
61
|
-
url: normalizedUrl,
|
|
62
|
-
markdown: cached.markdown,
|
|
63
|
-
title: cached.title,
|
|
64
|
-
});
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (this.options.respectRobots) {
|
|
68
|
-
const allowed = await isAllowedByRobots(normalizedUrl, this.options.userAgent);
|
|
69
|
-
if (!allowed) {
|
|
70
|
-
this.results.push({
|
|
71
|
-
url: normalizedUrl,
|
|
72
|
-
markdown: '',
|
|
73
|
-
error: 'Blocked by robots.txt',
|
|
74
|
-
});
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
const delay = await getCrawlDelay(normalizedUrl, this.options.userAgent);
|
|
78
|
-
if (delay > 0) {
|
|
79
|
-
await new Promise(resolve => setTimeout(resolve, delay * 1000));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const html = await fetchStream(normalizedUrl, {
|
|
83
|
-
userAgent: this.options.userAgent,
|
|
84
|
-
timeout: this.options.timeout,
|
|
85
|
-
});
|
|
86
|
-
if (!html || html.trim().length === 0) {
|
|
87
|
-
this.results.push({
|
|
88
|
-
url: normalizedUrl,
|
|
89
|
-
markdown: '',
|
|
90
|
-
error: 'Empty response from server',
|
|
91
|
-
});
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const dom = htmlToDom(html, normalizedUrl);
|
|
95
|
-
const article = extractArticle(dom);
|
|
96
|
-
if (!article) {
|
|
97
|
-
this.results.push({
|
|
98
|
-
url: normalizedUrl,
|
|
99
|
-
markdown: '',
|
|
100
|
-
error: 'Failed to extract article content',
|
|
101
|
-
});
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (!article.content || article.content.trim().length < 50) {
|
|
105
|
-
const fallbackMarkdown = `# ${article.title || 'Page Content'}\n\n` +
|
|
106
|
-
`*Note: This page appears to be JavaScript-rendered. Limited content extracted.*\n\n` +
|
|
107
|
-
(article.textContent
|
|
108
|
-
? article.textContent.substring(0, 1000) + '...'
|
|
109
|
-
: 'No text content available');
|
|
110
|
-
this.results.push({
|
|
111
|
-
url: normalizedUrl,
|
|
112
|
-
markdown: fallbackMarkdown,
|
|
113
|
-
title: article.title || normalizedUrl,
|
|
114
|
-
error: 'Limited content extracted (JavaScript-rendered page)',
|
|
115
|
-
});
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const markdown = formatArticleMarkdown(article);
|
|
119
|
-
await this.cache.put(normalizedUrl, markdown, article.title);
|
|
120
|
-
let links = [];
|
|
121
|
-
if (depth < this.options.depth) {
|
|
122
|
-
links = extractLinks(dom);
|
|
123
|
-
if (this.options.sameOriginOnly) {
|
|
124
|
-
links = links.filter(link => isSameOrigin(normalizedUrl, link));
|
|
125
|
-
}
|
|
126
|
-
links.forEach(link => {
|
|
127
|
-
const normalized = normalizeUrl(link);
|
|
128
|
-
if (!this.visited.has(normalized)) {
|
|
129
|
-
this.queue.push(normalized);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
this.results.push({
|
|
134
|
-
url: normalizedUrl,
|
|
135
|
-
markdown,
|
|
136
|
-
title: article.title,
|
|
137
|
-
links: links.length > 0 ? links : undefined,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
this.results.push({
|
|
142
|
-
url: normalizedUrl,
|
|
143
|
-
markdown: '',
|
|
144
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
package/dist/crawler/robots.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
interface RobotsChecker {
|
|
2
|
-
isAllowed(url: string, userAgent?: string): boolean;
|
|
3
|
-
getCrawlDelay(userAgent?: string): number | undefined;
|
|
4
|
-
}
|
|
5
|
-
export declare function getRobotsChecker(origin: string, userAgent?: string): Promise<RobotsChecker>;
|
|
6
|
-
export declare function isAllowedByRobots(url: string, userAgent?: string): Promise<boolean>;
|
|
7
|
-
export declare function getCrawlDelay(url: string, userAgent?: string): Promise<number>;
|
|
8
|
-
export {};
|
package/dist/crawler/robots.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { fetchStream } from './fetch.js';
|
|
2
|
-
const robotsCache = new Map();
|
|
3
|
-
export async function getRobotsChecker(origin, userAgent = '*') {
|
|
4
|
-
const cached = robotsCache.get(origin);
|
|
5
|
-
if (cached)
|
|
6
|
-
return cached;
|
|
7
|
-
try {
|
|
8
|
-
const robotsUrl = new URL('/robots.txt', origin).href;
|
|
9
|
-
const robotsTxt = await fetchStream(robotsUrl, {
|
|
10
|
-
timeout: 5000,
|
|
11
|
-
userAgent,
|
|
12
|
-
});
|
|
13
|
-
const robotsParserModule = (await import('robots-parser'));
|
|
14
|
-
const robotsParser = robotsParserModule.default || robotsParserModule;
|
|
15
|
-
const robots = robotsParser(robotsUrl, robotsTxt);
|
|
16
|
-
robotsCache.set(origin, robots);
|
|
17
|
-
return robots;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
const permissive = {
|
|
21
|
-
isAllowed: () => true,
|
|
22
|
-
getCrawlDelay: () => undefined,
|
|
23
|
-
};
|
|
24
|
-
robotsCache.set(origin, permissive);
|
|
25
|
-
return permissive;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export async function isAllowedByRobots(url, userAgent = '*') {
|
|
29
|
-
try {
|
|
30
|
-
const { origin } = new URL(url);
|
|
31
|
-
const checker = await getRobotsChecker(origin, userAgent);
|
|
32
|
-
return checker.isAllowed(url, userAgent);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export async function getCrawlDelay(url, userAgent = '*') {
|
|
39
|
-
try {
|
|
40
|
-
const { origin } = new URL(url);
|
|
41
|
-
const checker = await getRobotsChecker(origin, userAgent);
|
|
42
|
-
return checker.getCrawlDelay(userAgent) || 0;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
}
|
package/dist/parser/article.d.ts
DELETED
package/dist/parser/article.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { Readability } from '@mozilla/readability';
|
|
2
|
-
export function extractArticle(dom) {
|
|
3
|
-
const document = dom.window.document;
|
|
4
|
-
const baseUrl = dom.window.location.href;
|
|
5
|
-
const articleParagraph = document.querySelector('article p');
|
|
6
|
-
const hasStrongArticleIndicators = (document.querySelector('article') !== null &&
|
|
7
|
-
articleParagraph?.textContent &&
|
|
8
|
-
articleParagraph.textContent.length > 200) ||
|
|
9
|
-
document.querySelector('[itemtype*="BlogPosting"]') !== null ||
|
|
10
|
-
document.querySelector('[itemtype*="NewsArticle"]') !== null ||
|
|
11
|
-
document.querySelector('meta[property="article:published_time"]') !==
|
|
12
|
-
null;
|
|
13
|
-
if (hasStrongArticleIndicators) {
|
|
14
|
-
const documentClone = document.cloneNode(true);
|
|
15
|
-
const reader = new Readability(documentClone);
|
|
16
|
-
const article = reader.parse();
|
|
17
|
-
if (article && article.content && article.content.trim().length > 500) {
|
|
18
|
-
return {
|
|
19
|
-
title: article.title || 'Untitled',
|
|
20
|
-
content: article.content || '',
|
|
21
|
-
textContent: article.textContent || '',
|
|
22
|
-
length: article.length || 0,
|
|
23
|
-
excerpt: article.excerpt || '',
|
|
24
|
-
byline: article.byline || null,
|
|
25
|
-
dir: article.dir || null,
|
|
26
|
-
lang: article.lang || null,
|
|
27
|
-
siteName: article.siteName || null,
|
|
28
|
-
publishedTime: article.publishedTime || null,
|
|
29
|
-
baseUrl,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return extractContentManually(dom);
|
|
34
|
-
}
|
|
35
|
-
function extractContentManually(dom) {
|
|
36
|
-
try {
|
|
37
|
-
const document = dom.window.document;
|
|
38
|
-
const baseUrl = dom.window.location.href;
|
|
39
|
-
const title = document.querySelector('title')?.textContent ||
|
|
40
|
-
document.querySelector('h1')?.textContent ||
|
|
41
|
-
document
|
|
42
|
-
.querySelector('meta[property="og:title"]')
|
|
43
|
-
?.getAttribute('content') ||
|
|
44
|
-
document
|
|
45
|
-
.querySelector('meta[name="title"]')
|
|
46
|
-
?.getAttribute('content') ||
|
|
47
|
-
'Untitled Page';
|
|
48
|
-
const byline = document
|
|
49
|
-
.querySelector('meta[name="author"]')
|
|
50
|
-
?.getAttribute('content') ||
|
|
51
|
-
document.querySelector('[rel="author"]')?.textContent ||
|
|
52
|
-
document.querySelector('.author')?.textContent ||
|
|
53
|
-
null;
|
|
54
|
-
if (!document.body) {
|
|
55
|
-
const html = document.documentElement?.innerHTML || '';
|
|
56
|
-
return {
|
|
57
|
-
title: title.trim(),
|
|
58
|
-
content: html,
|
|
59
|
-
byline,
|
|
60
|
-
excerpt: '',
|
|
61
|
-
dir: null,
|
|
62
|
-
lang: document.documentElement?.lang || null,
|
|
63
|
-
length: html.length,
|
|
64
|
-
siteName: null,
|
|
65
|
-
textContent: document.documentElement?.textContent || '',
|
|
66
|
-
publishedTime: null,
|
|
67
|
-
baseUrl,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const contentClone = document.body.cloneNode(true);
|
|
71
|
-
const selectorsToRemove = ['script', 'style', 'noscript', 'template'];
|
|
72
|
-
selectorsToRemove.forEach(selector => {
|
|
73
|
-
try {
|
|
74
|
-
contentClone
|
|
75
|
-
.querySelectorAll(selector)
|
|
76
|
-
.forEach(el => el.remove());
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
const mainContent = contentClone;
|
|
82
|
-
const content = mainContent.innerHTML || mainContent.textContent || '';
|
|
83
|
-
return {
|
|
84
|
-
title: title.trim(),
|
|
85
|
-
content,
|
|
86
|
-
byline,
|
|
87
|
-
excerpt: '',
|
|
88
|
-
dir: null,
|
|
89
|
-
lang: document.documentElement?.lang || null,
|
|
90
|
-
length: content.length,
|
|
91
|
-
siteName: null,
|
|
92
|
-
textContent: mainContent.textContent || '',
|
|
93
|
-
publishedTime: null,
|
|
94
|
-
baseUrl,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
console.error('Error in manual extraction:', error);
|
|
99
|
-
return {
|
|
100
|
-
title: 'Error extracting content',
|
|
101
|
-
content: dom.window.document.body?.innerHTML ||
|
|
102
|
-
dom.window.document.documentElement?.innerHTML ||
|
|
103
|
-
'',
|
|
104
|
-
byline: null,
|
|
105
|
-
excerpt: '',
|
|
106
|
-
dir: null,
|
|
107
|
-
lang: null,
|
|
108
|
-
length: 0,
|
|
109
|
-
siteName: null,
|
|
110
|
-
textContent: dom.window.document.body?.textContent || '',
|
|
111
|
-
publishedTime: null,
|
|
112
|
-
baseUrl: dom.window.location.href,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
export function hasContent(html) {
|
|
117
|
-
const lowerHtml = html.toLowerCase();
|
|
118
|
-
if (lowerHtml.includes('<noscript>') &&
|
|
119
|
-
!lowerHtml.includes('<article') &&
|
|
120
|
-
!lowerHtml.includes('<main')) {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
const textContent = html.replace(/<[^>]*>/g, '').trim();
|
|
124
|
-
return textContent.length > 100;
|
|
125
|
-
}
|
package/dist/parser/dom.d.ts
DELETED
package/dist/parser/dom.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { JSDOM, VirtualConsole } from 'jsdom';
|
|
2
|
-
export function htmlToDom(html, url) {
|
|
3
|
-
try {
|
|
4
|
-
return new JSDOM(html, {
|
|
5
|
-
url,
|
|
6
|
-
contentType: 'text/html',
|
|
7
|
-
includeNodeLocations: false,
|
|
8
|
-
runScripts: undefined,
|
|
9
|
-
resources: undefined,
|
|
10
|
-
pretendToBeVisual: true,
|
|
11
|
-
virtualConsole: new VirtualConsole().sendTo(console, {
|
|
12
|
-
omitJSDOMErrors: true,
|
|
13
|
-
}),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
try {
|
|
18
|
-
return new JSDOM(html, {
|
|
19
|
-
url,
|
|
20
|
-
contentType: 'text/html',
|
|
21
|
-
virtualConsole: new VirtualConsole().sendTo(console, {
|
|
22
|
-
omitJSDOMErrors: true,
|
|
23
|
-
}),
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`, {
|
|
28
|
-
url,
|
|
29
|
-
contentType: 'text/html',
|
|
30
|
-
virtualConsole: new VirtualConsole().sendTo(console, {
|
|
31
|
-
omitJSDOMErrors: true,
|
|
32
|
-
}),
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
export function extractLinks(dom) {
|
|
38
|
-
const document = dom.window.document;
|
|
39
|
-
const links = [];
|
|
40
|
-
const baseUrl = dom.window.location.href;
|
|
41
|
-
const anchorElements = document.querySelectorAll('a[href]');
|
|
42
|
-
anchorElements.forEach(element => {
|
|
43
|
-
try {
|
|
44
|
-
const href = element.getAttribute('href');
|
|
45
|
-
if (!href)
|
|
46
|
-
return;
|
|
47
|
-
if (href.startsWith('mailto:') ||
|
|
48
|
-
href.startsWith('tel:') ||
|
|
49
|
-
href.startsWith('javascript:') ||
|
|
50
|
-
href.startsWith('#')) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const absoluteUrl = new URL(href, baseUrl).href;
|
|
54
|
-
links.push(absoluteUrl);
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
return [...new Set(links)];
|
|
60
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import TurndownService from 'turndown';
|
|
2
|
-
export declare function createTurndownService(): TurndownService;
|
|
3
|
-
export declare function htmlToMarkdown(html: string): string;
|
|
4
|
-
export declare function formatArticleMarkdown(article: {
|
|
5
|
-
title: string;
|
|
6
|
-
content: string;
|
|
7
|
-
byline?: string | null;
|
|
8
|
-
baseUrl?: string;
|
|
9
|
-
}): string;
|
package/dist/parser/markdown.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import TurndownService from 'turndown';
|
|
2
|
-
import { gfm } from 'turndown-plugin-gfm';
|
|
3
|
-
import { JSDOM } from 'jsdom';
|
|
4
|
-
function convertRelativeUrls(html, baseUrl) {
|
|
5
|
-
try {
|
|
6
|
-
const dom = new JSDOM(html, { url: baseUrl });
|
|
7
|
-
const document = dom.window.document;
|
|
8
|
-
document.querySelectorAll('a[href]').forEach(link => {
|
|
9
|
-
const href = link.getAttribute('href');
|
|
10
|
-
if (href &&
|
|
11
|
-
!href.startsWith('http://') &&
|
|
12
|
-
!href.startsWith('https://') &&
|
|
13
|
-
!href.startsWith('//') &&
|
|
14
|
-
!href.startsWith('mailto:') &&
|
|
15
|
-
!href.startsWith('tel:') &&
|
|
16
|
-
!href.startsWith('javascript:') &&
|
|
17
|
-
!href.startsWith('#')) {
|
|
18
|
-
try {
|
|
19
|
-
const absoluteUrl = new URL(href, baseUrl).href;
|
|
20
|
-
link.setAttribute('href', absoluteUrl);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
document.querySelectorAll('img[src]').forEach(img => {
|
|
27
|
-
const src = img.getAttribute('src');
|
|
28
|
-
if (src &&
|
|
29
|
-
!src.startsWith('http://') &&
|
|
30
|
-
!src.startsWith('https://') &&
|
|
31
|
-
!src.startsWith('//') &&
|
|
32
|
-
!src.startsWith('data:')) {
|
|
33
|
-
try {
|
|
34
|
-
const absoluteUrl = new URL(src, baseUrl).href;
|
|
35
|
-
img.setAttribute('src', absoluteUrl);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
const bodyElement = document.body || document.documentElement;
|
|
42
|
-
return bodyElement ? bodyElement.innerHTML : html;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return html;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
export function createTurndownService() {
|
|
49
|
-
const turndown = new TurndownService({
|
|
50
|
-
headingStyle: 'atx',
|
|
51
|
-
codeBlockStyle: 'fenced',
|
|
52
|
-
linkStyle: 'inlined',
|
|
53
|
-
emDelimiter: '_',
|
|
54
|
-
bulletListMarker: '-',
|
|
55
|
-
strongDelimiter: '**',
|
|
56
|
-
hr: '---',
|
|
57
|
-
blankReplacement: (_content, node) => {
|
|
58
|
-
return node.isBlock ? '\n\n' : '';
|
|
59
|
-
},
|
|
60
|
-
keepReplacement: (content, node) => {
|
|
61
|
-
return node.isBlock ? '\n\n' + content + '\n\n' : content;
|
|
62
|
-
},
|
|
63
|
-
defaultReplacement: (content, node) => {
|
|
64
|
-
return node.isBlock ? '\n\n' + content + '\n\n' : content;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
turndown.use(gfm);
|
|
68
|
-
turndown.addRule('media', {
|
|
69
|
-
filter: ['iframe', 'video', 'audio', 'embed'],
|
|
70
|
-
replacement: (_content, node) => {
|
|
71
|
-
const element = node;
|
|
72
|
-
const src = element.getAttribute('src') || element.getAttribute('data-src');
|
|
73
|
-
const title = element.getAttribute('title') ||
|
|
74
|
-
element.getAttribute('alt') ||
|
|
75
|
-
'media';
|
|
76
|
-
if (src) {
|
|
77
|
-
return `\n\n[${title}](${src})\n\n`;
|
|
78
|
-
}
|
|
79
|
-
return '';
|
|
80
|
-
},
|
|
81
|
-
});
|
|
82
|
-
turndown.addRule('figure', {
|
|
83
|
-
filter: 'figure',
|
|
84
|
-
replacement: (content, node) => {
|
|
85
|
-
const figure = node;
|
|
86
|
-
const caption = figure.querySelector('figcaption');
|
|
87
|
-
if (caption) {
|
|
88
|
-
const captionText = caption.textContent || '';
|
|
89
|
-
return `\n\n${content.trim()}\n*${captionText}*\n\n`;
|
|
90
|
-
}
|
|
91
|
-
return `\n\n${content.trim()}\n\n`;
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
return turndown;
|
|
95
|
-
}
|
|
96
|
-
export function htmlToMarkdown(html) {
|
|
97
|
-
const turndown = createTurndownService();
|
|
98
|
-
let markdown = turndown.turndown(html);
|
|
99
|
-
markdown = markdown
|
|
100
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
101
|
-
.replace(/\s+$/gm, '')
|
|
102
|
-
.trim();
|
|
103
|
-
return markdown;
|
|
104
|
-
}
|
|
105
|
-
export function formatArticleMarkdown(article) {
|
|
106
|
-
try {
|
|
107
|
-
const turndown = createTurndownService();
|
|
108
|
-
let markdown = '';
|
|
109
|
-
if (article.title && article.title.trim()) {
|
|
110
|
-
markdown = `# ${article.title}\n\n`;
|
|
111
|
-
}
|
|
112
|
-
if (article.byline) {
|
|
113
|
-
markdown += `*By ${article.byline}*\n\n---\n\n`;
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const processedContent = article.baseUrl
|
|
117
|
-
? convertRelativeUrls(article.content, article.baseUrl)
|
|
118
|
-
: article.content;
|
|
119
|
-
markdown += turndown.turndown(processedContent);
|
|
120
|
-
}
|
|
121
|
-
catch (conversionError) {
|
|
122
|
-
console.error('Error converting HTML to markdown:', conversionError);
|
|
123
|
-
const tempDiv = typeof document !== 'undefined'
|
|
124
|
-
? document.createElement('div')
|
|
125
|
-
: null;
|
|
126
|
-
if (tempDiv) {
|
|
127
|
-
tempDiv.innerHTML = article.content;
|
|
128
|
-
markdown += tempDiv.textContent || article.content;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
markdown += article.content
|
|
132
|
-
.replace(/<[^>]*>/g, ' ')
|
|
133
|
-
.replace(/\s+/g, ' ');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return markdown
|
|
137
|
-
.replace(/\n{3,}/g, '\n\n')
|
|
138
|
-
.replace(/\s+$/gm, '')
|
|
139
|
-
.trim();
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
console.error('Fatal error in formatArticleMarkdown:', error);
|
|
143
|
-
return article.title
|
|
144
|
-
? `# ${article.title}\n\n[Content extraction failed]`
|
|
145
|
-
: '[Content extraction failed]';
|
|
146
|
-
}
|
|
147
|
-
}
|