Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions scripts/fetch-github-projects.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export function truncate(text, max) {
return t.length <= max ? t : t.slice(0, max - 1).trimEnd() + '…';
}

// An image or link with an inline `(url)` or a reference `[label]` target, optionally wrapped in a link.
// Alt text, labels and URLs exclude brackets, and URLs allow only one level of parentheses,
// so every match attempt stops at the next bracket and matching stays linear.
const TARGET = String.raw`(?:\((?:[^()[\]\s]|\([^()[\]\s]*\))*\)|\[[^[\]]*\])`;
const BADGE = new RegExp(String.raw`\[?!?\[[^[\]]*\]${TARGET}(?:\]${TARGET})?`, 'g');

/** Pull the first real paragraph out of a README, skipping title/badges/code. */
export function readmeExcerpt(md) {
const lines = String(md).split('\n');
Expand All @@ -69,8 +75,8 @@ export function readmeExcerpt(md) {
if (fence) continue;
if (!line) { if (para.length) break; else continue; }
if (line.startsWith('#')) continue; // heading
// line of images or linked badges; alt and URL exclude brackets so matching stays linear
if (!line.replace(/\[?!?\[[^[\]]*\]\([^()\s]*\)(?:\]\([^()\s]*\))?/g, '').trim()) continue;
if (!line.replace(BADGE, '').trim()) continue; // line of images or (linked) badges
if (/^\[[^[\]]+\]:\s/.test(line)) continue; // link reference definition
if (/^[-=]{3,}$/.test(line)) continue; // setext underline / hr
para.push(line);
}
Expand Down
20 changes: 16 additions & 4 deletions scripts/fetch-github-projects.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,22 @@ test('the excerpt skips linked badges, tilde fences and plain link lines', () =>
assert.equal(readmeExcerpt(`# T\n\n${badges}\n\n~~~\ncode\n~~~\n\n[Docs](https://x/docs)\n\nReal text.`), 'Real text.');
assert.equal(readmeExcerpt('See [the docs](https://x/docs) for more.'), 'See [the docs](https://x/docs) for more.');
assert.equal(readmeExcerpt('~~~md\n```js\nx\n~~~\n\nReal text.'), 'Real text.');
// CodeQL's backtracking input: took seconds with the old badge pattern
const start = performance.now();
readmeExcerpt(`[]()${'[[]()'.repeat(28)}x`);
assert.ok(performance.now() - start < 100);
});

test('the excerpt skips reference-style badges and URLs with parentheses', () => {
const refs = '[![CI][ci-badge]][ci] ![Cov][cov]\n\n[ci-badge]: https://x/ci.svg';
assert.equal(readmeExcerpt(`${refs}\n\nReal text.`), 'Real text.');
assert.equal(readmeExcerpt('[![W](https://x/w_(a).svg)](https://x/w_(a))\n\nReal text.'), 'Real text.');
assert.equal(readmeExcerpt('See [a][b] for more.'), 'See [a][b] for more.');
});

test('badge matching stays fast on crafted lines', () => {
// the first is CodeQL's backtracking input, which took seconds with an earlier pattern
for (const line of [`[]()${'[[]()'.repeat(28)}x`, '[a]('.repeat(5000), '[a]['.repeat(5000), `[a](${'(b'.repeat(5000)}`]) {
const start = performance.now();
readmeExcerpt(line);
assert.ok(performance.now() - start < 100, line.slice(0, 8));
}
});

test('a repo link is found in a file with Windows line endings', () => {
Expand Down
Loading