diff --git a/gulpfile.js b/gulpfile.js index 627430b..ea6ba53 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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 diff --git a/package.json b/package.json index 20f04dc..7bf08e6 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/background/cache.js b/src/background/cache.js index b5f76cc..db405f3 100644 --- a/src/background/cache.js +++ b/src/background/cache.js @@ -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, diff --git a/src/content/content.chatgpt.js b/src/content/content.chatgpt.js new file mode 100644 index 0000000..63ea717 --- /dev/null +++ b/src/content/content.chatgpt.js @@ -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); +}); diff --git a/src/content/content.js b/src/content/content.js index 15c2626..427f153 100644 --- a/src/content/content.js +++ b/src/content/content.js @@ -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'); }; diff --git a/src/content/content.npmjs.js b/src/content/content.npmjs.js index 3996ead..78eb2d6 100644 --- a/src/content/content.npmjs.js +++ b/src/content/content.npmjs.js @@ -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); } }; diff --git a/src/content/content.pypi.js b/src/content/content.pypi.js index 16e528b..b1705bb 100644 --- a/src/content/content.pypi.js +++ b/src/content/content.pypi.js @@ -1,14 +1,10 @@ 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) { @@ -16,6 +12,7 @@ const addPackageReport = (packageID) => { return; } + const packageReport = createPackageReportElement(packageID, browser.runtime.getURL('custom-elements.css')); sidebar.insertBefore(packageReport, sidebarSection); }; diff --git a/src/content/content.stackoverflow.js b/src/content/content.stackoverflow.js index 3c6063e..7489e91 100644 --- a/src/content/content.stackoverflow.js +++ b/src/content/content.stackoverflow.js @@ -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)); diff --git a/src/content/create-element.js b/src/content/create-element.js new file mode 100644 index 0000000..9641627 --- /dev/null +++ b/src/content/create-element.js @@ -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; +}; diff --git a/src/content/stackoverflow/finder.js b/src/content/finder.js similarity index 65% rename from src/content/stackoverflow/finder.js rename to src/content/finder.js index 08a77bb..01ed6cf 100644 --- a/src/content/stackoverflow/finder.js +++ b/src/content/finder.js @@ -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); @@ -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; @@ -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); diff --git a/src/content/stackoverflow/finder.test.js b/src/content/finder.test.js similarity index 95% rename from src/content/stackoverflow/finder.test.js rename to src/content/finder.test.js index 39e5d12..d362311 100644 --- a/src/content/stackoverflow/finder.test.js +++ b/src/content/finder.test.js @@ -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, () => { @@ -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(`minimist`); + + 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) => { @@ -313,32 +321,36 @@ describe(findRanges.name, () => { }); }); - it.each([ - [ - 'comment', - 'My entry into this arena is trepanjs (npmjs.com/package/trepanjs). 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 github.com/rocky/trepanjs/wiki/Cool-things 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 (npmjs.com/package/trepanjs). 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 github.com/rocky/trepanjs/wiki/Cool-things 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); }); diff --git a/src/content/stackoverflow/range.js b/src/content/range.js similarity index 100% rename from src/content/stackoverflow/range.js rename to src/content/range.js diff --git a/src/content/stackoverflow/range.test.js b/src/content/range.test.js similarity index 100% rename from src/content/stackoverflow/range.test.js rename to src/content/range.test.js diff --git a/src/content/stackoverflow/indicator.js b/src/content/stackoverflow/indicator.js deleted file mode 100644 index a18ec06..0000000 --- a/src/content/stackoverflow/indicator.js +++ /dev/null @@ -1,9 +0,0 @@ -export const addIndicator = async (range, packageID) => { - console.debug('Adding indicator for', packageID); - - 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); -}; diff --git a/src/custom-elements/Indicator.vue b/src/custom-elements/Indicator.vue index 321b5a3..5a11c69 100644 --- a/src/custom-elements/Indicator.vue +++ b/src/custom-elements/Indicator.vue @@ -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, diff --git a/src/custom-elements/index.js b/src/custom-elements/index.js index 451dec4..48f642b 100644 --- a/src/custom-elements/index.js +++ b/src/custom-elements/index.js @@ -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'; @@ -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'); }); diff --git a/src/global.js b/src/global.js new file mode 100644 index 0000000..135dce3 --- /dev/null +++ b/src/global.js @@ -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'; diff --git a/src/manifest.chrome.json b/src/manifest.chrome.json index d83cf37..96bf209 100644 --- a/src/manifest.chrome.json +++ b/src/manifest.chrome.json @@ -18,6 +18,10 @@ { "matches": ["*://pypi.org/project/*"], "js": ["content.pypi.js"] + }, + { + "matches": ["*://chat.openai.com/*"], + "js": ["content.chatgpt.js"] } ], "background": { diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index a26eb31..85eafb7 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -18,6 +18,10 @@ { "matches": ["*://pypi.org/project/*"], "js": ["content.pypi.js"] + }, + { + "matches": ["*://chat.openai.com/*"], + "js": ["content.chatgpt.js"] } ], "background": { diff --git a/src/test-utils/html-builder.js b/src/test-utils/html-builder.js index 14ba161..8cb9a8a 100644 --- a/src/test-utils/html-builder.js +++ b/src/test-utils/html-builder.js @@ -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)); diff --git a/tests/stackoverflow.integration.test.js b/tests/stackoverflow.integration.test.js index 3b82e8b..814c095 100644 --- a/tests/stackoverflow.integration.test.js +++ b/tests/stackoverflow.integration.test.js @@ -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) => { @@ -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