Skip to content
Draft
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
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ async function buildBrowserExtension(browserType, version, fileExtension) {
buildContentScript(path.join(srcDirPath, 'content', 'content.stackoverflow.js'), outputDirPath);
buildContentScript(path.join(srcDirPath, 'content', 'content.npmjs.js'), outputDirPath);
buildContentScript(path.join(srcDirPath, 'content', 'content.pypi.js'), outputDirPath);
buildContentScript(path.join(srcDirPath, 'content', 'content.chatgpt.js'), outputDirPath);

// --------------
// background.js
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"lint:fix": "yarn lint --fix",
"lint:lockfile": "lockfile-lint --path yarn.lock --allowed-hosts npm yarn --validate-https --validate-package-names --validate-integrity --empty-hostname false",
"lint:firefox": "yarn build && web-ext lint --source-dir ./dist/firefox",
"__start": "web-ext run --start-url https://pypi.org/project/pandas/ --start-url https://www.npmjs.com/package/node-sass",
"__start": "web-ext run --start-url https://pypi.org/project/pandas/ --start-url https://www.npmjs.com/package/node-sass --start-url https://chat.openai.com/ --start-url https://stackoverflow.com/questions/33527653",
"start:chrome": "yarn __start --source-dir ./dist/chrome --target chromium",
"start:firefox": "yarn __start --source-dir ./dist/firefox"
},
Expand Down
4 changes: 1 addition & 3 deletions src/background/cache.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import LRUCache from 'lru-cache';

const SECOND = 1000;
const MINUTE = 60 * SECOND;
import { MINUTE } from '../global';

