@link-assistant/web-capture 1.1.2 → 1.4.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/CHANGELOG.md +17 -14
- package/README.md +256 -146
- package/bin/web-capture.js +403 -6
- package/package.json +11 -4
- package/src/animation.js +283 -0
- package/src/archive.js +212 -0
- package/src/batch.js +180 -0
- package/src/browser.js +150 -111
- package/src/docx.js +182 -0
- package/src/figures.js +208 -0
- package/src/gdocs.js +333 -0
- package/src/image.js +65 -8
- package/src/index.js +15 -0
- package/src/latex.js +127 -0
- package/src/lib.js +226 -1
- package/src/localize-images.js +202 -0
- package/src/metadata.js +333 -0
- package/src/pdf.js +76 -0
- package/src/popups.js +171 -0
- package/src/postprocess.js +265 -0
- package/src/retry.js +48 -0
- package/src/themed-image.js +167 -0
- package/src/verify.js +312 -0
- package/tests/e2e/docker.test.js +51 -12
- package/tests/e2e/process.test.js +34 -10
- package/tests/fixtures/habr-articles.js +45 -0
- package/tests/integration/api-endpoints.test.js +231 -0
- package/tests/integration/habr-article.test.js +337 -0
- package/tests/mock/index.test.js +5 -3
- package/tests/unit/animation.test.js +33 -0
- package/tests/unit/batch.test.js +114 -0
- package/tests/unit/browser.test.js +38 -2
- package/tests/unit/figures.test.js +74 -0
- package/tests/unit/gdocs.test.js +203 -0
- package/tests/unit/latex.test.js +123 -0
- package/tests/unit/localize-images.test.js +78 -0
- package/tests/unit/metadata.test.js +140 -0
- package/tests/unit/postprocess.test.js +108 -0
- package/tests/unit/retry.test.js +111 -0
- package/tests/unit/verify.test.js +182 -0
- package/.github/workflows/release.yml +0 -294
- package/ARCHITECTURE.md +0 -982
- package/LICENSE +0 -24
- package/apply.sh +0 -166
- package/docs/market-analysis-2025-05-13.md +0 -64
- package/scripts/changeset-version.mjs +0 -38
- package/scripts/create-github-release.mjs +0 -93
- package/scripts/create-manual-changeset.mjs +0 -88
- package/scripts/format-github-release.mjs +0 -83
- package/scripts/format-release-notes.mjs +0 -218
- package/scripts/instant-version-bump.mjs +0 -121
- package/scripts/publish-to-npm.mjs +0 -128
- package/scripts/setup-npm.mjs +0 -37
- package/scripts/validate-changeset.mjs +0 -106
- package/scripts/version-and-commit.mjs +0 -237
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
import {
|
|
3
|
+
extractMetadata,
|
|
4
|
+
formatMetadataBlock,
|
|
5
|
+
formatFooterBlock,
|
|
6
|
+
} from '../../src/metadata.js';
|
|
7
|
+
|
|
8
|
+
describe('metadata module', () => {
|
|
9
|
+
describe('extractMetadata', () => {
|
|
10
|
+
it('extracts author information', () => {
|
|
11
|
+
const $ = cheerio.load(
|
|
12
|
+
'<a class="tm-user-info__username" href="/user/john">john_doe</a>'
|
|
13
|
+
);
|
|
14
|
+
const meta = extractMetadata($);
|
|
15
|
+
expect(meta.author).toBe('john_doe');
|
|
16
|
+
expect(meta.authorUrl).toBe('/user/john');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('extracts publication date', () => {
|
|
20
|
+
const $ = cheerio.load(
|
|
21
|
+
'<time datetime="2024-01-15T10:00:00Z">Jan 15</time>'
|
|
22
|
+
);
|
|
23
|
+
const meta = extractMetadata($);
|
|
24
|
+
expect(meta.publishDate).toBe('2024-01-15T10:00:00Z');
|
|
25
|
+
expect(meta.publishDateText).toBe('Jan 15');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('extracts hubs', () => {
|
|
29
|
+
const $ = cheerio.load(`
|
|
30
|
+
<a class="tm-publication-hub__link"><span>Math</span></a>
|
|
31
|
+
<a class="tm-publication-hub__link"><span>Science</span></a>
|
|
32
|
+
`);
|
|
33
|
+
const meta = extractMetadata($);
|
|
34
|
+
expect(meta.hubs).toEqual(['Math', 'Science']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('extracts tags from meta keywords', () => {
|
|
38
|
+
const $ = cheerio.load(
|
|
39
|
+
'<meta name="keywords" content="math, science, theory">'
|
|
40
|
+
);
|
|
41
|
+
const meta = extractMetadata($);
|
|
42
|
+
expect(meta.tags).toEqual(['math', 'science', 'theory']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('extracts LD+JSON author name', () => {
|
|
46
|
+
const $ = cheerio.load(
|
|
47
|
+
'<script type="application/ld+json">{"author":{"name":"John Doe"},"dateModified":"2024-02-01"}</script>'
|
|
48
|
+
);
|
|
49
|
+
const meta = extractMetadata($);
|
|
50
|
+
expect(meta.authorFullName).toBe('John Doe');
|
|
51
|
+
expect(meta.dateModified).toBe('2024-02-01');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('extracts votes and comments', () => {
|
|
55
|
+
const $ = cheerio.load(`
|
|
56
|
+
<span class="tm-votes-meter__value">+42</span>
|
|
57
|
+
<span class="tm-article-comments-counter-link__value">15</span>
|
|
58
|
+
`);
|
|
59
|
+
const meta = extractMetadata($);
|
|
60
|
+
expect(meta.votes).toBe('+42');
|
|
61
|
+
expect(meta.comments).toBe('15');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns empty object for minimal HTML', () => {
|
|
65
|
+
const $ = cheerio.load('<div>plain content</div>');
|
|
66
|
+
const meta = extractMetadata($);
|
|
67
|
+
expect(meta).toBeDefined();
|
|
68
|
+
expect(meta.author).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('formatMetadataBlock', () => {
|
|
73
|
+
it('formats author line with link', () => {
|
|
74
|
+
const lines = formatMetadataBlock({
|
|
75
|
+
author: 'john',
|
|
76
|
+
authorUrl: '/user/john',
|
|
77
|
+
});
|
|
78
|
+
expect(lines).toContain('**Author:** [john](/user/john)');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('formats author with full name', () => {
|
|
82
|
+
const lines = formatMetadataBlock({
|
|
83
|
+
author: 'john',
|
|
84
|
+
authorFullName: 'John Doe',
|
|
85
|
+
authorUrl: '/user/john',
|
|
86
|
+
});
|
|
87
|
+
expect(lines[0]).toContain('John Doe (john)');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('formats publication date', () => {
|
|
91
|
+
const lines = formatMetadataBlock({
|
|
92
|
+
publishDate: '2024-01-15T10:00:00Z',
|
|
93
|
+
});
|
|
94
|
+
expect(lines.some((l) => l.includes('Published:'))).toBe(true);
|
|
95
|
+
expect(lines.some((l) => l.includes('January 15, 2024'))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('includes hubs and tags', () => {
|
|
99
|
+
const lines = formatMetadataBlock({
|
|
100
|
+
hubs: ['Math', 'Science'],
|
|
101
|
+
tags: ['theory', 'links'],
|
|
102
|
+
});
|
|
103
|
+
expect(lines.some((l) => l.includes('Math, Science'))).toBe(true);
|
|
104
|
+
expect(lines.some((l) => l.includes('theory, links'))).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('returns empty array for null metadata', () => {
|
|
108
|
+
expect(formatMetadataBlock(null)).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('formatFooterBlock', () => {
|
|
113
|
+
it('includes separator line', () => {
|
|
114
|
+
const lines = formatFooterBlock({ author: 'test' });
|
|
115
|
+
expect(lines[0]).toBe('---');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('formats tag links', () => {
|
|
119
|
+
const lines = formatFooterBlock({
|
|
120
|
+
tagLinks: [{ name: 'math', url: '/tag/math' }],
|
|
121
|
+
});
|
|
122
|
+
expect(lines.some((l) => l.includes('[math](/tag/math)'))).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('formats article stats', () => {
|
|
126
|
+
const lines = formatFooterBlock({
|
|
127
|
+
votes: '+10',
|
|
128
|
+
views: '5000',
|
|
129
|
+
comments: '3',
|
|
130
|
+
});
|
|
131
|
+
const statsLine = lines.find((l) => l.includes('Votes:'));
|
|
132
|
+
expect(statsLine).toContain('+10');
|
|
133
|
+
expect(statsLine).toContain('5000');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns empty array for null metadata', () => {
|
|
137
|
+
expect(formatFooterBlock(null)).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
postProcessMarkdown,
|
|
3
|
+
applyUnicodeNormalization,
|
|
4
|
+
applyLatexSpacingFixes,
|
|
5
|
+
applyPercentSignFix,
|
|
6
|
+
applyBoldFormattingFixes,
|
|
7
|
+
} from '../../src/postprocess.js';
|
|
8
|
+
|
|
9
|
+
describe('postprocess module', () => {
|
|
10
|
+
describe('applyUnicodeNormalization', () => {
|
|
11
|
+
it('replaces non-breaking spaces with regular spaces', () => {
|
|
12
|
+
expect(applyUnicodeNormalization('hello\u00A0world')).toBe('hello world');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('normalizes curly quotes to straight quotes', () => {
|
|
16
|
+
expect(applyUnicodeNormalization('\u2018hello\u2019')).toBe("'hello'");
|
|
17
|
+
expect(applyUnicodeNormalization('\u201Chello\u201D')).toBe('"hello"');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('normalizes en-dash to hyphen', () => {
|
|
21
|
+
expect(applyUnicodeNormalization('2\u20133')).toBe('2-3');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('normalizes ellipsis', () => {
|
|
25
|
+
expect(applyUnicodeNormalization('wait\u2026')).toBe('wait...');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('applyLatexSpacingFixes', () => {
|
|
30
|
+
it('adds space before formula after word character', () => {
|
|
31
|
+
const result = applyLatexSpacingFixes('where$x$is');
|
|
32
|
+
expect(result).toBe('where $x$ is');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('trims whitespace inside formula delimiters', () => {
|
|
36
|
+
const result = applyLatexSpacingFixes('$ x + y $');
|
|
37
|
+
expect(result).toBe('$x + y$');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not modify block formulas', () => {
|
|
41
|
+
const result = applyLatexSpacingFixes('$$x + y$$');
|
|
42
|
+
expect(result).toBe('$$x + y$$');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not modify blockquote block formulas', () => {
|
|
46
|
+
const result = applyLatexSpacingFixes('> $$x + y$$');
|
|
47
|
+
expect(result).toBe('> $$x + y$$');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles multiple formulas on one line', () => {
|
|
51
|
+
const result = applyLatexSpacingFixes('$a$and$b$');
|
|
52
|
+
expect(result).toBe('$a$ and $b$');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('applyPercentSignFix', () => {
|
|
57
|
+
it('fixes percent sign in inline formulas', () => {
|
|
58
|
+
const result = applyPercentSignFix('$50\\%$');
|
|
59
|
+
expect(result).toBe('$50\\\\%$');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('fixes \\text{%} notation', () => {
|
|
63
|
+
const result = applyPercentSignFix('$50\\text{%}$');
|
|
64
|
+
expect(result).toBe('$50\\\\%$');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('applyBoldFormattingFixes', () => {
|
|
69
|
+
it('removes empty bold markers', () => {
|
|
70
|
+
const result = applyBoldFormattingFixes('hello ** ** world');
|
|
71
|
+
expect(result).toBe('hello world');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('trims content inside bold markers', () => {
|
|
75
|
+
const result = applyBoldFormattingFixes('** hello **');
|
|
76
|
+
expect(result).toBe('**hello**');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('adds space between word and bold', () => {
|
|
80
|
+
const result = applyBoldFormattingFixes('word**bold**next');
|
|
81
|
+
expect(result).toBe('word **bold** next');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('postProcessMarkdown', () => {
|
|
86
|
+
it('applies all transformations by default', () => {
|
|
87
|
+
const input = 'hello\u00A0$x$world';
|
|
88
|
+
const result = postProcessMarkdown(input);
|
|
89
|
+
expect(result).toContain('hello');
|
|
90
|
+
expect(result).toContain('$x$');
|
|
91
|
+
expect(result).not.toContain('\u00A0');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('respects option flags', () => {
|
|
95
|
+
const input = 'hello\u00A0world';
|
|
96
|
+
const result = postProcessMarkdown(input, {
|
|
97
|
+
normalizeUnicode: false,
|
|
98
|
+
});
|
|
99
|
+
expect(result).toContain('\u00A0');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('removes stray standalone $ signs', () => {
|
|
103
|
+
const input = 'line1\n$\nline2';
|
|
104
|
+
const result = postProcessMarkdown(input);
|
|
105
|
+
expect(result).toBe('line1\n\nline2');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { retry } from '../../src/retry.js';
|
|
3
|
+
|
|
4
|
+
jest.setTimeout(30000);
|
|
5
|
+
|
|
6
|
+
describe('retry', () => {
|
|
7
|
+
it('returns result on first success', async () => {
|
|
8
|
+
const fn = jest.fn().mockResolvedValue('ok');
|
|
9
|
+
const result = await retry(fn, { retries: 3 });
|
|
10
|
+
expect(result).toBe('ok');
|
|
11
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('retries on failure and succeeds eventually', async () => {
|
|
15
|
+
let calls = 0;
|
|
16
|
+
const fn = jest.fn().mockImplementation(async () => {
|
|
17
|
+
calls++;
|
|
18
|
+
if (calls < 3) {
|
|
19
|
+
throw new Error('fail');
|
|
20
|
+
}
|
|
21
|
+
return 'success';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = await retry(fn, { retries: 3, baseDelay: 50 });
|
|
25
|
+
expect(result).toBe('success');
|
|
26
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('throws after exhausting retries', async () => {
|
|
30
|
+
const fn = jest.fn().mockRejectedValue(new Error('persistent failure'));
|
|
31
|
+
|
|
32
|
+
await expect(retry(fn, { retries: 2, baseDelay: 50 })).rejects.toThrow(
|
|
33
|
+
'persistent failure'
|
|
34
|
+
);
|
|
35
|
+
expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('calls onRetry callback before each retry', async () => {
|
|
39
|
+
let calls = 0;
|
|
40
|
+
const fn = jest.fn().mockImplementation(async () => {
|
|
41
|
+
calls++;
|
|
42
|
+
if (calls < 2) {
|
|
43
|
+
throw new Error('fail');
|
|
44
|
+
}
|
|
45
|
+
return 'ok';
|
|
46
|
+
});
|
|
47
|
+
const onRetry = jest.fn();
|
|
48
|
+
|
|
49
|
+
await retry(fn, { retries: 3, baseDelay: 50, onRetry });
|
|
50
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(onRetry).toHaveBeenCalledWith(expect.any(Error), 1, 50);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses exponential backoff delays', async () => {
|
|
55
|
+
const delays = [];
|
|
56
|
+
const onRetry = jest.fn((err, attempt, delay) => {
|
|
57
|
+
delays.push(delay);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const fn = jest.fn().mockRejectedValue(new Error('fail'));
|
|
61
|
+
|
|
62
|
+
await expect(
|
|
63
|
+
retry(fn, {
|
|
64
|
+
retries: 3,
|
|
65
|
+
baseDelay: 100,
|
|
66
|
+
factor: 2,
|
|
67
|
+
maxDelay: 1000,
|
|
68
|
+
onRetry,
|
|
69
|
+
})
|
|
70
|
+
).rejects.toThrow();
|
|
71
|
+
|
|
72
|
+
// Delays should be: 100, 200, 400
|
|
73
|
+
expect(delays).toEqual([100, 200, 400]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('caps delay at maxDelay', async () => {
|
|
77
|
+
const delays = [];
|
|
78
|
+
const onRetry = jest.fn((err, attempt, delay) => {
|
|
79
|
+
delays.push(delay);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const fn = jest.fn().mockRejectedValue(new Error('fail'));
|
|
83
|
+
|
|
84
|
+
await expect(
|
|
85
|
+
retry(fn, {
|
|
86
|
+
retries: 3,
|
|
87
|
+
baseDelay: 100,
|
|
88
|
+
factor: 10,
|
|
89
|
+
maxDelay: 500,
|
|
90
|
+
onRetry,
|
|
91
|
+
})
|
|
92
|
+
).rejects.toThrow();
|
|
93
|
+
|
|
94
|
+
// Delays: min(100, 500)=100, min(1000, 500)=500, min(10000, 500)=500
|
|
95
|
+
expect(delays).toEqual([100, 500, 500]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('passes attempt number to fn', async () => {
|
|
99
|
+
const attempts = [];
|
|
100
|
+
const fn = jest.fn().mockImplementation(async (attempt) => {
|
|
101
|
+
attempts.push(attempt);
|
|
102
|
+
if (attempt < 2) {
|
|
103
|
+
throw new Error('fail');
|
|
104
|
+
}
|
|
105
|
+
return 'ok';
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await retry(fn, { retries: 3, baseDelay: 50 });
|
|
109
|
+
expect(attempts).toEqual([0, 1, 2]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeText,
|
|
3
|
+
normalizeCode,
|
|
4
|
+
verifyMarkdownContent,
|
|
5
|
+
} from '../../src/verify.js';
|
|
6
|
+
|
|
7
|
+
describe('verify module', () => {
|
|
8
|
+
describe('normalizeText', () => {
|
|
9
|
+
it('lowercases text', () => {
|
|
10
|
+
expect(normalizeText('Hello World')).toBe('hello world');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('normalizes unicode spaces', () => {
|
|
14
|
+
expect(normalizeText('hello\u00A0world')).toBe('hello world');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('normalizes curly quotes', () => {
|
|
18
|
+
expect(normalizeText('\u201Chello\u201D')).toBe('"hello"');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('removes LaTeX delimiters', () => {
|
|
22
|
+
expect(normalizeText('$x + y$')).toBe('x + y');
|
|
23
|
+
expect(normalizeText('$$x + y$$')).toBe('x + y');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('normalizes arrows', () => {
|
|
27
|
+
expect(normalizeText('\u2192')).toBe('->');
|
|
28
|
+
expect(normalizeText('\\to')).toBe('->');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('removes \\displaystyle', () => {
|
|
32
|
+
expect(normalizeText('\\displaystyle x')).toBe('x');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('extracts text from \\text{}', () => {
|
|
36
|
+
expect(normalizeText('\\text{hello}')).toBe('hello');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('normalizeCode', () => {
|
|
41
|
+
it('lowercases and collapses whitespace', () => {
|
|
42
|
+
expect(normalizeCode(' Hello World ')).toBe('hello world');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('normalizes multiplication sign', () => {
|
|
46
|
+
expect(normalizeCode('2\u00D73')).toBe('2x3');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('verifyMarkdownContent', () => {
|
|
51
|
+
it('verifies title presence', () => {
|
|
52
|
+
const webContent = {
|
|
53
|
+
title: 'My Article',
|
|
54
|
+
headings: [],
|
|
55
|
+
paragraphs: [],
|
|
56
|
+
codeBlocks: [],
|
|
57
|
+
formulas: [],
|
|
58
|
+
blockquoteFormulas: [],
|
|
59
|
+
listItems: [],
|
|
60
|
+
links: [],
|
|
61
|
+
figures: [],
|
|
62
|
+
};
|
|
63
|
+
const markdown = '# My Article\n\nSome content here.';
|
|
64
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
65
|
+
expect(result.missing.title).toBe(false);
|
|
66
|
+
expect(result.passedChecks).toBeGreaterThan(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects missing title', () => {
|
|
70
|
+
const webContent = {
|
|
71
|
+
title: 'Missing Title',
|
|
72
|
+
headings: [],
|
|
73
|
+
paragraphs: [],
|
|
74
|
+
codeBlocks: [],
|
|
75
|
+
formulas: [],
|
|
76
|
+
blockquoteFormulas: [],
|
|
77
|
+
listItems: [],
|
|
78
|
+
links: [],
|
|
79
|
+
figures: [],
|
|
80
|
+
};
|
|
81
|
+
const markdown = '# Different Title\n\nContent.';
|
|
82
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
83
|
+
expect(result.missing.title).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('verifies headings', () => {
|
|
87
|
+
const webContent = {
|
|
88
|
+
title: '',
|
|
89
|
+
headings: [
|
|
90
|
+
{ level: 'h2', text: 'Introduction' },
|
|
91
|
+
{ level: 'h3', text: 'Background' },
|
|
92
|
+
],
|
|
93
|
+
paragraphs: [],
|
|
94
|
+
codeBlocks: [],
|
|
95
|
+
formulas: [],
|
|
96
|
+
blockquoteFormulas: [],
|
|
97
|
+
listItems: [],
|
|
98
|
+
links: [],
|
|
99
|
+
figures: [],
|
|
100
|
+
};
|
|
101
|
+
const markdown = '## Introduction\n\n### Background\n\nText.';
|
|
102
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
103
|
+
expect(result.missing.headings).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects missing headings', () => {
|
|
107
|
+
const webContent = {
|
|
108
|
+
title: '',
|
|
109
|
+
headings: [{ level: 'h2', text: 'Unique Heading XYZ' }],
|
|
110
|
+
paragraphs: [],
|
|
111
|
+
codeBlocks: [],
|
|
112
|
+
formulas: [],
|
|
113
|
+
blockquoteFormulas: [],
|
|
114
|
+
listItems: [],
|
|
115
|
+
links: [],
|
|
116
|
+
figures: [],
|
|
117
|
+
};
|
|
118
|
+
const markdown = '## Different Heading\n\nText.';
|
|
119
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
120
|
+
expect(result.missing.headings).toContain('Unique Heading XYZ');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('verifies code blocks with fuzzy matching', () => {
|
|
124
|
+
const webContent = {
|
|
125
|
+
title: '',
|
|
126
|
+
headings: [],
|
|
127
|
+
paragraphs: [],
|
|
128
|
+
codeBlocks: [
|
|
129
|
+
'function hello() {\n console.log("hello");\n return true;\n}',
|
|
130
|
+
],
|
|
131
|
+
formulas: [],
|
|
132
|
+
blockquoteFormulas: [],
|
|
133
|
+
listItems: [],
|
|
134
|
+
links: [],
|
|
135
|
+
figures: [],
|
|
136
|
+
};
|
|
137
|
+
const markdown =
|
|
138
|
+
'```javascript\nfunction hello() {\n console.log("hello");\n return true;\n}\n```';
|
|
139
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
140
|
+
expect(result.missing.codeBlocks).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('calculates pass rate', () => {
|
|
144
|
+
const webContent = {
|
|
145
|
+
title: 'Test',
|
|
146
|
+
headings: [{ level: 'h2', text: 'Found' }],
|
|
147
|
+
paragraphs: [],
|
|
148
|
+
codeBlocks: [],
|
|
149
|
+
formulas: [],
|
|
150
|
+
blockquoteFormulas: [],
|
|
151
|
+
listItems: [],
|
|
152
|
+
links: [],
|
|
153
|
+
figures: [],
|
|
154
|
+
};
|
|
155
|
+
const markdown = '# Test\n\n## Found\n\nContent.';
|
|
156
|
+
const result = verifyMarkdownContent(webContent, markdown);
|
|
157
|
+
expect(result.passRate).toBe(1.0);
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('checks figure images when hasLocalImages is set', () => {
|
|
162
|
+
const webContent = {
|
|
163
|
+
title: '',
|
|
164
|
+
headings: [],
|
|
165
|
+
paragraphs: [],
|
|
166
|
+
codeBlocks: [],
|
|
167
|
+
formulas: [],
|
|
168
|
+
blockquoteFormulas: [],
|
|
169
|
+
listItems: [],
|
|
170
|
+
links: [],
|
|
171
|
+
figures: [1, 2, 3],
|
|
172
|
+
};
|
|
173
|
+
const markdown =
|
|
174
|
+
'\n\n';
|
|
175
|
+
const result = verifyMarkdownContent(webContent, markdown, {
|
|
176
|
+
hasLocalImages: true,
|
|
177
|
+
expectedFigures: 3,
|
|
178
|
+
});
|
|
179
|
+
expect(result.missing.images).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|