├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── CD.yml │ └── CI.yml ├── .gitignore ├── .husky ├── .gitignore ├── post-checkout ├── post-commit ├── post-merge ├── pre-commit └── pre-push ├── LICENCE ├── README.md ├── __mocks__ └── webextension-polyfill-ts.ts ├── assets ├── Clerkent.pptx ├── chrome_toolbar_screenshot.png ├── clerkent.png ├── ecthr.png ├── icj.png ├── screenshot_sg.png └── screenshot_uk.png ├── babel.config.js ├── jest.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── scripts ├── generateIcons.ts └── getVersion.ts ├── src ├── Background.tsx ├── ContentScript │ ├── ContentScript.css │ ├── ContentScript.tsx │ ├── DownloadInterceptor.ts │ ├── Highlighter.ts │ ├── Searcher │ │ ├── LawNet.ts │ │ ├── LexisUK.ts │ │ ├── Searcher.ts │ │ ├── SearcherStorage.ts │ │ ├── WestlawUK.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── Tooltip.ts │ └── index.tsx ├── Options │ ├── Options.tsx │ ├── components │ │ ├── ClipboardPaste.tsx │ │ ├── Highlight.tsx │ │ ├── Institution.tsx │ │ └── KeyboardShortcut.tsx │ └── index.tsx ├── Popup │ ├── Popup.tsx │ ├── __tests__ │ │ ├── CaseResult.test.tsx │ │ ├── ClipboardSuggestion.test.tsx │ │ ├── ExternalLinks.test.tsx │ │ ├── QueryResult.test.tsx │ │ ├── ShowMore.test.tsx │ │ └── __snapshots__ │ │ │ ├── CaseResult.test.tsx.snap │ │ │ ├── ClipboardSuggestion.test.tsx.snap │ │ │ ├── ExternalLinks.test.tsx.snap │ │ │ ├── QueryResult.test.tsx.snap │ │ │ └── ShowMore.test.tsx.snap │ ├── components │ │ ├── CaseResult.tsx │ │ ├── ClipboardSuggestion.tsx │ │ ├── DatabaseOption.tsx │ │ ├── DatabaseStatus.tsx │ │ ├── ExternalLinks.tsx │ │ ├── JurisdictionSelect.tsx │ │ ├── LegislationResult.tsx │ │ ├── PopupContainer.tsx │ │ ├── QueryResult.tsx │ │ ├── ResultLink.tsx │ │ ├── ShowMore.tsx │ │ └── ShowNewResultsButton.tsx │ ├── hooks │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── useSearch.test.ts.snap │ │ │ └── useSearch.test.ts │ │ ├── useClipboard.ts │ │ ├── useCustomiseDatabase.ts │ │ ├── useFocusInput.ts │ │ ├── useMessenger.ts │ │ ├── usePopup.ts │ │ └── useSearch.ts │ ├── index.tsx │ └── views │ │ ├── CustomiseDatabase.tsx │ │ └── DefaultSearch.tsx ├── assets │ └── icons │ │ ├── docx.svg │ │ └── pdf.svg ├── components │ ├── Admonition.tsx │ ├── AnimatedLoading.tsx │ ├── JurisdictionFlag.tsx │ ├── SelectInput.tsx │ ├── TextLoading.tsx │ ├── Toggle.tsx │ └── __tests__ │ │ ├── Admonition.test.tsx │ │ ├── AnimatedLoading.test.tsx │ │ ├── SelectInput.test.tsx │ │ └── __snapshots__ │ │ ├── Admonition.test.tsx.snap │ │ ├── AnimatedLoading.test.tsx.snap │ │ └── SelectInput.test.tsx.snap ├── manifest.json ├── pages │ ├── Guide.tsx │ ├── MassCitations │ │ ├── MassCitations.tsx │ │ └── index.tsx │ └── Updates.tsx ├── styles │ └── tailwind.css ├── types.d.ts └── utils │ ├── Browser.ts │ ├── Clipboard.ts │ ├── Constants.ts │ ├── Finder │ ├── CaseCitationFinder │ │ ├── AU.ts │ │ ├── CA.ts │ │ ├── CaseCitationFinder.ts │ │ ├── ECHR.ts │ │ ├── EU.ts │ │ ├── HK.ts │ │ ├── MY.ts │ │ ├── NZ.ts │ │ ├── SG.ts │ │ ├── UK.ts │ │ ├── UN.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── Finder.ts │ ├── LegislationFinder.ts │ ├── StatuteAbbrs.ts │ ├── __tests__ │ │ ├── CaseCitationFinder.test.ts │ │ └── __snapshots__ │ │ │ └── CaseCitationFinder.test.ts.snap │ └── index.ts │ ├── Flag.ts │ ├── Helpers.ts │ ├── Logger.ts │ ├── Messenger.ts │ ├── OptionsStorage.ts │ ├── PDF.ts │ ├── Request.ts │ ├── Storage.ts │ ├── __tests__ │ └── Helpers.test.ts │ ├── index.ts │ └── scraper │ ├── AU │ ├── AU.ts │ ├── FCA.ts │ ├── HCA.ts │ ├── NSWCaseLaw.ts │ ├── QueenslandJudgments.ts │ ├── QueenslandSCL.ts │ ├── VictoriaLawLibrary.ts │ ├── WAECourts.ts │ ├── austlii.ts │ └── index.ts │ ├── CA │ ├── CA.ts │ ├── canlii.ts │ ├── index.ts │ └── scclexum.ts │ ├── ECHR │ ├── ECHR.ts │ ├── HUDOC.ts │ └── index.ts │ ├── EU │ ├── CURIA.ts │ ├── EPO.ts │ ├── EU.ts │ └── index.ts │ ├── HK │ ├── HK.ts │ ├── HKLIIHK.ts │ ├── HKLIIORG.ts │ ├── LRS.ts │ ├── __tests__ │ │ ├── LRS.test.ts │ │ ├── __snapshots__ │ │ │ └── LRS.test.ts.snap │ │ └── responses │ │ │ └── LRS │ │ │ ├── getCaseByCitation-result.txt │ │ │ ├── getCaseByCitation-search.txt │ │ │ └── getCaseByName.txt │ └── index.ts │ ├── MY │ ├── Kehakiman.ts │ ├── MY.ts │ └── index.ts │ ├── NZ │ ├── NZ.ts │ ├── index.ts │ └── nzlii.ts │ ├── SG │ ├── IPOS.ts │ ├── LawNet.ts │ ├── OpenLaw.ts │ ├── SG.ts │ ├── SGSC.ts │ ├── SLW.ts │ ├── STB.ts │ ├── __tests__ │ │ ├── OpenLaw.test.ts │ │ ├── STB.test.ts │ │ ├── __snapshots__ │ │ │ └── OpenLaw.test.ts.snap │ │ └── responses │ │ │ └── OpenLaw │ │ │ └── getCaseByCitation-result.txt │ ├── eLitigation.ts │ └── index.ts │ ├── Scraper.ts │ ├── UK │ ├── BAILII.ts │ ├── FindCaseLaw.ts │ ├── UK.ts │ ├── UKIPO.ts │ ├── Westlaw.ts │ └── index.ts │ ├── UN │ ├── ICJCIJ.ts │ ├── UN.ts │ └── index.ts │ ├── __tests__ │ └── utils.test.ts │ ├── common │ ├── CommonLII.ts │ └── index.ts │ ├── custom │ ├── Custom.ts │ ├── CustomDB.ts │ └── index.ts │ ├── index.ts │ └── utils.ts ├── tailwind.config.js ├── tests └── svgTransform.js ├── tsconfig.json ├── views ├── background.html ├── guide.html ├── mass-citations.html ├── options.html ├── popup.html └── updates.html └── webpack.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | generated/ 3 | extension/ 4 | .git/ 5 | .husky/ 6 | .vscode/ 7 | assets/ 8 | demo/ 9 | package.json 10 | src/manifest.json -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | webextensions: true, 4 | }, 5 | extends: [ 6 | `preact`, 7 | `plugin:sonarjs/recommended`, 8 | `plugin:unicorn/recommended`, 9 | `plugin:import/errors`, 10 | `plugin:import/warnings`, 11 | `plugin:import/typescript`, 12 | `plugin:react-hooks/recommended`, 13 | `plugin:@typescript-eslint/recommended`, 14 | ], 15 | globals: { 16 | __PATH_PREFIX__: true, 17 | }, 18 | parser: `@typescript-eslint/parser`, 19 | plugins: [ 20 | `@typescript-eslint`, 21 | `json-format`, 22 | `disable`, 23 | `sonarjs`, 24 | `sort-keys-fix`, 25 | `import`, 26 | `react-hooks`, 27 | `jest`, 28 | ], 29 | processor: `disable/disable`, 30 | root: true, 31 | rules: { 32 | "@typescript-eslint/ban-ts-comment": `warn`, 33 | "@typescript-eslint/no-empty-function": `warn`, 34 | "@typescript-eslint/no-explicit-any": `warn`, 35 | "@typescript-eslint/no-unused-vars": [ 36 | `warn`, 37 | { ignoreRestSiblings: true }, 38 | ], 39 | "comma-dangle": [ 40 | `warn`, 41 | `always-multiline`, 42 | ], 43 | // "import/no-duplicate-imports": `warn`, 44 | "import/no-named-as-default": `off`, 45 | "jsx-a11y/accessible-emoji": `off`, 46 | "no-duplicate-imports": `off`, 47 | "no-empty-function": `off`, 48 | "prefer-const": `warn`, 49 | quotes: [ 50 | `error`, 51 | `backtick`, 52 | ], 53 | "security/detect-non-literal-fs-filename": `off`, 54 | "security/detect-non-literal-require": `off`, 55 | "security/detect-object-injection": `off`, 56 | semi: [ 57 | `warn`, 58 | `never`, 59 | ], 60 | "sonarjs/no-nested-template-literals": `warn`, 61 | "sort-keys-fix/sort-keys-fix": `warn`, 62 | "unicorn/consistent-function-scoping": `off`, 63 | "unicorn/filename-case": [ 64 | `error`, 65 | { 66 | cases: { 67 | camelCase: true, 68 | pascalCase: true, 69 | }, 70 | ignore: [ 71 | `[A-Z]{2,4}`, 72 | `webextension-polyfill-ts`, 73 | ], 74 | }, 75 | ], 76 | "unicorn/no-array-reduce": `warn`, 77 | "unicorn/no-await-expression-member": `off`, 78 | "unicorn/no-null": 0, 79 | "unicorn/prefer-code-point": `warn`, 80 | "unicorn/prefer-dom-node-text-content": `off`, 81 | "unicorn/prefer-logical-operator-over-ternary": `warn`, 82 | "unicorn/prefer-module": `off`, 83 | "unicorn/prefer-node-protocol": `off`, 84 | "unicorn/prevent-abbreviations": [ 85 | `warn`, 86 | { 87 | allowList: { 88 | Props: true, 89 | eLitigation: true, 90 | props: true, 91 | }, 92 | }, 93 | ], 94 | }, 95 | settings: { 96 | "import/resolver": { 97 | node: { 98 | paths: [`src`], 99 | }, 100 | }, 101 | node: { 102 | tryExtensions: [`.tsx`], // append tsx to the list as well 103 | }, 104 | }, 105 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pptx filter=lfs diff=lfs merge=lfs -text 2 | *.png filter=lfs diff=lfs merge=lfs -text 3 | *.jpg filter=lfs diff=lfs merge=lfs -text 4 | *.jpeg filter=lfs diff=lfs merge=lfs -text 5 | *.gif filter=lfs diff=lfs merge=lfs -text -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: hueylee 4 | -------------------------------------------------------------------------------- /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | name: build & deploy 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | lfs: true 14 | 15 | - name: Use Node.JS 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | - uses: pnpm/action-setup@v3 20 | with: 21 | version: 8 22 | - uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.npm 26 | **/node_modules 27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: | 29 | ${{ runner.os }}-node- 30 | - name: install dependencies 31 | run: pnpm install --config.arch=x64 --config.platform=linux --config.libc=glibc 32 | - name: lint 33 | run: npm run lint 34 | - name: generate icons 35 | run: pnpm run generate:icons 36 | 37 | - name: version 38 | run: echo "CLERKENT_VERSION=$(npm run version --silent)" >> $GITHUB_ENV 39 | id: version 40 | 41 | - name: build 42 | run: | 43 | pnpm run build 44 | mv extension/chrome.zip extension/${{ env.CLERKENT_VERSION }}-chrome.zip 45 | mv extension/firefox.xpi extension/${{ env.CLERKENT_VERSION }}-firefox.xpi 46 | 47 | - name: Release 48 | uses: softprops/action-gh-release@v1 49 | if: startsWith(github.ref, 'refs/tags/v') 50 | with: 51 | generate_release_notes: true 52 | files: | 53 | extension/${{ env.CLERKENT_VERSION }}-chrome.zip 54 | extension/${{ env.CLERKENT_VERSION }}-firefox.xpi 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | GITHUB_REPOSITORY: lacuna-technologies/clerkent 58 | 59 | - uses: trmcnvn/firefox-addon@v1 60 | with: 61 | uuid: '{68470d9e-14a9-4697-a19b-413d6773c788}' 62 | xpi: extension/${{ env.CLERKENT_VERSION }}-firefox.xpi 63 | manifest: extension/firefox//manifest.json 64 | api-key: ${{ secrets.FIREFOX_API_KEY }} 65 | api-secret: ${{ secrets.FIREFOX_API_SECRET }} 66 | 67 | # - uses: mnao305/chrome-extension-upload@v5.0.0 68 | # with: 69 | # file-path: extension/${{ env.CLERKENT_VERSION }}-chrome.zip 70 | # extension-id: ogjefnociaddjemkkajgmfpmhmpokmhj 71 | # client-id: ${{ secrets.CHROME_CLIENT_ID }} 72 | # client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} 73 | # refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} 74 | # glob: false 75 | # publish: true 76 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: build & test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - ".github/**" 8 | - "README.md" 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | lfs: true 17 | - name: Use Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | - uses: pnpm/action-setup@v3 22 | with: 23 | version: 8 24 | - uses: actions/cache@v3 25 | with: 26 | path: | 27 | ~/.npm 28 | **/node_modules 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | - name: install dependencies 33 | run: pnpm install --config.arch=x64 --config.platform=linux --config.libc=glibc 34 | - name: lint 35 | run: pnpm run lint 36 | - name: generate icons 37 | run: pnpm run generate:icons 38 | - name: run tests 39 | run: pnpm run test:ci 40 | env: 41 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 42 | - name: run FOSSA scan 43 | uses: fossa-contrib/fossa-action@v3 44 | with: 45 | fossa-api-key: ${{ secrets.FOSSA_API_KEY }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .npm 3 | .eslintcache 4 | .env 5 | .cache 6 | extension/ 7 | generated/ 8 | .vscode 9 | .eslintcache 10 | coverage/ 11 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } 3 | git lfs post-checkout "$@" 4 | -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } 3 | git lfs post-commit "$@" 4 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } 3 | git lfs post-merge "$@" 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } 3 | git lfs pre-push "$@" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Clerkent 6 |

7 | 8 | Case law at your fingertips 9 | 10 | See [clerkent.huey.xyz](https://clerkent.huey.xyz) for more details 11 | 12 | ![GitHub](https://img.shields.io/badge/licence-EUPL--1.2-blue) 13 | ![Chrome Web Store](https://img.shields.io/chrome-web-store/users/ogjefnociaddjemkkajgmfpmhmpokmhj?label=Google%20Chrome%20users) 14 | ![Chrome Web Store](https://img.shields.io/chrome-web-store/v/ogjefnociaddjemkkajgmfpmhmpokmhj) 15 | ![Mozilla Add-on](https://img.shields.io/amo/users/clerkent?label=Mozilla%20Firefox%20users) 16 | ![Mozilla Add-on](https://img.shields.io/amo/v/clerkent) 17 | 18 | ## Install 19 | 20 | [](https://addons.mozilla.org/en-GB/firefox/addon/clerkent/) 21 | [](https://chrome.google.com/webstore/detail/clerkent/ogjefnociaddjemkkajgmfpmhmpokmhj) 22 | [](https://clerkent.huey.xyz/help#edge-installation) 23 | 24 | ## Screenshots 25 | 26 | ![UK screenshot](./assets/screenshot_uk.png) 27 | ![SG screenshot](./assets/screenshot_sg.png) 28 | 29 | ## Development 30 | 31 | Run in development mode for your preferred browser: 32 | 33 | ```bash 34 | pnpm i 35 | 36 | pnpm run dev:chrome 37 | pnpm run dev:firefox 38 | pnpm run dev:opera 39 | ``` 40 | 41 | Load it as an unpacked extension 42 | 43 | ### Tests 44 | 45 | There are some tests, mostly snapshot tests. 46 | 47 | ```bash 48 | pnpm run test 49 | ``` 50 | 51 | ### Roadmap 52 | 53 | Find out what features and other changes are coming soon on the development [roadmap](https://github.com/orgs/lacuna-technologies/projects/1). 54 | 55 | ## Dependencies 56 | 57 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B1364%2Fgit%40github.com%3Alacuna-technologies%2Fclerkent.git.svg?type=large)](https://app.fossa.com/projects/custom%2B1364%2Fgit%40github.com%3Alacuna-technologies%2Fclerkent.git?ref=badge_large) 58 | -------------------------------------------------------------------------------- /__mocks__/webextension-polyfill-ts.ts: -------------------------------------------------------------------------------- 1 | export const browser = { 2 | runtime: { 3 | getURL: () => ``, 4 | }, 5 | tabs: { 6 | create: ({ url }) => {}, 7 | executeScript: ({}) => {}, 8 | remove: () => {}, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /assets/Clerkent.pptx: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:48a391613f77724dd315c3272ba56fbcad794f152d74bdda85c86bda644d4c45 3 | size 43559 4 | -------------------------------------------------------------------------------- /assets/chrome_toolbar_screenshot.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e83eec56fd6726ea7aa01ecfe56e618adbea5bdbcaca07b1c228dd000e21f84b 3 | size 13928 4 | -------------------------------------------------------------------------------- /assets/clerkent.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d9b353d823d64240e4c21baae74cb7e795ea39229edfa5b1289a65c3a7ffe636 3 | size 19730 4 | -------------------------------------------------------------------------------- /assets/ecthr.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1d2d6d21906c1d1b24b7e32102d245831a418cd46e0d57eeffe7fb8bb79399c0 3 | size 11532 4 | -------------------------------------------------------------------------------- /assets/icj.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:27cac929ecda6e837634e785f76c3915e33fcaf7affc4ce4b42595cde4e30169 3 | size 644156 4 | -------------------------------------------------------------------------------- /assets/screenshot_sg.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:86d0a69d0f15d2f50ac56607e34af04e45ca402fcead332688102f3bbb885926 3 | size 33635 4 | -------------------------------------------------------------------------------- /assets/screenshot_uk.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9e67120f2be26d35902b9586b275e37fbfdf70fd2feda6f8295b7a47324e8221 3 | size 37744 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | exclude: [ 3 | `/node_modules/`, 4 | ], 5 | plugins: [ 6 | [ 7 | `@babel/plugin-transform-react-jsx`, 8 | { 9 | importSource: `preact`, 10 | runtime: `automatic`, 11 | }, 12 | ], 13 | [ 14 | // Polyfills the runtime needed for async/await and generators 15 | `@babel/plugin-transform-runtime`, 16 | { 17 | helpers: false, 18 | regenerator: true, 19 | }, 20 | ], 21 | ], 22 | presets: [ 23 | [ 24 | `@babel/preset-env`, 25 | { 26 | targets: `> 0.25%, not dead`, 27 | }, 28 | ], 29 | [`@babel/preset-typescript`, { jsxPragma: `h` }], 30 | ], 31 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest' 2 | import { defaults } from 'jest-config' 3 | 4 | const config: Config = { 5 | moduleDirectories: [ 6 | `node_modules`, 7 | `src`, 8 | ], 9 | moduleFileExtensions: [ 10 | ...defaults.moduleFileExtensions, 11 | `ts`, 12 | `tsx`, 13 | ], 14 | /* eslint-disable sort-keys-fix/sort-keys-fix */ 15 | moduleNameMapper: { 16 | "^react$": `preact/compat`, 17 | "^react-dom/test-utils$": `preact/test-utils`, 18 | "^react-dom$": `preact/compat`, 19 | "^react/jsx-runtime$": `preact/jsx-runtime`, 20 | }, 21 | /* eslint-enable sort-keys-fix/sort-keys-fix */ 22 | testEnvironment: `jsdom`, 23 | testPathIgnorePatterns: [ 24 | `/node_modules/`, 25 | ], 26 | transform: { 27 | "^.+\\.(tsx|jsx|js|ts|mjs)?$": `/node_modules/babel-jest`, 28 | "^.+\\.svg$": `/tests/svgTransform.js`, 29 | }, 30 | transformIgnorePatterns: [ 31 | `/node_modules/.pnpm/(?!(leven|@testing-library\\+preact|preact)@)`, 32 | ], 33 | } 34 | 35 | export default config -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | tailwindcss: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /scripts/generateIcons.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import sharp from 'sharp' 4 | import path from 'path' 5 | import fs from 'fs' 6 | 7 | const iconFile = path.join(__dirname, `..`, `assets`, `clerkent.png`) 8 | const destination = path.join(__dirname, `..`, `generated`) 9 | const iconSizes = [16, 32, 48, 64, 128] 10 | 11 | 12 | const init = async () => { 13 | try { 14 | if (!fs.existsSync(destination)) { 15 | fs.mkdirSync(destination) 16 | } 17 | 18 | for(const iconSize of iconSizes){ 19 | console.log(`🔨 generating ${iconSize} favicon`) 20 | await sharp(iconFile) 21 | .resize(iconSize) 22 | .png() 23 | .toFile(path.join(destination, `favicon-${iconSize}.png`)) 24 | } 25 | console.log(`✅ favicons generated`) 26 | } catch (error) { 27 | console.error(error) 28 | } 29 | } 30 | 31 | init() -------------------------------------------------------------------------------- /scripts/getVersion.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | const packageJson = JSON.parse(fs.readFileSync(`package.json`).toString()) 4 | 5 | console.log(packageJson.version) 6 | -------------------------------------------------------------------------------- /src/ContentScript/ContentScript.css: -------------------------------------------------------------------------------- 1 | html { 2 | --clerkent-font: -apple-system, BlinkMacSystemFont, "Apple Color Emoji", "Noto Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Segoe UI", "Calibri", Helvetica, sans-serif; 3 | } 4 | 5 | .clerkent.case { 6 | text-decoration: underline dotted; 7 | cursor: pointer; 8 | } 9 | .clerkent >.clerkent-tooltip { 10 | display: none; 11 | } 12 | 13 | .clerkent:hover>.clerkent-tooltip { 14 | display: block; 15 | } 16 | 17 | #clerkent-tooltip { 18 | font-family: var(--clerkent-font); 19 | display: none; 20 | position: fixed; 21 | background: white; 22 | color: black; 23 | padding: 0.5rem 0.8rem; 24 | max-width: 15rem; 25 | z-index: 999; 26 | border: 1px solid black; 27 | box-sizing: border-box; 28 | } 29 | 30 | #clerkent-tooltip >.clerkent-meta { 31 | display: flex; 32 | flex-direction: row; 33 | align-content: center; 34 | align-items: center; 35 | margin-bottom: 0.4rem; 36 | } 37 | 38 | #clerkent-tooltip a { 39 | color: black !important; 40 | text-decoration: none !important; 41 | } 42 | 43 | #clerkent-tooltip > a { 44 | font-weight: bold; 45 | } 46 | 47 | #clerkent-tooltip a[href]{ 48 | color: blue !important; 49 | cursor: pointer; 50 | } 51 | 52 | #clerkent-tooltip a[href]:hover { 53 | text-decoration: underline !important; 54 | } 55 | 56 | 57 | #clerkent-tooltip > .clerkent-links a.clerkent-pdf > img { 58 | height: 1rem; 59 | margin-left: 0.25rem; 60 | margin-bottom: -0.15rem; 61 | filter: invert(14%) sepia(89%) saturate(2676%) hue-rotate(350deg) brightness(84%) contrast(122%); 62 | } 63 | 64 | #clerkent-tooltip > .clerkent-links a.clerkent-pdf + a { 65 | margin-left: 1rem; 66 | } 67 | -------------------------------------------------------------------------------- /src/ContentScript/ContentScript.tsx: -------------------------------------------------------------------------------- 1 | import { browser } from 'webextension-polyfill-ts' 2 | import type { Runtime } from 'webextension-polyfill-ts' 3 | import { Messenger, Logger } from '../utils' 4 | import Tooltip from './Tooltip' 5 | import Highlighter from './Highlighter' 6 | import Searcher from './Searcher' 7 | import OptionsStorage, { OptionsSettings } from '../utils/OptionsStorage' 8 | import downloadInterceptor from './DownloadInterceptor' 9 | import './ContentScript.css' 10 | 11 | let port: Runtime.Port 12 | 13 | let highlightEnabled: OptionsSettings[`OPTIONS_HIGHLIGHT_ENABLED`] 14 | 15 | const onMessage = (message: Messenger.Message) => { 16 | Logger.log(`content script received:`, message) 17 | 18 | if (highlightEnabled && document.querySelector(`#clerkent-tooltip`) === null) { 19 | Tooltip.init() 20 | } 21 | 22 | if (message.target !== Messenger.TARGETS.contentScript) { 23 | return null // ignore 24 | } 25 | if (message.action === Messenger.ACTION_TYPES.viewCitation) { 26 | Highlighter.handleViewCitation(message) 27 | } 28 | } 29 | 30 | const init = async () => { 31 | port = browser.runtime.connect(``, { name: `contentscript-port` }) 32 | port.onMessage.addListener(onMessage) 33 | 34 | highlightEnabled = ( 35 | await OptionsStorage.highlight.get() as boolean 36 | ) 37 | 38 | if (highlightEnabled) { 39 | const hasHits = Highlighter.scanForCitations(port) 40 | 41 | if (hasHits) { 42 | Tooltip.init() 43 | } 44 | } 45 | 46 | downloadInterceptor(port) 47 | 48 | Searcher.init() 49 | } 50 | 51 | if (document.readyState === `complete`) { 52 | init() 53 | } else { 54 | document.addEventListener(`readystatechange`, init) 55 | } -------------------------------------------------------------------------------- /src/ContentScript/Searcher/LawNet.ts: -------------------------------------------------------------------------------- 1 | import { postFormData } from './utils' 2 | 3 | const init = (query: string) => { 4 | const url = `/lawnet/group/lawnet/result-page?p_p_id=legalresearchresultpage_WAR_lawnet3legalresearchportlet&p_p_lifecycle=1&p_p_state=normal&p_p_mode=view&p_p_col_id=column-2&p_p_col_count=1&_legalresearchresultpage_WAR_lawnet3legalresearchportlet_action=basicSeachActionURL&_legalresearchresultpage_WAR_lawnet3legalresearchportlet_searchType=0` 5 | 6 | const attributes = [ 7 | { basicSearchKey: query }, 8 | { grouping: 1 }, 9 | { category: 1 }, 10 | { category: 2 }, 11 | { category: 4 }, 12 | { category: 6 }, 13 | { category: 7 }, 14 | { category: 8 }, 15 | { category: 26 }, 16 | { category: 27 }, 17 | ] 18 | 19 | postFormData(url, attributes) 20 | } 21 | 22 | const LawNet = { 23 | init, 24 | } 25 | 26 | export default LawNet -------------------------------------------------------------------------------- /src/ContentScript/Searcher/LexisUK.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../../utils' 2 | import { waitTillReady } from './utils' 3 | 4 | const getInputBox = () => document.querySelector(`#inputTextId2`) as HTMLInputElement 5 | const getFindButton = () => document.querySelector(`#inputTextId3 + a`) as HTMLInputElement 6 | 7 | const isReady = () => ( 8 | document.querySelector(`body`).classList.contains(`yui-skin-sam`) && 9 | getInputBox() !== null && getFindButton() !== null 10 | ) 11 | 12 | const init = async (query: string) => { 13 | await waitTillReady(isReady) 14 | const inputBox = getInputBox() 15 | inputBox.value = query 16 | 17 | const findButton = getFindButton() 18 | findButton.click() 19 | Logger.log(inputBox, findButton) 20 | } 21 | 22 | const LexisUK = { 23 | init, 24 | } 25 | 26 | export default LexisUK -------------------------------------------------------------------------------- /src/ContentScript/Searcher/SearcherStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from '../../utils/Storage' 2 | 3 | const KEYS = { 4 | LAWNET_QUERY: `LAWNET_QUERY`, 5 | LEXIS_UK_QUERY: `SEARCHER_LEXIS_UK_QUERY`, 6 | } 7 | 8 | const storeLexisUKQuery = (query: string) => Storage.set(KEYS.LEXIS_UK_QUERY, query) 9 | const getLexisUKQuery = () => Storage.get(KEYS.LEXIS_UK_QUERY) 10 | const removeLexisUKQuery = () => Storage.remove(KEYS.LEXIS_UK_QUERY) 11 | 12 | const storeLawNetQuery = (query: string) => Storage.set(KEYS.LAWNET_QUERY, query) 13 | const getLawNetQuery = () => Storage.get(KEYS.LAWNET_QUERY) 14 | const removeLawNetQuery = () => Storage.remove(KEYS.LAWNET_QUERY) 15 | 16 | const SearcherStorage = { 17 | getLawNetQuery, 18 | getLexisUKQuery, 19 | removeLawNetQuery, 20 | removeLexisUKQuery, 21 | storeLawNetQuery, 22 | storeLexisUKQuery, 23 | } 24 | 25 | export default SearcherStorage -------------------------------------------------------------------------------- /src/ContentScript/Searcher/WestlawUK.ts: -------------------------------------------------------------------------------- 1 | import { waitTillReady } from './utils' 2 | 3 | const getSubmitButton = () => document.querySelector(`#co_search_advancedSearchButton_bottom`) as HTMLButtonElement 4 | const getCaseCitationInput = () => document.querySelector(`#co_search_advancedSearch_CI`) as HTMLInputElement 5 | const getPartyNameInput = () => document.querySelector(`#co_search_advancedSearch_TI`) as HTMLInputElement 6 | 7 | const isReady = () => ( 8 | document.querySelector(`body`).classList.contains(`keyboard-focus`) && 9 | getCaseCitationInput() !== null && getPartyNameInput() !== null && getSubmitButton() !== null 10 | ) 11 | 12 | const initCaseCitation = async (citation: string) => { 13 | await waitTillReady(isReady) 14 | const citationField = getCaseCitationInput() 15 | citationField.value = citation 16 | const searchButton = getSubmitButton() 17 | searchButton.click() 18 | } 19 | 20 | const initPartyName = async (partyName: string) => { 21 | await waitTillReady(isReady) 22 | const partyField = getPartyNameInput() 23 | partyField.value = partyName 24 | const searchButton = getSubmitButton() 25 | searchButton.click() 26 | } 27 | 28 | const WestlawUK = { 29 | initCaseCitation, 30 | initPartyName, 31 | } 32 | 33 | export default WestlawUK 34 | -------------------------------------------------------------------------------- /src/ContentScript/Searcher/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Searcher' -------------------------------------------------------------------------------- /src/ContentScript/Searcher/utils.ts: -------------------------------------------------------------------------------- 1 | export const createInput = (key: string, value: string) => { 2 | const newInput = document.createElement(`input`) 3 | newInput.setAttribute(`type`,`hidden`) 4 | newInput.setAttribute(`name`,key) 5 | newInput.setAttribute(`value`, value) 6 | return newInput 7 | } 8 | 9 | export const postFormData = (url: string, data: Record[]) => { 10 | const form = document.createElement(`form`) 11 | form.setAttribute(`method`,`post`) 12 | form.setAttribute(`action`, url) 13 | 14 | for (const attribute of data) { 15 | const [key] = Object.keys(attribute) 16 | const [value] = Object.values(attribute) 17 | const newInput = createInput(key, `${value}`) 18 | form.append(newInput) 19 | } 20 | 21 | document.querySelectorAll(`body`)[0].append(form) 22 | form.submit() 23 | } 24 | 25 | export const waitTillReady = (conditionFunction: () => boolean): Promise => new Promise((resolve) => { 26 | const keepChecking = window.setInterval(() => { 27 | if(conditionFunction()){ 28 | window.clearInterval(keepChecking) 29 | resolve() 30 | } 31 | }, 100) 32 | }) -------------------------------------------------------------------------------- /src/ContentScript/Tooltip.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '../utils' 2 | 3 | let tooltipTimeout: NodeJS.Timeout 4 | const timeoutDuration = 50 5 | 6 | const hideTooltip = () => { 7 | const tooltip: HTMLElement = document.querySelector(`#clerkent-tooltip`) 8 | tooltip.style.display = `none` 9 | } 10 | 11 | const startTimer = () => { 12 | tooltipTimeout = setTimeout(hideTooltip, timeoutDuration) 13 | } 14 | const stopTimer = () => clearTimeout(tooltipTimeout) 15 | 16 | const init = () => { 17 | Logger.log(`tooltip init`) 18 | const tooltip = document.createElement(`div`) 19 | tooltip.id = `clerkent-tooltip` 20 | tooltip.className = `clerkent` 21 | tooltip.addEventListener(`mouseover`, stopTimer) 22 | tooltip.addEventListener(`mouseout`, startTimer) 23 | document.body.prepend(tooltip) 24 | } 25 | 26 | const Tooltip = { 27 | hideTooltip, 28 | init, 29 | startTimer, 30 | stopTimer, 31 | } 32 | 33 | export default Tooltip -------------------------------------------------------------------------------- /src/ContentScript/index.tsx: -------------------------------------------------------------------------------- 1 | import './ContentScript' 2 | -------------------------------------------------------------------------------- /src/Options/Options.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'preact/hooks' 2 | import { browser } from 'webextension-polyfill-ts' 3 | import OptionsStorage from '../utils/OptionsStorage' 4 | import type { OptionShortName, OptionStorageContentType } from '../utils/OptionsStorage' 5 | import Highlight from './components/Highlight' 6 | import Institution from './components/Institution' 7 | import ClipboardPaste from './components/ClipboardPaste' 8 | import 'styles/tailwind.css' 9 | import KeyboardShortcut from './components/KeyboardShortcut' 10 | import type { FunctionComponent } from 'preact' 11 | 12 | export type updateOptionsType = ( 13 | key: K, 14 | value: ThenArgument> 15 | ) => void 16 | 17 | const Options: FunctionComponent = () => { 18 | const [optionsState, setOptionsState] = useState(OptionsStorage.defaultOptions) 19 | const { 20 | OPTIONS_HIGHLIGHT_ENABLED, 21 | OPTIONS_INSTITUTIONAL_LOGIN, 22 | OPTIONS_CLIPBOARD_PASTE_ENABLED, 23 | } = optionsState 24 | 25 | const fetchOptions = useCallback(() => { 26 | (async () => { 27 | const fetchedOptions = await OptionsStorage.getAll() 28 | setOptionsState(fetchedOptions) 29 | })() 30 | }, []) 31 | 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | const updateOptions = useCallback(((key, value) => { 34 | OptionsStorage[key].set(value as any) 35 | fetchOptions() 36 | }) as updateOptionsType, [fetchOptions]) 37 | 38 | useEffect(() => { 39 | fetchOptions() 40 | }, [fetchOptions]) 41 | 42 | return ( 43 |
44 |
45 |

46 | Clerkent Setup 47 |

48 | 52 | 56 | 60 | 61 | 62 | 63 |

64 | To open the Welcome Guide again, click  65 | 71 | here 72 | 73 | . 74 |

75 | 76 |
77 |
78 | ) 79 | } 80 | 81 | export default Options 82 | -------------------------------------------------------------------------------- /src/Options/components/ClipboardPaste.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'preact/hooks' 2 | import Toggle from 'components/Toggle' 3 | import type { OptionsSettings } from 'utils/OptionsStorage' 4 | import type { updateOptionsType } from '../Options' 5 | import type { FunctionComponent } from 'preact' 6 | 7 | interface Props { 8 | value: OptionsSettings[`OPTIONS_CLIPBOARD_PASTE_ENABLED`], 9 | updateOptions: updateOptionsType 10 | } 11 | 12 | const ClipboardPaste: FunctionComponent = ({ value, updateOptions }) => { 13 | 14 | const onChangeClipboardPaste = useCallback( 15 | (value: boolean) => updateOptions(`clipboardPaste`, value), 16 | [updateOptions], 17 | ) 18 | 19 | return ( 20 |
21 |
22 | Automatically paste clipboard contents 23 | 24 |
25 | 31 |
32 | ) 33 | } 34 | 35 | export default ClipboardPaste -------------------------------------------------------------------------------- /src/Options/components/Highlight.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'preact/hooks' 2 | import Toggle from 'components/Toggle' 3 | import type { OptionsSettings } from 'utils/OptionsStorage' 4 | import type { updateOptionsType } from '../Options' 5 | import type { FunctionComponent } from 'preact' 6 | 7 | interface Props { 8 | value: OptionsSettings[`OPTIONS_HIGHLIGHT_ENABLED`], 9 | updateOptions: updateOptionsType 10 | } 11 | 12 | const Highlight: FunctionComponent = ({ value, updateOptions }) => { 13 | 14 | const onChangeHighlight = useCallback( 15 | (value: boolean) => updateOptions(`highlight`, value), 16 | [updateOptions], 17 | ) 18 | 19 | return ( 20 |
21 |
22 | Highlighting 23 | 24 |
25 | 31 |
32 | ) 33 | } 34 | 35 | export default Highlight -------------------------------------------------------------------------------- /src/Options/components/Institution.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionComponent } from 'preact' 2 | import { useCallback } from 'preact/hooks' 3 | import SelectInput from '../../components/SelectInput' 4 | import Constants from '../../utils/Constants' 5 | import type { OptionsSettings } from '../../utils/OptionsStorage' 6 | import type { updateOptionsType } from '../Options' 7 | 8 | const institutionOptions = Object.entries(Constants.INSTITUTIONAL_LOGINS).map( 9 | ([key, value]) => ({ content: value, value: key }), 10 | ) 11 | 12 | interface Props { 13 | value: OptionsSettings[`OPTIONS_INSTITUTIONAL_LOGIN`], 14 | updateOptions: updateOptionsType 15 | } 16 | 17 | const Institution: FunctionComponent = ({ value, updateOptions }) => { 18 | const onChange = useCallback((value) => updateOptions(`institutionalLogin`, value), [updateOptions]) 19 | 20 | return ( 21 |
22 |
23 | Institutional Login 24 | 27 |
28 | 34 |
35 | ) 36 | } 37 | 38 | export default Institution -------------------------------------------------------------------------------- /src/Options/components/KeyboardShortcut.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'preact/hooks' 2 | import Browser from 'utils/Browser' 3 | import { browser } from 'webextension-polyfill-ts' 4 | 5 | const KeyboardShortcut = () => { 6 | const openChromeShortcuts = useCallback(() => { 7 | browser.tabs.create({ url: `chrome://extensions/shortcuts `}) 8 | }, []) 9 | return ( 10 |
11 |
12 | Keyboard shortcut 13 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default KeyboardShortcut -------------------------------------------------------------------------------- /src/Options/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'preact' 2 | import Options from './Options' 3 | 4 | const container = document.querySelector(`#options-root`) 5 | render(, container) 6 | -------------------------------------------------------------------------------- /src/Popup/Popup.tsx: -------------------------------------------------------------------------------- 1 | import Router from 'preact-router' 2 | import CustomiseDatabase from './views/CustomiseDatabase' 3 | import DefaultSearch from './views/DefaultSearch' 4 | 5 | const Popup = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default Popup -------------------------------------------------------------------------------- /src/Popup/__tests__/CaseResult.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/preact' 2 | import Constants from 'utils/Constants' 3 | import CaseResult from '../components/CaseResult' 4 | import useClipboard from 'Popup/hooks/useClipboard' 5 | 6 | jest.mock(`Popup/hooks/useClipboard`) 7 | const useClipboardMock = useClipboard as jest.Mock 8 | 9 | const mockCase: Law.Case = { 10 | citation: `[2006] EWCA Civ 145`, 11 | database: Constants.DATABASES.UK_bailii, 12 | jurisdiction: Constants.JURISDICTIONS.UK.id, 13 | links: [ 14 | { 15 | doctype: `Judgment`, 16 | filetype: `HTML`, 17 | url: `https://www.bailii.org/ew/cases/EWCA/Civ/2006/145.html`, 18 | }, 19 | ], 20 | name: `IDA v University of Southampton`, 21 | type: `case-name`, 22 | } 23 | const mockDownloadPDF = () => () => null 24 | 25 | describe(`CaseResult`, () => { 26 | beforeAll(() => { 27 | useClipboardMock.mockImplementation(() => ({ 28 | permissionGranted: true, 29 | promptGrant: jest.fn(), 30 | })) 31 | }) 32 | it(`renders without error`, () => { 33 | const tree = render( 34 | , 38 | ) 39 | expect(tree).toMatchSnapshot() 40 | }) 41 | }) -------------------------------------------------------------------------------- /src/Popup/__tests__/ClipboardSuggestion.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/preact' 2 | import ClipboardSuggestion from 'Popup/components/ClipboardSuggestion' 3 | import Clipboard from 'utils/Clipboard' 4 | import OptionsStorage from 'utils/OptionsStorage' 5 | import useClipboard from 'Popup/hooks/useClipboard' 6 | 7 | jest.mock(`Popup/hooks/useClipboard`) 8 | const useClipboardMock = useClipboard as jest.Mock 9 | 10 | jest.mock(`utils/Clipboard`) 11 | const getPopupSearchText = Clipboard.getPopupSearchText as jest.Mock 12 | 13 | jest.mock(`utils/OptionsStorage`) 14 | const clipboardPasteGet = OptionsStorage.clipboardPaste.get as jest.Mock 15 | 16 | const QUERY = `abc123` 17 | const CLIPBOARD_TEXT = `xyz789` 18 | const applyClipboardText = () => null 19 | 20 | describe(`ExternalLinks`, () => { 21 | it(`renders without error`, () => { 22 | getPopupSearchText.mockImplementation(() => Promise.resolve(CLIPBOARD_TEXT)) 23 | clipboardPasteGet.mockImplementation(() => Promise.resolve(true)) 24 | useClipboardMock.mockImplementation(() => ({ 25 | permissionGranted: true, 26 | promptGrant: jest.fn(), 27 | })) 28 | const tree = render( 29 | , 33 | ) 34 | expect(tree).toMatchSnapshot() 35 | }) 36 | }) -------------------------------------------------------------------------------- /src/Popup/__tests__/ExternalLinks.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/preact' 2 | import ExternalLinks from 'Popup/components/ExternalLinks' 3 | import { Constants } from 'utils' 4 | import OptionsStorage from 'utils/OptionsStorage' 5 | import SearcherStorage from 'ContentScript/Searcher/SearcherStorage' 6 | 7 | jest.mock(`utils/OptionsStorage`) 8 | const institutionalLoginGet = OptionsStorage.institutionalLogin.get as jest.Mock 9 | 10 | jest.mock(`ContentScript/Searcher/SearcherStorage`) 11 | const storeLawNetQuery = SearcherStorage.storeLawNetQuery as jest.Mock 12 | 13 | const TYPE: Law.Type = `case-name` 14 | const QUERY = `abc123` 15 | 16 | describe(`ExternalLinks`, () => { 17 | 18 | beforeAll(() => { 19 | institutionalLoginGet.mockImplementation(() => Promise.resolve(`None`)) 20 | storeLawNetQuery.mockImplementation(() => Promise.resolve()) 21 | }) 22 | 23 | it(`renders without error for UK`, () => { 24 | const tree = render( 25 | , 30 | ) 31 | expect(tree).toMatchSnapshot() 32 | }) 33 | 34 | it(`renders without error for Singapore`, () => { 35 | const tree = render( 36 | , 41 | ) 42 | expect(tree).toMatchSnapshot() 43 | }) 44 | 45 | it(`renders without error for EU`, () => { 46 | const tree = render( 47 | , 52 | ) 53 | expect(tree).toMatchSnapshot() 54 | }) 55 | }) -------------------------------------------------------------------------------- /src/Popup/__tests__/QueryResult.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/preact' 2 | import Constants from '../../utils/Constants' 3 | import QueryResult from '../components/QueryResult' 4 | import useClipboard from 'Popup/hooks/useClipboard' 5 | 6 | jest.mock(`Popup/hooks/useClipboard`) 7 | const useClipboardMock = useClipboard as jest.Mock 8 | 9 | const mockDownloadPDF = () => () => null 10 | const mockUpdateResults = () => null 11 | const mockCases: Law.Case[] = [ 12 | { 13 | citation: `[1884] EWHC 2 (QB)`, 14 | database: Constants.DATABASES.UK_bailii, 15 | jurisdiction: Constants.JURISDICTIONS.UK.id, 16 | links: [ 17 | { 18 | doctype: `Judgment`, 19 | filetype: `HTML`, 20 | url: `http://www.bailii.org/ew/cases/EWHC/QB/1884/2.html`, 21 | }, 22 | { 23 | doctype: `Judgment`, 24 | filetype: `PDF`, 25 | url: `http://www.bailii.org/ew/cases/EWHC/QB/1884/2.pdf`, 26 | }, 27 | ], 28 | name: `R v Dudley and Stephens`, 29 | type: `case-name`, 30 | }, 31 | ] 32 | 33 | describe(`QueryResult`, () => { 34 | beforeAll(() => { 35 | useClipboardMock.mockImplementation(() => ({ 36 | permissionGranted: true, 37 | promptGrant: jest.fn(), 38 | })) 39 | }) 40 | it(`renders no cases found`, () => { 41 | const tree = render( 42 | , 49 | ) 50 | expect(tree).toMatchSnapshot() 51 | }) 52 | 53 | it(`renders loading`, () => { 54 | const treeCase = render( 55 | , 62 | ) 63 | expect(treeCase).toMatchSnapshot() 64 | }) 65 | 66 | it(`renders list of UK cases`, () => { 67 | const tree = render( 68 | , 75 | ) 76 | expect(tree).toMatchSnapshot() 77 | }) 78 | }) -------------------------------------------------------------------------------- /src/Popup/__tests__/ShowMore.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/preact' 2 | import ShowMore from '../components/ShowMore' 3 | 4 | describe(`ShowMore`, () => { 5 | it(`renders without error`, () => { 6 | const tree = render( 7 | null} 9 | />, 10 | ) 11 | expect(tree).toMatchSnapshot() 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/Popup/__tests__/__snapshots__/ClipboardSuggestion.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ExternalLinks renders without error 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 | , 9 | "container":
, 10 | "debug": [Function], 11 | "findAllByAltText": [Function], 12 | "findAllByDisplayValue": [Function], 13 | "findAllByLabelText": [Function], 14 | "findAllByPlaceholderText": [Function], 15 | "findAllByRole": [Function], 16 | "findAllByTestId": [Function], 17 | "findAllByText": [Function], 18 | "findAllByTitle": [Function], 19 | "findByAltText": [Function], 20 | "findByDisplayValue": [Function], 21 | "findByLabelText": [Function], 22 | "findByPlaceholderText": [Function], 23 | "findByRole": [Function], 24 | "findByTestId": [Function], 25 | "findByText": [Function], 26 | "findByTitle": [Function], 27 | "getAllByAltText": [Function], 28 | "getAllByDisplayValue": [Function], 29 | "getAllByLabelText": [Function], 30 | "getAllByPlaceholderText": [Function], 31 | "getAllByRole": [Function], 32 | "getAllByTestId": [Function], 33 | "getAllByText": [Function], 34 | "getAllByTitle": [Function], 35 | "getByAltText": [Function], 36 | "getByDisplayValue": [Function], 37 | "getByLabelText": [Function], 38 | "getByPlaceholderText": [Function], 39 | "getByRole": [Function], 40 | "getByTestId": [Function], 41 | "getByText": [Function], 42 | "getByTitle": [Function], 43 | "queryAllByAltText": [Function], 44 | "queryAllByDisplayValue": [Function], 45 | "queryAllByLabelText": [Function], 46 | "queryAllByPlaceholderText": [Function], 47 | "queryAllByRole": [Function], 48 | "queryAllByTestId": [Function], 49 | "queryAllByText": [Function], 50 | "queryAllByTitle": [Function], 51 | "queryByAltText": [Function], 52 | "queryByDisplayValue": [Function], 53 | "queryByLabelText": [Function], 54 | "queryByPlaceholderText": [Function], 55 | "queryByRole": [Function], 56 | "queryByTestId": [Function], 57 | "queryByText": [Function], 58 | "queryByTitle": [Function], 59 | "rerender": [Function], 60 | "unmount": [Function], 61 | } 62 | `; 63 | -------------------------------------------------------------------------------- /src/Popup/__tests__/__snapshots__/ShowMore.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ShowMore renders without error 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 |
11 | 14 | 15 | Show More 16 | 17 | 18 |
19 |
20 | , 21 | "container":
22 |
25 | 28 | 29 | Show More 30 | 31 | 32 |
33 |
, 34 | "debug": [Function], 35 | "findAllByAltText": [Function], 36 | "findAllByDisplayValue": [Function], 37 | "findAllByLabelText": [Function], 38 | "findAllByPlaceholderText": [Function], 39 | "findAllByRole": [Function], 40 | "findAllByTestId": [Function], 41 | "findAllByText": [Function], 42 | "findAllByTitle": [Function], 43 | "findByAltText": [Function], 44 | "findByDisplayValue": [Function], 45 | "findByLabelText": [Function], 46 | "findByPlaceholderText": [Function], 47 | "findByRole": [Function], 48 | "findByTestId": [Function], 49 | "findByText": [Function], 50 | "findByTitle": [Function], 51 | "getAllByAltText": [Function], 52 | "getAllByDisplayValue": [Function], 53 | "getAllByLabelText": [Function], 54 | "getAllByPlaceholderText": [Function], 55 | "getAllByRole": [Function], 56 | "getAllByTestId": [Function], 57 | "getAllByText": [Function], 58 | "getAllByTitle": [Function], 59 | "getByAltText": [Function], 60 | "getByDisplayValue": [Function], 61 | "getByLabelText": [Function], 62 | "getByPlaceholderText": [Function], 63 | "getByRole": [Function], 64 | "getByTestId": [Function], 65 | "getByText": [Function], 66 | "getByTitle": [Function], 67 | "queryAllByAltText": [Function], 68 | "queryAllByDisplayValue": [Function], 69 | "queryAllByLabelText": [Function], 70 | "queryAllByPlaceholderText": [Function], 71 | "queryAllByRole": [Function], 72 | "queryAllByTestId": [Function], 73 | "queryAllByText": [Function], 74 | "queryAllByTitle": [Function], 75 | "queryByAltText": [Function], 76 | "queryByDisplayValue": [Function], 77 | "queryByLabelText": [Function], 78 | "queryByPlaceholderText": [Function], 79 | "queryByRole": [Function], 80 | "queryByTestId": [Function], 81 | "queryByText": [Function], 82 | "queryByTitle": [Function], 83 | "rerender": [Function], 84 | "unmount": [Function], 85 | } 86 | `; 87 | -------------------------------------------------------------------------------- /src/Popup/components/CaseResult.tsx: -------------------------------------------------------------------------------- 1 | import useClipboard from 'Popup/hooks/useClipboard' 2 | import type { FunctionComponent } from 'preact' 3 | import { useCallback } from 'preact/hooks' 4 | import Helpers from 'utils/Helpers' 5 | import ResultLink from './ResultLink' 6 | 7 | interface Props { 8 | case: Law.Case, 9 | downloadPDF: downloadPDFType 10 | } 11 | 12 | const CaseResult: FunctionComponent = ({ 13 | case: currentCase, 14 | downloadPDF, 15 | }) => { 16 | const { 17 | citation, 18 | name, 19 | links, 20 | database, 21 | } = currentCase 22 | const summaryURL = Helpers.getSummaryLink(links)?.url 23 | const judgmentLink = Helpers.getJudgmentLink(links) 24 | const opinionLink = Helpers.getOpinionLink(links) 25 | const orderLink = Helpers.getOrderLink(links) 26 | 27 | const caseNameClass = summaryURL ? `text-blue-700 border-0 bg-none outline-none p-0 underline cursor-pointer select-text hover:text-blue-900 hover:underline` : `` 28 | const { 29 | permissionGranted, 30 | promptGrant, 31 | } = useClipboard() 32 | const onClickCitation = useCallback(async () => { 33 | if(!permissionGranted){ 34 | await promptGrant() 35 | } 36 | await navigator.clipboard.writeText(citation) 37 | }, [permissionGranted, promptGrant, citation]) 38 | 39 | return ( 40 |
41 |
42 | { 43 | database && ( 44 | 45 | {database.name} 46 | 47 | ) 48 | } 49 |
50 | 57 | {name} 58 | 59 |
60 | 65 | {citation} 66 | 67 | 71 | 75 | 79 |
80 |
81 | ) 82 | } 83 | 84 | export default CaseResult -------------------------------------------------------------------------------- /src/Popup/components/ClipboardSuggestion.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'preact/compat' 2 | import Admonition from 'components/Admonition' 3 | import Clipboard from 'utils/Clipboard' 4 | import OptionsStorage from 'utils/OptionsStorage' 5 | import useClipboard from 'Popup/hooks/useClipboard' 6 | 7 | // eslint-disable-next-line sonarjs/cognitive-complexity 8 | const ClipboardSuggestion = ({ query, applyClipboardText }) => { 9 | const [clipboardText, setClipboardText] = useState(``) 10 | const [enabled, setEnabled] = useState(false) 11 | const { permissionGranted, promptGrant } = useClipboard() 12 | 13 | const onClick = useCallback(() => applyClipboardText(clipboardText), [clipboardText, applyClipboardText]) 14 | 15 | useEffect(() => { 16 | (async () => { 17 | if(permissionGranted){ 18 | const enabled = await OptionsStorage.clipboardPaste.get() 19 | setEnabled(enabled as boolean) 20 | if(enabled){ 21 | const text = await Clipboard.getPopupSearchText() 22 | if(text.length > 0){ 23 | setClipboardText(text) 24 | } 25 | } 26 | } 27 | })() 28 | }, [permissionGranted]) 29 | 30 | return (enabled && !permissionGranted) ? ( 31 | 32 | Click here to grant Clerkent permission to access your clipboard 33 | so that Clerkent can automatically paste text that looks like a citation into the search box. 34 | 35 | ) : ((enabled && clipboardText && query !== clipboardText) ? ( 36 | 37 | {clipboardText} 38 | 39 | ) : null) 40 | } 41 | 42 | export default ClipboardSuggestion -------------------------------------------------------------------------------- /src/Popup/components/DatabaseOption.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionComponent } from 'preact' 2 | import { useCallback } from 'preact/hooks' 3 | 4 | type DatabaseOptionProps = { 5 | name: string, 6 | id: string, 7 | enabled: boolean, 8 | toggleDatabase: (id: string) => Promise, 9 | } 10 | 11 | const DatabaseOption: FunctionComponent = ({ id, name, enabled, toggleDatabase }) => { 12 | const enabledClass = enabled ? `border-neutral-800 border-solid` : `text-neutral-500 border-neutral-500 border-dashed` 13 | const onClick = useCallback(() => { 14 | toggleDatabase(id) 15 | }, [toggleDatabase, id]) 16 | return ( 17 |
18 | {name} 19 |
20 | ) 21 | } 22 | 23 | export default DatabaseOption -------------------------------------------------------------------------------- /src/Popup/components/DatabaseStatus.tsx: -------------------------------------------------------------------------------- 1 | import type { FunctionComponent } from 'preact' 2 | import { useState, useEffect } from 'preact/hooks' 3 | import { Link } from 'preact-router/match' 4 | import { Constants, Storage } from "utils" 5 | 6 | const keys = { 7 | DATABASES_STATUS: `DATABASES_STATUS`, 8 | } 9 | 10 | type Props = { 11 | selectedJurisdiction: Law.JurisdictionCode, 12 | } 13 | 14 | const DatabaseStatus: FunctionComponent = ({ selectedJurisdiction }) => { 15 | const [databaseStatusString, setDatabaseStatusString] = useState(`0/0`) 16 | const [empty, setEmpty] = useState(false) 17 | 18 | useEffect(() => { 19 | (async () => { 20 | const databasesStatus: typeof Constants.DEFAULT_DATABASES_STATUS = await Storage.get(keys.DATABASES_STATUS) || Constants.DEFAULT_DATABASES_STATUS 21 | const relevantDatabases = databasesStatus[selectedJurisdiction] 22 | const totalDatabasesCount: number = Object.keys(relevantDatabases).length 23 | const enabledDatabasesCount: number = Object.entries(relevantDatabases).filter(([, enabled]) => enabled).length 24 | 25 | setEmpty(enabledDatabasesCount === 0) 26 | setDatabaseStatusString(`${ 27 | enabledDatabasesCount 28 | }/${totalDatabasesCount}`) 29 | })() 30 | }, [selectedJurisdiction]) 31 | 32 | return ( 33 | 39 | {databaseStatusString} 40 | 41 | ) 42 | } 43 | 44 | export default DatabaseStatus -------------------------------------------------------------------------------- /src/Popup/components/JurisdictionSelect.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'preact/hooks' 2 | import Select from 'react-select' 3 | import JurisdictionFlag from 'components/JurisdictionFlag' 4 | import { Constants } from '../../utils' 5 | import { FunctionComponent } from 'preact' 6 | 7 | const supportedJurisdictions = Constants.JURISDICTIONS 8 | const options = Object.values(supportedJurisdictions).map(({ id, name }) => ({ 9 | label: name, 10 | value: id, 11 | })) 12 | 13 | interface OptionProps { 14 | label: string, 15 | value: Law.JurisdictionCode 16 | } 17 | 18 | const Option: FunctionComponent = ({ label, value }) => { 19 | return ( 20 |
21 | 22 | {label} 23 |
24 | ) 25 | } 26 | 27 | type Props = { 28 | value: Law.JurisdictionCode, 29 | onChangeJurisdiction: (v: Law.JurisdictionCode) => void 30 | } 31 | 32 | const JurisdictionSelect = ({ value, onChangeJurisdiction }: Props) => { 33 | 34 | const onChange = useCallback(({ value }) => onChangeJurisdiction(value), [onChangeJurisdiction]) 35 | const optionValue = options.find(o => o.value === value) 36 | 37 | return ( 38 | // @ts-ignore 39 | 42 | {options.map(({ value, content }) => ( 43 | 49 | ))} 50 | 51 | ) 52 | } 53 | 54 | export default SelectInput -------------------------------------------------------------------------------- /src/components/TextLoading.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'preact/hooks' 2 | import Helpers from '../utils/Helpers' 3 | 4 | const thingsToLoad = [ 5 | `Loading`, 6 | `Refreshing the Supreme Court website`, 7 | `Searching BAILII`, 8 | `Looking very hard`, 9 | `Reading the judgment`, 10 | `Briefly entertaining the dissent`, 11 | `Binging it`, 12 | `Looking for PDFs`, 13 | `Grumbling about bad WiFi`, 14 | `Looking up journal abbreviations`, 15 | `Looking up an obscure latin term`, 16 | `Scouring the archives`, 17 | `Typesetting the judgment`, 18 | `Reading the search engine help page`, 19 | `Formulating a research question`, 20 | `Marvelling at Denning's way with words`, 21 | `Hoping this will load soon`, 22 | `Formatting the judgment`, 23 | `Waiting for the page to load`, 24 | `Clicking links`, 25 | `Consolidating findings`, 26 | ] 27 | 28 | const TextLoading = () => { 29 | 30 | const [loadingMessage, setLoadingMessage] = useState(`Loading`) 31 | 32 | const getAnotherLoadingMessage = useCallback(() => { 33 | setLoadingMessage(Helpers.getRandomElement(thingsToLoad)) 34 | setTimeout(getAnotherLoadingMessage, Helpers.getRandomInteger(50, 500)) 35 | }, []) 36 | 37 | useEffect(() => { 38 | getAnotherLoadingMessage() 39 | }, [getAnotherLoadingMessage]) 40 | 41 | return ( 42 | 43 | {loadingMessage}... 44 | 45 | ) 46 | } 47 | 48 | export default TextLoading -------------------------------------------------------------------------------- /src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'preact/hooks' 2 | import Helpers from '../utils/Helpers' 3 | 4 | const { classnames } = Helpers 5 | 6 | const Toggle = ({ 7 | leftText, 8 | rightText, 9 | value = false, 10 | onChange = (_: boolean): void => {}, 11 | }) => { 12 | const onChangeFalse = useCallback(() => value ? onChange(false) : null, [value, onChange]) 13 | const onChangeTrue = useCallback(() => value ? null : onChange(true), [value, onChange]) 14 | 15 | const divClass = `px-2 py-0.5 cursor-pointer flex items-center` 16 | const activeClass = `bg-slate-600 text-white` 17 | 18 | return ( 19 |
20 |
27 | {leftText} 28 |
29 |
36 | {rightText} 37 |
38 |
39 | ) 40 | } 41 | 42 | export default Toggle -------------------------------------------------------------------------------- /src/components/__tests__/Admonition.test.tsx: -------------------------------------------------------------------------------- 1 | import Admonition from "components/Admonition" 2 | import { render } from '@testing-library/preact' 3 | 4 | describe(`Admonition`, () => { 5 | it(`should render without errors`, () => { 6 | const tree = render( 7 | 8 |

9 | Fun will now commence 10 |

11 |
, 12 | ) 13 | expect(tree).toMatchSnapshot() 14 | }) 15 | }) -------------------------------------------------------------------------------- /src/components/__tests__/AnimatedLoading.test.tsx: -------------------------------------------------------------------------------- 1 | import AnimatedLoading from 'components/AnimatedLoading' 2 | import { render } from '@testing-library/preact' 3 | 4 | describe(`AnimatedLoading`, () => { 5 | it(`should render without errors`, () => { 6 | const tree = render( 7 | , 8 | ) 9 | expect(tree).toMatchSnapshot() 10 | }) 11 | }) -------------------------------------------------------------------------------- /src/components/__tests__/SelectInput.test.tsx: -------------------------------------------------------------------------------- 1 | import SelectInput from "components/SelectInput" 2 | import { render } from '@testing-library/preact' 3 | 4 | const OPTIONS = [ 5 | { content: `Jack Cloth`, value: `cloth` }, 6 | { content: `Anne Oldman`, value: `oldman` }, 7 | { content: `Tom Boss`, value: `boss` }, 8 | { content: `Asap Qureshi`, value: `qureshi` }, 9 | ] 10 | 11 | const ON_CHANGE = jest.fn() 12 | 13 | describe(`SelectInput`, () => { 14 | 15 | it(`should render without errors`, () => { 16 | const tree = render( 17 | , 23 | ) 24 | expect(tree).toMatchSnapshot() 25 | }) 26 | }) -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/AnimatedLoading.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AnimatedLoading should render without errors 1`] = ` 4 | { 5 | "asFragment": [Function], 6 | "baseElement": 7 |
8 |
11 |
14 | Loading... 15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | , 27 | "container":
28 |
31 |
34 | Loading... 35 |
36 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
, 46 | "debug": [Function], 47 | "findAllByAltText": [Function], 48 | "findAllByDisplayValue": [Function], 49 | "findAllByLabelText": [Function], 50 | "findAllByPlaceholderText": [Function], 51 | "findAllByRole": [Function], 52 | "findAllByTestId": [Function], 53 | "findAllByText": [Function], 54 | "findAllByTitle": [Function], 55 | "findByAltText": [Function], 56 | "findByDisplayValue": [Function], 57 | "findByLabelText": [Function], 58 | "findByPlaceholderText": [Function], 59 | "findByRole": [Function], 60 | "findByTestId": [Function], 61 | "findByText": [Function], 62 | "findByTitle": [Function], 63 | "getAllByAltText": [Function], 64 | "getAllByDisplayValue": [Function], 65 | "getAllByLabelText": [Function], 66 | "getAllByPlaceholderText": [Function], 67 | "getAllByRole": [Function], 68 | "getAllByTestId": [Function], 69 | "getAllByText": [Function], 70 | "getAllByTitle": [Function], 71 | "getByAltText": [Function], 72 | "getByDisplayValue": [Function], 73 | "getByLabelText": [Function], 74 | "getByPlaceholderText": [Function], 75 | "getByRole": [Function], 76 | "getByTestId": [Function], 77 | "getByText": [Function], 78 | "getByTitle": [Function], 79 | "queryAllByAltText": [Function], 80 | "queryAllByDisplayValue": [Function], 81 | "queryAllByLabelText": [Function], 82 | "queryAllByPlaceholderText": [Function], 83 | "queryAllByRole": [Function], 84 | "queryAllByTestId": [Function], 85 | "queryAllByText": [Function], 86 | "queryAllByTitle": [Function], 87 | "queryByAltText": [Function], 88 | "queryByDisplayValue": [Function], 89 | "queryByLabelText": [Function], 90 | "queryByPlaceholderText": [Function], 91 | "queryByRole": [Function], 92 | "queryByTestId": [Function], 93 | "queryByText": [Function], 94 | "queryByTitle": [Function], 95 | "rerender": [Function], 96 | "unmount": [Function], 97 | } 98 | `; 99 | -------------------------------------------------------------------------------- /src/pages/Guide.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'preact' 2 | import Browser from '../utils/Browser' 3 | import 'styles/tailwind.css' 4 | 5 | const Guide = () => { 6 | 7 | const isChrome = Browser.isChrome() 8 | 9 | return ( 10 |
11 | 🎉 12 |

Great job!

13 |

14 | You've successfully installed  15 | Clerkent logo 16 |  Clerkent. 17 |

18 | 19 |

20 | It should now appear in your browser toolbar (at the top-right corner of your browser window). 21 | Click the Clerkent icon and try searching for something (e.g. "[1892] EWCA Civ 1"). 22 |

23 | 24 |

25 | You can also open the search popup via the keyboard shortcut CtrlSpace. 26 |

27 | 28 |
29 | ↗️ 30 | (somewhere here) 31 |
32 | 33 | { 34 | isChrome ? ( 35 |
36 |

37 | On Chrome, newly-installed extensions might not immediately be visible. You can find hidden extensions by clicking the 3 dots in your browser toolbar. To save yourself a click each time, you can then pin Clerkent to your browser toolbar by clicking the pin icon. 38 |

39 | chrome screenshot 44 |
45 | ) : null 46 | } 47 | 48 |

49 | Tip: If you use an institutional login for proprietary databases (e.g. WestLaw or LawNet), you should set it on the  50 | Options page 51 |  so Clerkent can redirect you to the appropriate login page. 52 |

53 | 54 |

55 | Once you've clicked the icon and found the Clerkent search box, you can close this tab. 56 |

57 | 58 | 66 |
67 | ) 68 | } 69 | 70 | const container = document.querySelector(`#clerkent-guide-root`) 71 | render(, container) 72 | -------------------------------------------------------------------------------- /src/pages/MassCitations/MassCitations.tsx: -------------------------------------------------------------------------------- 1 | const keys = { 2 | MASS_CITATION_INPUT: `MASS_CITATION_INPUT`, 3 | } 4 | 5 | const MassCitations = () => { 6 | return null 7 | // const [inputText, setInputText] = useState(``) 8 | // const [parseResult, setParseResult] = useState([] as CaseCitationFinderResult[]) 9 | // const [pressedDone, setPressedDone] = useState(false) 10 | // const onInputChange = useCallback(({ target: { value }}) => { 11 | // setInputText(value) 12 | // setPressedDone(false) 13 | // }, []) 14 | // const onDone = useCallback(() => { 15 | // setPressedDone(true) 16 | // setParseResult(Finder.findCase(inputText)) 17 | // Storage.set(keys.MASS_CITATION_INPUT, inputText) 18 | // }, [inputText]) 19 | 20 | // const removeCase = useCallback(citation => () => setParseResult(parseResult.filter(result => result.citation !== citation)), [parseResult]) 21 | 22 | // useEffect(() => { 23 | // (async () => { 24 | // const storedInput = await Storage.get(keys.MASS_CITATION_INPUT) 25 | // Logger.log(storedInput) 26 | // if(storedInput !== null && storedInput.length > 0){ 27 | // onInputChange({ target: { value: storedInput }}) 28 | // } 29 | // })() 30 | // }, [onInputChange]) 31 | 32 | // return ( 33 | //
34 | //

35 | // You can paste in text from Word documents, PowerPoint presentations, and other files to download all the judgments cited therein. 36 | //

37 | //

38 | // Tip: most programs allow you to select all text by pressing Ctrl-A and copy the selected text by pressing Ctrl-C 39 | //

40 | //