const _cache = new LRUCache({
max: 500,
Expand Down
7 changes: 7 additions & 0 deletions src/content/content.chatgpt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SECOND } from '../global';
import { mountContentScript } from './content';
import { addIndicatorToFindingsInElement } from './create-element';

mountContentScript(async () => {
setInterval(() => addIndicatorToFindingsInElement(document.querySelector('main')), 5 * SECOND);
});
28 changes: 13 additions & 15 deletions src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,22 @@ const injectScriptTag = () => {
console.log('Injected link tag', link);
};

export const mountContentScript = (contentScript) => {
window.addEventListener('load', async () => {
console.log('Overlay is running');
export const mountContentScript = async (contentScript) => {
console.log('Overlay is running');

events.listen();
injectScriptTag();
events.listen();
injectScriptTag();

try {
await events.onScriptLoaded();
} catch (e) {
console.error('Injected script is not ready, aborting', e);
return;
}
try {
await events.onScriptLoaded();
} catch (e) {
console.error('Injected script is not ready, aborting', e);
return;
}

events.sendEventSettingsChangedToWebapp();
events.sendEventSettingsChangedToWebapp();

await contentScript();
await contentScript();

console.log('Overlay is finished');
});
console.log('Overlay loading is finished');
};
7 changes: 2 additions & 5 deletions src/content/content.npmjs.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { mountContentScript } from './content';
import { fetchPackageInfo } from './content-events';
import { createPackageReportElement } from './create-element';
import { urlParsers } from './registry/npm';

const addPackageReport = (packageID) => {
const packageReport = document.createElement('overlay-package-report');
packageReport.setAttribute('package-type', packageID.type);
packageReport.setAttribute('package-name', packageID.name);

const repository = document.querySelector('#repository');
if (repository) {
repository.parentElement.insertBefore(packageReport, repository);
repository.parentElement.insertBefore(createPackageReportElement(packageID), repository);
}
};

Expand Down
7 changes: 2 additions & 5 deletions src/content/content.pypi.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import browser from '../browser';
import { mountContentScript } from './content';
import { fetchPackageInfo } from './content-events';
import { createPackageReportElement } from './create-element';
import { urlParsers } from './registry/python';

const addPackageReport = (packageID) => {
const packageReport = document.createElement('overlay-package-report');
packageReport.setAttribute('package-type', packageID.type);
packageReport.setAttribute('package-name', packageID.name);
packageReport.setAttribute('stylesheet-url', browser.runtime.getURL('custom-elements.css'));

const sidebar = document.querySelector('.vertical-tabs__tabs');
const sidebarSection = sidebar?.querySelectorAll('.sidebar-section')[1];
if (!sidebarSection) {
console.log('No side section found (parent of .github-repo-info)');
return;
}

const packageReport = createPackageReportElement(packageID, browser.runtime.getURL('custom-elements.css'));
sidebar.insertBefore(packageReport, sidebarSection);
};

Expand Down
21 changes: 3 additions & 18 deletions src/content/content.stackoverflow.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
import { mountContentScript } from './content';
import { fetchPackageInfo } from './content-events';
import { findRanges } from './stackoverflow/finder';
import { addIndicator } from './stackoverflow/indicator';
import { addIndicatorToFindingsInElement } from './create-element';

mountContentScript(async () => {
const findings = findRanges(document.body);
console.debug({ findings });
const POST_SELECTOR = 'div.js-post-body';

const processed = {};
findings.forEach(({ range, ...packageId }) => {
addIndicator(range, packageId);
const packageKey = `${packageId.type}/${packageId.name}`;
if (processed[packageKey]) {
return;
}

processed[packageKey] = true;
fetchPackageInfo(packageId);
});
});
mountContentScript(() => addIndicatorToFindingsInElement(document.body, POST_SELECTOR));
43 changes: 43 additions & 0 deletions src/content/create-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { OVERLAY_INDICATOR, OVERLAY_PACKAGE_REPORT } from '../global';
import { fetchPackageInfo } from './content-events';
import { findRanges } from './finder';

export const addIndicatorToFindingsInElement = (element, contentElementSelector) => {
const findings = findRanges(element, contentElementSelector);
console.debug({ findings });

const processed = {};
findings
.filter(({ range }) => range.endContainer.parentElement.nodeName.toLowerCase() !== OVERLAY_INDICATOR) // For install command
.filter(({ range }) => range.commonAncestorContainer.nodeName.toLowerCase() !== OVERLAY_INDICATOR) // For links
.forEach(({ range, ...packageId }) => {
addIndicatorToRange(range, packageId);
const packageKey = `${packageId.type}/${packageId.name}`;
if (processed[packageKey]) {
return;
}

processed[packageKey] = true;
fetchPackageInfo(packageId);
});
};

const addIndicatorToRange = async (range, packageID) => {
console.debug('Adding indicator for', packageID, range);

const indicator = document.createElement(OVERLAY_INDICATOR);
indicator.setAttribute('package-type', packageID.type);
indicator.setAttribute('package-name', packageID.name);
indicator.appendChild(range.extractContents());
range.insertNode(indicator);
};

export const createPackageReportElement = (packageID, stylesheetUrl) => {
const packageReport = document.createElement(OVERLAY_PACKAGE_REPORT);
packageReport.setAttribute('package-type', packageID.type);
packageReport.setAttribute('package-name', packageID.name);
if (stylesheetUrl) {
packageReport.setAttribute('stylesheet-url', stylesheetUrl);
}
return packageReport;
};
22 changes: 14 additions & 8 deletions src/content/stackoverflow/finder.js → src/content/finder.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import * as go from '../registry/go';
import * as npm from '../registry/npm';
import * as python from '../registry/python';
import * as go from './registry/go';
import * as npm from './registry/npm';
import * as python from './registry/python';
import { getRangeOfPositions } from './range';

const POST_SELECTOR = 'div.js-post-body';

const validURL = (href) => {
try {
const url = new URL(href);
Expand All @@ -22,8 +20,16 @@ const urlParsers = {

const codeBlockParsers = [...npm.parseCommands, python.parseCommand, go.parseCommand];

export const findRanges = (body) => {
const links = Array.from(body.querySelectorAll(`${POST_SELECTOR} a`))
const querySelectorAllIncludeSelf = (element, selector) => {
const matches = Array.from(element.querySelectorAll(selector));
if (element.matches(selector)) {
matches.push(element);
}
return matches;
};

export const findRanges = (element, contentElementSelector = '') => {
const links = querySelectorAllIncludeSelf(element, `${contentElementSelector} a`)
.map((element) => {
const url = validURL(element.getAttribute('href'));
if (!url) return;
Expand All @@ -41,7 +47,7 @@ export const findRanges = (body) => {
})
.filter((p) => p);

const installCommands = Array.from(body.querySelectorAll(`${POST_SELECTOR} code`)).flatMap((element) => {
const installCommands = querySelectorAllIncludeSelf(element, `${contentElementSelector} code`).flatMap((element) => {
return codeBlockParsers.flatMap((parser) => {
const packages = parser(element.textContent);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { createCodeBlock, createPreCodeBlock, createRealAnswer, createRealComment } from '../../test-utils/html-builder';
import { createCodeBlock, createElement, createPreCodeBlock, createRealAnswer, createRealComment } from '../test-utils/html-builder';
import { findRanges } from './finder';

describe(findRanges.name, () => {
Expand Down Expand Up @@ -48,6 +48,14 @@ describe(findRanges.name, () => {
expect(range.startContainer.childNodes[range.startOffset].nodeType).not.toBe(Node.TEXT_NODE);
});

it('should find the whole element as it is a link', () => {
const { element } = createElement(`<a id="test" href="https://www.npmjs.com/package/minimist">minimist</a>`);

const foundElements = findRanges(element);

expect(foundElements.length).toBe(1);
});

it.each(['http://npmjs.org/', 'https://pypi.python.org/packages/source/v/virtualenv/virtualenv-12.0.7.tar.gz'])(
`Should not find any package in '%s'`,
(url) => {
Expand Down Expand Up @@ -313,32 +321,36 @@ describe(findRanges.name, () => {
});
});

it.each([
[
'comment',
'My entry into this arena is trepanjs (<a href="https://www.npmjs.com/package/trepanjs" rel="nofollow noreferrer">npmjs.com/package/trepanjs</a>). It has all of the goodness of the node debugger, but conforms better to gdb. It also has more features and commands like syntax highlighting, more extensive online help, and smarter evaluation. See <a href="https://github.com/rocky/trepanjs/wiki/Cool-things" rel="nofollow noreferrer">github.com/rocky/trepanjs/wiki/Cool-things</a> for some of its cool features.',
],
])('Should ignore packages in %s', (_reason, comment) => {
const { body } = createRealComment(comment);
it.each(['npm install -g', 'npm install PACKAGE-NAME', 'npm install packageName'])(`Should not find any package in '%s'`, (command) => {
const { body } = createCodeBlock(command);

const foundElements = findRanges(body);

expect(foundElements.length).toBe(0);
});

it.each(['npm install -g', 'npm install PACKAGE-NAME', 'npm install packageName'])(`Should not find any package in '%s'`, (command) => {
// issue #37, #38
it.each(['npm install git://github.com/user-c/dep-2#node0.8.0'])(`Future support '%s`, (command) => {
const { body } = createCodeBlock(command);

const foundElements = findRanges(body);

expect(foundElements.length).toBe(0);
});
});

// issue #37, #38
it.each(['npm install git://github.com/user-c/dep-2#node0.8.0'])(`Future support '%s`, (command) => {
const { body } = createCodeBlock(command);
describe('StackOverflow', () => {
const STACKOVERFLOW_POST_SELECTOR = 'div.js-post-body';

const foundElements = findRanges(body);
it.each([
[
'comment',
'My entry into this arena is trepanjs (<a href="https://www.npmjs.com/package/trepanjs" rel="nofollow noreferrer">npmjs.com/package/trepanjs</a>). It has all of the goodness of the node debugger, but conforms better to gdb. It also has more features and commands like syntax highlighting, more extensive online help, and smarter evaluation. See <a href="https://github.com/rocky/trepanjs/wiki/Cool-things" rel="nofollow noreferrer">github.com/rocky/trepanjs/wiki/Cool-things</a> for some of its cool features.',
],
])('Should ignore packages in %s', (_reason, comment) => {
const { body } = createRealComment(comment);

const foundElements = findRanges(body, STACKOVERFLOW_POST_SELECTOR);

expect(foundElements.length).toBe(0);
});
Expand Down
File renamed without changes.
File renamed without changes.
9 changes: 0 additions & 9 deletions src/content/stackoverflow/indicator.js

This file was deleted.

3 changes: 2 additions & 1 deletion src/custom-elements/Indicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import { defineComponent } from 'vue';
import Tooltip from './Tooltip.vue';
import PackageReport from './PackageReport.vue';
import { usePackageInfo } from './store';
import { OVERLAY_INDICATOR } from '../global';

const sum = (arr) => arr.reduce((a, b) => a + b, 0);

export default defineComponent({
name: 'overlay-indicator',
name: OVERLAY_INDICATOR,
components: {
Tooltip,
PackageReport,
Expand Down
5 changes: 3 additions & 2 deletions src/custom-elements/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@webcomponents/custom-elements';
import { defineCustomElement } from 'vue';
import { OVERLAY_INDICATOR, OVERLAY_PACKAGE_REPORT } from '../global';
import Indicator from './Indicator.vue';
import PackageReport from './PackageReport.vue';
import { initEventListenersAndStore } from './webapp-events';
Expand All @@ -13,9 +14,9 @@ Promise.all(Object.values(modules).map((module) => module())).then((modules) =>
Indicator.styles = [styles.flat().join('')];

const indicatorCustomElement = defineCustomElement(Indicator);
customElements.define('overlay-indicator', indicatorCustomElement);
customElements.define(OVERLAY_INDICATOR, indicatorCustomElement);
const packageReportCustomElement = defineCustomElement(PackageReport);
customElements.define('overlay-package-report', packageReportCustomElement);
customElements.define(OVERLAY_PACKAGE_REPORT, packageReportCustomElement);

console.log('Custom element defined');
});
5 changes: 5 additions & 0 deletions src/global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const SECOND = 1000;
export const MINUTE = 60 * SECOND;

export const OVERLAY_INDICATOR = 'overlay-indicator';
export const OVERLAY_PACKAGE_REPORT = 'overlay-package-report';
4 changes: 4 additions & 0 deletions src/manifest.chrome.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
{
"matches": ["*://pypi.org/project/*"],
"js": ["content.pypi.js"]
},
{
"matches": ["*://chat.openai.com/*"],
"js": ["content.chatgpt.js"]
}
],
"background": {
Expand Down
4 changes: 4 additions & 0 deletions src/manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
{
"matches": ["*://pypi.org/project/*"],
"js": ["content.pypi.js"]
},
{
"matches": ["*://chat.openai.com/*"],
"js": ["content.chatgpt.js"]
}
],
"background": {
Expand Down
1 change: 1 addition & 0 deletions src/test-utils/html-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const createElement = (html) => {
return { body, element };
};

// TODO: rename to createRealStackOverflowAnswer
export const createRealAnswer = (answer) => createElement(answerTemplate.replace('$$$ANSWER$$$', answer));
export const createRealComment = (comment) => createElement(answerTemplate.replace('$$$COMMENT$$$', comment));

Expand Down
5 changes: 3 additions & 2 deletions tests/stackoverflow.integration.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect, jest, test } from '@jest/globals';
import { findRanges } from '../src/content/stackoverflow/finder';
import { findRanges } from '../src/content/finder';
import { readRealExamples, writeResultsSnapshot } from './real-examples/real-examples';

const JEST_DEFAULT_TIMEOUT = 5000;
const STACKOVERFLOW_POST_SELECTOR = 'div.js-post-body';

const htmlParser = new DOMParser();
const getElementFromFragment = (fragment) => {
Expand All @@ -20,7 +21,7 @@ describe('Real Pages', () => {
const results = realExamples.map(({ html, ...example }) => {
const body = htmlParser.parseFromString(html, 'text/html').body;

const foundLinks = findRanges(body)
const foundLinks = findRanges(body, STACKOVERFLOW_POST_SELECTOR)
.map(({ range, ...rest }) => {
const element =
range.cloneContents().firstChild.nodeType === Node.TEXT_NODE
Expand Down