├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .git_deploy_key.enc
├── .github
└── workflows
│ ├── beta-publish.yml
│ ├── nodejs.yml
│ └── website-publish.yml
├── .gitignore
├── .jest.puppeteer.config.js
├── .nvmrc
├── .prettierrc
├── .releaserc.json
├── LICENSE.md
├── PRIVACY.md
├── README.md
├── background
└── background.js
├── chrome-img
├── JustNotSorry-240.png
├── screenshot-1.png
├── screenshot-2.png
├── screenshot-3.png
└── screenshot-4.png
├── e2e
├── global-setup.js
├── global-teardown.js
├── local.perf.test.js
└── utility.js
├── img
├── JustNotSorry-128.png
├── JustNotSorry-16.png
├── JustNotSorry-19.png
├── JustNotSorry-38.png
└── JustNotSorry-48.png
├── include.lst
├── jest-puppeteer.config.js
├── just-not-sorry.css
├── manifest.json
├── options
├── options.ejs
└── options.js
├── package-lock.json
├── package.json
├── package.sh
├── public
└── jns-test.html
├── site
├── .gitignore
├── .ruby-version
├── CNAME
├── Gemfile
├── Gemfile.lock
├── README.md
├── _config.yml
├── _data
│ └── .keep
├── _includes
│ ├── banner.html
│ ├── extension-description.html
│ ├── head-custom.html
│ └── supported-sites.html
├── _layouts
│ └── default.html
├── assets
│ └── css
│ │ └── style.scss
├── img
│ ├── CNBC.svg
│ ├── FastCompany.svg
│ ├── Forbes.svg
│ ├── Glamour.svg
│ ├── Gmail.svg
│ ├── JustNotSorry-logo.png
│ ├── NPR.svg
│ ├── NewYorkTimes.svg
│ ├── Outlook.svg
│ ├── Slate.svg
│ ├── Today.svg
│ ├── Vogue.svg
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── defmethod-logo.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ └── favicon.ico
├── index.md
├── phrases.md
├── releases.md
└── site.webmanifest
├── spec
├── ContentEditableDivSpec.test.js
├── JustNotSorrySpec.test.js
├── RangeFinderSpec.test.js
├── UtilSpec.test.js
├── WarningHighlightSpec.test.js
├── WarningMessagesSpec.test.js
├── WarningSpec.test.js
└── setupTests.js
├── src
├── callbacks
│ └── ContentEditableDiv.js
├── components
│ ├── JustNotSorry.js
│ ├── Warning.js
│ └── WarningHighlight.js
├── helpers
│ ├── RangeFinder.js
│ └── util.js
├── index.js
└── warnings
│ └── phrases.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": "current"
8 | }
9 | }
10 | ]
11 | ],
12 | "plugins": [
13 | ["@babel/plugin-transform-react-jsx", {
14 | "runtime": "automatic"
15 | }],
16 | ["@babel/plugin-proposal-class-properties", { "loose": true }]
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | dist/
4 | gulpfile.js
5 | webpack.config.js
6 | jest-puppeteer.config.js
7 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:import/errors",
9 | "prettier",
10 | "plugin:react/recommended"
11 | ],
12 | "globals": {
13 | "chrome": true,
14 | "analytics": true,
15 | "window": true,
16 | "document": true,
17 | "global": true,
18 | "page": true,
19 | "browser": true,
20 | "context": true,
21 | "jestPuppeteer": true
22 | },
23 | "parser": "babel-eslint",
24 | "settings": {},
25 | "parserOptions": {
26 | "ecmaFeatures": {
27 | "modules": true,
28 | "ecmaVersion": 2018,
29 | "jsx": true
30 | }
31 | },
32 | "plugins": ["react"],
33 | "rules": {
34 | "import/no-unresolved": "off",
35 | "react/prop-types": 0,
36 | "react/no-unknown-property": [2, { "ignore": ["class"] }]
37 | },
38 | "overrides": [
39 | {
40 | "files": "**/*.test.js",
41 | "env": {
42 | "node": true,
43 | "jest": true,
44 | "es6": true
45 | },
46 | "plugins": ["jest"],
47 | "extends": ["eslint:recommended", "plugin:jest/recommended", "prettier"]
48 | }
49 | ]
50 | }
51 |
--------------------------------------------------------------------------------
/.git_deploy_key.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/.git_deploy_key.enc
--------------------------------------------------------------------------------
/.github/workflows/beta-publish.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Publish to Chrome Web Store
3 |
4 | on:
5 | release:
6 | types: [published]
7 |
8 | jobs:
9 | publish:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Download release package
15 | run: curl -o just-not-sorry-chrome.zip `jq -j '.release.assets[0].url' $GITHUB_EVENT_PATH`
16 | - name: Upload to Chrome Web Store beta testers
17 | if: github.event.release.prerelease
18 | uses: trmcnvn/chrome-addon@v2
19 | with:
20 | extension: fgnoahpabaeffmkacgedecamkmddkebn # beta extension ID
21 | zip: just-not-sorry-chrome.zip
22 | client-id: ${{ secrets.CHROME_CLIENT_ID_DM }}
23 | client-secret: ${{ secrets.CHROME_CLIENT_SECRET_DM }}
24 | refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN_DM }}
25 | publish-target: "trustedTesters"
26 |
27 | # TODO: add publish to production Chrome Web Store
28 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | paths-ignore:
9 | - 'package.json'
10 | - 'manifest.json'
11 | - 'README.md'
12 |
13 | jobs:
14 | build:
15 |
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Use Node.js
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version-file: '.nvmrc'
24 | cache: 'npm'
25 | - name: Install dependencies
26 | run: npm ci
27 | - name: Verify that build works
28 | run: npm run build
29 | - name: Run tests
30 | run: npm test
31 | - name: Run e2e tests
32 | run: |
33 | export DISPLAY=:99
34 | sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
35 | npm run e2e
36 | - name: Semantic Release
37 | env:
38 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
39 | SSL_PASSPHRASE: ${{ secrets.SSL_PASSPHRASE }}
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | run: |
42 | gpg --passphrase=$GPG_PASSPHRASE --batch --output /tmp/git_deploy_key --decrypt .git_deploy_key.enc
43 | chmod 600 /tmp/git_deploy_key
44 | echo 'echo ${SSL_PASSPHRASE}' > /tmp/askpass && chmod +x /tmp/askpass
45 | eval "$(ssh-agent -s)"
46 | DISPLAY=":0.0" SSH_ASKPASS="/tmp/askpass" setsid ssh-add /tmp/git_deploy_key 0) {
12 | await page.waitForSelector('.jns-highlight', options);
13 | }
14 | const numWarnings = await page.$$('.jns-highlight');
15 | //longer phrases can wrap lines and create multiple tooltips
16 | await expect(numWarnings.length).toBeGreaterThanOrEqual(expected);
17 | }
18 |
19 | describe('Just Not Sorry', () => {
20 | beforeEach(async () => {
21 | await page.goto(`file://${TEST_PAGE}`);
22 | await page.waitForTimeout(500);
23 | await page.click('#email');
24 | await assertWarnings(0, { visible: false, timeout: TEST_WAIT_TIME });
25 | });
26 |
27 | it('should work', async () => {
28 | await page.keyboard.type(`just not sorry.`);
29 | await page.keyboard.press('Enter');
30 | await page.keyboard.press('Enter');
31 |
32 | //blur
33 | await page.keyboard.down('Shift');
34 | await page.keyboard.press('Tab');
35 | await page.keyboard.up('Shift');
36 | await assertWarnings(2, { visible: false, timeout: TEST_WAIT_TIME });
37 |
38 | //focus
39 | await page.keyboard.press('Tab');
40 | await assertWarnings(2, { visible: true, timeout: TEST_WAIT_TIME });
41 | });
42 |
43 | it('should display 500 words with 200 warnings', async () => {
44 | const fiftyWords = `Just actually sorry. Apologize. I think I'm no expert. Yes, um, literally, very, sort of, If that's okay, um, I should feel, we believe, in my opinion, This might be a silly idea. This might be a stupid question. I may be wrong. If I'm being honest. I guess. Maybe!!!`;
45 | expect(fiftyWords.split(' ').length).toBe(50);
46 |
47 | for (let i = 0; i < 500 / 50; i++) {
48 | await page.keyboard.type(fiftyWords);
49 | await page.keyboard.press('Enter');
50 | await page.keyboard.press('Enter');
51 |
52 | //blur
53 | await page.keyboard.down('Shift');
54 | await page.keyboard.press('Tab', { delay: 500 });
55 | await page.keyboard.up('Shift');
56 | const numExpected = 20 * (i + 1);
57 | await assertWarnings(numExpected, {
58 | visible: false,
59 | timeout: TEST_WAIT_TIME,
60 | });
61 |
62 | //focus
63 | await page.keyboard.press('Tab', { delay: 500 });
64 | await assertWarnings(numExpected, {
65 | visible: true,
66 | timeout: TEST_WAIT_TIME,
67 | });
68 | }
69 | });
70 |
71 | it('should display 1000 words with 400 warnings with blur', async () => {
72 | const fiftyWords = `Just actually sorry. Apologize. I think I'm no expert. Yes, um, literally, very, sort of, If that's okay, um, I should feel, we believe, in my opinion, This might be a silly idea. This might be a stupid question. I may be wrong. If I'm being honest. I guess. Maybe!!!`;
73 | expect(fiftyWords.split(' ').length).toBe(50);
74 |
75 | for (let i = 0; i < 1000 / 50; i++) {
76 | await page.keyboard.type(fiftyWords);
77 | await page.keyboard.press('Enter');
78 | await page.keyboard.press('Enter');
79 |
80 | //blur
81 | await page.keyboard.down('Shift');
82 | await page.keyboard.press('Tab', { delay: 500 });
83 | await page.keyboard.up('Shift');
84 | const numExpected = 20 * (i + 1);
85 | await assertWarnings(numExpected, {
86 | visible: false,
87 | timeout: TEST_WAIT_TIME,
88 | });
89 |
90 | //focus
91 | await page.keyboard.press('Tab', { delay: 500 });
92 | await assertWarnings(numExpected, {
93 | visible: true,
94 | timeout: TEST_WAIT_TIME,
95 | });
96 | }
97 | });
98 |
99 | it('should display 1000 words with 400 warnings with delay', async () => {
100 | const fiftyWords = `Just actually sorry. Apologize. I think I'm no expert. Yes, um, literally, very, sort of, If that's okay, um, I should feel, we believe, in my opinion, This might be a silly idea. This might be a stupid question. I may be wrong. If I'm being honest. I guess. Maybe!!!`;
101 | expect(fiftyWords.split(' ').length).toBe(50);
102 |
103 | for (let i = 0; i < 1000 / 50; i++) {
104 | await page.keyboard.type(fiftyWords);
105 | await page.keyboard.press('Enter');
106 | await page.keyboard.press('Enter');
107 |
108 | await page.waitForTimeout(500);
109 | await assertWarnings(20 * (i + 1), {
110 | visible: true,
111 | timeout: TEST_WAIT_TIME,
112 | });
113 | }
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/e2e/utility.js:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 |
4 | // eslint-disable-next-line no-undef
5 | const manifestPath = path.join(__dirname, '..', 'build', 'manifest.json');
6 | export function e2eSetup() {
7 | fs.readFile(manifestPath, (err, data) => {
8 | if (err) throw err;
9 | const newValue = data
10 | .toString()
11 | .replace(
12 | 'https://mail.google.com/*',
13 | 'file:///*/just-not-sorry/public/jns-test.html'
14 | );
15 | fs.writeFile(manifestPath, newValue, 'utf-8', function (err) {
16 | if (err) throw err;
17 | });
18 | });
19 | }
20 | export function e2eTeardown() {
21 | fs.readFile(manifestPath, (err, data) => {
22 | if (err) throw err;
23 | const newValue = data
24 | .toString()
25 | .replace(
26 | 'file:///*/just-not-sorry/public/jns-test.html',
27 | 'https://mail.google.com/*'
28 | );
29 | fs.writeFile(manifestPath, newValue, 'utf-8', function (err) {
30 | if (err) throw err;
31 | });
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/img/JustNotSorry-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/img/JustNotSorry-128.png
--------------------------------------------------------------------------------
/img/JustNotSorry-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/img/JustNotSorry-16.png
--------------------------------------------------------------------------------
/img/JustNotSorry-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/img/JustNotSorry-19.png
--------------------------------------------------------------------------------
/img/JustNotSorry-38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/img/JustNotSorry-38.png
--------------------------------------------------------------------------------
/img/JustNotSorry-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/img/JustNotSorry-48.png
--------------------------------------------------------------------------------
/include.lst:
--------------------------------------------------------------------------------
1 | src/*.js
2 | lib/*.js
3 | img/*.png
4 | options/*.*
5 | just-not-sorry.css
6 | manifest.json
7 | README.md
8 | PRIVACY.md
9 |
--------------------------------------------------------------------------------
/jest-puppeteer.config.js:
--------------------------------------------------------------------------------
1 | //This filename is the default path for jest config
2 | module.exports = {
3 | launch: {
4 | headless: false,
5 | slowMo: false,
6 | devtools: false,
7 | args: [
8 | `--disable-extensions-except=build`,
9 | `--load-extension=build`,
10 | `--window-size=800,800`,
11 | ],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/just-not-sorry.css:
--------------------------------------------------------------------------------
1 | .jns-highlight {
2 | border-bottom: 2px dashed #ff9933;
3 | position: absolute;
4 | padding: 0;
5 | }
6 |
7 | .jns-tooltip {
8 | max-width: 380px;
9 | border: 1px solid darkgray;
10 | z-index: 9999;
11 | }
12 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Just Not Sorry -- the Chrome extension",
4 | "short_name": "JustNotSorry",
5 | "author": "Steve Brudz, Manish Kakwani, Tami Reiss, and Eric Tillberg of Def Method",
6 | "version": "2.5.3",
7 | "version_name": "2.5.3",
8 | "description": "A Chrome extension that warns you when you write emails using words which undermine your message",
9 | "icons": {
10 | "16": "img/JustNotSorry-16.png",
11 | "48": "img/JustNotSorry-48.png",
12 | "128": "img/JustNotSorry-128.png"
13 | },
14 | "content_scripts": [
15 | {
16 | "matches": [
17 | "https://mail.google.com/*",
18 | "https://outlook.office.com/*",
19 | "https://outlook.live.com/*",
20 | "https://outlook.office365.com/*"
21 | ],
22 | "css": ["./just-not-sorry.css"],
23 | "js": ["bundle.js"]
24 | }
25 | ],
26 | "background": {
27 | "service_worker": "background.js"
28 | },
29 | "action": {
30 | "default_icon": {
31 | "19": "img/JustNotSorry-19.png",
32 | "38": "img/JustNotSorry-38.png"
33 | },
34 | "default_title": "Just Not Sorry"
35 | },
36 | "options_ui": {
37 | "page": "options.html",
38 | "open_in_tab": false
39 | },
40 | "web_accessible_resources": [],
41 | "permissions": ["notifications"]
42 | }
43 |
--------------------------------------------------------------------------------
/options/options.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Just Not Sorry Options
5 |
43 |
44 |
45 |
46 |
47 | We're Just NOT Sorry! Let's stop qualifying our message and
48 | diminishing our voice. Inspired by the writings of Tara Mohr
49 | and others, this Chrome Extension for Gmail will warn you
50 | when you use words or phrases that undermine your message.
51 | Words will be underlined for correction with additional
52 | information about how using the phrase is perceived.
53 | (Don't worry, the underline won't get sent as part of your
54 | email if you decide to ignore it.)
55 |
56 |
57 | Created by Tami Reiss, Steve Brudz, Manish Kakwani, and Eric Tillberg. Maintained by
58 | Def Method .
59 |
60 |
61 | All code has been open-sourced and is available on
62 | GitHub .
63 |
64 |
65 | We never collect anything personally identifiable and don't collect
66 | anything about the email that you're writing. See our
67 | privacy policy
68 | for more information.
69 |
70 |
71 |
72 | Words and Phrases That Trigger Warnings
73 |
74 |
75 | phrase
76 | message
77 |
78 |
79 |
80 | <% _.forEach(allWarnings, (warning) => { %>
81 |
82 |
83 |
84 | <% _.forEach(warning.displayLabel, (label) => { %>
85 | "<%= label %>"
86 | <% }); %>
87 |
88 |
89 |
90 | <%= warning.message %> source
91 |
92 |
93 | <% }); %>
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/options/options.js:
--------------------------------------------------------------------------------
1 | // javascript that runs on the options page goes here
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "just-not-sorry",
3 | "description": "Chrome extension that warns you when you write emails using words which undermine your message",
4 | "version": "2.5.3",
5 | "author": "Steve Brudz, Manish Kakwani, Tami Reiss, and Eric Tillberg of Def Method",
6 | "license": "MIT",
7 | "repository": "git@github.com:defmethodinc/just-not-sorry.git",
8 | "homepage": "https://github.com/defmethodinc/just-not-sorry",
9 | "bugs": "https://github.com/defmethodinc/just-not-sorry/issues/new",
10 | "scripts": {
11 | "e2e": "npm run build && jest --verbose --config='.jest.puppeteer.config.js' ./e2e",
12 | "test": "jest --verbose ./spec",
13 | "test:watch": "npm run test -- --watch",
14 | "build": "webpack --config webpack.config.js",
15 | "build:watch": "webpack --config webpack.config.js --watch",
16 | "webext:run": "sleep 10 && web-ext run --source-dir ./build/ --start-url gmail.com",
17 | "webext:lint": "web-ext lint -s ./build",
18 | "start:firefox": "concurrently \"npm:build:watch\" \"npm:webext:run\"",
19 | "start:chrome": "concurrently \"npm:build:watch\" \"npm:webext:run -- -t chromium\"",
20 | "format": "prettier --loglevel warn --write \"{src,spec}/*.{js,css}\" \"*.{md,css,json,js}\"",
21 | "lint": "eslint . --cache --fix",
22 | "site:version": "echo \"{\\\"commit\\\": \\\"$(git rev-parse --short HEAD)\\\"}\" > ./site/_data/version.json",
23 | "site:phrases": "cp ./src/warnings/phrases.json ./site/_data",
24 | "predeploy": "npm run site:phrases && npm run site:version",
25 | "deploy": "gh-pages -d site",
26 | "semantic-release": "cross-env GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) semantic-release"
27 | },
28 | "devDependencies": {
29 | "@babel/core": "^7.9.6",
30 | "@babel/plugin-proposal-class-properties": "^7.12.1",
31 | "@babel/plugin-transform-react-jsx": "^7.9.4",
32 | "@babel/preset-env": "^7.9.6",
33 | "@semantic-release/exec": "^6.0.3",
34 | "@semantic-release/git": "^10.0.1",
35 | "@testing-library/jest-dom": "^5.16.5",
36 | "@testing-library/react": "^14.0.0",
37 | "babel": "^6.23.0",
38 | "babel-eslint": "^8.2.6",
39 | "babel-loader": "^8.0.6",
40 | "commitizen": "^4.2.4",
41 | "concurrently": "^5.2.0",
42 | "copy-webpack-plugin": "^9.0.1",
43 | "cz-conventional-changelog": "^3.1.0",
44 | "eslint": "^6.8.0",
45 | "eslint-config-prettier": "^6.11.0",
46 | "eslint-plugin-import": "^2.20.2",
47 | "eslint-plugin-jasmine": "^4.1.1",
48 | "eslint-plugin-jest": "^23.13.1",
49 | "eslint-plugin-react": "^7.20.0",
50 | "gh-pages": "^4.0.0",
51 | "html-webpack-plugin": "^5.3.2",
52 | "husky": "^4.2.5",
53 | "jest": "^29.4.3",
54 | "jest-environment-jsdom": "^29.4.3",
55 | "jest-environment-puppeteer": "^7.0.1",
56 | "jest-puppeteer": "^7.0.1",
57 | "lint-staged": "^10.2.2",
58 | "prettier": "^2.0.5",
59 | "semantic-release": "^19.0.2",
60 | "style-loader": "^3.3.1",
61 | "web-ext": "^7.1.1",
62 | "webpack": "^5.51.1",
63 | "webpack-cli": "^4.8.0",
64 | "webpack-dev-server": "^4.0.0"
65 | },
66 | "dependencies": {
67 | "css-loader": "^6.7.3",
68 | "prop-types": "^15.7.2",
69 | "range-at-index": "^1.0.4",
70 | "react": "^18.2.0",
71 | "react-dom": "^18.2.0",
72 | "react-tooltip": "^5.8.1"
73 | },
74 | "husky": {
75 | "hooks": {
76 | "pre-commit": "npm test && lint-staged",
77 | "pre-push": "npm test",
78 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true"
79 | }
80 | },
81 | "config": {
82 | "commitizen": {
83 | "path": "./node_modules/cz-conventional-changelog"
84 | }
85 | },
86 | "lint-staged": {
87 | "*.js": [
88 | "prettier --write",
89 | "eslint --cache --fix"
90 | ],
91 | "*.{md,css,json}": [
92 | "prettier --write"
93 | ]
94 | },
95 | "jest": {
96 | "testEnvironment": "jsdom",
97 | "setupFilesAfterEnv": [
98 | "/spec/setupTests.js"
99 | ]
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | VERSION_NAME=$1
3 | VERSION=$(echo "$VERSION_NAME" | sed 's/-beta//')
4 | sed -i.old "s/\"version\": \".*\"/\"version\": \"$VERSION_NAME\"/" package.json
5 | sed -i.old -e "s/\"version_name\": \".*\"/\"version_name\": \"$VERSION_NAME\"/" -e "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" manifest.json
6 | npm run build
7 | mkdir -p dist
8 | cd build
9 | zip -r "../dist/just-not-sorry-chrome.zip" . *
10 |
--------------------------------------------------------------------------------
/public/jns-test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
JustNotSorry Test
6 |
14 |
15 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 | .jekyll-cache
4 | _site
5 | # copied from src/warnings
6 | _data/phrases.json
7 | # from git commit sha
8 | _data/version.json
9 |
--------------------------------------------------------------------------------
/site/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.1.1
2 |
--------------------------------------------------------------------------------
/site/CNAME:
--------------------------------------------------------------------------------
1 | justnotsorry.com
--------------------------------------------------------------------------------
/site/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'github-pages', '~> 219', group: :jekyll_plugins
4 | gem 'jekyll-theme-minimal'
5 |
6 | gem "webrick", "~> 1.8"
7 |
--------------------------------------------------------------------------------
/site/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (6.0.6.1)
5 | concurrent-ruby (~> 1.0, >= 1.0.2)
6 | i18n (>= 0.7, < 2)
7 | minitest (~> 5.1)
8 | tzinfo (~> 1.1)
9 | zeitwerk (~> 2.2, >= 2.2.2)
10 | addressable (2.8.1)
11 | public_suffix (>= 2.0.2, < 6.0)
12 | coffee-script (2.4.1)
13 | coffee-script-source
14 | execjs
15 | coffee-script-source (1.11.1)
16 | colorator (1.1.0)
17 | commonmarker (0.17.13)
18 | ruby-enum (~> 0.5)
19 | concurrent-ruby (1.2.0)
20 | dnsruby (1.61.9)
21 | simpleidn (~> 0.1)
22 | em-websocket (0.5.3)
23 | eventmachine (>= 0.12.9)
24 | http_parser.rb (~> 0)
25 | ethon (0.16.0)
26 | ffi (>= 1.15.0)
27 | eventmachine (1.2.7)
28 | execjs (2.8.1)
29 | faraday (2.7.4)
30 | faraday-net_http (>= 2.0, < 3.1)
31 | ruby2_keywords (>= 0.0.4)
32 | faraday-net_http (3.0.2)
33 | ffi (1.15.5)
34 | forwardable-extended (2.6.0)
35 | gemoji (3.0.1)
36 | github-pages (219)
37 | github-pages-health-check (= 1.17.7)
38 | jekyll (= 3.9.0)
39 | jekyll-avatar (= 0.7.0)
40 | jekyll-coffeescript (= 1.1.1)
41 | jekyll-commonmark-ghpages (= 0.1.6)
42 | jekyll-default-layout (= 0.1.4)
43 | jekyll-feed (= 0.15.1)
44 | jekyll-gist (= 1.5.0)
45 | jekyll-github-metadata (= 2.13.0)
46 | jekyll-mentions (= 1.6.0)
47 | jekyll-optional-front-matter (= 0.3.2)
48 | jekyll-paginate (= 1.1.0)
49 | jekyll-readme-index (= 0.3.0)
50 | jekyll-redirect-from (= 0.16.0)
51 | jekyll-relative-links (= 0.6.1)
52 | jekyll-remote-theme (= 0.4.3)
53 | jekyll-sass-converter (= 1.5.2)
54 | jekyll-seo-tag (= 2.7.1)
55 | jekyll-sitemap (= 1.4.0)
56 | jekyll-swiss (= 1.0.0)
57 | jekyll-theme-architect (= 0.2.0)
58 | jekyll-theme-cayman (= 0.2.0)
59 | jekyll-theme-dinky (= 0.2.0)
60 | jekyll-theme-hacker (= 0.2.0)
61 | jekyll-theme-leap-day (= 0.2.0)
62 | jekyll-theme-merlot (= 0.2.0)
63 | jekyll-theme-midnight (= 0.2.0)
64 | jekyll-theme-minimal (= 0.2.0)
65 | jekyll-theme-modernist (= 0.2.0)
66 | jekyll-theme-primer (= 0.6.0)
67 | jekyll-theme-slate (= 0.2.0)
68 | jekyll-theme-tactile (= 0.2.0)
69 | jekyll-theme-time-machine (= 0.2.0)
70 | jekyll-titles-from-headings (= 0.5.3)
71 | jemoji (= 0.12.0)
72 | kramdown (= 2.3.1)
73 | kramdown-parser-gfm (= 1.1.0)
74 | liquid (= 4.0.3)
75 | mercenary (~> 0.3)
76 | minima (= 2.5.1)
77 | nokogiri (>= 1.10.4, < 2.0)
78 | rouge (= 3.26.0)
79 | terminal-table (~> 1.4)
80 | github-pages-health-check (1.17.7)
81 | addressable (~> 2.3)
82 | dnsruby (~> 1.60)
83 | octokit (~> 4.0)
84 | public_suffix (>= 3.0, < 5.0)
85 | typhoeus (~> 1.3)
86 | html-pipeline (2.14.3)
87 | activesupport (>= 2)
88 | nokogiri (>= 1.4)
89 | http_parser.rb (0.8.0)
90 | i18n (0.9.5)
91 | concurrent-ruby (~> 1.0)
92 | jekyll (3.9.0)
93 | addressable (~> 2.4)
94 | colorator (~> 1.0)
95 | em-websocket (~> 0.5)
96 | i18n (~> 0.7)
97 | jekyll-sass-converter (~> 1.0)
98 | jekyll-watch (~> 2.0)
99 | kramdown (>= 1.17, < 3)
100 | liquid (~> 4.0)
101 | mercenary (~> 0.3.3)
102 | pathutil (~> 0.9)
103 | rouge (>= 1.7, < 4)
104 | safe_yaml (~> 1.0)
105 | jekyll-avatar (0.7.0)
106 | jekyll (>= 3.0, < 5.0)
107 | jekyll-coffeescript (1.1.1)
108 | coffee-script (~> 2.2)
109 | coffee-script-source (~> 1.11.1)
110 | jekyll-commonmark (1.3.1)
111 | commonmarker (~> 0.14)
112 | jekyll (>= 3.7, < 5.0)
113 | jekyll-commonmark-ghpages (0.1.6)
114 | commonmarker (~> 0.17.6)
115 | jekyll-commonmark (~> 1.2)
116 | rouge (>= 2.0, < 4.0)
117 | jekyll-default-layout (0.1.4)
118 | jekyll (~> 3.0)
119 | jekyll-feed (0.15.1)
120 | jekyll (>= 3.7, < 5.0)
121 | jekyll-gist (1.5.0)
122 | octokit (~> 4.2)
123 | jekyll-github-metadata (2.13.0)
124 | jekyll (>= 3.4, < 5.0)
125 | octokit (~> 4.0, != 4.4.0)
126 | jekyll-mentions (1.6.0)
127 | html-pipeline (~> 2.3)
128 | jekyll (>= 3.7, < 5.0)
129 | jekyll-optional-front-matter (0.3.2)
130 | jekyll (>= 3.0, < 5.0)
131 | jekyll-paginate (1.1.0)
132 | jekyll-readme-index (0.3.0)
133 | jekyll (>= 3.0, < 5.0)
134 | jekyll-redirect-from (0.16.0)
135 | jekyll (>= 3.3, < 5.0)
136 | jekyll-relative-links (0.6.1)
137 | jekyll (>= 3.3, < 5.0)
138 | jekyll-remote-theme (0.4.3)
139 | addressable (~> 2.0)
140 | jekyll (>= 3.5, < 5.0)
141 | jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
142 | rubyzip (>= 1.3.0, < 3.0)
143 | jekyll-sass-converter (1.5.2)
144 | sass (~> 3.4)
145 | jekyll-seo-tag (2.7.1)
146 | jekyll (>= 3.8, < 5.0)
147 | jekyll-sitemap (1.4.0)
148 | jekyll (>= 3.7, < 5.0)
149 | jekyll-swiss (1.0.0)
150 | jekyll-theme-architect (0.2.0)
151 | jekyll (> 3.5, < 5.0)
152 | jekyll-seo-tag (~> 2.0)
153 | jekyll-theme-cayman (0.2.0)
154 | jekyll (> 3.5, < 5.0)
155 | jekyll-seo-tag (~> 2.0)
156 | jekyll-theme-dinky (0.2.0)
157 | jekyll (> 3.5, < 5.0)
158 | jekyll-seo-tag (~> 2.0)
159 | jekyll-theme-hacker (0.2.0)
160 | jekyll (> 3.5, < 5.0)
161 | jekyll-seo-tag (~> 2.0)
162 | jekyll-theme-leap-day (0.2.0)
163 | jekyll (> 3.5, < 5.0)
164 | jekyll-seo-tag (~> 2.0)
165 | jekyll-theme-merlot (0.2.0)
166 | jekyll (> 3.5, < 5.0)
167 | jekyll-seo-tag (~> 2.0)
168 | jekyll-theme-midnight (0.2.0)
169 | jekyll (> 3.5, < 5.0)
170 | jekyll-seo-tag (~> 2.0)
171 | jekyll-theme-minimal (0.2.0)
172 | jekyll (> 3.5, < 5.0)
173 | jekyll-seo-tag (~> 2.0)
174 | jekyll-theme-modernist (0.2.0)
175 | jekyll (> 3.5, < 5.0)
176 | jekyll-seo-tag (~> 2.0)
177 | jekyll-theme-primer (0.6.0)
178 | jekyll (> 3.5, < 5.0)
179 | jekyll-github-metadata (~> 2.9)
180 | jekyll-seo-tag (~> 2.0)
181 | jekyll-theme-slate (0.2.0)
182 | jekyll (> 3.5, < 5.0)
183 | jekyll-seo-tag (~> 2.0)
184 | jekyll-theme-tactile (0.2.0)
185 | jekyll (> 3.5, < 5.0)
186 | jekyll-seo-tag (~> 2.0)
187 | jekyll-theme-time-machine (0.2.0)
188 | jekyll (> 3.5, < 5.0)
189 | jekyll-seo-tag (~> 2.0)
190 | jekyll-titles-from-headings (0.5.3)
191 | jekyll (>= 3.3, < 5.0)
192 | jekyll-watch (2.2.1)
193 | listen (~> 3.0)
194 | jemoji (0.12.0)
195 | gemoji (~> 3.0)
196 | html-pipeline (~> 2.2)
197 | jekyll (>= 3.0, < 5.0)
198 | kramdown (2.3.1)
199 | rexml
200 | kramdown-parser-gfm (1.1.0)
201 | kramdown (~> 2.0)
202 | liquid (4.0.3)
203 | listen (3.8.0)
204 | rb-fsevent (~> 0.10, >= 0.10.3)
205 | rb-inotify (~> 0.9, >= 0.9.10)
206 | mercenary (0.3.6)
207 | minima (2.5.1)
208 | jekyll (>= 3.5, < 5.0)
209 | jekyll-feed (~> 0.9)
210 | jekyll-seo-tag (~> 2.1)
211 | minitest (5.17.0)
212 | nokogiri (1.14.0-arm64-darwin)
213 | racc (~> 1.4)
214 | nokogiri (1.14.0-x86_64-darwin)
215 | racc (~> 1.4)
216 | nokogiri (1.14.0-x86_64-linux)
217 | racc (~> 1.4)
218 | octokit (4.25.1)
219 | faraday (>= 1, < 3)
220 | sawyer (~> 0.9)
221 | pathutil (0.16.2)
222 | forwardable-extended (~> 2.6)
223 | public_suffix (4.0.7)
224 | racc (1.6.2)
225 | rb-fsevent (0.11.2)
226 | rb-inotify (0.10.1)
227 | ffi (~> 1.0)
228 | rexml (3.2.5)
229 | rouge (3.26.0)
230 | ruby-enum (0.9.0)
231 | i18n
232 | ruby2_keywords (0.0.5)
233 | rubyzip (2.3.2)
234 | safe_yaml (1.0.5)
235 | sass (3.7.4)
236 | sass-listen (~> 4.0.0)
237 | sass-listen (4.0.0)
238 | rb-fsevent (~> 0.9, >= 0.9.4)
239 | rb-inotify (~> 0.9, >= 0.9.7)
240 | sawyer (0.9.2)
241 | addressable (>= 2.3.5)
242 | faraday (>= 0.17.3, < 3)
243 | simpleidn (0.2.1)
244 | unf (~> 0.1.4)
245 | terminal-table (1.8.0)
246 | unicode-display_width (~> 1.1, >= 1.1.1)
247 | thread_safe (0.3.6)
248 | typhoeus (1.4.0)
249 | ethon (>= 0.9.0)
250 | tzinfo (1.2.10)
251 | thread_safe (~> 0.1)
252 | unf (0.1.4)
253 | unf_ext
254 | unf_ext (0.0.8.2)
255 | unicode-display_width (1.8.0)
256 | webrick (1.8.1)
257 | zeitwerk (2.6.6)
258 |
259 | PLATFORMS
260 | arm64-darwin-21
261 | x86_64-darwin-20
262 | x86_64-linux
263 |
264 | DEPENDENCIES
265 | github-pages (~> 219)
266 | jekyll-theme-minimal
267 | webrick (~> 1.8)
268 |
269 | BUNDLED WITH
270 | 2.3.9
271 |
--------------------------------------------------------------------------------
/site/README.md:
--------------------------------------------------------------------------------
1 | # Just Not Sorry web page
2 |
3 | The Just Not Sorry web site is hosted on GitHub Pages at both https://justnotsorry.com and https://defmethodinc.github.io/just-not-sorry/
4 |
5 | It is built using [Jekyll](https://jekyllrb.com/) and a customized version of the [minimal theme](https://github.com/pages-themes/minimal).
6 |
7 | ## Development
8 |
9 | Prerequisites:
10 |
11 | - ruby
12 | - bundler
13 |
14 | To install dependencies:
15 |
16 | ```
17 | bundle install
18 | ```
19 |
20 | Previewing the site locally:
21 |
22 | ```
23 | bundle exec jekyll serve
24 | ```
25 |
26 | To make changes, create a new branch, commit your changes to the `site` directory, and submit a PR against the `main` branch. Once the PR is merged, a GitHub Actions CI build will build the site and push it to the `gh-pages` remote branch, which will trigger a deployment to https://justnotsorry.com.
27 |
28 | Note that the "List of Warning Phrases" page is dynamically generated, so any changes to the warning phrases (`../src/warnings/phrases.json`) on the `main` branch will trigger an update to justnotsorry.com.
29 |
--------------------------------------------------------------------------------
/site/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
2 | logo: img/JustNotSorry-logo.png
3 | title: Just Not Sorry
--------------------------------------------------------------------------------
/site/_data/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/_data/.keep
--------------------------------------------------------------------------------
/site/_includes/banner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Now with support for both {% include supported-sites.html %}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/site/_includes/extension-description.html:
--------------------------------------------------------------------------------
1 | The Chrome Extension for {% include supported-sites.html %} that helps you send more confident emails by warning you when you use words which undermine your message.
--------------------------------------------------------------------------------
/site/_includes/head-custom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% include head-custom-google-analytics.html %}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/site/_includes/supported-sites.html:
--------------------------------------------------------------------------------
1 |
2 | Gmail and
4 | Outlook for web
6 |
--------------------------------------------------------------------------------
/site/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% seo %}
9 |
10 |
13 | {% include head-custom.html %}
14 |
15 |
16 |
17 |
36 |
37 | {% include banner.html %}
38 | {{ content }}
39 |
40 |
41 |
42 | This project is maintained by
43 |
44 |
45 |
46 | Hosted on GitHub Pages / Theme by orderedlist
47 | Site version {{site.data.version.commit}} / {{ site.time | date: '%B %d, %Y' }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/site/assets/css/style.scss:
--------------------------------------------------------------------------------
1 | ---
2 | ---
3 |
4 | @import "{{ site.theme }}";
5 |
6 | a:hover, a:focus {
7 | font-weight: revert;
8 | text-decoration: underline;
9 | }
10 | a.button {
11 | cursor: pointer!important;
12 | border: 0.2em solid #267CB9;
13 | text-decoration: none;
14 | color: white;
15 | background-color: #267CB9;
16 | border-radius: 1em;
17 | padding: 0.5em;
18 | transition: border-color .8s ease 0s, background-color .8s ease 0s;
19 | box-shadow: 0 1px 4px rgba(0,0,0,0.6);
20 | }
21 | a:hover.button, a:focus.button {
22 | font-weight: normal;
23 | background-color: #069;
24 | border-color: #069;
25 | transition: border-color .4s ease 0s, background-color .4s ease 0s;
26 | }
27 | .h-center {
28 | display: flex;
29 | align-items: center;
30 | justify-content: center;
31 | }
32 | table ul {
33 | padding: 0;
34 | margin: 0;
35 | list-style-type: none;
36 | }
37 | .gallery {
38 | width: 100%;
39 | display: flex;
40 | flex-wrap: wrap;
41 | justify-content: space-evenly;
42 | align-items: center;
43 | margin-bottom: 15px;
44 | gap: 12px;
45 | }
46 | .gallery img {
47 | height: 25px;
48 | }
49 | .gallery img.tall {
50 | height: 40px;
51 | }
52 | .wrapper {
53 | width: 920px;
54 | }
55 | section {
56 | width: 560px;
57 | }
58 | footer {
59 | bottom: 0;
60 | }
61 | li.active a {
62 | color: #069;
63 | font-weight: bold;
64 | cursor: default;
65 | }
66 | li.active a:hover, li.active a:focus {
67 | text-decoration: none;
68 | }
69 | @media print, screen and (max-width: 960px) {
70 | section {
71 | width: auto;
72 | }
73 | }
74 | @media print, screen and (max-width: 480px) {
75 | .logo {
76 | width: 50%;
77 | }
78 | }
79 | .video-container {
80 | position: relative;
81 | padding-bottom: 56.25%;
82 | height: 0;
83 | overflow: hidden;
84 | max-width: 100%;
85 | margin-bottom: 20px;
86 | }
87 | .video-container iframe,
88 | .video-container object,
89 | .video-container embed {
90 | position: absolute;
91 | top: 0;
92 | left: 0;
93 | width: 100%;
94 | height: 100%;
95 | }
96 |
97 | $color-banner: gold;
98 | $color-banner-ends: darken($color-banner, 5);
99 |
100 | .banner {
101 | padding: 10px;
102 | background: $color-banner;
103 | position: relative;
104 | box-shadow: 0 3px 1px -1px rgba(0, 0, 0, 0.4);
105 | margin: 0 auto 30px;
106 | top: 12px;
107 | height: 20px;
108 |
109 | &:before, &:after {
110 | content: "";
111 | width: 12px;
112 | bottom: -10px;
113 | position: absolute;
114 | display: block;
115 | border: 20px solid $color-banner-ends;
116 | box-shadow: 0 2px 1px -1px rgba(0, 0, 0, 0.4);
117 | z-index: -2;
118 | }
119 |
120 | &:before {
121 | border-color: $color-banner-ends $color-banner-ends $color-banner-ends transparent;
122 | left: -36px;
123 | border-right-width: 1.05em;
124 | }
125 |
126 | &:after {
127 | border-color: $color-banner-ends transparent $color-banner-ends $color-banner-ends;
128 | right: -36px;
129 | border-left-width: 1.05em;
130 | }
131 | }
132 |
133 | .inner-banner {
134 | &:before, &:after {
135 | content: "";
136 | bottom: -0.75em;
137 | position: absolute;
138 | display: block;
139 | border-style: solid;
140 | border-color: #555 transparent transparent transparent;
141 | z-index: -1;
142 | }
143 |
144 | &:before {
145 | left: 0;
146 | border-width: .8em 0 0 .8em;
147 | }
148 |
149 | &:after {
150 | right: 0;
151 | border-width: .8em .8em 0 0;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/site/img/CNBC.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
11 |
15 |
17 |
21 |
29 |
32 |
36 |
39 |
41 |
43 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/site/img/FastCompany.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
24 |
28 |
29 |
32 |
36 |
37 |
40 |
44 |
45 |
47 |
51 |
52 |
74 |
76 |
77 |
79 | image/svg+xml
80 |
82 |
83 |
84 |
85 |
86 |
91 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/site/img/Forbes.svg:
--------------------------------------------------------------------------------
1 |
2 |
13 |
15 |
16 |
18 | image/svg+xml
19 |
21 |
22 |
23 |
24 |
25 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/site/img/Glamour.svg:
--------------------------------------------------------------------------------
1 | Glamour
2 |
--------------------------------------------------------------------------------
/site/img/Gmail.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
20 |
22 | image/svg+xml
23 |
25 | logo
26 |
27 |
28 |
29 |
49 |
50 | logo
52 | Created with Sketch.
54 |
56 |
59 |
65 |
71 |
77 |
83 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/site/img/JustNotSorry-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/JustNotSorry-logo.png
--------------------------------------------------------------------------------
/site/img/NPR.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
20 |
23 |
27 |
28 |
31 |
35 |
36 |
39 |
43 |
44 |
47 |
51 |
52 |
55 |
59 |
60 |
63 |
68 |
69 |
72 |
76 |
77 |
80 |
84 |
85 |
88 |
92 |
93 |
96 |
100 |
101 |
102 |
124 |
133 |
134 |
136 |
137 |
139 | image/svg+xml
140 |
142 |
143 |
144 |
145 |
146 |
151 |
156 |
161 |
166 |
169 |
174 |
175 |
178 |
183 |
184 |
187 |
192 |
193 |
194 |
195 |
--------------------------------------------------------------------------------
/site/img/NewYorkTimes.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/img/Outlook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ]>
13 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 |
48 |
52 |
55 |
58 |
61 |
63 |
66 |
68 |
70 |
71 |
72 |
73 |
74 |
75 |
78 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/site/img/Slate.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
20 |
42 |
51 |
52 |
54 |
55 |
57 | image/svg+xml
58 |
60 |
61 |
62 |
63 |
64 |
69 |
75 |
80 |
85 |
90 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/site/img/Today.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
22 |
24 | image/svg+xml
25 |
27 |
28 |
29 |
30 |
31 |
33 |
58 |
61 |
64 |
69 |
74 |
79 |
80 |
83 |
88 |
94 |
100 |
105 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/site/img/Vogue.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/site/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/site/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/site/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/site/img/defmethod-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/defmethod-logo.png
--------------------------------------------------------------------------------
/site/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/favicon-16x16.png
--------------------------------------------------------------------------------
/site/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/favicon-32x32.png
--------------------------------------------------------------------------------
/site/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defmethodinc/just-not-sorry/b02a8ced23c3543af237bae6e24aa04eb55586c6/site/img/favicon.ico
--------------------------------------------------------------------------------
/site/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Home
3 | ---
4 |
5 |
6 |
7 |
8 |
9 |
10 | Install Just Not Sorry
11 |
12 |
13 | {% include extension-description.html %}
14 |
15 | Just Not Sorry has been covered in [NYTimes](https://www.nytimes.com/2019/09/27/style/exclamation-points.html), [Glamour](https://www.glamourmagazine.co.uk/article/just-not-sorry-app), [Vogue](https://www.vogue.com/article/just-not-sorry-plugin), The Today Show, [CNBC](https://www.cnbc.com/2019/04/16/saying-im-sorry-can-make-people-think-poorly-of-you-research-heres-what-successful-people-do-instead.html), [Slate](https://slate.com/human-interest/2015/12/new-chrome-app-helps-women-stop-saying-just-and-sorry-in-emails.html), [NPR](https://www.npr.org/2016/01/01/461714341/just-not-sorry-gmail-tackles-qualifying-words-in-professional-communication), [Fast Company](https://www.fastcompany.com/3055071/new-gmail-plug-in-highlights-words-and-phrases-that-undermine-your-messag), [Forbes](https://www.forbes.com/sites/carolinecastrillon/2019/07/14/how-women-can-stop-apologizing-and-take-their-power-back/), and many many more news outlets.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | Inspired by the writings of Tara Mohr and others, Just Not Sorry will warn you when you use words or phrases that undermine your message. Commonly used qualifying words and phrases are underlined for you to choose how you want to address them.
30 |
31 | Created by [Tami Reiss](https://linkedin.com/in/tamireiss), Steve Brudz, Manish Kakwani, and Eric Tillberg of [Def Method](https://www.defmethod.com/). Read the [backstory here](https://medium.com/@tamireiss/just-not-sorry-the-backstory-33f54b30fe48).
32 |
--------------------------------------------------------------------------------
/site/phrases.md:
--------------------------------------------------------------------------------
1 | # List of Warning Phrases
2 |
3 |
4 |
5 |
6 | phrase
7 | message
8 |
9 |
10 |
11 | {% for phrase in site.data.phrases %}
12 |
13 |
14 |
15 | {% for label in phrase.displayLabel %}
16 | "{{label}}"
17 | {% endfor %}
18 |
19 |
20 |
21 | {{phrase.message}} source
22 |
23 |
24 | {% endfor %}
25 |
26 |
27 |
--------------------------------------------------------------------------------
/site/releases.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | ### v2.4.0 - Nov 22, 2022
4 |
5 | - Upgraded to [v3 of the Chrome Web Extension Manifest](https://developer.chrome.com/docs/extensions/mv3/intro/) to stay current with Chrome Web Store requirements
6 |
7 | ### v2.3.0 - Mar 31, 2022
8 |
9 | - Added "if that makes sense" as a warning
10 |
11 | ### v2.2.0 - Mar 31, 2022
12 |
13 | - Added support for Outlook for web using the [https://outlook.office365.com](https://outlook.office365.com) url
14 |
15 | ### v2.1.2 - Jan 13, 2022
16 |
17 | - Added support for Outlook for web - [https://outlook.live.com/](https://outlook.live.com/) and [https://outlook.office.com/](https://outlook.office.com/)
18 | - Major rewrite of code for better maintainability
19 | - Added display of all warning phrases to the options UI, along with links to the source articles. Click the JustNotSorry extension button in your toolbar to see them.
20 | - Improved message box when you hover over an underline
21 |
22 | ### v1.6.3 - Apr 27, 2020
23 |
24 | - Performance enhancements for longer emails
25 |
26 | ### v1.6.2 - Apr 20, 2020
27 |
28 | - Fixed issue with JustNotSorry breaking Send button in Gmail.
29 |
30 | ### v1.6.1 - Apr 15, 2020
31 |
32 | - Fixed issue with positioning of underlines
33 | - Removed Google Inbox support because Inbox has been retired
34 |
35 | ### v1.5.1 - Feb 13, 2017
36 |
37 | - Quick fix for crashing tab when used with certain other extensions.
38 |
39 | ### v1.5.0 - Feb 6, 2017
40 |
41 | - This release adds Inbox support (in addition to the existing Gmail support) and fixes a couple of small UX bugs.
42 |
43 | ### v1.0.0 - Apr 3, 2016
44 |
45 | - Additional warning phrases
46 | - Improved highlighting
47 |
48 | ### v0.2.6 - Jan 18, 2016
49 |
50 | - This release fixes issues with the cursor jumping to the previous paragraph when all the characters in a paragraph are deleted.
51 |
52 | ### v0.2.1 - Dec 18, 2015
53 |
54 | - The v0.2.0 package was missing options.html so the extension would not install. This release fixes that problem.
55 |
56 | ### v0.2.0 - Dec 18, 2015
57 |
58 | - Capture data about who is installing JustNotSorry via Google Analytics.
59 |
60 | ### v0.1.3 - Dec 18, 2015
61 |
62 | - Fixed issue where Gmail would crash when both JustNotSorry and SalesforceIQ Chrome Extensions were installed.
63 |
64 | ### v0.1.2 - Dec 16, 2015
65 |
66 | - The first public release that was published to the Chrome Web Store.
67 |
--------------------------------------------------------------------------------
/site/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"Just Not Sorry","short_name":"Just Not Sorry","icons":[{"src":"/img/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/img/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/spec/ContentEditableDivSpec.test.js:
--------------------------------------------------------------------------------
1 | import { forEachUniqueContentEditable } from '../src/callbacks/ContentEditableDiv.js';
2 |
3 | const buildMutation = (type, target) => {
4 | return { type, target };
5 | };
6 |
7 | describe('handleContentEditableDivChange', () => {
8 | let mockCallback, handler;
9 | beforeEach(() => {
10 | mockCallback = jest.fn((mutation) => mutation);
11 | handler = forEachUniqueContentEditable(mockCallback);
12 | });
13 | describe('when target is node with contentEditable attribute', () => {
14 | it('should not call the action if mutation is not type childList', () => {
15 | const div = document.createElement('div');
16 | div.setAttribute('contentEditable', 'true');
17 |
18 | const mutation = buildMutation('characterData', div);
19 | handler([mutation]);
20 |
21 | expect(mockCallback.mock.calls.length).toBe(0);
22 | });
23 |
24 | it('should not call the action callback more than once for each unique mutation of type childList', () => {
25 | const div = document.createElement('div');
26 | div.setAttribute('contentEditable', 'true');
27 | div.setAttribute('id', 'uniqueID');
28 |
29 | const mutation = buildMutation('childList', div);
30 | handler([mutation]);
31 | handler([mutation]);
32 | expect(mockCallback.mock.calls.length).toBe(1);
33 | });
34 |
35 | it('should call the action callback once for each unique mutation of type childList', () => {
36 | const div = document.createElement('div');
37 | div.setAttribute('contentEditable', 'true');
38 |
39 | const mutation = buildMutation('childList', div);
40 | handler([mutation]);
41 |
42 | expect(mockCallback.mock.calls.length).toBe(1);
43 | expect(mockCallback.mock.calls[0][0]).toBe(mutation);
44 | });
45 | });
46 |
47 | describe('when target is node without contentEditable attribute', () => {
48 | it('should not call the action callback for each mutation of type childList', () => {
49 | const div = document.createElement('div');
50 |
51 | const mutation = buildMutation('childList', div);
52 | handler([mutation]);
53 |
54 | expect(mockCallback.mock.calls.length).toBe(0);
55 | });
56 |
57 | it('should not call the action if mutation is not type childList', () => {
58 | const div = document.createElement('div');
59 |
60 | const mutation = buildMutation('characterData', div);
61 | handler([mutation]);
62 |
63 | expect(mockCallback.mock.calls.length).toBe(0);
64 | });
65 | });
66 | });
67 |
--------------------------------------------------------------------------------
/spec/JustNotSorrySpec.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import JustNotSorry from '../src/components/JustNotSorry.js';
3 | import { fireEvent, render, screen, waitFor } from '@testing-library/react';
4 | import { act } from 'react-dom/test-utils';
5 |
6 | jest.useFakeTimers();
7 |
8 | document.createRange = jest.fn(() => ({
9 | setStart: jest.fn(),
10 | setEnd: jest.fn(),
11 | commonAncestorContainer: {
12 | nodeName: 'BODY',
13 | ownerDocument: document,
14 | },
15 | startContainer:
16 | "The word 'very' does not communicate enough information. Find a stronger, more meaningful adverb, or omit it completely. --Andrea Ayres",
17 | getClientRects: jest.fn(() => [{}]),
18 | }));
19 |
20 | const emailContaining = (text) => {
21 | const email = document.createElement('div');
22 | email.setAttribute('id', 'test');
23 | email.setAttribute('contentEditable', 'true');
24 | email.append(text ?? 'some text');
25 | document.body.appendChild(email);
26 | return email;
27 | };
28 |
29 | describe('JustNotSorry', () => {
30 | let mutationObserverMock;
31 |
32 | const simulateEvent = (node, event) => {
33 | act(() => {
34 | expect(mutationObserverMock.mock.instances.length).toBe(1);
35 | const documentObserver = mutationObserverMock.mock.instances[0];
36 | documentObserver.trigger([{ type: 'childList', target: node }]);
37 | expect(fireEvent[event](node)).toBe(true);
38 | jest.runOnlyPendingTimers();
39 | });
40 | };
41 |
42 | beforeEach(() => {
43 | mutationObserverMock = jest.fn(function MutationObserver(callback) {
44 | this.observe = jest.fn();
45 | this.disconnect = jest.fn();
46 | this.trigger = (mockedMutationList) => {
47 | callback(mockedMutationList, this);
48 | };
49 | });
50 | global.MutationObserver = mutationObserverMock;
51 | });
52 |
53 | it('listens for structural changes to the content editable div in document body', () => {
54 | render( );
55 |
56 | const observerInstances = mutationObserverMock.mock.instances;
57 | expect(observerInstances.length).toBe(1);
58 | expect(observerInstances[0].observe).toHaveBeenCalledWith(document.body, {
59 | childList: true,
60 | subtree: true,
61 | });
62 | });
63 |
64 | it('on event does nothing when given an empty string', async () => {
65 | const email = emailContaining('');
66 | render(
67 |
76 | );
77 | simulateEvent(email, 'focus');
78 | await waitFor(() => {
79 | expect(screen.queryAllByTestId('jns-warning').length).toEqual(0);
80 | });
81 | });
82 |
83 | it('on event checks for warnings', async () => {
84 | const email = emailContaining('just not');
85 | render(
86 |
95 | );
96 | simulateEvent(email, 'focus');
97 |
98 | await waitFor(() => {
99 | expect(screen.getAllByTestId('jns-warning').length).toEqual(1);
100 | });
101 | });
102 |
103 | it('should clear warnings on blur event', async () => {
104 | const div = emailContaining('just not');
105 |
106 | render(
107 |
116 | );
117 | simulateEvent(div, 'blur');
118 |
119 | await waitFor(() => {
120 | expect(screen.queryAllByTestId('jns-warning').length).toEqual(0);
121 | });
122 | });
123 |
124 | it('does not add warnings for partial matches', async () => {
125 | const email = emailContaining('test justify test');
126 |
127 | render(
128 |
137 | );
138 | simulateEvent(email, 'focus');
139 |
140 | await waitFor(() => {
141 | expect(screen.queryAllByTestId('jns-warning').length).toEqual(0);
142 | });
143 | });
144 |
145 | it('catches the warnings when email contains div with phrase', async () => {
146 | const div = emailContaining(`just not
147 | just not
`);
148 |
149 | render(
150 |
159 | );
160 | simulateEvent(div, 'focus');
161 |
162 | await waitFor(() => {
163 | expect(screen.getAllByTestId('jns-warning').length).toEqual(2);
164 | });
165 | });
166 | });
167 |
--------------------------------------------------------------------------------
/spec/RangeFinderSpec.test.js:
--------------------------------------------------------------------------------
1 | import { calculateWarnings } from '../src/helpers/RangeFinder';
2 |
3 | describe('RangeFinder', () => {
4 | const PHRASE_TO_FIND = {
5 | regex: new RegExp('textToFind', 'ig'),
6 | message: 'text found!',
7 | };
8 | let element;
9 | beforeEach(() => {
10 | element = document.createElement('div');
11 | });
12 | it('should return empty array if given empty array', () => {
13 | const ranges = calculateWarnings(element, []);
14 | expect(ranges.length).toBe(0);
15 | });
16 |
17 | it('should return array with message and valid range for each match found', () => {
18 | element.textContent = 'textToFind';
19 |
20 | const ranges = calculateWarnings(element, [PHRASE_TO_FIND]);
21 |
22 | expect(ranges.length).toBe(1);
23 | expect(ranges).toEqual([
24 | { message: 'text found!', rangeToHighlight: new Range() },
25 | ]);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/spec/UtilSpec.test.js:
--------------------------------------------------------------------------------
1 | import * as Util from '../src/helpers/util.js';
2 |
3 | describe('Util', () => {
4 | describe('#debounce', () => {
5 | let func;
6 | let debouncedFunc;
7 |
8 | beforeEach(() => {
9 | jest.useFakeTimers();
10 | func = jest.fn();
11 | debouncedFunc = Util.debounce(func, Util.WAIT_TIME);
12 | });
13 |
14 | it('should execute a function only once if the debounced function is invoked several times within the wait time', () => {
15 | for (let i = 0; i < 3; i++) {
16 | debouncedFunc();
17 | }
18 | jest.runAllTimers();
19 | expect(func).toBeCalledTimes(1);
20 | });
21 |
22 | it('should execute a function as many times as the debounced function if called beyond the wait time', () => {
23 | for (let i = 0; i < 3; i++) {
24 | setTimeout(() => debouncedFunc(), Util.WAIT_TIME + 100);
25 | jest.advanceTimersByTime(Util.WAIT_TIME + 100);
26 | expect(func).toBeCalledTimes(i);
27 | }
28 | jest.runAllTimers();
29 | expect(func).toBeCalledTimes(3);
30 | });
31 | });
32 |
33 | describe('match', () => {
34 | it('should find a match if it exists', () => {
35 | const div = document.createElement('div');
36 | div.textContent = 'just checking';
37 |
38 | const ranges = Util.match(div, /just/gi);
39 | expect(ranges.length).toEqual(1);
40 | });
41 |
42 | it('should find multiple a match if it exists', () => {
43 | const div = document.createElement('div');
44 | div.textContent = 'just checking just';
45 |
46 | const ranges = Util.match(div, /just/gi);
47 | expect(ranges.length).toEqual(2);
48 | });
49 |
50 | it('should not find a match if it doesnt exist', () => {
51 | const div = document.createElement('div');
52 | div.textContent = 'just checking';
53 |
54 | const ranges = Util.match(div, /bogus/gi);
55 | expect(ranges.length).toEqual(0);
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/spec/WarningHighlightSpec.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import WarningHighlight from '../src/components/WarningHighlight.js';
3 | import { render, waitFor, screen } from '@testing-library/react';
4 |
5 | describe(' ', () => {
6 | const testProps = {
7 | style: {
8 | left: '10px',
9 | height: '3px',
10 | width: '25px',
11 | },
12 | message: 'test-message',
13 | };
14 |
15 | beforeEach(() => {
16 | render(
17 |
30 | );
31 | });
32 |
33 | it('should return a highlight div', async () => {
34 | await waitFor(() => {
35 | const jnsHighlights = screen.getAllByTestId('jns-highlight');
36 | expect(jnsHighlights.length).toBe(1);
37 | expect(jnsHighlights[0].tagName).toEqual('DIV');
38 | });
39 | });
40 |
41 | it('should have the correct data and style attributes', async () => {
42 | await waitFor(() => {
43 | const jnsHighlights = screen.getAllByTestId('jns-highlight');
44 | expect(jnsHighlights.length).toBe(1);
45 | const jnsHighlight = jnsHighlights[0];
46 | expect(jnsHighlight.tagName).toEqual('DIV');
47 | expect(jnsHighlight.dataset.tooltipContent).toEqual('test-message');
48 | expect(jnsHighlight).toHaveStyle('left: 10px; height: 3px; width: 25px;');
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/spec/WarningMessagesSpec.test.js:
--------------------------------------------------------------------------------
1 | import WARNING_MESSAGES from '../src/warnings/phrases.json';
2 |
3 | describe('WARNING_MESSAGES', () => {
4 | function isBlank(str) {
5 | return !str || /^\s*$/.test(str);
6 | }
7 |
8 | // from http://stackoverflow.com/a/8317014
9 | function isValidUrl(str) {
10 | return (
11 | str &&
12 | /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
13 | str
14 | )
15 | );
16 | }
17 |
18 | // from http://stackoverflow.com/a/14313213
19 | function findNonASCIIChars(str) {
20 | // eslint-disable-next-line no-control-regex
21 | return str.match(/[^\x00-\x7F]+/g);
22 | }
23 |
24 | function findIndexOfFirstNonASCIIChar(str) {
25 | // eslint-disable-next-line no-control-regex
26 | return str.search(/[^\x00-\x7F]+/);
27 | }
28 |
29 | it('contains an array of warnings', () => {
30 | expect(WARNING_MESSAGES instanceof Array).toBeTruthy();
31 | });
32 |
33 | WARNING_MESSAGES.forEach(function (warning, index) {
34 | describe(
35 | 'for warning at index ' + index + ' (pattern: "' + warning.pattern + '")',
36 | () => {
37 | describe('the pattern', () => {
38 | it('should be present', () => {
39 | expect(
40 | Object.prototype.hasOwnProperty.call(warning, 'pattern')
41 | ).toBeTruthy();
42 | });
43 |
44 | it('should be non-blank', () => {
45 | expect(isBlank(warning.pattern)).toBeFalsy();
46 | });
47 | });
48 |
49 | describe('the displayLabel', () => {
50 | it('should be present', () => {
51 | expect(isBlank(warning.displayLabel)).toBeFalsy();
52 | });
53 | it('should be an array', () => {
54 | expect(Array.isArray(warning.displayLabel)).toBeTruthy();
55 | });
56 | });
57 | describe('the source', () => {
58 | it('should be present', () => {
59 | expect(
60 | Object.prototype.hasOwnProperty.call(warning, 'source')
61 | ).toBeTruthy();
62 | });
63 |
64 | it('should be non-blank', () => {
65 | expect(isBlank(warning.source)).toBeFalsy();
66 | });
67 |
68 | it('should be a valid url', () => {
69 | expect(isValidUrl(warning.source)).toBeTruthy();
70 | });
71 | });
72 |
73 | describe('the message', () => {
74 | it('should be present', () => {
75 | expect(
76 | Object.prototype.hasOwnProperty.call(warning, 'message')
77 | ).toBeTruthy();
78 | });
79 |
80 | it('should be non-blank', () => {
81 | expect(isBlank(warning.message)).toBeFalsy();
82 | });
83 |
84 | it('should contain only ASCII characters', () => {
85 | expect(findNonASCIIChars(warning.message)).toEqual(null);
86 | expect(findIndexOfFirstNonASCIIChar(warning.message)).toEqual(-1);
87 | });
88 | });
89 | }
90 | );
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/spec/WarningSpec.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, waitFor, screen } from '@testing-library/react';
3 | import Warning from '../src/components/Warning.js';
4 |
5 | describe(' ', () => {
6 | it('should not return a warning div', async () => {
7 | render( );
8 | await waitFor(() => {
9 | const jnsWarnings = screen.queryAllByTestId('jns-warning');
10 | expect(jnsWarnings.length).toBe(0);
11 | });
12 | });
13 |
14 | it('should return a warning div', async () => {
15 | const rangeToHighlight = {
16 | setStart: jest.fn(),
17 | setEnd: jest.fn(),
18 | commonAncestorContainer: {
19 | nodeName: 'BODY',
20 | ownerDocument: document,
21 | },
22 | getClientRects: jest.fn(() => []),
23 | };
24 |
25 | render(
26 |
31 | );
32 | await waitFor(() => {
33 | const jnsWarnings = screen.getAllByTestId('jns-warning');
34 | expect(jnsWarnings.length).toBe(1);
35 | expect(jnsWarnings[0].tagName).toEqual('DIV');
36 | });
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/spec/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
3 | // Work-around for jsdom not supporting offsetParent
4 | // https://github.com/jsdom/jsdom/issues/1261
5 | Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
6 | get() {
7 | return this.parentNode;
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/callbacks/ContentEditableDiv.js:
--------------------------------------------------------------------------------
1 | const isEmailMessageBody = ({ target, type }) =>
2 | target.hasAttribute('contentEditable') && type === 'childList';
3 |
4 | export const forEachUniqueContentEditable = (action) => {
5 | const uniqueIds = new Set();
6 | let tempId = 0;
7 | return (mutations) => {
8 | mutations.forEach((mutation) => {
9 | if (isEmailMessageBody(mutation) && !uniqueIds.has(mutation.target.id)) {
10 | const id = mutation.target.id || tempId++;
11 | uniqueIds.add(id);
12 | action(mutation);
13 | }
14 | });
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/JustNotSorry.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Warning from './Warning.js';
4 | import * as Util from '../helpers/util.js';
5 | import { forEachUniqueContentEditable } from '../callbacks/ContentEditableDiv';
6 | import { calculateWarnings } from '../helpers/RangeFinder';
7 |
8 | const JustNotSorry = ({ onEvents, phrases }) => {
9 | const email = useRef(null);
10 | const [observer, setObserver] = useState(null);
11 | const [warnings, setWarnings] = useState([]);
12 |
13 | const hideWarnings = () => setWarnings([]);
14 |
15 | const showWarnings = (target) => {
16 | email.current = target;
17 | setWarnings(calculateWarnings(target, phrases));
18 | };
19 |
20 | const applyEventListeners = ({ target }) => {
21 | const searchHandler = Util.debounce(
22 | () => showWarnings(target),
23 | Util.WAIT_TIME
24 | );
25 | onEvents.map((onEvent) => target.addEventListener(onEvent, searchHandler));
26 | target.addEventListener('blur', hideWarnings);
27 | };
28 |
29 | useEffect(() => {
30 | if (observer) return;
31 |
32 | const callback = forEachUniqueContentEditable(applyEventListeners);
33 | const obs = new MutationObserver(callback);
34 | obs.observe(document.body, {
35 | subtree: true,
36 | childList: true,
37 | });
38 | setObserver(obs);
39 | return () => {
40 | if (observer) {
41 | observer.disconnect();
42 | }
43 | };
44 | }, [
45 | forEachUniqueContentEditable,
46 | applyEventListeners,
47 | setObserver,
48 | observer,
49 | ]);
50 |
51 | const isEmailOpen = () => email.current && email.current.offsetParent;
52 |
53 | if (isEmailOpen() && warnings.length > 0) {
54 | const currentEmail = email.current;
55 | const parentRect = currentEmail.offsetParent.getBoundingClientRect();
56 | const warningComponents = warnings.map((warning, i) => {
57 | let key = i;
58 | if (warning?.startContainer?.parentElement) {
59 | const { offsetTop, offsetLeft } = warning.startContainer.parentElement;
60 | key = `${offsetTop + warning.startOffset}x${
61 | offsetLeft + warning.endOffset
62 | }`;
63 | }
64 | return (
65 |
72 | );
73 | });
74 | return ReactDOM.createPortal(warningComponents, currentEmail.offsetParent);
75 | }
76 | };
77 |
78 | JustNotSorry.defaultProps = {
79 | onEvents: ['input', 'focus', 'cut'],
80 | phrases: [],
81 | };
82 | export default JustNotSorry;
83 |
--------------------------------------------------------------------------------
/src/components/Warning.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import WarningHighlight from './WarningHighlight.js';
3 |
4 | export default function Warning(props) {
5 | if (!props.textArea) {
6 | return;
7 | }
8 | const clientRects = props.range.getClientRects();
9 | const highlights = [];
10 | for (let i = 0; i < clientRects.length; i++) {
11 | const number = props.number + i * 10;
12 | highlights.push(
13 |
20 | );
21 | }
22 | return {highlights}
;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/WarningHighlight.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Tooltip } from 'react-tooltip';
3 |
4 | const YPOS_ADJUSTMENT = 3;
5 | function calculatePosition(container, bounds) {
6 | return {
7 | top: `${
8 | bounds.top + bounds.height - container.top - YPOS_ADJUSTMENT
9 | }px`,
10 | left: `${bounds.left - container.left}px`,
11 | width: `${bounds.width}px`,
12 | height: `${bounds.height * 0.2}px`,
13 | };
14 | }
15 |
16 | export default function Highlight({ number, message, container, bounds }) {
17 | return (
18 |
25 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/helpers/RangeFinder.js:
--------------------------------------------------------------------------------
1 | import * as Util from './util';
2 |
3 | export function calculateWarnings(target, patternsToFind) {
4 | const ranges = [];
5 | let nextNode;
6 | const textNodeIterator = document.createNodeIterator(
7 | target,
8 | NodeFilter.SHOW_TEXT
9 | );
10 | while ((nextNode = textNodeIterator.nextNode()) !== null) {
11 | ranges.push(
12 | ...patternsToFind.flatMap((pattern) => {
13 | return Util.match(nextNode, pattern.regex).map((range) => ({
14 | message: pattern.message,
15 | rangeToHighlight: range,
16 | }));
17 | })
18 | );
19 | }
20 | return ranges;
21 | }
22 |
--------------------------------------------------------------------------------
/src/helpers/util.js:
--------------------------------------------------------------------------------
1 | // from underscore.js
2 | // Returns a function, that, as long as it continues to be invoked, will not
3 | // be triggered. The function will be called after it stops being called for
4 | // N milliseconds. If `immediate` is passed, trigger the function on the
5 | // leading edge, instead of the trailing.
6 | import RangeAtIndex from 'range-at-index';
7 |
8 | export function debounce(func, wait, immediate) {
9 | var timeout;
10 | return function () {
11 | var context = this,
12 | args = arguments;
13 | var later = function () {
14 | timeout = null;
15 | if (!immediate) func.apply(context, args);
16 | };
17 | var callNow = immediate && !timeout;
18 | clearTimeout(timeout);
19 | timeout = setTimeout(later, wait);
20 | if (callNow) func.apply(context, args);
21 | };
22 | }
23 |
24 | // from dom-regexp-match
25 | export function match(el, regexp) {
26 | const ranges = [];
27 | const text = el.textContent;
28 | let m;
29 | while ((m = regexp.exec(text)) !== null) {
30 | const offset = m.index + m[0].length;
31 | ranges.push(new RangeAtIndex(el, m.index, offset));
32 | // if the RegExp doesn't have the "global" flag then bail,
33 | // to avoid an infinite loop
34 | if (!regexp.global) break;
35 | }
36 | return ranges;
37 | }
38 |
39 | export const WAIT_TIME = 500;
40 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as ReactDOM from 'react-dom/client';
3 | import 'react-tooltip/dist/react-tooltip.css';
4 | import JustNotSorry from './components/JustNotSorry';
5 | import PHRASES from './warnings/phrases.json';
6 |
7 | const MESSAGE_PATTERNS = PHRASES.map((phrase) => ({
8 | regex: new RegExp(phrase.pattern, 'gi'),
9 | message: phrase.message,
10 | }));
11 |
12 | ReactDOM.hydrateRoot(
13 | document.body,
14 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/warnings/phrases.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "displayLabel": ["Just"],
4 | "pattern": "\\b(just)\\b",
5 | "source": "http://www.taramohr.com/8-ways-women-undermine-themselves-with-their-words/",
6 | "message": "\"Just\" demeans what you have to say. \"Just\" shrinks your power. It's time to say goodbye to the justs. --Tara Sophia Mohr"
7 | },
8 | {
9 | "displayLabel": ["Actually"],
10 | "pattern": "\\b(actually)\\b",
11 | "source": "http://www.taramohr.com/8-ways-women-undermine-themselves-with-their-words/",
12 | "message": "\"Actually\" communicates a sense of surprise that you have something to say. Of course you want to add something. Of course you have questions. There's nothing surprising about it. --Tara Sophia Mohr"
13 | },
14 | {
15 | "displayLabel": ["Sorry"],
16 | "pattern": "\\b(sorry)\\b",
17 | "source": "http://www.fastcompany.com/3032112/strong-female-lead/sorry-not-sorry-why-women-need-to-stop-apologizing-for-everything",
18 | "message": "Using \"sorry\" frequently undermines your gravitas and makes you appear unfit for leadership. --Sylvia Ann Hewlett"
19 | },
20 | {
21 | "displayLabel": ["Apologize", "Apologies", "Forgive"],
22 | "pattern": "\\b(apologize|apologies|forgive)\\b",
23 | "source": "http://www.fastcompany.com/3032112/strong-female-lead/sorry-not-sorry-why-women-need-to-stop-apologizing-for-everything",
24 | "message": "Apologizing unnecessarily puts you in a subservient position and makes people lose respect for you --Bonnie Marcus"
25 | },
26 | {
27 | "displayLabel": ["I think", "We think"],
28 | "pattern": "\\b(I think|We think)\\b",
29 | "source": "http://www.fastcompany.com/3049609/the-future-of-work/4-types-of-useless-phrases-you-need-to-eliminate-from-your-emails",
30 | "message": "\"I think\" undermines your idea and displays an overall lack of self-confidence. --Lydia Dishman"
31 | },
32 | {
33 | "displayLabel": [
34 | "I'm no expert",
35 | "We're no expert",
36 | "We're no experts",
37 | "We're not experts"
38 | ],
39 | "pattern": "\\b(I'm no expert|We're no expert|We're no experts|We're not experts)\\b",
40 | "source": "http://www.fastcompany.com/3049609/the-future-of-work/4-types-of-useless-phrases-you-need-to-eliminate-from-your-emails",
41 | "message": "\"I'm no expert\" undermines your idea and displays an overall lack of self-confidence. --Lydia Dishman"
42 | },
43 | {
44 | "displayLabel": ["Yes, but"],
45 | "pattern": "\\b(Yes, but)\\b",
46 | "source": "http://www.strategicserendipityforlife.com/documents/Articles/Communication_8TipsForFearlessCommunicationInTheWorkplace.pdf",
47 | "message": "The \"Yes, but\" syndrome is entirely counterproductive, particularly in a work setting. You will become an integral part of any team if you are willing to build ideas rather than discard them. --Victoria Simon, Ph.D. and Holly Pedersen, Ph.D."
48 | },
49 | {
50 | "displayLabel": ["Literally"],
51 | "pattern": "\\b(literally)\\b",
52 | "source": "https://expresswriters.com/50-weak-words-and-phrases-to-cut-out-of-your-blogging/",
53 | "message": "If something is literal, your readers should know it without you needing to use this word to clarify it. More often than not, the word \"literally\" makes writing sound flabby and juvenile, which is probably not what you're going for. --Julia McCoy"
54 | },
55 | {
56 | "displayLabel": ["Very"],
57 | "pattern": "\\b(very)\\b",
58 | "source": "http://blog.crew.co/5-weak-words-to-avoid/",
59 | "message": "The word 'very' does not communicate enough information. Find a stronger, more meaningful adverb, or omit it completely. --Andrea Ayres"
60 | },
61 | {
62 | "displayLabel": ["Kind of", "Sort of"],
63 | "pattern": "\\b(kind of|sort of)\\b",
64 | "source": "http://www.strategicserendipityforlife.com/documents/Articles/Communication_8TipsForFearlessCommunicationInTheWorkplace.pdf",
65 | "message": "This qualifier weakens the message as well as the authority of the writer. --Victoria Simon, Ph.D. and Holly Pedersen, Ph.D."
66 | },
67 | {
68 | "displayLabel": [
69 | "Does that make sense",
70 | "Did that make sense",
71 | "Does this make sense",
72 | "Did this make sense",
73 | "If that makes sense",
74 | "If that's okay"
75 | ],
76 | "pattern": "(does that make sense|did that make sense|does this make sense|did this make sense|if that makes sense|if that's (okay|ok))",
77 | "source": "https://goop.com/how-women-undermine-themselves-with-words/",
78 | "message": "\"does that make sense\" comes across either as condescending (like your audience can't understand) or it implies you feel you've been incoherent. A better way to close is something like \"I look forward to hearing your thoughts.\" You can leave it up to the other party to let you know if they are confused about something, rather than implying that you \"didn't make sense.\" --Tara Sophia Mohr"
79 | },
80 | {
81 | "displayLabel": ["Try", "Trying", "Tried"],
82 | "pattern": "\\b(try|trying|tried)\\b",
83 | "source": "http://www.lifehack.org/articles/communication/7-things-not-to-say-and-7-things-to-start-saying.html",
84 | "message": "\"Do or do not. There is no try.\" --Yoda"
85 | },
86 | {
87 | "displayLabel": ["I should"],
88 | "pattern": "\\b(I should)\\b",
89 | "source": "http://www.lifehack.org/articles/communication/7-things-not-to-say-and-7-things-to-start-saying.html",
90 | "message": "The word \"should\" is inherently negative. \"Should\" implies a lose: lose situation and it's just not conducive to positive outcomes in life. It's a form of criticism, and it's best left out of your everyday language. Instead of beating yourself up for what you should have done, focus on what you have the power to change. -- Zoe B"
91 | },
92 | {
93 | "displayLabel": ["I feel"],
94 | "pattern": "\\b(I feel)\\b",
95 | "source": "http://www.freelancewriting.com/articles/ten-words-to-avoid-when-writing.php",
96 | "message": "If you write an opinion, the reader understands that you also believe it is right. --David Bowman"
97 | },
98 | {
99 | "displayLabel": ["I believe", "We believe", "We feel"],
100 | "pattern": "\\b(I believe|we believe|we feel)\\b",
101 | "source": "https://hbr.org/2011/12/replace-meaningless-words-with",
102 | "message": "Phrases containing \"we believe,\" \"we think,\" and \"we feel\" pervade presentation narratives to such a degree that they spill over into sentences where caution is unnecessary...the spillage weakens what should otherwise be assertive language. --Jerry Weissman"
103 | },
104 | {
105 | "displayLabel": ["I'm just saying"],
106 | "pattern": "\\b(I'm just saying)\\b",
107 | "source": "http://101books.net/2012/03/02/7-annoying-words-that-should-die-a-horrible-death/",
108 | "message": "I think what you're saying is that you said something. If you're using it to mitigate something that may be offensive or embarrassing, then don't say it. Say something else. Otherwise, say what you're saying without the \"just saying.\" We already know you're saying it... after all, you just said it! --Robert Bruce"
109 | },
110 | {
111 | "displayLabel": ["In my opinion"],
112 | "pattern": "\\b(In my opinion)\\b",
113 | "source": "https://preciseedit.wordpress.com/2009/06/19/in-my-opinion-i-think-that-i-believe-this-is-bad-writing/",
114 | "message": "Phrases such as \"in my opinion,\" \"I think that,\" and \"I believe\" create three problems for writers: 1. They delay the writer's message; 2. They demonstrate insecurity; and 3. They tell the reader what he already knows. Remove that phrase, or any similar phrase, and get to the point. --David Bowman"
115 | },
116 | {
117 | "displayLabel": [
118 | "This might be a stupid question",
119 | "This might be a silly idea"
120 | ],
121 | "pattern": "\\b(This might be a stupid question|This might be a silly idea)\\b",
122 | "source": "http://www.vogue.com/13362056/things-working-women-should-never-email/",
123 | "message": "Like they said in school, there are no stupid questions. Well, sometimes there are--but ask, don't caveat. --Alexandra Macon"
124 | },
125 | {
126 | "displayLabel": ["I may be wrong", "I might be wrong", "I could be wrong"],
127 | "pattern": "\\b(I may be wrong|I might be wrong|I could be wrong)\\b",
128 | "source": "http://www.vogue.com/13362056/things-working-women-should-never-email/",
129 | "message": "Don't lessen the impact of what you say before you say it. --Alexandra Macon"
130 | },
131 | {
132 | "displayLabel": [
133 | "I'm just being honest here",
134 | "If I'm being honest",
135 | "Honestly",
136 | "to be honest",
137 | "if I'm honest",
138 | "to be perfectly honest",
139 | "in all honesty"
140 | ],
141 | "pattern": "\\b(I'm just being honest here|If I'm being honest|Honestly|to be honest|if I'm honest|to be perfectly honest|in all honesty)\\b",
142 | "source": "https://lifehacker.com/the-verbal-tee-ups-that-often-reveal-dishonesty-1505870461",
143 | "message": "Tee-ups like this may make the reader shut down and respond negatively to your comment. --Thorin Klosowski"
144 | },
145 | {
146 | "displayLabel": ["I guess"],
147 | "pattern": "\\b(I guess)\\b",
148 | "source": "https://www.inc.com/mary-rezek/cut-kinda-sorta-i-guess-how-to-end-your-filler-word-bad-habits.html",
149 | "message": "If you're sure of something, \"guessing\" detracts from your message and opens doubt in the reader's mind. --Mary Rezek"
150 | },
151 | {
152 | "displayLabel": ["Maybe"],
153 | "pattern": "\\b(Maybe)\\b",
154 | "source": "https://www.monster.com/career-advice/article/7-words-that-make-you-sound-less-confident-in-emails-0916",
155 | "message": "By adding \"maybe\" to your sentence, it makes it seem like you aren't confident in your answer/suggestion. Say what you mean. If you mean no, say no, if you mean yes, say yes."
156 | },
157 | {
158 | "displayLabel": ["!!!"],
159 | "pattern": "!{3,}",
160 | "source": "https://www.theatlantic.com/technology/archive/2018/06/exclamation-point-inflation/563774/?utm_source=feed",
161 | "message": "We get it, you're excited!!! Use that many exclamation points with your friends, not your coworkers or clients. This is not social media. --Julie Beck"
162 | }
163 | ]
164 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 |
5 | module.exports = {
6 | plugins: [
7 | new HtmlWebpackPlugin({
8 | chunks: ['options'],
9 | filename: 'options.html',
10 | template: 'options/options.ejs',
11 | templateParameters: {
12 | allWarnings: require('./src/warnings/phrases.json'),
13 | },
14 | }),
15 | new CopyPlugin({
16 | patterns: [
17 | { from: 'img', to: 'img' },
18 | { from: 'manifest.json' },
19 | { from: 'just-not-sorry.css' },
20 | ],
21 | }),
22 | ],
23 | mode: 'production',
24 | entry: {
25 | bundle: './src/index.js',
26 | options: './options/options.js',
27 | background: './background/background.js',
28 | },
29 | output: {
30 | path: path.join(__dirname, 'build'),
31 | filename: '[name].js',
32 | },
33 | module: {
34 | rules: [
35 | {
36 | test: /\.css$/i,
37 | use: ['style-loader', 'css-loader'],
38 | },
39 | {
40 | test: /\.jsx?$/,
41 | exclude: /(node_modules)/,
42 | use: {
43 | loader: 'babel-loader',
44 | options: {
45 | presets: ['@babel/preset-env'],
46 | plugins: [['@babel/plugin-transform-react-jsx']],
47 | },
48 | },
49 | },
50 | ],
51 | },
52 | resolve: {
53 | extensions: ['.js', '.jsx'],
54 | },
55 | devtool: 'inline-source-map',
56 | performance: {
57 | hints: process.env.NODE_ENV === 'production' ? 'warning' : false,
58 | },
59 | devServer: {
60 | static: {
61 | directory: path.join(__dirname, 'public'),
62 | },
63 | compress: true,
64 | port: 9021,
65 | },
66 | };
67 |
--------------------------------------------------------------------------------