mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
Some checks are pending
Binaries / harper-cli - macOS-aarch64 (push) Waiting to run
Binaries / harper-cli - Linux-aarch64-GNU (push) Waiting to run
Binaries / harper-cli - Linux-aarch64-musl (push) Waiting to run
Binaries / harper-cli - macOS-x86_64 (push) Waiting to run
Binaries / harper-cli - Linux-x86_64-GNU (push) Waiting to run
Binaries / harper-cli - Linux-x86_64-musl (push) Waiting to run
Binaries / harper-cli - Windows-x86_64 (push) Waiting to run
Binaries / harper-ls - macOS-aarch64 (push) Waiting to run
Binaries / harper-ls - Linux-aarch64-GNU (push) Waiting to run
Binaries / harper-ls - Linux-aarch64-musl (push) Waiting to run
Binaries / harper-ls - macOS-x86_64 (push) Waiting to run
Binaries / harper-ls - Linux-x86_64-GNU (push) Waiting to run
Binaries / harper-ls - Linux-x86_64-musl (push) Waiting to run
Binaries / harper-ls - Windows-x86_64 (push) Waiting to run
Build Web / build-web (push) Waiting to run
Chrome Plugin / chrome-plugin (push) Waiting to run
Just Checks / just build-obsidian (push) Waiting to run
Just Checks / just test-harperjs (push) Waiting to run
Just Checks / just test-obsidian (push) Waiting to run
Just Checks / just test-rust (push) Waiting to run
Just Checks / just test-vscode (push) Waiting to run
VS Code Plugin / alpine-arm64 (push) Waiting to run
VS Code Plugin / darwin-arm64 (push) Waiting to run
VS Code Plugin / linux-armhf (push) Waiting to run
VS Code Plugin / linux-x64 (push) Waiting to run
WordPress Plugin / wp-plugin (push) Waiting to run
Just Checks / just check-js (push) Waiting to run
Just Checks / just check-rust (push) Waiting to run
Just Checks / just test-chrome-plugin (push) Waiting to run
Just Checks / just test-firefox-plugin (push) Waiting to run
VS Code Plugin / alpine-x64 (push) Waiting to run
VS Code Plugin / darwin-x64 (push) Waiting to run
VS Code Plugin / linux-arm64 (push) Waiting to run
VS Code Plugin / win32-arm64 (push) Waiting to run
VS Code Plugin / win32-x64 (push) Waiting to run
453 lines
13 KiB
TypeScript
453 lines
13 KiB
TypeScript
import { BinaryModule, Dialect, type LintConfig, LocalLinter } from 'harper.js';
|
|
import { type UnpackedLintGroups, unpackLint } from 'lint-framework';
|
|
import type { PopupState } from '../PopupState';
|
|
import {
|
|
ActivationKey,
|
|
type AddToUserDictionaryRequest,
|
|
createUnitResponse,
|
|
type GetActivationKeyResponse,
|
|
type GetConfigRequest,
|
|
type GetConfigResponse,
|
|
type GetDefaultStatusResponse,
|
|
type GetDialectRequest,
|
|
type GetDialectResponse,
|
|
type GetDomainStatusRequest,
|
|
type GetDomainStatusResponse,
|
|
type GetEnabledDomainsResponse,
|
|
type GetLintDescriptionsRequest,
|
|
type GetLintDescriptionsResponse,
|
|
type GetUserDictionaryResponse,
|
|
type IgnoreLintRequest,
|
|
type LintRequest,
|
|
type LintResponse,
|
|
type OpenReportErrorRequest,
|
|
type PostFormDataRequest,
|
|
type PostFormDataResponse,
|
|
type Request,
|
|
type Response,
|
|
type SetActivationKeyRequest,
|
|
type SetConfigRequest,
|
|
type SetDefaultStatusRequest,
|
|
type SetDialectRequest,
|
|
type SetDomainStatusRequest,
|
|
type SetUserDictionaryRequest,
|
|
type UnitResponse,
|
|
} from '../protocol';
|
|
|
|
console.log('background is running');
|
|
|
|
chrome.runtime.onInstalled.addListener((details) => {
|
|
if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
|
|
chrome.runtime.setUninstallURL('https://writewithharper.com/uninstall-browser-extension');
|
|
chrome.tabs.create({ url: 'https://writewithharper.com/install-browser-extension' });
|
|
}
|
|
});
|
|
|
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|
handleRequest(request).then(sendResponse);
|
|
|
|
return true;
|
|
});
|
|
|
|
let linter: LocalLinter;
|
|
|
|
getDialect().then(setDialect);
|
|
|
|
async function enableDefaultDomains() {
|
|
const defaultEnabledDomains = [
|
|
'old.reddit.com',
|
|
'chatgpt.com',
|
|
'www.perplexity.ai',
|
|
'textarea.online',
|
|
'webmail.porkbun.com',
|
|
'mail.google.com',
|
|
'trix-editor.org',
|
|
'github.com',
|
|
'messages.google.com',
|
|
'blank.page',
|
|
'blankpage.im',
|
|
'froala.com',
|
|
'playground.lexical.dev',
|
|
'discord.com',
|
|
'www.youtube.com',
|
|
'www.google.com',
|
|
'www.instagram.com',
|
|
'web.whatsapp.com',
|
|
'outlook.live.com',
|
|
'www.reddit.com',
|
|
'www.linkedin.com',
|
|
'bsky.app',
|
|
'pootlewriter.com',
|
|
'www.tumblr.com',
|
|
'dayone.me',
|
|
'medium.com',
|
|
'x.com',
|
|
'www.notion.so',
|
|
'hashnode.com',
|
|
'www.slatejs.org',
|
|
'localhost',
|
|
'writewithharper.com',
|
|
'prosemirror.net',
|
|
'draftjs.org',
|
|
'gitlab.com',
|
|
'core.trac.wordpress.org',
|
|
'write.ellipsus.com',
|
|
'www.facebook.com',
|
|
'www.upwork.com',
|
|
'news.ycombinator.com',
|
|
'classroom.google.com',
|
|
];
|
|
|
|
for (const item of defaultEnabledDomains) {
|
|
if (!(await isDomainSet(item))) {
|
|
setDomainEnable(item, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
enableDefaultDomains();
|
|
|
|
function handleRequest(message: Request): Promise<Response> {
|
|
console.log(`Handling ${message.kind} request`);
|
|
|
|
switch (message.kind) {
|
|
case 'lint':
|
|
return handleLint(message);
|
|
case 'getConfig':
|
|
return handleGetConfig(message);
|
|
case 'setConfig':
|
|
return handleSetConfig(message);
|
|
case 'getLintDescriptions':
|
|
return handleGetLintDescriptions(message);
|
|
case 'setDialect':
|
|
return handleSetDialect(message);
|
|
case 'getDialect':
|
|
return handleGetDialect(message);
|
|
case 'getDomainStatus':
|
|
return handleGetDomainStatus(message);
|
|
case 'setDomainStatus':
|
|
return handleSetDomainStatus(message);
|
|
case 'addToUserDictionary':
|
|
return handleAddToUserDictionary(message);
|
|
case 'ignoreLint':
|
|
return handleIgnoreLint(message);
|
|
case 'setDefaultStatus':
|
|
return handleSetDefaultStatus(message);
|
|
case 'getDefaultStatus':
|
|
return handleGetDefaultStatus();
|
|
case 'getEnabledDomains':
|
|
return handleGetEnabledDomains();
|
|
case 'getUserDictionary':
|
|
return handleGetUserDictionary();
|
|
case 'setUserDictionary':
|
|
return handleSetUserDictionary(message);
|
|
case 'getActivationKey':
|
|
return handleGetActivationKey();
|
|
case 'setActivationKey':
|
|
return handleSetActivationKey(message);
|
|
case 'openReportError':
|
|
return handleOpenReportError(message);
|
|
case 'openOptions':
|
|
chrome.runtime.openOptionsPage();
|
|
return Promise.resolve(createUnitResponse());
|
|
case 'postFormData':
|
|
return handlePostFormData(message);
|
|
}
|
|
}
|
|
|
|
/** Handle a request for linting. */
|
|
async function handleLint(req: LintRequest): Promise<LintResponse> {
|
|
if (!(await enabledForDomain(req.domain))) {
|
|
return { kind: 'lints', lints: {} };
|
|
}
|
|
|
|
const grouped = await linter.organizedLints(req.text);
|
|
const unpackedEntries = await Promise.all(
|
|
Object.entries(grouped).map(async ([source, lints]) => {
|
|
const unpacked = await Promise.all(lints.map((lint) => unpackLint(req.text, lint, linter)));
|
|
return [source, unpacked] as const;
|
|
}),
|
|
);
|
|
const unpackedBySource = Object.fromEntries(unpackedEntries) as UnpackedLintGroups;
|
|
return { kind: 'lints', lints: unpackedBySource };
|
|
}
|
|
|
|
async function handleGetConfig(_req: GetConfigRequest): Promise<GetConfigResponse> {
|
|
return { kind: 'getConfig', config: await getLintConfig() };
|
|
}
|
|
|
|
async function handleSetConfig(req: SetConfigRequest): Promise<UnitResponse> {
|
|
await setLintConfig(req.config);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleSetDialect(req: SetDialectRequest): Promise<UnitResponse> {
|
|
await setDialect(req.dialect);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleGetDialect(_req: GetDialectRequest): Promise<GetDialectResponse> {
|
|
return { kind: 'getDialect', dialect: await getDialect() };
|
|
}
|
|
|
|
async function handleIgnoreLint(req: IgnoreLintRequest): Promise<UnitResponse> {
|
|
await linter.ignoreLintHash(BigInt(req.contextHash));
|
|
await setIgnoredLints(await linter.exportIgnoredLints());
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleGetDefaultStatus(): Promise<GetDefaultStatusResponse> {
|
|
return {
|
|
kind: 'getDefaultStatus',
|
|
enabled: await enabledByDefault(),
|
|
};
|
|
}
|
|
|
|
async function handleGetEnabledDomains(): Promise<GetEnabledDomainsResponse> {
|
|
const all = await chrome.storage.local.get(null as any);
|
|
const prefix = formatDomainKey(''); // yields 'domainStatus '
|
|
const domains = Object.entries(all)
|
|
.filter(([k, v]) => typeof v === 'boolean' && v === true && k.startsWith(prefix))
|
|
.map(([k]) => k.substring(prefix.length))
|
|
.sort((a, b) => a.localeCompare(b));
|
|
|
|
return { kind: 'getEnabledDomains', domains };
|
|
}
|
|
|
|
async function handleGetDomainStatus(
|
|
req: GetDomainStatusRequest,
|
|
): Promise<GetDomainStatusResponse> {
|
|
return {
|
|
kind: 'getDomainStatus',
|
|
domain: req.domain,
|
|
enabled: await enabledForDomain(req.domain),
|
|
};
|
|
}
|
|
|
|
async function handleSetDomainStatus(req: SetDomainStatusRequest): Promise<UnitResponse> {
|
|
await setDomainEnable(req.domain, req.enabled, req.overrideValue);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleSetDefaultStatus(req: SetDefaultStatusRequest): Promise<UnitResponse> {
|
|
await setDefaultEnable(req.enabled);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleGetLintDescriptions(
|
|
_req: GetLintDescriptionsRequest,
|
|
): Promise<GetLintDescriptionsResponse> {
|
|
return { kind: 'getLintDescriptions', descriptions: await linter.getLintDescriptionsHTML() };
|
|
}
|
|
|
|
async function handleSetUserDictionary(req: SetUserDictionaryRequest): Promise<UnitResponse> {
|
|
await resetDictionary();
|
|
await addToDictionary(req.words);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleAddToUserDictionary(req: AddToUserDictionaryRequest): Promise<UnitResponse> {
|
|
await addToDictionary(req.words);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleGetUserDictionary(): Promise<GetUserDictionaryResponse> {
|
|
const dict = await getUserDictionary();
|
|
|
|
return { kind: 'getUserDictionary', words: dict };
|
|
}
|
|
|
|
async function handleGetActivationKey(): Promise<GetActivationKeyResponse> {
|
|
const key = await getActivationKey();
|
|
|
|
return { kind: 'getActivationKey', key };
|
|
}
|
|
|
|
async function handleSetActivationKey(req: SetActivationKeyRequest): Promise<UnitResponse> {
|
|
if (!Object.values(ActivationKey).includes(req.key)) {
|
|
throw new Error(`Invalid activation key: ${req.key}`);
|
|
}
|
|
await setActivationKey(req.key);
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handleOpenReportError(req: OpenReportErrorRequest): Promise<UnitResponse> {
|
|
const popupState: PopupState = {
|
|
page: 'report-error',
|
|
example: req.example,
|
|
rule_id: req.rule_id,
|
|
feedback: req.feedback,
|
|
};
|
|
|
|
await chrome.storage.local.set({ popupState });
|
|
|
|
if (chrome.action?.openPopup) {
|
|
try {
|
|
await chrome.action.openPopup();
|
|
} catch (error) {
|
|
console.error('Failed to open popup for report error', error);
|
|
}
|
|
}
|
|
|
|
return createUnitResponse();
|
|
}
|
|
|
|
async function handlePostFormData(req: PostFormDataRequest): Promise<PostFormDataResponse> {
|
|
const formData = new FormData();
|
|
for (const [key, value] of Object.entries(req.formData)) {
|
|
formData.append(key, value);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(req.url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
return { kind: 'postFormData', success: response.ok };
|
|
} catch (error) {
|
|
console.error('Failed to post form data', error);
|
|
return { kind: 'postFormData', success: false };
|
|
}
|
|
}
|
|
|
|
/** Set the lint configuration inside the global `linter` and in permanent storage. */
|
|
async function setLintConfig(lintConfig: LintConfig): Promise<void> {
|
|
await linter.setLintConfig(lintConfig);
|
|
|
|
const json = await linter.getLintConfigAsJSON();
|
|
|
|
await chrome.storage.local.set({ lintConfig: json });
|
|
}
|
|
|
|
/** Get the lint configuration from permanent storage. */
|
|
async function getLintConfig(): Promise<LintConfig> {
|
|
const json = await linter.getLintConfigAsJSON();
|
|
const resp = await chrome.storage.local.get({ lintConfig: json });
|
|
return JSON.parse(resp.lintConfig);
|
|
}
|
|
|
|
/** Get the ignored lint state from permanent storage. */
|
|
async function setIgnoredLints(state: string): Promise<void> {
|
|
await linter.importIgnoredLints(state);
|
|
|
|
const json = await linter.exportIgnoredLints();
|
|
|
|
await chrome.storage.local.set({ ignoredLints: json });
|
|
}
|
|
|
|
/** Get the ignored lint state from permanent storage. */
|
|
async function getIgnoredLints(): Promise<string> {
|
|
const state = await linter.exportIgnoredLints();
|
|
const resp = await chrome.storage.local.get({ ignoredLints: state });
|
|
return resp.ignoredLints;
|
|
}
|
|
|
|
async function getDialect(): Promise<Dialect> {
|
|
const resp = await chrome.storage.local.get({ dialect: Dialect.American });
|
|
return resp.dialect;
|
|
}
|
|
|
|
async function getActivationKey(): Promise<ActivationKey> {
|
|
const resp = await chrome.storage.local.get({ activationKey: ActivationKey.Off });
|
|
return resp.activationKey;
|
|
}
|
|
|
|
async function setActivationKey(key: ActivationKey) {
|
|
await chrome.storage.local.set({ activationKey: key });
|
|
}
|
|
|
|
function initializeLinter(dialect: Dialect) {
|
|
linter = new LocalLinter({
|
|
binary: new BinaryModule(chrome.runtime.getURL('./wasm/harper_wasm_bg.wasm')),
|
|
dialect,
|
|
});
|
|
|
|
getIgnoredLints().then((i) => linter.importIgnoredLints(i));
|
|
getUserDictionary().then((u) => linter.importWords(u));
|
|
getLintConfig().then((c) => linter.setLintConfig(c));
|
|
linter.setup();
|
|
}
|
|
|
|
async function setDialect(dialect: Dialect) {
|
|
await chrome.storage.local.set({ dialect });
|
|
initializeLinter(dialect);
|
|
}
|
|
|
|
/** Format the key to be used in local storage to store domain status. */
|
|
function formatDomainKey(domain: string): string {
|
|
return `domainStatus ${domain}`;
|
|
}
|
|
|
|
/** Check if Harper has been enabled for a given domain. */
|
|
async function enabledForDomain(domain: string): Promise<boolean | null> {
|
|
const req = await chrome.storage.local.get({
|
|
[formatDomainKey(domain)]: await enabledByDefault(),
|
|
});
|
|
return req[formatDomainKey(domain)];
|
|
}
|
|
|
|
/** Set whether Harper is enabled for a given domain.
|
|
*
|
|
* @param overrideValue dictates whether this should override a previous setting.
|
|
* */
|
|
async function setDomainEnable(domain: string, status: boolean, overrideValue = true) {
|
|
let shouldSet = !(await isDomainSet(domain));
|
|
|
|
if (overrideValue) {
|
|
shouldSet = true;
|
|
}
|
|
|
|
if (shouldSet) {
|
|
await chrome.storage.local.set({ [formatDomainKey(domain)]: status });
|
|
}
|
|
}
|
|
|
|
/** Set whether Harper is enabled by default. */
|
|
async function setDefaultEnable(status: boolean) {
|
|
await chrome.storage.local.set({ defaultEnable: status });
|
|
}
|
|
|
|
/** Check if Harper has been enabled by default. */
|
|
async function enabledByDefault(): Promise<boolean> {
|
|
const req = await chrome.storage.local.get({ defaultEnable: false });
|
|
return req.defaultEnable;
|
|
}
|
|
|
|
/** Check whether Harper's state has been set for a given domain. */
|
|
async function isDomainSet(domain: string): Promise<boolean> {
|
|
const resp = await chrome.storage.local.get(formatDomainKey(domain));
|
|
return typeof resp[formatDomainKey(domain)] == 'boolean';
|
|
}
|
|
|
|
/** Reset the persistent user dictionary. */
|
|
async function resetDictionary(): Promise<void> {
|
|
await chrome.storage.local.set({ userDictionary: null });
|
|
|
|
initializeLinter(await linter.getDialect());
|
|
}
|
|
|
|
/** Add words to the persistent user dictionary. */
|
|
async function addToDictionary(words: string[]): Promise<void> {
|
|
const exported = await linter.exportWords();
|
|
exported.push(...words);
|
|
|
|
await Promise.all([
|
|
linter.importWords(exported),
|
|
chrome.storage.local.set({ userDictionary: exported }),
|
|
]);
|
|
}
|
|
|
|
/** Grab the user dictionary from persistent storage. */
|
|
async function getUserDictionary(): Promise<string[]> {
|
|
const resp = await chrome.storage.local.get({ userDictionary: [] });
|
|
return resp.userDictionary;
|
|
}
|