@link-assistant/web-capture 2.0.0 → 2.0.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 -0
- package/bin/web-capture.js +2 -5
- package/package.json +36 -23
- package/src/archive.js +0 -1
- package/src/binary-download.js +0 -1
- package/src/docx.js +13 -10
- package/src/fetch.js +0 -1
- package/src/gdocs-images.js +0 -1
- package/src/gdocs.js +0 -1
- package/src/github.js +0 -1
- package/src/lib.js +0 -1
- package/src/markdown.js +0 -1
- package/src/shared-dialog.js +0 -1
- package/src/stream.js +0 -1
- package/src/transport.js +23 -5
- package/src/txt.js +0 -1
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.dockerignore +0 -6
- package/.jscpd.json +0 -19
- package/.lenv +0 -8
- package/.prettierignore +0 -8
- package/.prettierrc +0 -10
- package/Dockerfile +0 -52
- package/docker-compose.yml +0 -11
- package/eslint.config.js +0 -112
- package/examples/js/capture_cache.js +0 -12
- package/examples/js/engine_comparison.js +0 -39
- package/examples/js/html_download.js +0 -23
- package/examples/js/image_download.js +0 -36
- package/examples/js/markdown_download.js +0 -23
- package/examples/js/playwright_example.js +0 -23
- package/examples/python/capture_client.py +0 -20
- package/examples/python/html_download.py +0 -3
- package/examples/python/image_download.py +0 -16
- package/examples/python/markdown_download.py +0 -3
- package/examples/python/playwright_example.py +0 -22
- package/jest.config.mjs +0 -17
- package/tests/ci/dependency-freshness.test.js +0 -74
- package/tests/ci/detect-code-changes.test.js +0 -187
- package/tests/ci/docker-install.test.js +0 -31
- package/tests/ci/rust-dockerfile.test.js +0 -54
- package/tests/ci/workflow-policy.test.js +0 -79
- package/tests/e2e/docker.test.js +0 -186
- package/tests/e2e/process.test.js +0 -215
- package/tests/fixtures/bold-coalesce-and-leakage.html +0 -27
- package/tests/fixtures/deepwiki/deepwiki-full-page.png +0 -0
- package/tests/fixtures/deepwiki/index.md +0 -1500
- package/tests/fixtures/deepwiki/part-1.md +0 -776
- package/tests/fixtures/habr-articles.js +0 -45
- package/tests/fixtures/heading-numbering.html +0 -19
- package/tests/fixtures/list-item-with-br.html +0 -8
- package/tests/fixtures/ol-continuous-numbering.html +0 -12
- package/tests/fixtures/paragraph-vs-line-break.html +0 -11
- package/tests/helpers/nock.js +0 -10
- package/tests/helpers/screenshot.js +0 -10
- package/tests/helpers/search-fixture.cjs +0 -17
- package/tests/helpers/temporary-directory.js +0 -12
- package/tests/integration/api-endpoints.test.js +0 -418
- package/tests/integration/archive-default-layout.test.js +0 -37
- package/tests/integration/browser-engines.test.js +0 -80
- package/tests/integration/browser-packaged-path.test.js +0 -45
- package/tests/integration/deepwiki-download.test.js +0 -144
- package/tests/integration/deepwiki-rendered-page.mock.test.js +0 -121
- package/tests/integration/formalai-contract.test.js +0 -214
- package/tests/integration/gdocs-public-doc.test.js +0 -315
- package/tests/integration/github-readme.test.js +0 -133
- package/tests/integration/google-drive-image.test.js +0 -246
- package/tests/integration/habr-article.test.js +0 -337
- package/tests/integration/search-endpoint.test.js +0 -88
- package/tests/integration/stackoverflow-download.test.js +0 -173
- package/tests/integration/wikipedia-download.test.js +0 -134
- package/tests/jest.setup.mjs +0 -7
- package/tests/mock/index.test.js +0 -225
- package/tests/unit/animation.test.js +0 -33
- package/tests/unit/batch.test.js +0 -114
- package/tests/unit/bold-coalesce-and-leakage.test.js +0 -54
- package/tests/unit/browser-launch-options.test.js +0 -42
- package/tests/unit/browser.test.js +0 -172
- package/tests/unit/cache.test.js +0 -182
- package/tests/unit/cli.test.js +0 -399
- package/tests/unit/convertGoogleDriveUrl.test.js +0 -89
- package/tests/unit/convertRelativeUrls.test.js +0 -34
- package/tests/unit/extract-images.test.js +0 -259
- package/tests/unit/fetch-html.test.js +0 -54
- package/tests/unit/figures.test.js +0 -74
- package/tests/unit/gdocs-export-headers.test.js +0 -28
- package/tests/unit/gdocs-preprocess.test.js +0 -36
- package/tests/unit/gdocs.test.js +0 -1490
- package/tests/unit/github.test.js +0 -156
- package/tests/unit/heading-image.test.js +0 -49
- package/tests/unit/heading-numbering.test.js +0 -28
- package/tests/unit/html2md-br-in-list-item.test.js +0 -23
- package/tests/unit/html2md-ol-numbering.test.js +0 -20
- package/tests/unit/html2md.test.js +0 -695
- package/tests/unit/image-mode-defaults.test.js +0 -54
- package/tests/unit/kreuzberg.test.js +0 -249
- package/tests/unit/latex.test.js +0 -123
- package/tests/unit/localize-images.test.js +0 -114
- package/tests/unit/markdown.test.js +0 -36
- package/tests/unit/metadata.test.js +0 -140
- package/tests/unit/package-runtime-dependencies.test.js +0 -74
- package/tests/unit/paragraph-vs-line-break.test.js +0 -56
- package/tests/unit/postprocess.test.js +0 -110
- package/tests/unit/proxy-headers.test.js +0 -100
- package/tests/unit/publish-verification.test.js +0 -54
- package/tests/unit/retry.test.js +0 -111
- package/tests/unit/search.test.js +0 -339
- package/tests/unit/shared-dialog.test.js +0 -173
- package/tests/unit/stackoverflow.test.js +0 -32
- package/tests/unit/stream.test.js +0 -83
- package/tests/unit/verify.test.js +0 -182
- package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +0 -375
- package/tests/xpaste/xpaste.e2e.test.js +0 -36
- package/tests/xpaste/xpaste.integration.test.js +0 -41
- package/tests/xpaste/xpaste.mock.test.js +0 -261
- package/tests/xpaste/xpaste.unit.test.js +0 -119
|
@@ -1,140 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'fs';
|
|
2
|
-
import { dirname, resolve } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const jsRoot = resolve(__dirname, '../..');
|
|
8
|
-
|
|
9
|
-
function readJson(relativePath) {
|
|
10
|
-
return JSON.parse(readFileSync(resolve(jsRoot, relativePath), 'utf8'));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function collectProductionDependencies(packageLock, packageName) {
|
|
14
|
-
const visited = new Set();
|
|
15
|
-
|
|
16
|
-
function visit(name) {
|
|
17
|
-
if (visited.has(name)) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
visited.add(name);
|
|
21
|
-
|
|
22
|
-
const entry = packageLock.packages[`node_modules/${name}`];
|
|
23
|
-
for (const dependency of Object.keys(entry?.dependencies || {})) {
|
|
24
|
-
visit(dependency);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
visit(packageName);
|
|
29
|
-
return visited;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const runtimeImports = [
|
|
33
|
-
{
|
|
34
|
-
packageName: 'turndown-plugin-gfm',
|
|
35
|
-
sourceFile: 'src/lib.js',
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
describe('runtime package dependencies', () => {
|
|
40
|
-
test('archiver production dependencies do not include deprecated glob', () => {
|
|
41
|
-
const packageLock = readJson('package-lock.json');
|
|
42
|
-
const dependencies = collectProductionDependencies(packageLock, 'archiver');
|
|
43
|
-
|
|
44
|
-
expect([...dependencies]).not.toContain('glob');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test.each(runtimeImports)(
|
|
48
|
-
'$packageName is declared as a production dependency',
|
|
49
|
-
({ packageName, sourceFile }) => {
|
|
50
|
-
const source = readFileSync(resolve(jsRoot, sourceFile), 'utf8');
|
|
51
|
-
const packageJson = readJson('package.json');
|
|
52
|
-
|
|
53
|
-
expect(source).toContain(`from '${packageName}'`);
|
|
54
|
-
expect(packageJson.dependencies).toHaveProperty(packageName);
|
|
55
|
-
expect(packageJson.devDependencies || {}).not.toHaveProperty(packageName);
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
test.each(runtimeImports)(
|
|
60
|
-
'$packageName is not marked dev-only in package-lock.json',
|
|
61
|
-
({ packageName }) => {
|
|
62
|
-
const packageLock = readJson('package-lock.json');
|
|
63
|
-
|
|
64
|
-
expect(packageLock.packages[''].dependencies).toHaveProperty(packageName);
|
|
65
|
-
expect(packageLock.packages[''].devDependencies || {}).not.toHaveProperty(
|
|
66
|
-
packageName
|
|
67
|
-
);
|
|
68
|
-
expect(packageLock.packages[`node_modules/${packageName}`]).toBeDefined();
|
|
69
|
-
expect(packageLock.packages[`node_modules/${packageName}`].dev).not.toBe(
|
|
70
|
-
true
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
);
|
|
74
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import MarkdownIt from 'markdown-it';
|
|
5
|
-
import {
|
|
6
|
-
convertHtmlToMarkdown,
|
|
7
|
-
convertHtmlToMarkdownEnhanced,
|
|
8
|
-
} from '../../src/lib.js';
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
|
-
const HTML = fs.readFileSync(
|
|
13
|
-
path.join(__dirname, '..', 'fixtures', 'paragraph-vs-line-break.html'),
|
|
14
|
-
'utf8'
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
describe('<br><br> coalesces to a paragraph break (not two hard breaks)', () => {
|
|
18
|
-
it('renders a paragraph break between Caption A and Caption B (convertHtmlToMarkdown)', () => {
|
|
19
|
-
const md = convertHtmlToMarkdown(HTML);
|
|
20
|
-
const lines = md.split('\n');
|
|
21
|
-
const idxA = lines.findIndex((l) => l.includes('Caption A:'));
|
|
22
|
-
const idxB = lines.findIndex((l) => l.includes('Caption B:'));
|
|
23
|
-
expect(idxA).toBeGreaterThanOrEqual(0);
|
|
24
|
-
expect(idxB).toBeGreaterThan(idxA);
|
|
25
|
-
|
|
26
|
-
const between = lines.slice(idxA + 1, idxB);
|
|
27
|
-
expect(between.some((l) => l === '')).toBe(true);
|
|
28
|
-
|
|
29
|
-
expect(md).not.toMatch(/^[ \t]+ {2}$/m);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('renders a paragraph break between Caption A and Caption B (convertHtmlToMarkdownEnhanced)', () => {
|
|
33
|
-
const { markdown } = convertHtmlToMarkdownEnhanced(HTML);
|
|
34
|
-
const lines = markdown.split('\n');
|
|
35
|
-
const idxA = lines.findIndex((l) => l.includes('Caption A:'));
|
|
36
|
-
const idxB = lines.findIndex((l) => l.includes('Caption B:'));
|
|
37
|
-
expect(idxA).toBeGreaterThanOrEqual(0);
|
|
38
|
-
expect(idxB).toBeGreaterThan(idxA);
|
|
39
|
-
|
|
40
|
-
const between = lines.slice(idxA + 1, idxB);
|
|
41
|
-
expect(between.some((l) => l === '')).toBe(true);
|
|
42
|
-
|
|
43
|
-
expect(markdown).not.toMatch(/^[ \t]+ {2}$/m);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('CommonMark renders Caption A and Caption B as separate paragraphs', () => {
|
|
47
|
-
const md = convertHtmlToMarkdown(HTML);
|
|
48
|
-
const html = new MarkdownIt('commonmark').render(md);
|
|
49
|
-
const aIdx = html.indexOf('Caption A:');
|
|
50
|
-
const bIdx = html.indexOf('Caption B:');
|
|
51
|
-
expect(aIdx).toBeGreaterThan(-1);
|
|
52
|
-
expect(bIdx).toBeGreaterThan(aIdx);
|
|
53
|
-
const between = html.slice(aIdx, bIdx);
|
|
54
|
-
expect(between).toMatch(/<\/p>\s*<p>/);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1,110 +0,0 @@
|
|
|
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('preserves non-breaking spaces as entities', () => {
|
|
12
|
-
expect(applyUnicodeNormalization('hello\u00A0world')).toBe(
|
|
13
|
-
'hello world'
|
|
14
|
-
);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('normalizes curly quotes to straight quotes', () => {
|
|
18
|
-
expect(applyUnicodeNormalization('\u2018hello\u2019')).toBe("'hello'");
|
|
19
|
-
expect(applyUnicodeNormalization('\u201Chello\u201D')).toBe('"hello"');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('normalizes en-dash to hyphen', () => {
|
|
23
|
-
expect(applyUnicodeNormalization('2\u20133')).toBe('2-3');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('normalizes ellipsis', () => {
|
|
27
|
-
expect(applyUnicodeNormalization('wait\u2026')).toBe('wait...');
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('applyLatexSpacingFixes', () => {
|
|
32
|
-
it('adds space before formula after word character', () => {
|
|
33
|
-
const result = applyLatexSpacingFixes('where$x$is');
|
|
34
|
-
expect(result).toBe('where $x$ is');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('trims whitespace inside formula delimiters', () => {
|
|
38
|
-
const result = applyLatexSpacingFixes('$ x + y $');
|
|
39
|
-
expect(result).toBe('$x + y$');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('does not modify block formulas', () => {
|
|
43
|
-
const result = applyLatexSpacingFixes('$$x + y$$');
|
|
44
|
-
expect(result).toBe('$$x + y$$');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('does not modify blockquote block formulas', () => {
|
|
48
|
-
const result = applyLatexSpacingFixes('> $$x + y$$');
|
|
49
|
-
expect(result).toBe('> $$x + y$$');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('handles multiple formulas on one line', () => {
|
|
53
|
-
const result = applyLatexSpacingFixes('$a$and$b$');
|
|
54
|
-
expect(result).toBe('$a$ and $b$');
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('applyPercentSignFix', () => {
|
|
59
|
-
it('fixes percent sign in inline formulas', () => {
|
|
60
|
-
const result = applyPercentSignFix('$50\\%$');
|
|
61
|
-
expect(result).toBe('$50\\\\%$');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('fixes \\text{%} notation', () => {
|
|
65
|
-
const result = applyPercentSignFix('$50\\text{%}$');
|
|
66
|
-
expect(result).toBe('$50\\\\%$');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
describe('applyBoldFormattingFixes', () => {
|
|
71
|
-
it('removes empty bold markers', () => {
|
|
72
|
-
const result = applyBoldFormattingFixes('hello ** ** world');
|
|
73
|
-
expect(result).toBe('hello world');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('trims content inside bold markers', () => {
|
|
77
|
-
const result = applyBoldFormattingFixes('** hello **');
|
|
78
|
-
expect(result).toBe('**hello**');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('adds space between word and bold', () => {
|
|
82
|
-
const result = applyBoldFormattingFixes('word**bold**next');
|
|
83
|
-
expect(result).toBe('word **bold** next');
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('postProcessMarkdown', () => {
|
|
88
|
-
it('applies all transformations by default', () => {
|
|
89
|
-
const input = 'hello\u00A0$x$world';
|
|
90
|
-
const result = postProcessMarkdown(input);
|
|
91
|
-
expect(result).toContain('hello');
|
|
92
|
-
expect(result).toContain('$x$');
|
|
93
|
-
expect(result).not.toContain('\u00A0');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('respects option flags', () => {
|
|
97
|
-
const input = 'hello\u00A0world';
|
|
98
|
-
const result = postProcessMarkdown(input, {
|
|
99
|
-
normalizeUnicode: false,
|
|
100
|
-
});
|
|
101
|
-
expect(result).toContain('\u00A0');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('removes stray standalone $ signs', () => {
|
|
105
|
-
const input = 'line1\n$\nline2';
|
|
106
|
-
const result = postProcessMarkdown(input);
|
|
107
|
-
expect(result).toBe('line1\n\nline2');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
copyProxyResponseHeaders,
|
|
3
|
-
getProxyHeaderEntries,
|
|
4
|
-
} from '../../src/proxy-headers.js';
|
|
5
|
-
|
|
6
|
-
describe('proxy header helpers', () => {
|
|
7
|
-
it('removes hop-by-hop headers and headers named by Connection', () => {
|
|
8
|
-
const entries = getProxyHeaderEntries(
|
|
9
|
-
headers({
|
|
10
|
-
'Content-Type': 'text/html',
|
|
11
|
-
Connection: 'keep-alive, X-Internal-Hop',
|
|
12
|
-
'Keep-Alive': 'timeout=5',
|
|
13
|
-
TE: 'trailers',
|
|
14
|
-
Trailer: 'Expires',
|
|
15
|
-
Upgrade: 'websocket',
|
|
16
|
-
'Proxy-Authenticate': 'Basic',
|
|
17
|
-
'Proxy-Authorization': 'Basic token',
|
|
18
|
-
'Transfer-Encoding': 'chunked',
|
|
19
|
-
'X-Internal-Hop': 'remove me',
|
|
20
|
-
'X-End-To-End': 'keep me',
|
|
21
|
-
})
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
expect(Object.fromEntries(entries)).toEqual({
|
|
25
|
-
'content-type': 'text/html',
|
|
26
|
-
'x-end-to-end': 'keep me',
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('preserves content length only for unencoded streaming responses', () => {
|
|
31
|
-
expect(
|
|
32
|
-
Object.fromEntries(
|
|
33
|
-
getProxyHeaderEntries(
|
|
34
|
-
headers({
|
|
35
|
-
'Content-Type': 'text/plain',
|
|
36
|
-
'Content-Length': '12',
|
|
37
|
-
}),
|
|
38
|
-
{ preserveContentLength: true }
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
).toEqual({
|
|
42
|
-
'content-type': 'text/plain',
|
|
43
|
-
'content-length': '12',
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(
|
|
47
|
-
Object.fromEntries(
|
|
48
|
-
getProxyHeaderEntries(
|
|
49
|
-
headers({
|
|
50
|
-
'Content-Type': 'text/plain',
|
|
51
|
-
'Content-Length': '12',
|
|
52
|
-
'Content-Encoding': 'gzip',
|
|
53
|
-
}),
|
|
54
|
-
{ preserveContentLength: true }
|
|
55
|
-
)
|
|
56
|
-
)
|
|
57
|
-
).toEqual({
|
|
58
|
-
'content-type': 'text/plain',
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('sets a default content type while copying safe headers', () => {
|
|
63
|
-
const res = responseRecorder();
|
|
64
|
-
|
|
65
|
-
copyProxyResponseHeaders(
|
|
66
|
-
headers({
|
|
67
|
-
ETag: '"abc"',
|
|
68
|
-
}),
|
|
69
|
-
res
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
expect(res.headers).toEqual({
|
|
73
|
-
'content-type': 'text/plain',
|
|
74
|
-
etag: '"abc"',
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
function headers(values) {
|
|
80
|
-
const map = new Map(
|
|
81
|
-
Object.entries(values).map(([key, value]) => [key.toLowerCase(), value])
|
|
82
|
-
);
|
|
83
|
-
return {
|
|
84
|
-
get(key) {
|
|
85
|
-
return map.get(key.toLowerCase()) || null;
|
|
86
|
-
},
|
|
87
|
-
entries() {
|
|
88
|
-
return map.entries();
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function responseRecorder() {
|
|
94
|
-
return {
|
|
95
|
-
headers: {},
|
|
96
|
-
setHeader(key, value) {
|
|
97
|
-
this.headers[key.toLowerCase()] = value;
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
|
|
3
|
-
jest.unstable_mockModule('execa', () => ({
|
|
4
|
-
$: () => async () => ({ exitCode: 0, stdout: '', stderr: '' }),
|
|
5
|
-
}));
|
|
6
|
-
jest.unstable_mockModule('lino-arguments', () => ({
|
|
7
|
-
makeConfig: () => ({ shouldPull: false, jsRoot: 'js' }),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
jest
|
|
12
|
-
.spyOn(global, 'fetch')
|
|
13
|
-
.mockRejectedValue(new Error('unexpected remote code download'));
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterAll(() => {
|
|
17
|
-
global.fetch.mockRestore();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('publish verification', () => {
|
|
21
|
-
test('keeps polling verification through transient npm 404s', async () => {
|
|
22
|
-
const { verifyPublishedVersionWithRunner } =
|
|
23
|
-
await import('../../../scripts/publish-to-npm.mjs');
|
|
24
|
-
|
|
25
|
-
const runVerify = jest
|
|
26
|
-
.fn()
|
|
27
|
-
.mockResolvedValueOnce({
|
|
28
|
-
exitCode: 1,
|
|
29
|
-
stdout: '',
|
|
30
|
-
stderr: 'npm error code E404',
|
|
31
|
-
})
|
|
32
|
-
.mockResolvedValueOnce({
|
|
33
|
-
exitCode: 1,
|
|
34
|
-
stdout: '',
|
|
35
|
-
stderr: 'npm error code E404',
|
|
36
|
-
})
|
|
37
|
-
.mockResolvedValueOnce({
|
|
38
|
-
exitCode: 0,
|
|
39
|
-
stdout: '1.7.1\n',
|
|
40
|
-
stderr: '',
|
|
41
|
-
});
|
|
42
|
-
const sleepFn = jest.fn().mockResolvedValue(undefined);
|
|
43
|
-
|
|
44
|
-
const published = await verifyPublishedVersionWithRunner(
|
|
45
|
-
'1.7.1',
|
|
46
|
-
runVerify,
|
|
47
|
-
sleepFn
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
expect(published).toBe(true);
|
|
51
|
-
expect(runVerify).toHaveBeenCalledTimes(3);
|
|
52
|
-
expect(sleepFn).toHaveBeenCalledTimes(2);
|
|
53
|
-
});
|
|
54
|
-
});
|
package/tests/unit/retry.test.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
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
|
-
});
|